diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 873065e..632d9cb 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,53 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## 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] « » »`, 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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 1894bc6..25aed04 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -8457,6 +8457,106 @@ 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") +# ============================================================================ +# 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, +# pas seulement déclarée. +# ---------------------------------------------------------------------------- +# Quatre modules (demo/scenarios · frontend/chat_otoia · frontend/portails · +# mobile/app_config) publient dans out/MANIFEST.json (byte-gaté par +# check_artifacts) un `roadmap_ref` de la forme : +# « 04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · Sprint N l.A[-B] « » » +# Ce libellé est une TRANSCRIPTION VERBATIM du bullet de la roadmap à la ligne +# citée — mais 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 — l'un ne contredit jamais l'autre) ; +# l'ancrage roadmap de qa/acceptance (INV11) ne touche QUE les `roadmap_line` +# NUMÉRIQUES de SON artefact, jamais le libellé cité par les autres specs ; +# l'ancrage Expo (mobile) ne vérifie que la présence de « roadmap » + « Expo N » +# dans `expo_sdk_source`, pas la citation complète. → REORDONNER la roadmap (les +# n° de ligne glissent) ou REFORMULER un bullet laisse ces 4 citations pointer +# une ligne qui dit AUTRE CHOSE : « vert trompeur » de traçabilité (#6) +# qu'aucune suite tests/ (FONCTIONS de résolution, jamais l'ancre) n'attrape. +# Prouvé par mutation : reformuler le bullet l.63 laissait les 6 gates VERTS +# pendant que chat_otoia citait encore « … Chat OTOIA embedded … » à l.63. +# On ANCRE : pour chaque MANIFEST portant un `roadmap_ref` citant (l.A[-B] «…»), +# (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 ; (3) le spec du module porte le MÊME `roadmap_ref` +# (identité spec↔artefact publié). Les 4 modules connus sont EXIGÉS présents +# (anti-évaporation #6) ; tout AUTRE MANIFEST à citation est ancré en bonus. +# ============================================================================ +import glob as _glob +ROADMAP_REL = "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md" +RMREF_EXPECTED = {"demo/scenarios", "frontend/chat_otoia", + "frontend/portails", "mobile/app_config"} +def _rmn(s): # normalise une ligne markdown pour comparaison de libellé + s = unicodedata.normalize("NFC", str(s)) + s = re.sub(r"^\s*[-*+]\s+", "", s) # marqueur de liste en tête de ligne + s = s.replace("*", "") # emphase **gras** / *ital* + return re.sub(r"\s+", " ", s).strip() +try: + _rm_lines = open(ROADMAP_REL, encoding="utf-8").read().splitlines() +except OSError as e: + bad(f"Traçabilité roadmap · {ROADMAP_REL} illisible : {e}"); _rm_lines = None +if _rm_lines is not None: + _man_paths = sorted(set(_glob.glob(f"{D}/*/*/out/MANIFEST.json") + + _glob.glob(f"{D}/*/out/MANIFEST.json"))) + _rm_seen = set() + for _mp in _man_paths: + _mod = _mp[len(D) + 1:].rsplit("/out/", 1)[0] # ex. « demo/scenarios » + try: + _ref = json.load(open(_mp, encoding="utf-8")).get("roadmap_ref", "") or "" + except (OSError, ValueError) as e: + bad(f"Traçabilité roadmap · {_mp} illisible : {e}"); continue + _mln = re.search(r"l\.(\d+)(?:-(\d+))?", _ref) + _mq = re.search(r"«\s*(.+?)\s*»", _ref) + if not (_mln and _mq): + continue # pas une citation ligne+libellé → hors périmètre de ce gate + _rm_seen.add(_mod) + _path = _ref.split(" · ", 1)[0].strip() + _a = int(_mln.group(1)); _b = int(_mln.group(2) or _mln.group(1)) + _quote = _mq.group(1) + # (1) fichier cité == roadmap canonique + if _path != ROADMAP_REL: + bad(f"Traçabilité roadmap · {_mod} — cite « {_path} » ≠ roadmap " + f"canonique « {ROADMAP_REL} »"); continue + # bornes de ligne valides (renumérotation hors-plage attrapée ici) + if not (1 <= _a <= _b <= len(_rm_lines)): + bad(f"Traçabilité roadmap · {_mod} — plage l.{_a}-{_b} hors roadmap " + f"(1..{len(_rm_lines)})"); continue + _hay = " ".join(_rmn(_rm_lines[_i - 1]) for _i in range(_a, _b + 1)) + _rng = f"l.{_a}" + ("" if _a == _b else f"-{_b}") + # (2) libellé cité == sous-chaîne des lignes réelles A..B + if _rmn(_quote) not in _hay: + bad(f"Traçabilité roadmap · {_mod} — libellé cité « {_quote} » ABSENT " + f"des lignes {_rng} de la roadmap (qui disent « {_hay} ») → " + f"REFORMULATION/RENUMÉROTATION : aligner le roadmap_ref (ou la roadmap)") + continue + # (3) spec du module porte le MÊME roadmap_ref (identité spec↔artefact publié) + _specs = _glob.glob(f"{D}/{_mod}/*_spec.json") + _spec_ok = True + if len(_specs) == 1: + try: + _sref = json.load(open(_specs[0], encoding="utf-8")).get("roadmap_ref") + except (OSError, ValueError): + _sref = None + if _sref is not None and _sref != _ref: + _spec_ok = False + bad(f"Traçabilité roadmap · {_mod} — spec roadmap_ref « {_sref} » ≠ " + f"MANIFEST « {_ref} » (artefact interne incohérent)") + if _spec_ok: + good(f"Traçabilité roadmap · {_mod} — citation {_rng} « {_quote} » " + f"== roadmap") + # anti-évaporation : les 4 citations connues doivent TOUTES subsister + _rm_missing = sorted(RMREF_EXPECTED - _rm_seen) + if _rm_missing: + bad(f"Traçabilité roadmap · citation roadmap_ref ÉVAPORÉE pour {_rm_missing} " + f"(module attendu sans (l.N «…») dans out/MANIFEST.json · #6)") + elif _rm_seen: + good(f"Traçabilité roadmap · {len(_rm_seen)} citation(s) ancrée(s), " + f"4 modules attendus présents") + sys.exit(1 if FAIL else 0) PY rc=$?