Viney Claude Opus 5 (1M context) commited on
Commit
82ea202
·
1 Parent(s): 2b68947

docs: fix the real bootstrap block - pass 1 gated on a value it cannot have

Browse files

Moving holdings discover to Tier 2 addressed the second link and left the
first. Pass 1 promoted only accounts with fit >= 25, but fit reaches 25 only
with an EDGAR overlap, read from confirmed holdings that do not exist on a
fresh database. So pass 1 promoted nobody, contacts enrich filtered on tier 1
and saw none, and the named-contact gate then closed Tier 1 for good. Pass 1
now admits any account with declared equity exposure - which is what choosing
who to pay to enrich actually means - while MIN_FIT still gates final Tier 1.
The spec was wrong here too, not only the plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016XzekHxqiXutBW4gw3Arsx

docs/superpowers/plans/2026-08-29-prospection-buyside.md CHANGED
@@ -1275,12 +1275,22 @@ def test_pass1_never_includes_reach_points():
1275
  assert not any(r.startswith("reach:") for r in result.reasons)
1276
 
1277
 
 
 
 
 
 
 
 
 
 
 
1278
  def test_provisional_assignment_caps_at_60_and_requires_min_fit():
1279
  results = [(i, scoring.ScoreResult(total=80, fit=30, reasons=[])) for i in range(70)]
1280
- results.append((999, scoring.ScoreResult(total=95, fit=10, reasons=[])))
1281
  tiers = scoring.assign_provisional(results)
1282
  assert sum(1 for t in tiers.values() if t == 1) == 60
1283
- assert tiers[999] == 2 # fit sous MIN_FIT mais non nul : Tier 2, pas Tier 1
1284
 
1285
 
1286
  def test_final_assignment_demotes_accounts_without_a_named_contact():
@@ -1438,8 +1448,22 @@ def score_pass2(base: ScoreResult, contacts: list[dict]) -> ScoreResult:
1438
  reasons=[*base.reasons, *reach_reasons])
1439
 
1440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1441
  def _eligible(result: ScoreResult) -> bool:
1442
- """Éligible au Tier 1 : la porte haute."""
1443
  return result.fit >= config.MIN_FIT
1444
 
1445
 
@@ -1456,7 +1480,7 @@ def assign_provisional(results: list[tuple[int, ScoreResult]]) -> dict[int, int]
1456
  # Tri secondaire sur l'identifiant : le compte qui occupe la dernière place
1457
  # sous le plafond décide qui reçoit une démo. Il ne doit pas changer d'une
1458
  # exécution à l'autre parce que l'appelant a listé les comptes autrement.
1459
- ranked = sorted((r for r in results if _eligible(r[1])),
1460
  key=lambda r: (-r[1].total, r[0]))
1461
  promoted = {account_id for account_id, _ in ranked[:config.TIER1_PROVISIONAL_CAP]}
1462
  return {
@@ -4763,6 +4787,34 @@ def test_universe_to_tier_runs_end_to_end_with_no_paid_provider(tmp_path):
4763
  "une position confirmée doit ouvrir le Tier 1, sans aucun provider payant")
4764
 
4765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4766
  def test_no_paid_provider_is_enabled_by_default():
4767
  assert [p.name for p in registry.build_providers() if p.enabled()] == ["pattern"]
4768
 
 
1275
  assert not any(r.startswith("reach:") for r in result.reasons)
1276
 
1277
 
1278
+ def test_pass1_promotes_on_declared_equity_not_on_a_confirmed_holding():
1279
+ """La passe 1 choisit qui vaut la dépense d'enrichissement, à un moment où
1280
+ aucune position n'est encore confirmée. Exiger MIN_FIT ici ne promeut
1281
+ personne, et le pipeline entier se bloque à `contacts enrich`."""
1282
+ fresh = scoring.score_pass1(_account(), has_equity_funds=True,
1283
+ has_edgar_overlap=False, signals=[])
1284
+ assert fresh.fit == 20 # sous MIN_FIT, mais non nul
1285
+ assert scoring.assign_provisional([(1, fresh)])[1] == 1
1286
+
1287
+
1288
  def test_provisional_assignment_caps_at_60_and_requires_min_fit():
1289
  results = [(i, scoring.ScoreResult(total=80, fit=30, reasons=[])) for i in range(70)]
1290
+ results.append((999, scoring.ScoreResult(total=95, fit=0, reasons=[])))
1291
  tiers = scoring.assign_provisional(results)
1292
  assert sum(1 for t in tiers.values() if t == 1) == 60
1293
+ assert tiers[999] == 0 # fit nul : aucune poche actions, exclu quel que soit le total
1294
 
1295
 
1296
  def test_final_assignment_demotes_accounts_without_a_named_contact():
 
1448
  reasons=[*base.reasons, *reach_reasons])
1449
 
1450
 
1451
+ def _enrichable(result: ScoreResult) -> bool:
1452
+ """Éligible à la PASSE 1 — la population qu'on accepte de payer pour enrichir.
1453
+
1454
+ Le seuil est `fit > 0`, pas `MIN_FIT`, et c'est structurel. `MIN_FIT` n'est
1455
+ atteint qu'avec un recouvrement EDGAR, lequel se lit dans les positions
1456
+ confirmées à la main. Sur une base vierge il n'y en a aucune : exiger
1457
+ `MIN_FIT` ici ne promeut personne, `contacts enrich` ne voit alors aucun
1458
+ compte, aucun contact n'est jamais créé, et la porte « contact nommé » de la
1459
+ passe 2 ferme le Tier 1 définitivement. La passe 1 sert à choisir qui vaut
1460
+ la dépense d'enrichissement, à un moment où l'on ignore encore les positions.
1461
+ """
1462
+ return result.fit > 0
1463
+
1464
+
1465
  def _eligible(result: ScoreResult) -> bool:
1466
+ """Éligible au Tier 1 final : la porte haute, une fois les positions connues."""
1467
  return result.fit >= config.MIN_FIT
1468
 
1469
 
 
1480
  # Tri secondaire sur l'identifiant : le compte qui occupe la dernière place
1481
  # sous le plafond décide qui reçoit une démo. Il ne doit pas changer d'une
1482
  # exécution à l'autre parce que l'appelant a listé les comptes autrement.
1483
+ ranked = sorted((r for r in results if _enrichable(r[1])),
1484
  key=lambda r: (-r[1].total, r[0]))
1485
  promoted = {account_id for account_id, _ in ranked[:config.TIER1_PROVISIONAL_CAP]}
1486
  return {
 
4787
  "une position confirmée doit ouvrir le Tier 1, sans aucun provider payant")
4788
 
4789
 
4790
+ def test_the_documented_sequence_reaches_an_enrichable_population(tmp_path):
4791
+ """Le maillon que la narration d'amorçage tenait pour acquis.
4792
+
4793
+ `contacts enrich` ne visite que le Tier 1 provisoire. Si la passe 1 ne
4794
+ promeut personne, aucun contact n'est jamais créé et la porte « contact
4795
+ nommé » ferme le Tier 1 pour toujours — le pipeline se bloque à la
4796
+ quatrième commande, pas à la découverte des positions.
4797
+ """
4798
+ for row in amf_registry.parse_rows(FIXTURE):
4799
+ if amf_registry.is_in_scope(row)[0]:
4800
+ db.upsert_account({**row, "effectif": 18, "activites": row.get("activite"),
4801
+ "dirigeants": ["Marie Durand"],
4802
+ "independante": int(amf_registry.is_independent(row["nom"]))})
4803
+
4804
+ results = []
4805
+ for account in db.list_accounts():
4806
+ equity, overlap, signals = cli._score_inputs(account)
4807
+ assert overlap is False, "aucune position n'est confirmée à ce stade"
4808
+ results.append((account["id"],
4809
+ scoring.score_pass1(account, has_equity_funds=equity,
4810
+ has_edgar_overlap=overlap, signals=signals)))
4811
+
4812
+ provisional = scoring.assign_provisional(results)
4813
+ assert 1 in provisional.values(), (
4814
+ "la passe 1 doit produire une population enrichissable, sinon "
4815
+ "`contacts enrich` ne visite aucun compte et le Tier 1 reste fermé")
4816
+
4817
+
4818
  def test_no_paid_provider_is_enabled_by_default():
4819
  assert [p.name for p in registry.build_providers() if p.enabled()] == ["pattern"]
4820
 
docs/superpowers/specs/2026-08-29-prospection-buyside-design.md CHANGED
@@ -183,8 +183,16 @@ Chaque point écrit sa raison dans `score_reasons`.
183
  qu'après enrichissement et que l'enrichissement coûte :
184
 
185
  - **Passe 1 — `score`** : fit + accessibilité + signal (0–90). Produit
186
- `tier_provisoire`. Les 60 meilleurs comptes avec fit ≥ 25 deviennent
187
  `tier_provisoire = 1` : c'est la seule population enrichie.
 
 
 
 
 
 
 
 
188
  - **Passe 2 — `score --final`**, après `contacts enrich` : ajoute la
189
  joignabilité et applique la porte finale.
190
 
@@ -341,7 +349,7 @@ python -m prospection.cli signals scan # signaux d'intention
341
  python -m prospection.cli score # passe 1 → tier provisoire (60 max)
342
  python -m prospection.cli contacts enrich # sur le tier provisoire 1 seulement
343
  python -m prospection.cli score --final # passe 2 → Tier 1 définitif (40 max)
344
- python -m prospection.cli holdings discover --tier 1
345
  python -m prospection.cli holdings confirm # revue humaine
346
  python -m prospection.cli demo build --tier 1 # ingest → brief → PDF
347
  python -m prospection.cli campaign draft --tier 1
 
183
  qu'après enrichissement et que l'enrichissement coûte :
184
 
185
  - **Passe 1 — `score`** : fit + accessibilité + signal (0–90). Produit
186
+ `tier_provisoire`. Les 60 meilleurs comptes **avec fit > 0** deviennent
187
  `tier_provisoire = 1` : c'est la seule population enrichie.
188
+
189
+ Le seuil est `fit > 0` et non `MIN_FIT`, et c'est structurel : `MIN_FIT` n'est
190
+ atteint qu'avec un recouvrement EDGAR, lequel se lit dans les positions
191
+ confirmées à la main. Sur une base vierge il n'en existe aucune, donc exiger
192
+ `MIN_FIT` dès la passe 1 ne promeut personne, `contacts enrich` ne visite alors
193
+ aucun compte, et la porte « contact nommé » de la passe 2 ferme le Tier 1
194
+ définitivement. La passe 1 choisit qui vaut la dépense d'enrichissement, à un
195
+ moment où les positions sont encore inconnues.
196
  - **Passe 2 — `score --final`**, après `contacts enrich` : ajoute la
197
  joignabilité et applique la porte finale.
198
 
 
349
  python -m prospection.cli score # passe 1 → tier provisoire (60 max)
350
  python -m prospection.cli contacts enrich # sur le tier provisoire 1 seulement
351
  python -m prospection.cli score --final # passe 2 → Tier 1 définitif (40 max)
352
+ python -m prospection.cli holdings discover # Tier 2 par défaut : amorçage
353
  python -m prospection.cli holdings confirm # revue humaine
354
  python -m prospection.cli demo build --tier 1 # ingest → brief → PDF
355
  python -m prospection.cli campaign draft --tier 1