From 342b571b29dd743d611c410358e4475eb6fbfed0 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Fri, 31 Jul 2026 20:08:05 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20Faisabilit=C3=A9/generator=20:=20les=20PARAM=C3=88TRES?= =?UTF-8?q?=20CANONIQUES=20du=20g=C3=A9n=C3=A9rateur=204=20volets=20(?= =?UTF-8?q?=C2=AB=203=20%=20=C3=A9dition=20=C2=B7=208.5=20%=20marketing=20?= =?UTF-8?q?=C2=B7=2052=20%=20point=20d'=C3=A9quilibre=20=C2=B7=20USD+DOP?= =?UTF-8?q?=20=C2=B7=20Cardnet=20=C2=B7=20Letter=20US=20=C2=BB)=20?= =?UTF-8?q?=E2=80=94=20la=20garantie=20anti-invention=20(#6)=20impos=C3=A9?= =?UTF-8?q?e=20=C3=A0=20CHAQUE=20faisabilit=C3=A9=20=E2=80=94=20=C3=A9taie?= =?UTF-8?q?nt=20RECOPI=C3=89S=20dans=20genlib/model.py::CANONICAL=20(le=20?= =?UTF-8?q?dict=20=C3=A9mis=20dans=20chaque=20rendu,=20HORS=20out/=20donc?= =?UTF-8?q?=20invisible=20=C3=A0=20check=5Fartifacts)=20ET=20dans=20la=20p?= =?UTF-8?q?rose=20README:28,=20sans=20AUCUN=20ancrage=20=C3=A0=20leur=20so?= =?UTF-8?q?urce=20faisant=20autorit=C3=A9=20:=20les=20contraintes=20NON-N?= =?UTF-8?q?=C3=89GOCIABLES=20#9/#10=20de=20CLAUDE.md.=20Preuve=20reproduit?= =?UTF-8?q?e=20:=20changer=20CLAUDE.md=20#9=20=C2=AB=20Point=20=C3=A9quili?= =?UTF-8?q?bre=2052%=20=E2=86=92=2055%=20=C2=BB=20laisse=20model.py,=20le?= =?UTF-8?q?=20test=20tests/=20ET=20le=20README=20=C3=A0=20=C2=AB=2052=20%?= =?UTF-8?q?=20=C2=BB=20=E2=80=94=20tous=20internement=20coh=C3=A9rents,=20?= =?UTF-8?q?TOUS=20contredisant=20CLAUDE.md=20=E2=86=92=20le=20g=C3=A9n?= =?UTF-8?q?=C3=A9rateur=20=C3=A9mettrait=2052=20%=20dans=20chaque=20faisab?= =?UTF-8?q?ilit=C3=A9=20alors=20que=20le=20mandat=20dit=2055=20%,=20une=20?= =?UTF-8?q?invention=20silencieuse=20de=20la=20classe=20m=C3=AAme=20que=20?= =?UTF-8?q?#6=20interdit.=20PI=C3=88GE=20:=20model.py=20n'est=20pas=20un?= =?UTF-8?q?=20out/*.json=20(check=5Fartifacts=20aveugle)=20et=20le=20SEUL?= =?UTF-8?q?=20contr=C3=B4le=20existant=20(test=5Fgenerator.py::test=5Fcano?= =?UTF-8?q?nical=5Fparams=5Falways=5Fpresent)=20assert=20que=20le=20RENDU?= =?UTF-8?q?=20contient=20=C2=AB=203=20%=20=C2=BB/=C2=AB=2052=20%=20=C2=BB?= =?UTF-8?q?=E2=80=A6=20mais=20ces=20marqueurs=20sont=20HARDCOD=C3=89S=20DA?= =?UTF-8?q?NS=20le=20test=20=E2=80=94=20une=20copie=20de=20plus,=20JAMAIS?= =?UTF-8?q?=20compar=C3=A9e=20=C3=A0=20CLAUDE.md=20=E2=86=92=20aucune=20su?= =?UTF-8?q?ite=20tests/=20(qui=20teste=20des=20FONCTIONS,=20pas=20l'ancrag?= =?UTF-8?q?e)=20n'attrape=20la=20d=C3=A9rive.=20M=C3=AAme=20esprit=20que?= =?UTF-8?q?=20le=20catalogue=20projets=20dossier=5Fvente=20et=20le=20roadm?= =?UTF-8?q?ap-anchor=20(INV11)=20:=20on=20ANCRE=20les=20consommateurs=20?= =?UTF-8?q?=C3=A0=20leur=20source.=20Nouveau=20bloc=20dans=20ci/check=5Fre?= =?UTF-8?q?adme=5Fclaims.sh=20:=20RECOMPUTE=20les=206=20valeurs=20depuis?= =?UTF-8?q?=20les=20lignes=20#9/#10=20de=20CLAUDE.md=20(z=C3=A9ro=20duplic?= =?UTF-8?q?ation=20=C2=B7=20source=20unique)=20et=20exige=20qu'elles=20?= =?UTF-8?q?=C3=A9galent=20EXACTEMENT=20(1)=20model.py::CANONICAL=20(%=20pa?= =?UTF-8?q?r=20valeur=20num=C3=A9rique=20=C2=B7=20textes=20accent/espace-i?= =?UTF-8?q?nsensibles=20via=20NFKD)=20et=20(2)=20l'=C3=A9num=C3=A9ration?= =?UTF-8?q?=20de=20la=20prose=20README=20(set-diff=20absent=20ET=20en=20tr?= =?UTF-8?q?op).=20Sanity=20CLAUDE.md=20en=20bonus=20(3=20pcts=20+=203=20va?= =?UTF-8?q?leurs=20en=20gras=20lisibles).=20Un=20claim=20absent=20=C3=A9ch?= =?UTF-8?q?oue=20AUSSI.=207=20morsures=20v=C3=A9rifi=C3=A9es=20:=20CLAUDE.?= =?UTF-8?q?md=20#9=2052=20%=E2=86=9255=20%=20(model=20+=20README=20stale?= =?UTF-8?q?=20=C2=B7=20model.py=20D=C3=89RIVE=20point=5Fequilibre)=20?= =?UTF-8?q?=C2=B7=20model.py=20marketing=208.5=20%=E2=86=929=20%=20=C2=B7?= =?UTF-8?q?=20CLAUDE.md=20#10=20Cardnet=E2=86=92Azul=20(model=20stale=20?= =?UTF-8?q?=C2=B7=20paiement)=20=C2=B7=20README=20retire=20Letter=20US=20(?= =?UTF-8?q?absents=3D[letterus])=20=C2=B7=20README=20ajoute=20token=20IBAN?= =?UTF-8?q?=20(en=20trop=3D[iban])=20=C2=B7=20=C3=A9num=C3=A9ration=20READ?= =?UTF-8?q?ME=20supprim=C3=A9e=20(INTROUVABLE)=20=C2=B7=20model.py=20USD?= =?UTF-8?q?=20+=20DOP=20=E2=86=92=20USD=20(devises)=20;=20restaur=C3=A9=20?= =?UTF-8?q?=3D=20green=20:=20CLAUDE.md=20#9/#10=20=3D=3D=20model.py=20CANO?= =?UTF-8?q?NICAL=20(6=20valeurs)=20=3D=3D=20prose=20README=20=C2=B7=20exit?= =?UTF-8?q?=200.=20=C3=89tat=20courant=20:=20aucune=20valeur=20p=C3=A9rim?= =?UTF-8?q?=C3=A9e=20(anti-invention=20#6,=20rien=20=C3=A0=20r=C3=A9=C3=A9?= =?UTF-8?q?crire)=20=E2=80=94=20le=20d=C3=A9faut=20est=20la=20surface=20un?= =?UTF-8?q?gated.=20ci/README.md=20(table=20+=20d=C3=A9tail=20=C2=AB=20par?= =?UTF-8?q?am=C3=A8tres=20canoniques=20ancr=C3=A9s=20sur=20CLAUDE.md=20#9/?= =?UTF-8?q?#10=20=C2=BB)=20mis=20=C3=A0=20jour=20=C2=B7=207=20gates=20re-v?= =?UTF-8?q?erts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-07-31.md | 62 +++++++++++++++++ ci/README.md | 23 ++++++- ci/check_readme_claims.sh | 124 ++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 6d79d5d..e4e7752 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,67 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_200130` · Buffer S8 · Domaine Faisabilité/generator : les **paramètres canoniques** du générateur 4 volets (« `3 %` édition · `8.5 %` marketing · `52 %` point d'équilibre · `USD+DOP` · `Cardnet` · `Letter US` ») — la garantie anti-invention (#6) du module, imposée à CHAQUE faisabilité — étaient **RECOPIÉS** dans `genlib/model.py::CANONICAL` (dict émis dans chaque rendu, **hors `out/`** donc invisible à check_artifacts) **et** dans la prose README:28, **sans aucun ancrage** à leur source faisant autorité : les contraintes NON-NÉGOCIABLES **#9/#10 de CLAUDE.md**. + +**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive (CLAUDE.md #6). Cette session ANCRE une +surface data-derived jamais gatée — les 6 valeurs canoniques du générateur de +faisabilité — à CLAUDE.md, comme le catalogue projets `dossier_vente` et le +roadmap-anchor (INV11). + +**Dérive silencieuse fermée** : +- Source faisant autorité : **CLAUDE.md #9** (« Frais édition doc 3% · Marketing + 8.5% · Point équilibre 52% ») + **#10** (« USD + DOP devises · Letter US format + · Cardnet paiements »). +- Consommateurs NON gatés : (a) `05_deliverables_mvp/faisabilite/generator/genlib/model.py` + `CANONICAL` — le dict émis TEL QUEL dans chaque rendu, qui n'est **pas** un + `out/*.json` → jamais vu par `check_artifacts` ; (b) `README.md:28` qui les + énumère. +- **Seul contrôle existant** : `tests/test_generator.py::test_canonical_params_always_present` + assert que le RENDU contient « `3 %` », « `52 %` »… — mais ces marqueurs sont + **hardcodés DANS le test**, une copie de plus, JAMAIS comparée à CLAUDE.md. Donc + si la contrainte #9 change (`52 %→55 %`), model.py, le test ET le README restent + `52 %` : **tous internement cohérents, TOUS contredisant CLAUDE.md** → le + générateur émettrait `52 %` dans chaque faisabilité alors que le mandat dit + `55 %` : une **invention silencieuse** de la classe même que #6 interdit, qu'aucune + suite `tests/` (qui teste des FONCTIONS, pas l'ancrage à CLAUDE.md) n'attrape. +- **État courant** : **aucune valeur périmée** — les 6 canoniques recoupent + CLAUDE.md exactement (anti-invention #6, rien à réécrire). Le défaut est la + **surface ungated**. + +**Nouveau bloc dans `ci/check_readme_claims.sh`** : RECOMPUTE les 6 valeurs depuis +les lignes #9/#10 de CLAUDE.md (zéro duplication · source unique) et exige qu'elles +égalent **exactement** (1) `model.py::CANONICAL` (% par valeur numérique · textes +accent/espace-insensibles via NFKD) et (2) l'énumération de la prose README (set-diff +absent ET en trop). Sanity CLAUDE.md en bonus (les 3 pcts + 3 valeurs en gras lisibles). +Un claim absent échoue AUSSI (traçabilité). + +### Preuve (7 morsures vérifiées + restauration verte) + +| Injection | Résultat | +|---|---| +| CLAUDE.md #9 `52 %→55 %` (model + README stale) | ✗ model.py DÉRIVE : `point_equilibre_pct='52 %'` attendu 55% (+ README) · exit 1 | +| model.py marketing `8.5 %→9 %` | ✗ model.py DÉRIVE : `marketing_pct='9 %'` attendu 8.5% · exit 1 | +| CLAUDE.md #10 `Cardnet→Azul` (model stale) | ✗ model.py DÉRIVE : `paiement='Cardnet'` attendu 'Azul' (+ README) · exit 1 | +| README retire `Letter US` | ✗ prose absents=`[letterus]` · exit 1 | +| README ajoute un token `IBAN` | ✗ prose en trop=`[iban]` · exit 1 | +| README énumération supprimée (reformulée) | ✗ « paramètres canoniques (… CLAUDE.md #9/#10) » INTROUVABLE · exit 1 | +| model.py `USD + DOP → USD` | ✗ model.py DÉRIVE : `devises='USD'` attendu 'USD + DOP' · exit 1 | +| restauré | ✓ CLAUDE.md #9/#10 == model.py CANONICAL (6 valeurs) == prose README · exit 0 | + +- **`ci/README.md`** — surface ajoutée à la table récap du pipeline + paragraphe + descriptif (« paramètres canoniques ancrés sur CLAUDE.md #9/#10 »). + +Suite CI complète re-verte (7 gates) : `guard_constraints` · `validate_json` · +`check_docs` · `check_artifacts` · `check_regression` · `check_ci_integrity` · +`check_readme_claims`. + +**Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; +édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact). + +**Auto-score 4Big** : 96/100. + +--- + ## Session `20260731_183124` · Buffer S8 · Domaine CRM/dossier_vente (2ᵉ surface) : le **catalogue de projets** du DocType porteur (« Projet (`Select` **P01..P09** ancré sur CLAUDE.md) ») était **ancré à CLAUDE.md EN PROSE** sans AUCUN gate d'identité — le bloc CRM pipeline existant ne recompute que le COMPTE d'états (« les **9 états** du pipeline ») du même README. **Tâche** : **Sprint 8 · buffer L60** (DevOps CI/CD · QA). Roadmap fonctionnellement diff --git a/ci/README.md b/ci/README.md index 720df61..f06a8b2 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -529,6 +529,27 @@ retire `Publiciste Log` (sous-ensemble) · énumération supprimée (INTROUVABLE retire du catalogue un DocType encore câblé (incohérence interne : câblé hors catalogue) · catalogue MANIFEST non trié (byte-déterminisme) ; restauré = vert. +Enfin, mêmes racines pour les **paramètres canoniques du générateur de faisabilité** +(`faisabilite/generator`) : les 6 valeurs imposées à CHAQUE faisabilité — frais d'édition +`3 %`, marketing `8.5 %`, point d'équilibre `52 %` (CLAUDE.md #9) · devises `USD+DOP`, +format `Letter US`, paiement `Cardnet` (CLAUDE.md #10) — sont la garantie anti-invention +(#6) du module. Elles vivent, RECOPIÉES, dans `genlib/model.py::CANONICAL` (le dict émis +dans chaque rendu — **hors `out/` donc invisible à check_artifacts**) et dans la prose +README:28, **sans aucun ancrage**. Le seul contrôle existant +(`test_generator.py::test_canonical_params…`) assert que le RENDU contient « `3 %` », +« `52 %` »… mais ces marqueurs sont **hardcodés dans le test** — une copie de plus, jamais +comparée à CLAUDE.md ; si la contrainte #9 change (`52 %→55 %`), model.py, le test ET le +README restent `52 %` : tous cohérents entre eux, TOUS contredisant CLAUDE.md → le +générateur émettrait `52 %` dans chaque faisabilité alors que le mandat dit `55 %`. Comme +le catalogue projets `dossier_vente` et le roadmap-anchor (INV11), on **ancre** les +consommateurs à leur source : on RECOMPUTE les 6 valeurs des lignes #9/#10 de `CLAUDE.md` +(source unique) et on exige qu'elles égalent EXACTEMENT (1) `model.py::CANONICAL` (% par +valeur numérique · textes accent/espace-insensibles) et (2) l'énumération de la prose +(set-diff : absent ET en trop). 7 morsures vérifiées : CLAUDE.md #9 `52 %→55 %` (model + +README périmés) · model.py marketing `8.5 %→9 %` · CLAUDE.md #10 `Cardnet→Azul` (model +périmé) · README retire `Letter US` (absent) · README ajoute un token `IBAN` (en trop) · +énumération README supprimée (INTROUVABLE) · model.py `USD + DOP → USD` ; restauré = vert. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 6b5f8cf..c502f3b 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2216,6 +2216,130 @@ if pc_cat is not None: bad(f"Portails catalogue · {PC_RD} — hand-off énumère {sorted(got)} MAIS " f"MANIFEST {sorted(exp)}{detail} → régénérer le README") +# ============================================================================ +# Faisabilité/generator · PARAMÈTRES CANONIQUES « ancrés sur CLAUDE.md #9/#10 » +# — les 6 valeurs imposées à CHAQUE faisabilité (jamais issues du brief). +# ---------------------------------------------------------------------------- +# Le générateur 4 volets tient sa garantie anti-invention (#6) d'un jeu de +# PARAMÈTRES CANONIQUES émis TELS QUELS pour tous les projets : frais d'édition +# 3 %, marketing 8.5 %, point d'équilibre 52 % (CLAUDE.md #9) · devises USD+DOP, +# format Letter US, paiement Cardnet (CLAUDE.md #10). Ces 6 valeurs sont la +# SOURCE FAISANT AUTORITÉ = les contraintes NON-NÉGOCIABLES #9/#10 de CLAUDE.md. +# Elles sont RECOPIÉES à deux endroits, tous deux NON gatés : +# (a) genlib/model.py `CANONICAL` — le dict que le générateur émet dans CHAQUE +# rendu (n'est PAS un out/*.json → invisible à check_artifacts) ; +# (b) README:28 « 3 % édition · 8.5 % marketing · 52 % point d'équilibre · +# USD+DOP · Cardnet · Letter US, CLAUDE.md #9/#10 » — la prose du module. +# Le SEUL contrôle existant (tests/test_generator.py::test_canonical_params…) +# assert que le RENDU contient les marqueurs « 3 % », « 52 % »… — mais ces +# marqueurs sont HARDCODÉS DANS LE TEST, une copie de plus, JAMAIS comparée à +# CLAUDE.md. Donc si la contrainte #9 change (ex. point d'équilibre 52 %→55 %), +# model.py, le test ET le README restent « 52 % » : tous internement cohérents, +# TOUS contredisant CLAUDE.md — le générateur émettrait alors 52 % dans chaque +# faisabilité alors que le mandat dit 55 % : une invention silencieuse de la +# classe même que #6 interdit, qu'aucune suite tests/ (qui teste des FONCTIONS, +# pas l'ancrage à CLAUDE.md) n'attrape. Même esprit que le roadmap-anchor (INV11) +# et le catalogue projets dossier_vente : on ANCRE les consommateurs à CLAUDE.md. +# On RECOMPUTE les 6 valeurs depuis les lignes #9/#10 de CLAUDE.md (zéro +# duplication · source unique) et on exige que (1) model.py `CANONICAL` les +# égale EXACTEMENT (% par valeur numérique · textes accent/espace-insensibles) +# et (2) la prose README les énumère EXACTEMENT (set-diff : absent ET en trop). +# Un claim absent échoue AUSSI (traçabilité). +# ============================================================================ +import ast +def _ai(s): # accent-insensible, minuscule, sans espaces (comparaison de tokens) + return "".join(c for c in unicodedata.normalize("NFKD", s) + if unicodedata.category(c) != "Mn").lower().replace(" ", "") +def _pnorm(v): # 3.0 → "3%" · 8.5 → "8.5%" (format déterministe) + return f"{'%g' % v}%" +def _pct(s): # "3 %"/"52%"/… → 3.0/52.0 + mm = re.search(r"(\d+(?:\.\d+)?)\s*%", s) + return float(mm.group(1)) if mm else None +try: + cm_raw2 = open("CLAUDE.md", encoding="utf-8").read() + l9 = re.search(r"^9\.\s+(.+)$", cm_raw2, re.M) + l10 = re.search(r"^10\.\s+(.+)$", cm_raw2, re.M) + if not l9 or not l10: + raise ValueError("contrainte #9 ou #10 INTROUVABLE") + # #9 : trois pourcentages, associés par mot-clé (accent-insensible) + seg9 = re.split(r"·", l9.group(1)) + def _pct_kw(kw): + for s in seg9: + if kw in _ai(s): + return _pct(s) + return None + exp = { + "frais_edition_pct": _pct_kw("frais"), + "marketing_pct": _pct_kw("marketing"), + "point_equilibre_pct": _pct_kw("equilibre"), + } + # #10 : trois valeurs en gras, associées par leur rôle (devises/format/paiement) + seg10 = re.split(r"·", l10.group(1)) + def _bold_kw(kw): + for s in seg10: + if kw in _ai(s): + mm = re.search(r"\*\*(.+?)\*\*", s) + return mm.group(1).strip() if mm else None + return None + exp["devises"] = _bold_kw("devise") + exp["format_doc"] = _bold_kw("format") + exp["paiement"] = _bold_kw("paiement") + if None in exp.values(): + raise ValueError(f"valeurs canoniques illisibles dans #9/#10 : {exp}") +except (OSError, ValueError) as e: + bad(f"Faisabilité canoniques · CLAUDE.md #9/#10 illisible/inattendu : {e}"); exp = None +if exp is not None: + good(f"Faisabilité canoniques · CLAUDE.md #9/#10 sain : {_pnorm(exp['frais_edition_pct'])} " + f"édition · {_pnorm(exp['marketing_pct'])} mkt · {_pnorm(exp['point_equilibre_pct'])} " + f"équilibre · {exp['devises']} · {exp['format_doc']} · {exp['paiement']}") + _num_keys = ("frais_edition_pct", "marketing_pct", "point_equilibre_pct") + _txt_keys = ("devises", "format_doc", "paiement") + # 1) ANCRAGE : genlib/model.py CANONICAL == CLAUDE.md #9/#10 + MP = f"{D}/faisabilite/generator/genlib/model.py" + try: + msrc = open(MP, encoding="utf-8").read() + mm = re.search(r"CANONICAL\s*=\s*(\{.*?\})", msrc, re.S) + canon = ast.literal_eval(mm.group(1)) + except (OSError, ValueError, SyntaxError, AttributeError) as e: + bad(f"Faisabilité canoniques · {MP} CANONICAL illisible/inattendu : {e}"); canon = None + if canon is not None: + drift = [] + for k in _num_keys: + g = _pct(str(canon.get(k, ""))) + if g is None or g != exp[k]: + drift.append(f"{k}={canon.get(k)!r} (attendu {_pnorm(exp[k])})") + for k in _txt_keys: + if _ai(str(canon.get(k, ""))) != _ai(exp[k]): + drift.append(f"{k}={canon.get(k)!r} (attendu {exp[k]!r})") + if drift: + bad(f"Faisabilité canoniques · model.py CANONICAL DÉRIVE de CLAUDE.md #9/#10 : " + f"{' · '.join(drift)} → aligner model.py (ou CLAUDE.md)") + else: + good("Faisabilité canoniques · model.py CANONICAL == CLAUDE.md #9/#10 (6 valeurs)") + # 2) PROSE : README:28 énumère les 6 valeurs EXACTEMENT (absent ET en trop) + FG_RD = f"{D}/faisabilite/generator/README.md" + fg_rd = read_norm(FG_RD) + if fg_rd is not None: + mr = re.search(r"param[eè]tres canoniques\*\* \((.+?), CLAUDE\.md", fg_rd) + if mr is None: + bad(f"Faisabilité canoniques · {FG_RD} — énumération « paramètres canoniques " + f"(… CLAUDE.md #9/#10) » INTROUVABLE") + else: + got = set() + for tok in re.split(r"·", mr.group(1)): + p = _pct(tok) + got.add(_pnorm(p) if p is not None else _ai(tok)) + want = {_pnorm(exp[k]) for k in _num_keys} | {_ai(exp[k]) for k in _txt_keys} + if got == want: + good(f"Faisabilité canoniques · {FG_RD} — prose énumère les 6 valeurs == " + f"CLAUDE.md #9/#10") + else: + miss = sorted(want - got); extra = sorted(got - want) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"Faisabilité canoniques · {FG_RD} — prose énumère {sorted(got)} MAIS " + f"CLAUDE.md {sorted(want)}{detail} → régénérer le README") + sys.exit(1 if FAIL else 0) PY rc=$?