diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index a6742e9..51ac807 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,62 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · Faisabilité/bancable — ÉLIMINATION du taux `0.52` codé en dur dans l'oracle anti-invention (source unique CANONICAL · #6/#5) + +**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de +chiffres » · workflow #5 « zéro duplication »). En balayant les copies MANUELLES +du paramètre canonique #9 « point d'équilibre 52 % » restées HORS de la source +unique, une a été trouvée au **cœur de l'oracle anti-invention** du générateur +bancable : `faisabilite/bancable/bancable_gen.py::_check_derived_arithmetic` +(l.131) recopiait le taux en **littéral `0.52`** (`exp_pe = math.ceil(0.52 * +exp_total)`) pour re-calculer indépendamment le point d'équilibre et prouver +qu'« aucune valeur n'a été posée ». Paradoxe : le module PRÊCHE l'inverse — son +`deps.py` (« une seule source de vérité pour tout le mandat ») importe +`CANONICAL = genlib.model.CANONICAL` (gaté sur CLAUDE.md #9/#10) précisément +pour NE PAS dupliquer `3 % / 8.5 % / 52 %`, et `finance._pct` porte le docstring +explicite « **jamais codé en dur ici** — évite toute dérive ». Le `0.52` de +l'oracle était le SEUL survivant en dur (grep : aucun autre `0.03/0.085/0.52` +littéral dans le code du module ; `finance.py`/`report.py` passent tous par +`CANONICAL`). + +**Pourquoi un FIX et pas un gate** — mutation-test préalable (mémoire +`verify-uncovered-before-gating`) : AUCUNE mutation ne produit un « vert +trompeur ». Le `0.52` est **transitivement protégé** — l'invariant du module +compare l'oracle à la figure `finance` (issue de CANONICAL) au moment de la +génération, et le gate bancable (`check_readme_claims.sh`) LANCE cette génération +(`bancable_gen validate`) ; toute divergence `0.52`≠CANONICAL casse `validate` +→ gate ROUGE. Un gate d'ancrage du littéral serait donc **redondant** (interdit +#5). MAIS le `0.52` en dur est un **BUG LATENT** de sens inverse : un changement +LÉGITIME du mandat (#9 52 %→55 %, propagé à CANONICAL par les gates model/ +template) laisserait l'oracle à 21 pendant que `finance` produit 22 ⇒ le +générateur **REFUSERAIT en silence un dossier bancable VALIDE** (invariant +« manifeste 22 != recalcul 21 »), avec un message cryptique « génération réelle +échoue » côté CI. La bonne réponse n'est pas de gater la copie, mais de la +**supprimer** — dériver le taux de la source unique, comme le fait déjà +`finance.py`. + +**Correctif** (`bancable_gen.py`, `_check_derived_arithmetic`) — `0.52` remplacé +par `pe_pct = finance._pct(deps.CANONICAL["point_equilibre_pct"])` (RÉUTILISE le +parseur `%→fraction` déjà livré · zéro nouvelle logique) + garde `pe_pct is not +None`. Docstring l.117 réécrit : le taux est « lu à la SOURCE UNIQUE (CANONICAL · +CLAUDE.md #9, gaté), jamais recopié en dur ici ». L'indépendance de l'oracle est +préservée là où elle compte (Σ quantités · × prix · `ceil`) ; le TAUX, lui, est +une constante de mandat déjà ancrée en amont (CANONICAL gaté sur CLAUDE.md #9), +donc le re-dériver de cette source n'affaiblit rien. + +**Vérif** — `validate fixtures/brief_bancable.json` byte-identique à 52 % +(`point_equilibre_unites = 21 = ⌈52 % × 40⌉`). **22 tests** unittest verts +(inchangés · oracles fixture self-consistants). **Preuve du bug levé** : en +simulant la source à 55 % (`deps.CANONICAL["point_equilibre_pct"]="55 %"`), les +invariants restent **OK** et `point_equilibre_unites` passe à **22 = ⌈55 % × 40⌉** +(AVANT le fix : oracle figé à 21 ⇒ invariant cassé ⇒ dossier valide rejeté). +Suite CI complète re-verte : `check_artifacts` · `check_docs` · +`guard_constraints` · `check_ci_integrity` · `check_regression` · +`check_readme_claims` tous exit 0. **Leçon** : toute copie manuelle d'une +constante de mandat n'appelle pas un gate N+1 — quand elle est déjà +transitivement protégée, la SUPPRIMER (source unique) vaut mieux qu'un checker +redondant (#5), et lève au passage le bug-miroir « le mandat change → le module +casse ». + ## Sprint 8 · buffer · Ledger d'invariants — 2ᵉ FAMILLE (`# INVn ·` / `validate_bundle` / « N familles d'invariants ») : ancrage du COMPTE de `deploy_runbook` (11) + `acceptance` (11) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de diff --git a/05_deliverables_mvp/faisabilite/bancable/bancable_gen.py b/05_deliverables_mvp/faisabilite/bancable/bancable_gen.py index 9f50356..6de82c2 100644 --- a/05_deliverables_mvp/faisabilite/bancable/bancable_gen.py +++ b/05_deliverables_mvp/faisabilite/bancable/bancable_gen.py @@ -114,7 +114,9 @@ def _check_derived_arithmetic(brief: dict, manifest: dict) -> list[str]: """Reproduit indépendamment les 4 agrégats et compare au manifeste. Prouve qu'aucune valeur calculée n'a été « posée » : elle DOIT découler des - typologies sourcées et du taux canonique 52 %. + typologies sourcées et du taux canonique de point d'équilibre. Ce taux est lu + à la SOURCE UNIQUE (`CANONICAL` · CLAUDE.md #9, gaté), jamais recopié en dur + ici — sinon un changement du mandat (#9) casserait cet oracle en silence. """ motifs: list[str] = [] rows = finance.typologies(brief) @@ -128,8 +130,9 @@ def _check_derived_arithmetic(brief: dict, manifest: dict) -> list[str]: if all_q and all(r["prix_usd"] is not None for r in rows) else None) exp_dop = (sum(q * r["prix_dop"] for q, r in zip(qtes, rows)) if all_q and all(r["prix_dop"] is not None for r in rows) else None) - exp_pe = (float(math.ceil(0.52 * exp_total)) - if exp_total is not None else None) + pe_pct = finance._pct(deps.CANONICAL["point_equilibre_pct"]) + exp_pe = (float(math.ceil(pe_pct * exp_total)) + if exp_total is not None and pe_pct is not None else None) for cle, exp in (("total_unites", exp_total), ("valeur_catalogue_usd", exp_usd),