[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:
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user