Files
oto-enterprise-os-dtp/05_activity_log/2026-08-02.md
T

59 KiB
Raw Blame History

Activity log · 2026-08-02 · Claude Code DTP Worker

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 chiffres »). La classe « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source des marqueurs numérotés de la fonction _validate, jamais une liste à la main) avait été déclarée close à 8 générateurs — mais sur le SEUL dialecte # N · / « N invariants » / _validate(_validate_bundle). La mémoire invariant-ledger-count-gate consigne précisément le risque : « un prior CLOSED après 7 était FAUX — un style de marqueur différent avait glissé ; re-scanner LES DEUX styles avant de croire la clôture ». Un re-scan systématique a exhumé une DEUXIÈME famille entièrement HORS gate : marqueurs # INVn · (préfixe INV) dans validate_bundle, phrasé « N **familles d'**invariants ». DEUX générateurs la portent :

  • devops/deploy_runbook/deploy_runbook_gen.py — ledger # INV1..INV11 dans validate_bundle, compte « 11 familles d'invariants » recopié à 3 surfaces vives : générateur l.272 (message CLI de succès) · README l.73 (bloc de commande) · suite tests l.4 (docstring) ;
  • qa/acceptance/acceptance_gen.py — ledger # INV1..INV11 dans validate_bundle, compte à 2 surfaces vives : README l.46 « 11 invariants » · suite tests l.4 « 11 familles d'invariants ». (Le .py acceptance ne porte AUCUN compte numéroté → surface non exigée.)

Le « vert trompeur » — ajouter un contrôle # INV12 · à validate_bundle sans toucher ces chaînes ⇒ README + self-reports + docstrings de test mentent en silence pendant que la CI applique 12 contrôles. Ni check_artifacts (le .py source n'est pas un out/*.json) ni les suites tests/ (FONCTIONS de validation, jamais la prose) n'attrapent la dérive. Prouvé (M4 SILENT-GREEN) : injecter # INV12 · dans validate_bundle (ledger→12, toute la prose reste 11) ⇒ les 3 surfaces deploy_runbook mordent « [11] MAIS ledger en compte 12 » — preuve que l'autorité est le CODE-registre, pas la cohérence interne de la prose.

Particularités — Ledger PROPRE 1..N compté À PART du schéma (« schéma + N invariants »). Les mentions NON numérotées de « invariants » (docstrings « invariants de déploiement/recette », README « Un invariant refuse … ») ne sont pas ancrées par un chiffre → aucun faux positif. Le compte-regex accepte les DEUX formes (« N invariants » ET « N familles d'invariants ») via un préfixe optionnel (?:familles d')?. Les daily_reports/activity_log (snapshots datés — ex. acceptance « 10 » d'avant la croissance 10→11) ne sont PAS des surfaces vives et ne sont volontairement pas ancrés (aucune dérive à surveiller sur un log figé).

Gate ajouté (ci/check_readme_claims.sh, bloc « DevOps deploy_runbook + QA acceptance · IDENTITÉ du COMPTE d'INVARIANTS de validate_bundle », juste après le bloc Mobile) — paramétré sur les 2 modules (zéro duplication). RECOMPUTE le ledger depuis le SEUL registre-source (# INVn · de validate_bundle, corps extrait jusqu'au def suivant) et exige : (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; (b) CHAQUE surface déclarée porte ≥1 mention « N [familles d']invariants » toutes == |ledger|. Une surface sans mention = self-report ÉVAPORÉ (échec · anti-évaporation #6).

Vérif — 7 checks verts sur l'arbre propre (2 ledgers 1..11 + 5 surfaces). 8 morsures adversariales, gate lancé SEUL, reverts par cp ciblé (jamais git checkout large — cf. incident consigné) : M1 deploy .py 11→12 (b générateur) · M2 deploy README 11→12 (b) · M3 deploy test 11→12 (b) · M4 SILENT-GREEN # INV12 · injecté ⇒ 3 surfaces deploy mordent (autorité = CODE) · M5 # INV7 · supprimé ⇒ ledger non contigu (a) + 3 surfaces désync · M6 acceptance README 11→12 (b) · M7 acceptance test 11→12 (b) · M8 compte retiré du README acceptance ⇒ self-report ÉVAPORÉ. Arbre restauré (git status propre). Suites unittest re-vertes : deploy_runbook 29 tests OK · acceptance 37 tests OK (générateurs inchangés). Suite complète re-verte : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0. Les DEUX familles de ledger sont désormais gatées ; re-scanner tout NOUVEAU dialecte de marqueur (préfixe/phrasé) avant de re-déclarer la classe close.

Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'encadré « Défense en profondeur » (NOM du rôle détenteur de set_user_permissions · scope · famille) sur l'artefact byte-gaté

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention »). Le §2 « Modèle RBAC » de 05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md ferme sur un encadré de sécurité « Défense en profondeur » : le pouvoir set_user_permissions (élévation de privilège) est « réservé au SEUL rôle OTO Plateforme RBAC Admin (scope_donnees = groupe, famille = plateforme) — séparation des pouvoirs vérifiée par test automatisé ». La SPEC se réclame donc d'un fait de sécurité tiré du contrat rbac_50_roles.json, sur trois attributs : NOM du détenteur · scope · famille. Or le test cité (test_rbac::test_set_user_permissions_reserve_admin) impose bien scope==groupe ET famille==plateforme à TOUT détenteur — MAIS jamais le NOM du rôle (OTO Plateforme RBAC Admin) NI cette prose §2. L'UNICITÉ (« au SEUL rôle ») et le NOM étaient DÉJÀ gatés côté artefact (bloc « RBAC fixtures », singleton len(fx_sup)==1 depuis custom_docperm.json byte-gaté + prose du README fixtures « émis que pour le rôle NAME ») — mais PAS la 3ᵉ surface prose, le §2 de la SPEC.

Le « vert trompeur » — RENOMMER le détenteur dans le contrat (+régénérer fixtures/out + audit 4Big) laisse le §2 citer un rôle DISPARU en silence : check_artifacts ne voit qu'un HASH de TAILLE du doc (agnostique au nom · la SPEC n'est pas un out/*.json), les 10 tests restent verts (scope/famille inchangés). Prouvé (M6 SILENT-GREEN) : renommer …RBAC Admin…Securite Admin dans le contrat + régénérer les 4 générateurs RBAC & l'audit ⇒ check_artifacts VERT pendant que le nouveau gate mord « §2 nomme ['OTO Plateforme RBAC Admin'] MAIS set_user_permissions réservé à 'OTO Plateforme Securite Admin' » — preuve que l'autorité est l'ARTEFACT byte-gaté, pas la cohérence interne de la SPEC.

Gate ajouté (ci/check_readme_claims.sh, extension du bloc « RBAC fixtures », points 4 & 5 — pas un bloc neuf) — RÉUTILISE fx_sup (le singleton déjà byte-gaté ci-dessus · zéro re-dérivation de l'unicité, interdit #5 « jamais de doublon ») comme NOM-autorité et src_roles (contrat byte-gaté) pour scope/famille. Capte l'encadré **Défense en profondeur** … vérifiée par test automatisé (spec_rd whitespace-normalisé) et exige : le pouvoir set_user_permissions y est nommé ; (a) le NOM du détenteur cité (accents/casse neutralisés) == le seul porteur du flag ; (b)/(c) les scope_donnees = … / famille = … cités == attributs du détenteur dans rbac_50_roles.json ; (5) §6 (row « Séparation des pouvoirs ») cite bien set_user_permissions réservé (anti-évaporation). Encadré/row INTROUVABLE = échec (#6). La glose « (gestion des habilitations) » N'est PAS gatée (éditoriale).

Note process (interdit #5) — un premier jet ajoutait un bloc §2 autonome RECALCULANT le singleton depuis src_roles : doublon de la vérification d'unicité déjà portée par le bloc fixtures (via custom_docperm.json byte-gaté, équivalent par check_artifacts). Écarté ; les 4 checks réécrits en extension du bloc existant, sans ré-affirmer l'invariant d'unicité.

Vérif — 4 checks verts sur l'arbre propre. 6 morsures adversariales, gate lancé seul, reverts par cp/tar ciblés (jamais git checkout large — cf. incident consigné) : M1 §2 renomme le détenteur (prose) ⇒ NOM ≠ artefact · M2 §2 scope groupeentite ⇒ (b) ≠ détenteur · M3 §2 famille plateformeventes ⇒ (c) ≠ détenteur · M4 encadré supprimé ⇒ INTROUVABLE · M5 §6 retire set_user_permissions ⇒ ancre évaporée · M6 SILENT-GREEN (ci-dessus) contrat renommé + artefacts régénérés, check_artifacts VERT ⇒ §2 (a) mord. Suite complète re-verte : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.

Sprint 8 · buffer · Traçabilité roadmap — ancrage de la CITATION roadmap_ref VERBATIM (n° ligne + libellé) des artefacts sur la ligne RÉELLE de la roadmap

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention · toujours vérifier sources archives »). Le pattern « citation vérifiée, pas déclarée » (INV11 de qa/acceptance ancre les roadmap_line NUMÉRIQUES de son artefact au bullet DELIVERABLE réel) portait un angle mort : quatre modules — demo/scenarios, frontend/chat_otoia, frontend/portails, mobile/app_config — publient dans leur out/MANIFEST.json (byte-gaté) un champ roadmap_ref de forme « …ROADMAP… · Sprint N l.A[-B] « <libellé bullet> » », où le libellé est une transcription VERBATIM du bullet de la roadmap à la ligne citée. Or aucun gate ne vérifiait que la ligne A[-B] porte réellement ce texte : check_artifacts ne prouve QUE spec==MANIFEST byte-for-byte (la MÊME citation recopiée des deux côtés, jamais contredite) ; INV11 ne touche QUE les roadmap_line numériques de SON artefact ; l'ancrage Expo mobile ne teste que « roadmap » + « Expo N » dans expo_sdk_source, pas la citation complète.

Le « vert trompeur » — RÉORDONNER la roadmap (les n° de ligne glissent) ou REFORMULER un bullet laisse ces 4 citations pointer une ligne qui dit AUTRE CHOSE : un agent lisant le MANIFEST pour retrouver la promesse d'origine tomberait sur le mauvais bullet. Prouvé par mutation : reformuler le bullet l.63 (Chat OTOIA embedded…) laissait les 6 gates VERTS pendant que chat_otoia/out/MANIFEST.json citait encore « … Chat OTOIA embedded … » à l.63. Aucune suite tests/ (FONCTIONS de résolution, jamais l'ancre à la roadmap) ne mord.

Gate ajouté (ci/check_readme_claims.sh, bloc « Traçabilité roadmap », juste avant sys.exit) — DÉCOUVRE tous les out/MANIFEST.json (glob 1 & 2 niveaux) portant un roadmap_ref citant (l.A[-B] «…») et exige, par module : (1) le fichier cité == la roadmap canonique ; (2) le libellé, NORMALISÉ (markdown **/- retirés · espaces compressés · NFC), est SOUS-CHAÎNE des lignes A..B réelles (robuste à la troncature du bullet et à la plage multi-lignes l.56-57) ; (3) le spec du module porte le MÊME roadmap_ref (identité spec↔artefact publié). Bornes de ligne hors-plage attrapées (renumérotation). Les 4 modules connus sont EXIGÉS présents (anti-évaporation #6 : retirer un roadmap_ref échoue) ; tout AUTRE MANIFEST à citation est ancré en bonus (future-proof).

Vérif — 5 checks verts sur l'arbre propre (4 citations ancrées + recensement « 4 modules attendus présents »). 7 morsures adversariales, gate lancé SEUL, reverts par cp ciblé (jamais git checkout large — cf. incident consigné) : M1 SILENT-GREEN — reformuler le bullet l.63 (AUTORITÉ), MANIFEST inchangé ⇒ libellé ABSENT (preuve que l'autorité est la ROADMAP, pas la cohérence interne de l'artefact) · M2 renuméroter le MANIFEST l.63→l.62 (ligne existante mais autre bullet) ⇒ ABSENT · M3 dériver le libellé du MANIFEST lui-même ⇒ ABSENT · M4 citer un autre fichier roadmap ⇒ fichier ≠ canonique · M5 dériver la citation du SEUL spec ⇒ spec ≠ MANIFEST (artefact incohérent) · M6 retirer roadmap_ref du MANIFEST ⇒ ÉVAPORÉE ['frontend/chat_otoia'] · M7 citer l.9999 ⇒ plage hors roadmap (1..92). Suite complète re-verte : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.

Sprint 8 · buffer · RBAC SPEC §6 — ancrage de l'ÉNUMÉRATION des DocTypes custom sur le catalogue du contrat (2ᵉ surface prose)

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention »). Le générateur rbac_fixtures_gen émet MANIFEST.custom_doctypes_a_confirmer = l'ensemble des DocTypes custom: true du contrat rbac_50_roles.json (les DocTypes DTP à créer côté VPS avant import — Faisabilité, Publiciste Log, CONFOTUR Application, API Access). Cet ensemble data-derived est transcrit À LA MAIN à deux surfaces prose : (1) rbac/fixtures_gen/README.mddéjà gatée (bloc « RBAC fixtures · catalogue custom ») ; (2) RBAC_50_ROLES_SPEC.md §6 l.117 (« DocTypes custom: true (Faisabilité, Publiciste Log, CONFOTUR Application, API Access) ») — NON gatée. Le bloc apply_plan ne gate que le COMPTE (len(custom_doctypes)), aveugle aux NOMS ; check_artifacts est aveugle (la SPEC n'est pas un out/*.json).

Le « vert trompeur » — AJOUTER un DocType custom au contrat (le MANIFEST rebâtit l'entrée) ou RENOMMER/ÉCHANGER l'un des 4 laisse §6 à ses 4 noms périmés en silence : la cible d'application VPS énumérée par la SPEC ne correspond plus aux DocTypes que l'agent ERPNext doit réellement créer. Vérifié par mutation : dropper Publiciste Log de §6 laissait le gate à exit 0. Même patron que la 2ᵉ surface CONFOTUR/entités (README gaté, fiche non).

Gate ajouté (ci/check_readme_claims.sh, extension du bloc « RBAC fixtures », point 3) — RÉUTILISE fx_cdt (= MANIFEST.custom_doctypes_a_confirmer, byte-gaté, déjà dérivé par le point 1) et le normaliseur d'accents _fx_acc (zéro duplication du contrat) : capte l'énumération §6 (DocTypes custom: true** (…) sur le texte whitespace-normalisé, robuste au retour-ligne 117↔118), extrait les tokens backtickés, et exige set-diff EXACT == catalogue MANIFEST (absent ET en trop, casse/ accents neutralisés). Énumération INTROUVABLE = échec (anti-évaporation #6).

Vérif — 1 check vert sur l'arbre propre (les 4 == MANIFEST). 5 morsures adversariales : A dropper Publiciste Log ⇒ absent · B renommer API AccessAPI Acces ⇒ absent+en-trop · C ajouter Ghost DocType fantôme ⇒ en-trop · D SILENT-GREEN : le MANIFEST GAGNE un 5ᵉ DocType custom (§6 inchangé) ⇒ mord « absents= [new custom dt] » — preuve que l'autorité est l'ARTEFACT, pas la cohérence interne de la SPEC · E énumération §6 supprimée ⇒ INTROUVABLE. Reverts par chemin ciblé (jamais git checkout large — cf. incident consigné). Suite complète re-verte : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.

Note process — un candidat initial (ancrage des attributs de rôle des fiches mobile/rendu/ifc_speckle) s'est révélé DÉJÀ couvert par les blocs existants « Fiche Mobile » / « Fiches Faisabilité » : la ligne d'index MEMORY.md était périmée vs le corps détaillé de la mémoire (classe close le 2026-08-01). Doublon écarté (interdit #5 « jamais accumuler doublons »), index mémoire corrigé.


Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'ÉNUMÉRATION des scope_donnees (ligne Row-level) sur l'ENUM du schéma

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La table §2 « Modèle RBAC » de 05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md ouvre par « trois niveaux, tous couverts par le schéma » puis sa 3ᵉ ligne (« Row-level (User Permission) | scope_donnees | … ») ÉNUMÈRE à la main l'ensemble EXHAUSTIF des portées row-level : own · equipe · entite (Company) · groupe (consolidé). Par sa phrase d'intro, cet ensemble EST l'enum scope_donnees de rbac.schema.json (la DÉFINITION du modèle). Or aucun gate ne le touchait : §3 compte les rôles par portail, §4 les entités, §5 le résumé, et le bloc userperm gate la table scope → mécanisme d'un autre artefact (userperm_gen/out) — tous aveugles à l'énumération du §2. Même classe que §4 (colonne ancrée sur une source-de-vérité structurée), côté schéma cette fois.

Le « vert trompeur » — si le schéma AJOUTE une portée (region pour un besoin ops réel) ou en RETIRE une (own supprimé, schéma resserré), le §2 reste PÉRIMÉ en silence : il documente un modèle qui CONTREDIT le schéma tout en se réclamant « couvert par le schéma ». Ni check_artifacts (la SPEC n'est pas un out/*.json) ni tests/ (FONCTIONS de validation, jamais l'ancre SPEC↔schéma) ne mordent.

Gate ajouté (ci/check_readme_claims.sh, bloc « 1octies », juste après le §5) — charge l'enum scope_donnees depuis rbac.schema.json (source de vérité du modèle) puis exige : (a) ENSEMBLE des jetons scope backtickés de la ligne §2 == enum (set-diff absent ET fantôme) ; (b) ANCRAGE — la colonne du milieu NOMME bien scope_donnees (sinon on gate la mauvaise ligne) ; (c) sanity — toute portée RÉELLEMENT exercée par le contrat ∈ enum (une portée en usage jamais dans l'enum = schéma/contrat incohérents). Les gloses parenthétiques (Company)/ (consolidé) NE sont PAS gatées (éditoriales · #6). Ligne/section/enum absents = échec (traçabilité #6).

Vérif — 3 checks verts sur l'arbre propre (4 portées own/equipe/entite/groupe == enum). 6 morsures adversariales : M1 §2 retire own ⇒ manquant (a) · M2 §2 ajoute fantôme region ⇒ fantôme (a) · M3 SILENT-GREEN : le schéma GAGNE region, §2 inchangé ⇒ (a) mord manquant region — preuve que l'autorité est le SCHÉMA, pas la cohérence interne de la SPEC · M4 colonne du milieu renommée scope_donneesscope ⇒ ancre perdue (b) · M5 ligne Row-level supprimée ⇒ INTROUVABLE · M6 le schéma RETIRE own (encore exercé par 2 rôles) ⇒ (a) fantôme + (c) portée hors enum. Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Sprint 8 · buffer · Faisabilité — ancrage du DOMAINE DES CODES PROJET (garde d'entrée _validate_brief + 2 schémas JSON) sur CLAUDE.md §Projets

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La classe « constante constitutionnelle recopiée dans une GARDE DE VALIDATION en code, jamais ancrée » (déjà couverte pour le CONTRAT DE FORMAT e-NCF de ncf.py) portait un candidat resté NON gaté et paradoxalement au cœur du Livrable Sprint 3 (« P07 Aqua Terra <1h ») : le DOMAINE des codes projet valides du générateur 4 volets. Ce domaine EST exactement l'ensemble CLAUDE.md §Projets (P01..P09), recopié À LA MAIN à QUATRE surfaces, aucune ancrée, toutes invisibles à check_artifacts (ni .py ni schémas ne sont des out/*.json) :

  1. faisabilite/generator/faisabilite_gen.py::_validate_brief — la GARDE runtime code[:2] == "P0" and code[2] in "123456789" (le domaine EFFECTIF) + le message « … invalide (attendu P01..P09) » (les bornes) ;
  2. faisabilite/version.schema.jsonproperties.projet.pattern "^P0[1-9]$" (validation du version.json par projet) + sa description ;
  3. faisabilite/generator/brief.schema.json — même pattern "^P0[1-9]$" (validation du brief d'entrée) + sa description (qui n'en avait aucune).

Le « vert trompeur » — si CLAUDE.md §Projets AJOUTE un 10ᵉ projet (P10 …, évènement de portefeuille réel), la garde ET les deux patterns REJETTENT P10 comme « invalide (attendu P01..P09) » : le générateur REFUSE de scaffolder un projet LÉGITIMEMENT mandaté (bug d'exécution) pendant que la suite tests/ reste VERTE (oracles P01/P02 self-consistants, jamais comparés à §Projets). Symétriquement, RETIRER un projet laisse la garde trop PERMISSIVE (accepte un code démobilisé). Ni check_artifacts (schémas/.pyout/*.json) ni tests/ (teste la FONCTION _validate_brief, pas son ANCRAGE à §Projets) ne mordent.

Gate ajouté (ci/check_readme_claims.sh, bloc « Faisabilité/generator · DOMAINE DES CODES PROJET », juste après « Fiches #10 ») — RE-DÉRIVE le catalogue de codes depuis CLAUDE.md §Projets (\bP\d{2,}\b sur la ligne ## Projets · source unique) puis exige que chaque surface accepte EXACTEMENT cet ensemble : (a) la garde _validate_brief — reconstruit l'ensemble ACCEPTÉ {préfixe+chiffre} depuis le code source et set-diff == §Projets ; le message cite les bornes min..max ; (b)/(c) les 2 patterns JSON — test COMPORTEMENTAL (compile le pattern, l'applique à une sonde §Projets {P00,P10,P99,P0,PXX,P1,P010,Q01}, exige accepté == §Projets · robuste à la forme de la classe de caractères) + la description DÉCLARE l'ancrage « CLAUDE.md §Projets » et cite les bornes. Une surface INTROUVABLE échoue AUSSI (ancre morte · anti-évaporation #6). Deux descriptions de schéma enrichies pour porter l'ancrage (brief.schema en gagne une).

Vérif — 6 checks verts sur l'arbre propre. 6 morsures adversariales : M1 garde retire le chiffre 7 (validateur trop restrictif) ⇒ set ≠ · M2 message P01..P09→P01..P08 ⇒ bornes ≠ · M3 pattern version élargi ^P0[0-9]$ ⇒ accepte P00 en trop · M4 brief.schema perd l'ancrage §Projets ⇒ ne DÉCLARE plus · M5 SILENT-GREEN : CLAUDE.md §Projets ajoute P10 SEUL (les 4 surfaces inchangées) ⇒ CASCADE sur les 6 lignes (garde accepte≠, 2 patterns manquent P10, message +2 descriptions bornes P09≠P10) — preuve que l'autorité est §Projets, pas la cohérence interne · M6 version.schema perd les bornes de sa description. Les 16 tests unittest du générateur restent verts (garde inchangée). Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Sprint 8 · buffer · RBAC SPEC §3 — ancrage de la colonne « Familles rattachées » PAR portail sur le contrat

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La table §3 « Cartographie portails ↔ familles » de 05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md porte DEUX colonnes data-derived du contrat rbac_50_roles.json : « Nb rôles » (déjà gatée, bloc §3 count) et — juste à sa gauche — « Familles rattachées » : pour chaque portail, l'ENSEMBLE des famille distinctes de ses rôles (ex. direction → {direction, finance, faisabilite, legal}). Cette 2ᵉ colonne était NON gatée : le bloc §3 count la capture avec [^|]* (il ne lit QUE le compte), le §5 « résumé » traite volontairement les regroupements comme ÉDITORIAUX (pas de mapping famille↔groupe, cf. memory rbac-spec-portail-count-gate), et check_artifacts est aveugle (la SPEC n'est pas un out/*.json). Même classe que « colonne SET gatée seulement sur son COUNT voisin » (roleprofile per-profil count vs quels rôles).

Le « vert trompeur » — reclasser la famille d'un rôle À L'INTÉRIEUR d'un portail change le SET de familles sans changer le « Nb rôles » du portail → la cellule §3 ment en silence pendant que le count-gate reste VERT. Prouvé (M4) : reclasser les DEUX rôles legal de direction (Directeur Juridique + Agent ONAPI) en finance laisse le compte à 9==9 VERT tandis que le nouveau gate mord FANTÔME {legal} (la SPEC liste encore legal, le contrat ne l'a plus). Ajouter/retirer une famille (rôle d'une famille inédite) laisse aussi la cellule périmée. Aucune suite tests/ (FONCTIONS de validation, pas l'ancre SPEC↔contrat) ne mord.

Gate ajouté (ci/check_readme_claims.sh, bloc « 1quater-bis » juste après le §3 count-gate, RÉUTILISE src_roles/spec_rd déjà chargés) — RECOMPUTE le SET de familles distinctes PAR portail depuis rbac_50_roles.json[roles][].famille et exige, par ENSEMBLE (absent ET fantôme), l'égalité avec les jetons famille de la cellule. On ne gate QUE les jetons famille bruts (identifiant de tête de chaque item séparé par ,, via (?:^|,)\s*([a-z]+)), PAS les parenthèses ÉDITORIALES (« finance (CFO) », « faisabilite (analyste/rendu/IFC/économiste) » — paraphrase, pas contrat, #6). Ordre NON gaté (réordonner les rôles dans le contrat byte-gaté est sémantiquement neutre pour l'appartenance). Un portail sans cellule échoue AUSSI (INTROUVABLE = régression de traçabilité #6).

Vérif — 6 checks verts sur l'arbre propre (un par portail). 5 morsures adversariales : M1 SPEC retire legal de direction ⇒ MANQUE {legal} · M2 SPEC ajoute rh à achat ⇒ FANTÔME {rh} · M3 SPEC financefinanc sur compta ⇒ MANQUE {finance} · FANTÔME {financ} · M4 SILENT-GREEN : contrat reclasse les 2 rôles legalfinance dans direction, compte inchangé (count-gate 9==9 VERT) ⇒ familles-gate mord FANTÔME {legal} — preuve que l'autorité est le CONTRAT, pas la cohérence interne de la SPEC · M5 SPEC supprime la ligne direction ⇒ cellule INTROUVABLE. Gates re-verts : check_artifacts · check_ci_integrity · check_docs · check_readme_claims · check_regression · guard_constraints tous exit 0.


Sprint 8 · buffer · Faisabilité — ancrage du TEMPLATE CANONIQUE v1.0 (5 surfaces #9/#10) sur CLAUDE.md

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « constante canonique CLAUDE.md recopiée à la main dans une surface data-derived » (re-dérive la valeur depuis la contrainte, jamais la cohérence interne) couvre déjà le générateur (model.py/README/renderer/scorer/ oracle-test), les fiches ERPNext/CRM (« Fiches #10 »), le module fiscal (moneda), la marque SEO §Entités, la persona Chat OTOIA, les tokens branding #4, etc. Un fichier restait NON gaté (grep : absent de tous les ci/*.sh) et c'est paradoxalement le plus normatif : 05_deliverables_mvp/faisabilite/ TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md, le DOCUMENT-MODÈLE « CANONIQUE » (Livrable Sprint 1) que toute faisabilité en production DOIT reproduire, et que le générateur 4 volets + le Publiciste + la régénération batch S4 sont censés suivre.

Ce template recopie à la main les 6 valeurs de CLAUDE.md #9/#10 à CINQ surfaces structurées, toutes se réclamant de « contrainte #9 »/« #10 », aucune gatée :

  1. l.28 intro §0 « frais 3 % · marketing 8.5 % · point d'équilibre 52 % » (3 pourcentages #9, associés par mot-clé) ;
  2. l.144-146 §5.2 — 3 lignes de table « N % (fixe · contrainte #9) » ;
  3. l.29 intro §0 « USD + DOP · format Letter US · paiements Cardnet » ;
  4. l.149-151 §5.2 — 3 lignes « valeur (… contrainte #10) » (la ligne paiement citant l'exclusion « pas-X ») ;
  5. l.196 §7 checklist « 3 % / 8.5 % / 52 % · USD+DOP · Letter US · Cardnet présents » (énumération des 6 marqueurs).

Le « vert trompeur » — si CLAUDE.md #9 passe 52 %→55 % (ou #10 renomme le prestataire) et que les modules s'alignent, ce TEMPLATE — la RÉFÉRENCE de toutes les faisabilités — reste PÉRIMÉ en silence, imposant à chaque projet un paramètre qui CONTREDIT le mandat pendant qu'il se réclame « contrainte #9/#10 ». Ni check_artifacts (ce n'est pas un out/*.json) ni check_docs/guard_constraints ni aucune suite tests/ (FONCTIONS, jamais l'ancre à CLAUDE.md) ne mordent.

Gate ajouté (ci/check_readme_claims.sh, bloc « TEMPLATE FAISABILITÉ CANONIQUE v1.0 · PARAMÈTRES #9/#10 », juste après « Fiches #10 ») — RÉUTILISE exp (les 6 valeurs déjà recomputées des lignes #9/#10 de CLAUDE.md · zéro re-dérivation) + exp_excl (prestataire exclu du (pas X)). Exige que chacune des 5 surfaces == CLAUDE.md : (1) 3 pcts associés par mot-clé ; (2) SET des 3 lignes « (fixe · contrainte #9) » == {3 %,8.5 %,52 %} (mord aussi ligne ajoutée/retirée) ; (3)/(4) chaque valeur #10 présente en gras dans sa région ancrée + l'exclusion ; (5) énumération des 6 marqueurs == ensemble EXACT (set-diff absent ET en trop). Une surface INTROUVABLE échoue AUSSI (recensement évaporé = régression #6).

Vérif — 5 checks verts sur l'arbre propre. 11 morsures adversariales : M1 intro#9 52→55 · M2 ligne#9 8.5→9 · M3 intro#10 Cardnet→Azul · M4 ligne#10 Letter US→A4 · M5 exclusion Stripe→PayPal · M6 checklist perd Cardnet · M7 checklist 52→60 · M8 intro#9 supprimée (INTROUVABLE) · M9 checklist « présents »→ « présent » (INTROUVABLE) · M10 CLAUDE.md #9 52→55 SEUL ⇒ cascade sur 3 régions (1)(2)(5) · M11 CLAUDE.md #10 Cardnet→Azul SEUL ⇒ cascade sur 3 régions (3)(4)(5) — les deux preuves que l'AUTORITÉ est CLAUDE.md, pas la cohérence interne du template. Un piège corrigé en route : le groupe capturé est le NOMBRE nu (sans %), donc _pct() (qui exige un %) renvoyait Nonefloat() direct. Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Sprint 8 · buffer · Fiscal e-CF — ancrage du CONTRAT DE FORMAT e-NCF dans le COMPOSEUR ncf.py (CODE) + son ORACLE de test

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le bloc « Fiscal · e-CF DGII · CONTRAT DE FORMAT » existant ancre le format de l'identifiant fiscal DGII (E + tipoeCF(2) + secuencia(10) = 13, Ley 32-23) sur le SEUL bloc e_ncf byte-gaté de out/ecf_plan.json (dérivé de ecf_spec.json) et couvre 4 surfaces de PROSE : README ×3 + fiche erpnext_backend. Mais le composeur d'exécution lui-mêmeecflib/ncf.py, le code Frappe qui VALIDE et COMPOSE réellement le numéro e-NCF côté VPS/Compupar — porte TROIS copies INDÉPENDANTES du format, AUCUNE dérivée de l'artefact, + un oracle de test qui les bénit, tous NON gatés :

  1. ENCF_RE = re.compile(r"^E([0-9]{2})([0-9]{10})$") (l.26) — la regex de VALIDATION (cœur de is_valid_encf/parse_encf) ;
  2. FORMULE = "E + tipoeCF(2) + secuencia(10)" (l.27) — la chaîne publiée par compose_encf (renvoyée au caller VPS) ;
  3. docstring l.8 « "E" + tipoeCF (2 chiffres) + secuencia (10 chiffres) → 13 caracteres » + commentaire l.25 « Ley 32-23 : E + tipo(2) + secuencia(10) = 13 » (deux proses-en-code) ;
  4. tests/test_ecf_dgii.py:246 — oracle hardcodé assertEqual(…["formule"], "E + tipoeCF(2) + secuencia(10)").

Le « vert trompeur » — un rango DGII à 12 chiffres (évènement ops réel, cité dans le commentaire du gate existant) régénère ecf_spec→ecf_plan, donc les 4 proses gatées SUIVENT. Mais ENCF_RE reste {2}{10} : compose_encf zfill à 10 et is_valid_encf REJETTE un e-NCF désormais valide → bug d'exécution du composeur fiscal, pendant que la suite tests/ reste VERTE (son oracle == FORMULE périmée, self-consistante). Ni check_artifacts (ncf.py n'est pas un out/*.json) ni tests/ (teste les FONCTIONS, pas leur ANCRAGE à l'artefact) ne mordent. Même patron que l'oracle/formateur du Publiciste déjà gaté.

Gate ajouté (ci/check_readme_claims.sh, extension du bloc e-NCF · surfaces 5→9) — RÉUTILISE prefix/tipo_len/seq_len/total déjà dérivés du bloc e_ncf byte-gaté + le helper ecf_check (zéro re-dérivation du format #6). Ancre les 5 surfaces CODE/test au SEUL artefact : (5) ENCF_RE, (6) FORMULE, (7) docstring, (8) commentaire, (9) oracle de test. Une surface absente échoue AUSSI (INTROUVABLE · anti-évaporation #6).

Vérif — 5 checks verts sur l'arbre propre. 7 morsures adversariales : M1 ENCF_RE {10}→{12} · M2 FORMULE secuencia(10)→(12) · M3 docstring 10→12 chiffres · M4 commentaire secuencia(10)→(12) · M5 oracle test (10)→(12) · M6 SILENT-GREEN — l'ARTEFACT passe à seq=12 pendant que ncf.py reste à 10 ⇒ les 5 surfaces mordent TOUTES (preuve que l'autorité est l'artefact byte-gaté, pas la cohérence interne du composeur) · M7 FORMULE renommée ⇒ INTROUVABLE. Les 39 tests unittest e-CF restent verts (composeur inchangé). Gates re-verts : check_artifacts · check_ci_integrity · check_docs · check_readme_claims · check_regression · guard_constraints tous exit 0.


Sprint 8 · buffer · CRM Commissions — gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate (7ᵉ instance du ledger)

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source # N · de _validate, jamais une liste à la main) a déjà couvert Chat OTOIA, SEO, Fiscal e-CF, Legal CONFOTUR, CRM Dossier Vente et QA Audit 5D. Le 7ᵉ générateur — dernier candidat restant recensé au ledger (crm/commissions ·10) — était NON gaté : crm/commissions/commissions_gen.py (barème de commissions vendeurs · roadmap l.51).

Le compte « 10 invariants » y est transcrit à la main à CINQ endroits, aucun gaté, tous invisibles à check_artifacts (le .py source n'est pas un out/*.json) :

  1. message de succès CLI validate « … schéma + 10 invariants verts. » (l.209) ;
  2. aide du sous-parseur validate « schéma + 10 invariants » (l.221) ;
  3. README bloc de commande « schéma + 10 invariants » (l.60) ;
  4. README titre de section « ## Les 10 invariants » (l.64) ;
  5. README prose de score « 10 invariants de cross-cohérence » (l.92).

Ajouter un contrôle # 11 · à _validate sans toucher les 5 chaînes ⇒ le README ET les self-reports du .py mentent en silence pendant que la CI applique 11 contrôles. Aucune suite tests/ (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ».

Particularité du registre Commissions (≈ Dossier Vente) : le ledger est PROPRE 1..10 — le schéma est compté À PART (phrasé « schéma + N invariants »), donc pas de schéma-#1 non marqué et pas de split a/b ; on garde la détection de doublon nu comme filet. Le module porte AUSSI un SECOND nombre — « 25 tests » (la suite unittest, déjà gatée par ailleurs) présent dans le README : l'ancre sur le mot « invariants » l'exclut nativement (« 25 tests » ≠ « invariants ») ⇒ aucun faux positif. Comme Dossier Vente, ce README A un titre « ## Les N invariants » ET une énumération numérotée du ledger → on gate (a)(b)(c)(d)(e).

Gate ajouté (ci/check_readme_claims.sh, bloc « CRM · Commissions · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# N · de _validate) et exige :

  • (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ;
  • (b) toute mention « N invariants » du .py == |ledger| ;
  • (c) l'annonce du titre README « ## Les N invariants » == |ledger| ;
  • (d) toute mention « N invariants » du README == |ledger| ;
  • (e) l'ÉNUMÉRATION numérotée de la section (« 1. … 2. … N. ») contiguë 1..N. Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation).

Vérif — 5 checks verts sur l'arbre propre (ledger 1..10 = 10 marqueurs # N ·, schéma compté à part). 7 morsures adversariales : M1 self-report .py 10→11 (b) · M2 titre README 10→11 (c+d) · M3 prose README 10→11 (d) · M4 ajout marqueur # 11 · ⇒ ledger→11 pendant que la prose reste 10 (mord .py ET README titre/mention ET l'énumération, preuve que l'autorité est le CODE) · M5 suppression # 7 · ⇒ gap non-contigu (a) · M6 doublon nu # 5 · ⇒ détection de vrai doublon (a) · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e). Le distinct « 25 tests » reste INVISIBLE au gate (ancre « invariants »). Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0. Série ledger close : les 7 générateurs portant le nombre magique d'invariants sont désormais tous gatés.


Sprint 8 · buffer · QA Audit 5D — gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate (6ᵉ instance du ledger)

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source # N · de _validate, jamais une liste à la main) a déjà couvert Chat OTOIA, SEO, Fiscal e-CF, Legal CONFOTUR et CRM Dossier Vente. Le 6ᵉ générateur portant le même nombre magique restait NON gaté : qa/audit_5d/audit_5d_gen.py (candidat restant recensé : crm/commissions ·10, ledger propre non gaté).

Le compte « 15 invariants » y est transcrit à la main à SIX endroits, aucun gaté, tous invisibles à check_artifacts (le .py source n'est pas un out/*.json) :

  1. docstring sous-commande module (l.19 « valide schéma + 15 invariants ») ;
  2. docstring _validate (l.80 « 15 invariants de cohérence audit/contrôles ») ;
  3. message de succès CLI validate « … 15 invariants — verdict … » (l.218) ;
  4. aide du sous-parseur validate « valide schéma + 15 invariants » (l.229) ;
  5. README bloc de commande « schéma + 15 invariants » (l.66) ;
  6. README prose d'architecture « CLI build/validate + 15 invariants » (l.81).

Ajouter un contrôle # 16 · à _validate sans toucher les 6 chaînes ⇒ le README ET les self-reports du .py mentent en silence pendant que la CI applique 16 contrôles. Aucune suite tests/ (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ».

Particularité du registre Audit 5D (≈ Dossier Vente, ≠ Fiscal) : le ledger est PROPRE 1..15 — le schéma est compté À PART (phrasé « schéma + N invariants »), donc pas de schéma-#1 non marqué et pas de split a/b ; on garde la détection de doublon nu comme filet. MAIS le module porte un SECOND nombre distinct — « 17 contrôles » (le compte de fonctions-contrôle de qalib/controls.py, déjà gaté par ailleurs), présent dans le .py ET le README : un \d+ invariants ancré sur le mot « invariants » l'exclut nativement (« 17 contrôles » ≠ mot « invariants ») ⇒ aucun faux positif sur l'arbre propre. Contrairement à Dossier Vente/Fiscal, ce README n'a NI titre « ## Les N invariants » NI énumération numérotée du ledger → on ne gate que (a)(b)(d).

Gate ajouté (ci/check_readme_claims.sh, bloc « QA · Audit 5D · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# N · de _validate) et exige :

  • (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ;
  • (b) toute mention « N invariants » du .py == |ledger| ;
  • (d) toute mention « N invariants » du README == |ledger|. Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation).

Vérif — 3 checks verts sur l'arbre propre (ledger 1..15 = 15 marqueurs # N ·, schéma compté à part). 6 morsures adversariales : M1 self-report .py 15→16 (b) · M2 mention README 15→16 (d) · M3 ajout marqueur # 16 · ⇒ ledger→16 pendant que la prose reste 15 (mord .py ET README, preuve que l'autorité est le CODE) · M4 suppression # 7 · ⇒ gap non-contigu (a) · M5 doublon nu # 5 · ⇒ détection de vrai doublon (a) · M6 no-false-positive : le distinct « 17 contrôles » reste INVISIBLE au gate (l'ancre « invariants » l'exclut, arbre propre VERT). Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Sprint 8 · buffer · CRM Dossier Vente — gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate (5ᵉ instance du ledger)

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source # N · de _validate, jamais une liste à la main) a déjà couvert Chat OTOIA, SEO, Fiscal e-CF et Legal CONFOTUR. Le 5ᵉ générateur portant le même nombre magique restait NON gaté : crm/dossier_vente/doctype_dossier_vente_gen.py (candidats restants recensés : qa/audit_5d ·15, crm/commissions ·10, tous ledgers propres non gatés).

Le compte « 12 invariants » y est transcrit à la main à SEPT endroits, aucun gaté, tous invisibles à check_artifacts (le .py source n'est pas un out/*.json) :

  1. docstring module (l.14 « schéma + 12 invariants de ») ;
  2. docstring _validate (l.77 « 12 invariants de cross-cohérence ») ;
  3. message de succès CLI validate « … schéma + 12 invariants verts. » (l.218) ;
  4. aide du sous-parseur validate (l.230) ;
  5. README bloc de commande « schéma + 12 invariants » (l.56) ;
  6. README titre de section « ## Les 12 invariants » (l.60) ;
  7. README prose de score « 12 invariants de cross-cohérence » (l.83).

Ajouter un contrôle # 13 · à _validate sans toucher les 7 chaînes ⇒ le README ET les self-reports du .py mentent en silence pendant que la CI applique 13 contrôles. Aucune suite tests/ (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ».

Particularité du registre Dossier Vente (≈ CONFOTUR, ≠ Fiscal) : le ledger est PROPRE 1..12 — le schéma est compté À PART (phrasé « schéma + N invariants »), donc pas de schéma-#1 non marqué et pas de split a/b. On garde néanmoins la détection de doublon nu comme filet. Le README porte AUSSI une énumération numérotée sous le titre de section (item 1 = conformité au schéma, puis les contrôles), gatée en contiguïté 1..N.

Gate ajouté (ci/check_readme_claims.sh, bloc « CRM · Dossier Vente · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# N · de _validate) et exige :

  • (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ;
  • (b) toute mention « N invariants » du .py == |ledger| ;
  • (c) l'annonce du titre README « ## Les N invariants » == |ledger| ;
  • (d) toute mention « N invariants » du README == |ledger| ;
  • (e) l'ÉNUMÉRATION numérotée de la section (« 1. … 2. … N. ») contiguë 1..N. Une mention ou le registre absent échoue AUSSI (traçabilité #6).

Vérif — 5 checks verts sur l'arbre propre (ledger 1..12 = 12 marqueurs # N ·, schéma compté à part). 7 morsures adversariales : M1 self-report .py 12→13 (b) · M2 titre README 12→13 (c+d) · M3 prose README 12→13 (d) · M4 ajout marqueur # 13 · ⇒ ledger→13 pendant que la prose reste 12 (mord .py ET README ET l'énumération, preuve que l'autorité est le CODE) · M5 suppression # 7 · ⇒ gap non-contigu (a) · M6 doublon nu # 5 · ⇒ détection de vrai doublon (a) · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e). Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source # N · de _validate, jamais une liste à la main) a déjà couvert Chat OTOIA, SEO et Fiscal e-CF. Le 4ᵉ générateur portant le même nombre magique restait NON gaté : legal/confotur/confotur_application_gen.py.

Le compte « 14 invariants » y est transcrit à la main à SIX endroits, aucun gaté, tous invisibles à check_artifacts (le .py source n'est pas un out/*.json) :

  1. docstring module (l.24 « schéma + 14 invariants de ») ;
  2. docstring _validate (l.87 « 14 invariants de cross-coherence ») ;
  3. message de succès CLI validate « … schéma + 14 invariants verts. » (l.283) ;
  4. aide du sous-parseur validate (l.295) ;
  5. README bloc de commande « schéma + 14 invariants » (l.56) ;
  6. README commentaire d'arborescence « CLI build/validate · 14 invariants » (l.82).

Ajouter un contrôle # 15 · à _validate sans toucher les 6 chaînes ⇒ le README ET les self-reports du .py mentent en silence pendant que la CI applique 15 contrôles. Aucune suite tests/ (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ».

Particularité du registre CONFOTUR (≠ Fiscal) : le ledger est PROPRE 1..14 (pas de schéma-#1 non marqué, pas de split a/b). MAIS le README porte AUSSI un compte de SOUS-ENSEMBLE éditorial — « 2 invariants refusent tout champ monétaire » (l.92) — à NE PAS confondre avec le total. Un \d+ invariants naïf mordrait donc l'arbre PROPRE (il verrait {2, 14}). Le gate exclut le sous-ensemble par lookahead négatif (\d+)\s+invariants(?!\s+refusent), appliqué au .py comme au README.

Gate ajouté (ci/check_readme_claims.sh, bloc « Legal · CONFOTUR · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# N · de _validate) et exige :

  • (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ;
  • (b) toute mention-total « N invariants » du .py (hors « refusent ») == |ledger| ;
  • (c) toute mention-total du README (hors « refusent ») == |ledger|. Une mention ou le registre absent échoue AUSSI (disparition d'un chiffre = régression de traçabilité #6).

Vérif — 3 checks verts sur l'arbre propre (ledger 1..14 = 14 marqueurs # N ·). 6 morsures adversariales : M1 self-report .py 14→15 (b) · M2 README commande 14→15 (c) · M3 README arborescence 14→13 (c) · M4 ajout marqueur # 15 · ⇒ ledger→15 pendant que la prose reste 14 (mord .py ET README, preuve que l'autorité est le CODE) · M5 suppression # 7 · ⇒ gap non-contigu (a) · M6 doublon nu # 5 · ⇒ détection de vrai doublon (a). M7 no-false-positive : sous-ensemble « 2 → 3 invariants refusent » reste VERT (le lookahead exclut bien le sous-ensemble). Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_regression · check_readme_claims tous exit 0.


Sprint 8 · buffer · Publiciste — gate d'ANCRAGE des DEVISES du site public sur CLAUDE.md #10

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le bloc Publiciste/branding existant ancre les 4 tokens DESIGN de CLAUDE.md #4 (2 couleurs + 2 typographies) sur leurs 4 copies. Mais le MÊME module publiciste porte une 2ᵉ constante constitutionnelle — les devises CLAUDE.md #10 (« USD + DOP ») — recopiée à 4 endroits data-derived, aucun gaté, tous invisibles à check_artifacts (pas un out/*.json) :

  1. lib/branding.py DEVISE_PRIMAIRE/DEVISE_SECONDAIRE — constantes auto- étiquetées « contrainte #10 · USD + DOP », sans gate ;
  2. lib/generator.py _fmt_usd/_fmt_dop — les formateurs qui IMPRIMENT le code devise sur la grille de prix du site public vente.otov7.com (sortie face-public : une devise hors #10 = mandat trahi en silence) ;
  3. lib/generator.py footer HTML « Prix … en USD + DOP (Cardnet) » ;
  4. tests/test_publiciste.py — l'oracle hardcodé assertIn("USD 150 000", …), une copie de plus jamais comparée à CLAUDE.md.

Aucune suite tests/ (qui teste des fonctions de rendu, pas l'ancre à CLAUDE.md) n'attrape la dérive : renommer la devise #10 et aligner le module reste vert alors que le site émettrait la mauvaise devise ; ne rien bouger reste vert alors que branding.py se dit « #10 » en mentant.

Gate ajouté (ci/check_readme_claims.sh, bloc « Publiciste/generator+branding · DEVISES CANONIQUES ») — RE-PARSE les 2 devises depuis la ligne #10 de CLAUDE.md (indépendamment de exp amont · aucun skip hérité) puis exige :

  • (a) branding.py DEVISE_PRIMAIRE/SECONDAIRE == #10 (ordre inclus) ;
  • (b) l'ensemble des codes émis par les formateurs generator.py == #10 ;
  • (c) le footer HTML cite les 2 devises dans l'ordre + (Cardnet) ;
  • (d) l'oracle du test == devise primaire #10. Un claim absent échoue AUSSI (traçabilité #6 · anti-évaporation).

Vérif — 5 checks verts sur l'arbre propre. 6 morsures adversariales : M1 branding primaire USD→EUR · M2 formateur _fmt_usd→EUR (hors #10) · M3 footer USD+DOP→USD+EUR · M4 oracle test → EUR · M5 CLAUDE.md #10 USD→EUR = cascade sur les 4 copies simultanément (preuve que l'ancre est CLAUDE.md, pas la cohérence interne) · M6 footer supprimé → INTROUVABLE. Gates re-verts : check_readme_claims · check_docs · guard_constraints · check_ci_integrity tous exit 0.


Sprint 8 · buffer · Fiscal e-CF DGII : gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » a déjà été appliqué à Chat OTOIA et SEO (recompute le nombre d'invariants CI depuis le registre-source # N · de _validate, jamais une liste à la main). Le 3ᵉ générateur portant le même nombre magique restait NON couvert : fiscal/ecf_dgii/ecf_dgii_gen.py.

Le compte « 12 invariants » y est transcrit à la main à SEPT endroits, aucun gaté, tous invisibles à check_artifacts (le .py source n'est pas un out/*.json) :

  1. ecf_dgii_gen.py docstring module (l.24) ;
  2. docstring _validate (l.110) ;
  3. message de succès CLI validate « … schéma + 12 invariants verts. » (l.277) ;
  4. aide du sous-parseur validate (l.289) ;
  5. README bloc de commande « schéma + 12 invariants » (l.65) ;
  6. README titre de section « ## Les 12 invariants » (l.69) ;
  7. README prose de score « 12 invariants de cross-cohérence » (l.105).

Ajouter un contrôle # 13 · à _validate sans toucher les 7 chaînes ⇒ le README ET les self-reports du .py mentent en silence pendant que la CI applique 13 contrôles. Aucune suite tests/ (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ».

Particularité du registre fiscal (≠ SEO/Chat OTOIA au ledger propre 1..N) : l'invariant 1 est la validation de SCHÉMA (validate(bundle, schema), non marquée # 1 ·), les marqueurs couvrent 2..N, et l'invariant 12 est splitté en # 12a · (unicité évènements) / # 12b · (comptes du manifeste) — deux volets d'un même invariant. Le gate reconstruit donc le ledger = {1:schéma} marqueurs-top-level, en collapsant 12a/12b, tout en mordant un vrai doublon d'un numéro NON suffixé.

Gate ajouté (ci/check_readme_claims.sh, bloc « Fiscal · e-CF DGII · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# N · de _validate) et exige :

  • (a) ledger CONTIGU 1..N (schéma #1 présent + marqueurs 2..N sans lacune ; doublon nu = défaut mordu ; 12a/12b comptés 1×) ;
  • (b) toute mention « N invariants » du .py == |ledger| ;
  • (c) l'annonce du titre README == |ledger| ;
  • (d) toute mention « N invariants » du README == |ledger| ;
  • (e) l'ÉNUMÉRATION numérotée du README (« 1. … 2. … N. ») elle-même contiguë 1..N (item ni sauté ni dupliqué).

Vérif — 5 checks verts sur l'arbre propre (ledger 1..12 = schéma #1 + 11 marqueurs). 8 morsures adversariales : M1 self-report .py 12→13 (b) · M2 titre README 12→13 (c+d) · M3 mention README 12→13 (d) · M4 ajout marqueur # 13 · ⇒ ledger→13 pendant que la prose reste 12 (b+c+d, preuve que l'autorité est le CODE, pas la prose) · M5 suppression # 7 · ⇒ gap non-contigu (a) · M6 doublon nu # 5 · ⇒ détection de vrai doublon · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e) · M8 suppression validate(bundle, schema) ⇒ invariant 1 introuvable. Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_readme_claims · check_regression tous exit 0.


Sprint 8 · buffer · Mobile app_config : gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate_bundle (8ᵉ instance)

Contexte — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La classe « ledger d'invariants » (recompute le nombre d'invariants CI depuis le registre-source numéroté de la fonction de validation, jamais une liste à la main) était déjà appliquée à 7 générateurs : Chat OTOIA · SEO · Fiscal e-CF · Legal CONFOTUR · CRM Dossier Vente · QA Audit 5D · CRM Commissions. Le 8ᵉ générateur portant le même nombre magique restait NON couvert : mobile/app_config/app_config_gen.py.

Le compte « 12 invariants » y est transcrit à la main à QUATRE endroits, aucun gaté, tous invisibles à check_artifacts (le .py/.md/test n'est pas un out/*.json) :

  1. app_config_gen.py message de succès CLI « … schéma + 12 invariants verts. » (l.228) ;
  2. README.md bloc de commande « schéma + 12 invariants » (l.48) ;
  3. README.md prose de récap « Schéma + 12\ninvariants verts » (l.67-68 — le « 12 » et le mot séparés par un saut de ligne) ;
  4. tests/test_mobile_app_config.py docstring « … + les 12 invariants du CLI » (l.4).

Ajouter un contrôle # Invariant 13 · à _validate_bundle sans toucher les 4 chaînes ⇒ le README, le self-report CLI ET la docstring de test mentent en silence pendant que la CI applique 13 contrôles. Aucune suite tests/ (qui EXÉCUTE la validation mais ne compte jamais la prose) ne l'attrape.

Particularités vs les 7 blocs précédents (documentées dans l'en-tête du gate) :

  • le registre-source utilise le style # Invariant N · (le mot « Invariant » précède le numéro) — les blocs ancrés sur le style nu # N · NE le voient PAS ; ancrage sur #\s*Invariant\s+(\d+)… ;
  • la fonction s'appelle _validate_bundle (pas _validate) — l'ancre def _validate\b des autres blocs échoue (\b échoue avant _bundle) ;
  • ce README n'a NI titre « ## Les N invariants » NI énumération numérotée → on gate (a)(b)(b')(d), en AJOUTANT la surface fichier-de-tests (b'), absente des 7 blocs précédents (leurs tests ne portaient pas le chiffre).

Gate ajouté (ci/check_readme_claims.sh, bloc « Mobile · app_config · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (# Invariant N · de _validate_bundle) et exige :

  • (a) ledger CONTIGU 1..N (schéma compté à part ; lacune OU doublon nu = défaut mordu) ;
  • (b) toute mention « N invariants » du .py == |ledger| ;
  • (b') toute mention « N invariants » du fichier de tests == |ledger| ;
  • (d) toute mention « N invariants » du README == |ledger|.

Vérif — 4 checks verts sur l'arbre propre (ledger 1..12 = 12 marqueurs). 8 morsures adversariales : M1 self-report CLI 12→13 (b) · M2 README bloc-commande 12→13 (d) · M3 README récap (newline-split) 12→13 (d, preuve que \s+ traverse le saut de ligne) · M4 docstring test 12→13 (b', la surface neuve) · M5 ajout marqueur # Invariant 13 · ⇒ ledger→13 pendant que les 3 surfaces prose restent 12 (b+b'+d, preuve que l'autorité est le CODE, pas la prose) · M6 suppression # Invariant 7 · ⇒ gap non-contigu (a) + désync prose · M7 doublon nu # Invariant 5 · ⇒ détection de vrai doublon · M8 renommage _validate_bundle ⇒ registre introuvable. Gates re-verts : check_artifacts · check_docs · guard_constraints · check_ci_integrity · check_readme_claims · check_regression tous exit 0.


Sprint 8 · buffer · Doc QA : documentation du 2ᵉ étage QA (batterie de gates statiques ci/*.sh) dans 03_agents/qa/AGENT.md

Démarche — vérifier AVANT de gater (mémoire verify-uncovered-before-gating). La série récente empile des gates anti-dérive ; avant d'en ajouter un 9ᵉ, j'ai d'abord cherché une vraie dérive présente plutôt qu'un gate spéculatif :

  1. Scan ledger-vs-prose sur les 12 générateurs à ledger d'invariants. Faux signal relevé : fiscal/ecf_dgii semblait afficher ledger_markers=10 vs prose=12. Investigation : le ledger n'est pas nu 1..12 — l'invariant #1 (validation de schéma, validate(bundle, schema)) n'est pas marqué # 1 · par design, et #12 est scindé en 12a/12b. Le gate fiscal (check_readme_claims.sh l.1908) synthétise déjà ledger = marqueurs{2..12} {1} et vérifie self-report / titre README / mentions / énumération = 12. Aucune dérive ; mon compteur grossier ne voyait ni le schéma-#1 ni 12a/12b.
  2. Sondé les comptes de 03_agents/{crm,qa}/AGENT.md (« Total CRM 81 (25+31+25) », « 17 contrôles / 5 dimensions », « 8 livrables + 7 métriques ») → tous gatés (l.681, 871, 2176, 2624, 3671…). Coverage très dense, conforme à la mémoire.

Conclusion : pas de gate à ajouter (ç'aurait été redondant · CLAUDE.md #5).

Trou documentaire réel comblé. Le titre + le scope de qa/AGENT.md promettent des « gates de méta-niveau », mais le corps ne tabulait que les 4 modules Python d'audit (audit_5d · audit_4big · regression · acceptance) — il omettait entièrement la batterie de gates statiques ci/*.sh, qui sont pourtant l'application méta-niveau littérale (moitié de l'enforcement QA). Un lecteur croyait donc QA = 4 modules.

Ajout — section « Deuxième étage QA : la batterie de gates statiques (ci/*.sh) » listant les 7 gates courants par nom + rôle (check_ci_integrity, check_readme_claims, check_artifacts, check_regression, check_docs, guard_constraints, validate_json). Anti-dérive by design : le texte ancre l'autorité du périmètre sur check_ci_integrity (qui découvre l'ensemble via git ls-files 'ci/*.sh' et refuse tout gate décâblé/orphelin) et présente la liste comme l'état courant, PAS une constante figée → aucun compte agrégé driftable introduit.

Sûreté — prose sans lien Markdown neuf (check_docs), sans URL git interdite ni terme CRM/paiement banni nu ; les lignes citant les prohibitions portent un marqueur (proscrites/interdits/bannie) donc guard_constraints les laisse passer. Vérif : 7 gates re-verts (guard_constraints · check_docs · check_readme_claims · check_ci_integrity · check_artifacts · check_regression · validate_json tous exit 0).