From 448c310398adb7876aac07a8b71208449edfe221 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sun, 2 Aug 2026 08:43:04 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260802=5F083529]=20Sprint=208?= =?UTF-8?q?=20=C2=B7=20buffer=20=C2=B7=20Faisabilit=C3=A9/bancable=20:=20?= =?UTF-8?q?=C3=A9limination=20du=20taux=200.52=20cod=C3=A9=20en=20dur=20da?= =?UTF-8?q?ns=20l'oracle=20=5Fcheck=5Fderived=5Farithmetic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le seul survivant en dur du paramètre canonique #9 (point d'équilibre 52 %) vivait au cœur de l'oracle anti-invention de bancable_gen.py — alors que le module PRÊCHE la source unique (deps.py « une seule source de vérité » ; finance._pct « jamais codé en dur »). Transitivement protégé (donc gate redondant, #5) mais BUG LATENT inverse : #9 52 %→55 % aurait fait rejeter en silence un dossier bancable VALIDE. Fix = dériver le taux de CANONICAL (finance._pct), pas un checker de plus. Vérif : validate byte-identique à 52 % (pe=21) · 22 tests verts · preuve levée du bug (source→55 % ⇒ pe=22, invariants OK) · 6 gates CI exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-02.md | 57 +++++++++++++++++++ .../faisabilite/bancable/bancable_gen.py | 9 ++- 2 files changed, 63 insertions(+), 3 deletions(-) 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),