Spaces:
Sleeping
Sleeping
docs: fix the real bootstrap block - pass 1 gated on a value it cannot have
Browse filesMoving 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=
|
| 1281 |
tiers = scoring.assign_provisional(results)
|
| 1282 |
assert sum(1 for t in tiers.values() if t == 1) == 60
|
| 1283 |
-
assert tiers[999] ==
|
| 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
|
| 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
|
| 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
|
| 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
|