diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index ee7685e..9ed09db 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -718,3 +718,61 @@ CODE, pas la prose) · M5 suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 dou 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. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 4d2a2bf..777c367 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -8523,6 +8523,129 @@ if cmi_src is not None: bad(f"Commissions invariants · {CMI_RD} — énumération prose {cmi_enum} " f"≠ 1..{cmi_ninv} (item sauté/dupliqué) → régénérer le README") +# ============================================================================ +# Mobile · app_config · IDENTITÉ du COMPTE D'INVARIANTS de `_validate_bundle` — +# 8ᵉ instance de la classe « ledger d'invariants » (après Chat OTOIA · SEO · +# Fiscal e-CF · Legal CONFOTUR · CRM Dossier Vente · QA Audit 5D · CRM +# Commissions), appliquée au générateur de la config app mobile (roadmap Sprint 5 +# l.56-57 « Rebuild Expo 54 + submit »). +# ---------------------------------------------------------------------------- +# Le générateur app_config_gen.py applique un COMPTE d'invariants de cohérence +# (onglets=portails RBAC · rôles=surface exacte · marque #4 · devises #10 · Expo +# SDK · anti-invention a_confirmer · déterminisme…) dont la source d'autorité +# n'est PAS un artefact JSON (invisible à check_artifacts qui byte-gate les +# out/*.json) mais le LEDGER NUMÉROTÉ de `_validate_bundle` : les marqueurs +# `# Invariant N ·` (1..12) qui ouvrent chaque contrôle. Le nombre « 12 » est +# transcrit À LA MAIN à QUATRE endroits qu'AUCUN gate ne couvrait : UNE chaîne du +# .py (message de succès CLI « … schéma + 12 invariants verts »), DEUX du README +# (bloc de commande « schéma + 12 invariants », prose de récap « Schéma + 12 +# invariants verts » — le « 12 » et le mot séparés par un saut de ligne) + UNE du +# fichier de tests (docstring « … + les 12 invariants du CLI »). Ajouter un +# contrôle `# Invariant 13 ·` à `_validate_bundle` sans toucher ces chaînes ⇒ le +# README, le self-report CLI ET la docstring de test mentent en silence pendant +# que la CI applique 13 contrôles — « vert trompeur » (#6) que ni les suites +# tests/ (qui EXÉCUTENT la validation mais ne comptent jamais la prose) ni le +# byte-gate d'artefact (aveugle au .py source) n'attrapent. +# PARTICULARITÉS vs les 7 blocs précédents : +# · le REGISTRE-source utilise le style `# Invariant N ·` (le mot « Invariant » +# précède le numéro) — les blocs Chat OTOIA/SEO/Fiscal/CONFOTUR/Dossier/Audit +# 5D/Commissions ancrés sur le style nu `# N ·` NE le voient PAS ; on ancre +# donc sur `#\s*Invariant\s+(\d+)…` ; +# · la fonction s'appelle `_validate_bundle` (pas `_validate`) — l'ancre `def +# _validate\b` des autres blocs ne matche pas (`\b` échoue avant `_bundle`) ; +# · ledger PROPRE 1..N compté À PART du schéma (phrasé « schéma + N invariants ») +# — pas de schéma-#1 non marqué, pas de split a/b ; on garde le filet doublon +# nu ; +# · ce README n'a NI titre « ## Les N invariants » NI énumération numérotée du +# ledger → on gate (a)(b)(b')(d), en AJOUTANT la surface fichier-de-tests (b') +# absente des blocs précédents (leurs tests ne portaient pas le chiffre). +# On RECOMPUTE le ledger depuis le SEUL registre-source (les `# Invariant N ·` de +# `_validate_bundle`) et on exige : (a) ledger CONTIGU 1..N (lacune OU doublon nu +# = vrai défaut de registre, 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|. Mention ou registre +# absent = échec (disparition d'un chiffre = régression de traçabilité #6). +# ============================================================================ +MBI_GEN = f"{D}/mobile/app_config/app_config_gen.py" +MBI_RD = f"{D}/mobile/app_config/README.md" +MBI_TS = f"{D}/mobile/app_config/tests/test_mobile_app_config.py" +try: + mbi_src = open(MBI_GEN, encoding="utf-8").read() +except OSError as e: + bad(f"Mobile invariants · générateur {MBI_GEN} illisible : {e}"); mbi_src = None +if mbi_src is not None: + _mfn = re.search(r"def _validate_bundle\b", mbi_src) + if _mfn is None: + bad(f"Mobile invariants · fonction `_validate_bundle` INTROUVABLE dans {MBI_GEN} " + "(registre-source du ledger absent → impossible de re-dériver)") + else: + _bs = _mfn.end() + _mn = re.search(r"\ndef ", mbi_src[_bs:]) + _body = mbi_src[_bs: _bs + _mn.start()] if _mn else mbi_src[_bs:] + _pairs = re.findall(r"#\s*Invariant\s+(\d+)([a-z]?)\s*·", _body) + _dup_bare = sorted({n for n in {p[0] for p in _pairs} + if [s for num, s in _pairs if num == n and s == ""] + and sum(1 for num, _ in _pairs if num == n) > 1}) + _top = sorted({int(n) for n, _ in _pairs}) + if not _top: + bad(f"Mobile invariants · aucun marqueur `# Invariant N ·` dans `_validate_bundle` " + f"de {MBI_GEN} (ledger vide → impossible de re-dériver)") + elif _dup_bare: + bad(f"Mobile invariants · marqueur(s) `# Invariant N ·` dupliqué(s) sans suffixe " + f"a/b dans `_validate_bundle` : {_dup_bare} — vrai doublon de registre") + else: + mbi_ninv = len(_top) + if _top != list(range(1, mbi_ninv + 1)): + bad(f"Mobile invariants · ledger `_validate_bundle` NON contigu : {_top} " + f"(attendu 1..{mbi_ninv}) — lacune/doublon dans les marqueurs `# Invariant N ·`") + else: + good(f"Mobile invariants · ledger `_validate_bundle` contigu 1..{mbi_ninv} " + f"({mbi_ninv} marqueurs `# Invariant N ·` · schéma compté à part)") + # (b) self-report(s) du .py « N invariants ». + mbi_py_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", mbi_src)}) + if not mbi_py_rep: + bad(f"Mobile invariants · aucune mention « N invariants » dans {MBI_GEN} " + f"(self-report absent → attendu {mbi_ninv})") + elif mbi_py_rep == [mbi_ninv]: + good(f"Mobile invariants · {MBI_GEN} — self-report(s) {mbi_py_rep} " + f"== ledger ({mbi_ninv})") + else: + bad(f"Mobile invariants · {MBI_GEN} — self-report(s) « N invariants » " + f"{mbi_py_rep} MAIS ledger en compte {mbi_ninv} " + "(une chaîne du générateur désynchronisée du registre)") + # (b') self-report du fichier de tests « N invariants » (docstring). + try: + mbi_ts_raw = open(MBI_TS, encoding="utf-8").read() + except OSError as e: + bad(f"Mobile invariants · {MBI_TS} illisible : {e}"); mbi_ts_raw = None + if mbi_ts_raw is not None: + mbi_ts_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", mbi_ts_raw)}) + if not mbi_ts_rep: + bad(f"Mobile invariants · aucune mention « N invariants » dans {MBI_TS} " + f"(self-report tests absent → attendu {mbi_ninv})") + elif mbi_ts_rep == [mbi_ninv]: + good(f"Mobile invariants · {MBI_TS} — docstring {mbi_ts_rep} " + f"== ledger ({mbi_ninv})") + else: + bad(f"Mobile invariants · {MBI_TS} — docstring « N invariants » " + f"{mbi_ts_rep} MAIS ledger en compte {mbi_ninv} → régénérer la docstring") + # (d) toute mention « N invariants » du README == ledger. + try: + mbi_rd_raw = open(MBI_RD, encoding="utf-8").read() + except OSError as e: + bad(f"Mobile invariants · {MBI_RD} illisible : {e}"); mbi_rd_raw = None + if mbi_rd_raw is not None: + mbi_rd_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", mbi_rd_raw)}) + if not mbi_rd_rep: + bad(f"Mobile invariants · aucune mention « N invariants » dans {MBI_RD} " + f"(self-report README absent → attendu {mbi_ninv})") + elif mbi_rd_rep == [mbi_ninv]: + good(f"Mobile invariants · {MBI_RD} — mention(s) {mbi_rd_rep} " + f"== ledger (aucune dérive)") + else: + bad(f"Mobile invariants · {MBI_RD} — mention(s) « N invariants » " + f"{mbi_rd_rep} MAIS ledger en compte {mbi_ninv} → régénérer le README") + # ============================================================================ # Traçabilité roadmap · le `roadmap_ref` VERBATIM des artefacts — la CITATION # (n° de ligne + « libellé bullet ») ANCRÉE sur la ligne RÉELLE de la roadmap,