[DTP-Worker 20260802_083529] Sprint 8 · buffer · Faisabilité/bancable : élimination du taux 0.52 codé en dur dans l'oracle _check_derived_arithmetic

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) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-02 08:43:04 +00:00
parent ccb86fe7ca
commit 448c310398
2 changed files with 63 additions and 3 deletions
+57
View File
@@ -1,5 +1,62 @@
# Activity log · 2026-08-02 · Claude Code DTP Worker # 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) ## 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 **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
@@ -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. """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 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] = [] motifs: list[str] = []
rows = finance.typologies(brief) 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) 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)) 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) 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)) pe_pct = finance._pct(deps.CANONICAL["point_equilibre_pct"])
if exp_total is not None else None) 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), for cle, exp in (("total_unites", exp_total),
("valeur_catalogue_usd", exp_usd), ("valeur_catalogue_usd", exp_usd),