[DTP-Worker 20260802_060518] Sprint 8 · buffer · Traçabilité : ancrage de la CITATION roadmap_ref VERBATIM (l.N + libellé) des 4 MANIFESTs sur la ligne réelle de la roadmap

Gate anti-dérive #6 : demo/scenarios · frontend/chat_otoia · frontend/portails ·
mobile/app_config publient un roadmap_ref « …l.A[-B] « <bullet> » » dans
out/MANIFEST.json (byte-gaté). check_artifacts ne prouve QUE spec==MANIFEST ;
INV11 n'ancre que les roadmap_line NUMÉRIQUES de qa/acceptance. Reformuler/
renuméroter la roadmap laissait les 4 citations périmées en silence (silent-green
prouvé : reword l.63 ⇒ 6 gates verts, chat_otoia cite toujours l'ancien texte).

check_readme_claims : découvre chaque MANIFEST à citation et exige (1) fichier ==
roadmap canonique, (2) libellé normalisé ⊆ lignes A..B réelles, (3) spec ==
MANIFEST ; 4 modules connus EXIGÉS présents (anti-évaporation). 5 checks verts ·
7 morsures (M1 silent-green reword roadmap · M2 renumber · M3 drift citation ·
M4 wrong file · M5 spec≠manifest · M6 évaporation · M7 hors-plage). Suite complète
exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-02 06:15:53 +00:00
parent 0d4fd0942c
commit 215c9e6f25
2 changed files with 148 additions and 0 deletions
+100
View File
@@ -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] « <libellé bullet> » »
# 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=$?