[DTP-Worker] Sprint 8 · buffer · Chat OTOIA : IDENTITÉ persona (Amélie/voix multilingual_v2) + 4 CAPABILITIES OTOIA (aec.py+knowledge.py+prompt_engine.py+chat.py) du montage Chat par portail frontend/chat_otoia ANCRÉE sur CLAUDE.md §Architecture cible

Les artefacts `out/chat_mount.json` (5 configs runtime, une par portail) +
`out/MANIFEST.json` portent la persona + les capabilities, BYTE-GATÉS pour la
REPRODUCTIBILITÉ par check_artifacts (reconstruction depuis `chat_spec.json`) mais
JAMAIS ANCRÉS à CLAUDE.md §Architecture cible (l.30 « Voix Amélie QC (multilingual_v2) »
· l.28 « OTOIA capabilities : aec.py + knowledge.py + prompt_engine.py + chat.py »),
leur source faisant autorité. Le bloc chat_otoia amont ne gate que la COMPOSITION
(portails métier · 1 block⇔1 mount) ; le SEUL contrôle d'identité vit dans tests/
(`test_persona_amelie_sourcee`/`test_capabilities_sourcees_sans_ajout`) mais son ORACLE
est HARDCODÉ (`"Amélie"`, `["aec.py",…]`) — une copie de plus, jamais comparée à
CLAUDE.md. Piège #6 : renommer la persona (`Amélie`→`Sophie`), changer la voix, ou
renommer/RETIRER une capability dans `chat_spec.json` (ou dans CLAUDE.md) reconstruit
l'artefact fidèlement (byte-gate VERTE) sans toucher l'oracle (tests VERTS) → le chat
monté dans CHAQUE portail annoncerait une persona/voix/capabilities CONTREDISANT le
mandat — « vert trompeur » de la classe des tokens branding ancrés sur #4 et de la
marque SEO §Entités. Aucune suite tests/ (FONCTIONS de génération, jamais l'ancre à
CLAUDE.md) ne l'attrape.

Gate ajouté (bloc « Chat OTOIA · persona + capabilities ancrées CLAUDE.md ») :
persona+capabilities re-dérivées de §Architecture cible (regex `Voix … QC (…)` +
`OTOIA capabilities : …` split `+`, NFC), puis (a) `chat_spec.json`
persona/capabilities == CLAUDE.md (ordre exact) · (b) le spec DÉCLARE l'ancrage
(`persona.source` + chaque `capability.source` citent CLAUDE.md) · (c) CHACUN des 5
mounts + le MANIFEST == CLAUDE.md · (d) README cite l'ancre §Architecture cible + nomme
persona + chaque capability (notation compacte matchée par radical) · (e) l'ORACLE du
test == CLAUDE.md (set-diff). Un claim absent échoue AUSSI.

8 morsures vérifiées : CLAUDE.md `Amélie→Sophie` (mord SIMULTANÉMENT
spec/mount/README/oracle — l'ancre est vive) · CLAUDE.md retire `chat.py` · spec
`aec.py→exfil.py` (capability injectée) · spec `persona.source` sans CLAUDE.md · UN
SEUL mount à voix altérée (byte-gate aveugle) · README retire le radical
`prompt_engine` · oracle du test persona `Amélie→Bob` · oracle du test capabilities
réordonné/tronqué ; restauré = green, exit 0. Working tree byte-restauré
(`git checkout --`, JAMAIS `git clean`) · 7 gates re-verts · unittest module re-vert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 06:41:19 +00:00
parent 6f88383939
commit 5de6b23c02
3 changed files with 246 additions and 1 deletions
+33 -1
View File
File diff suppressed because one or more lines are too long
+152
View File
@@ -4210,6 +4210,158 @@ if cm_ents is not None:
good(f"Confotur entités · (e) README cite CLAUDE.md + nomme les "
f"{len(cm_ents)} entités")
# ============================================================================
# Chat OTOIA · l'IDENTITÉ de la persona (`Amélie` / voix `multilingual_v2`) et
# des 4 CAPABILITIES OTOIA (`aec.py` + `knowledge.py` + `prompt_engine.py` +
# `chat.py`) — ANCRÉES sur CLAUDE.md §Architecture cible, jamais gatées.
# ----------------------------------------------------------------------------
# out/chat_mount.json (5 configs runtime, une par portail) + out/MANIFEST.json
# sont byte-gatés par check_artifacts : la reproductibilité prouve qu'ils SE
# RECONSTRUISENT depuis chat_spec.json, PAS que leur persona/capabilities ==
# ce que NOMME CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) »
# · « OTOIA capabilities : aec.py + knowledge.py + prompt_engine.py + chat.py »).
# Le bloc chat_otoia amont gate la COMPOSITION (portails métier · 1 block⇔1 mount).
# Le SEUL contrôle d'identité vit dans tests/ (test_persona_amelie_sourcee /
# test_capabilities_sourcees_sans_ajout) mais son ORACLE est HARDCODÉ
# (`"Amélie"`, `["aec.py",…]`) — une copie de plus, JAMAIS comparée à CLAUDE.md.
# Piège #6 : renommer la persona (`Amélie`→`Sophie`), changer la voix
# (`multilingual_v2`→…), ou renommer/retirer une capability dans chat_spec.json
# (ou dans CLAUDE.md §Architecture cible) laisse le chat monté dans CHAQUE portail
# annoncer une persona/voix/capabilities qui CONTREDIT le mandat pendant que la
# byte-gate ET les tests restent VERTS (l'artefact se reconstruit fidèlement…
# depuis un spec dérivé ; l'oracle du test est une copie de plus) — « vert
# trompeur » de la classe des tokens branding ancrés sur CLAUDE.md #4 et de la
# marque SEO §Entités. On RE-DÉRIVE persona+capabilities de CLAUDE.md
# §Architecture cible (source unique) et on exige : (a) chat_spec.json
# persona/capabilities == CLAUDE.md ; (b) le spec DÉCLARE l'ancrage (persona.source
# + chaque capability.source citent CLAUDE.md) ; (c) CHACUN des 5 mounts + le
# MANIFEST portent persona/capabilities == CLAUDE.md ; (d) le README cite l'ancre
# §Architecture cible + nomme persona + chaque capability ; (e) l'ORACLE du test
# == CLAUDE.md (set-diff : absent ET en trop) — sinon la copie de copie survit à
# un renommage. État courant : aucune valeur périmée — le défaut est l'ancrage
# ungated.
# ============================================================================
_nfc = lambda s: unicodedata.normalize("NFC", str(s)).strip()
CHAT = f"{D}/frontend/chat_otoia"
try:
_cm = open("CLAUDE.md", encoding="utf-8").read()
_mp = re.search(r"Voix\s+(.+?)\s+QC\s*\(([^)]+)\)", _cm)
_mc = re.search(r"OTOIA capabilities\s*:\s*(.+)", _cm)
if not _mp or not _mc:
raise ValueError("« Voix … QC (…) » ou « OTOIA capabilities : … » "
"INTROUVABLE dans CLAUDE.md §Architecture cible")
cm_persona = {"nom": _nfc(_mp.group(1)), "voix": _nfc(_mp.group(2))}
cm_caps = [c.strip() for c in _mc.group(1).split("+") if c.strip()]
if len(cm_caps) < 2:
raise ValueError(f"capabilities CLAUDE.md mal formées : {cm_caps}")
except (OSError, ValueError) as e:
bad(f"Chat OTOIA · persona/capabilities CLAUDE.md §Architecture cible "
f"illisible/inattendu : {e}")
cm_persona = cm_caps = None
try:
chat_spec = json.load(open(f"{CHAT}/chat_spec.json", encoding="utf-8"))
chat_mounts = load("frontend/chat_otoia/out/chat_mount.json")
chat_man = load("frontend/chat_otoia/out/MANIFEST.json")
except (OSError, KeyError, ValueError) as e:
bad(f"Chat OTOIA · spec/artefacts illisibles/inattendus : {e}")
chat_spec = None
if cm_persona is not None and chat_spec is not None:
# (a) ANCRAGE — chat_spec.json persona/capabilities == CLAUDE.md §Architecture cible
sp_persona = {"nom": _nfc(chat_spec["persona"].get("nom", "")),
"voix": _nfc(chat_spec["persona"].get("voix", ""))}
sp_caps = [str(c.get("module", "")).strip() for c in chat_spec.get("capabilities", [])]
if sp_persona == cm_persona:
good(f"Chat OTOIA · (a) spec persona {sp_persona} == CLAUDE.md §Architecture cible")
else:
bad(f"Chat OTOIA · (a) spec persona {sp_persona} ≠ CLAUDE.md {cm_persona} "
f"→ l'assistant monté contredirait le mandat (aligner spec ou CLAUDE.md)")
if sp_caps == cm_caps:
good(f"Chat OTOIA · (a) spec capabilities {sp_caps} == CLAUDE.md (ordre exact)")
else:
bad(f"Chat OTOIA · (a) spec capabilities {sp_caps} ≠ CLAUDE.md {cm_caps} "
f"→ surface de capacités inventée/périmée")
# (b) le spec DÉCLARE l'ancrage : persona.source + chaque capability.source citent CLAUDE.md
_psrc = str(chat_spec["persona"].get("source", ""))
if "CLAUDE.md" not in _psrc or cm_persona["nom"] not in _nfc(_psrc):
bad(f"Chat OTOIA · (b) chat_spec persona.source ne DÉCLARE pas l'ancrage "
f"CLAUDE.md/{cm_persona['nom']!r} : {_psrc!r}")
else:
_bad_src = [c.get("module") for c in chat_spec["capabilities"]
if "CLAUDE.md" not in str(c.get("source", ""))]
if _bad_src:
bad(f"Chat OTOIA · (b) capabilities sans `source` CLAUDE.md : {_bad_src}")
else:
good("Chat OTOIA · (b) spec DÉCLARE l'ancrage CLAUDE.md (persona + chaque capability)")
# (c) CHAQUE mount + le MANIFEST portent persona/capabilities == CLAUDE.md
_bad_m = []
for m in chat_mounts:
pm = {"nom": _nfc((m.get("persona") or {}).get("nom", "")),
"voix": _nfc((m.get("persona") or {}).get("voix", ""))}
if pm != cm_persona or list(m.get("capabilities", [])) != cm_caps:
_bad_m.append(m.get("portail"))
man_ok = ({"nom": _nfc((chat_man.get("persona") or {}).get("nom", "")),
"voix": _nfc((chat_man.get("persona") or {}).get("voix", ""))} == cm_persona
and list(chat_man.get("capabilities", [])) == cm_caps)
if not chat_mounts:
bad("Chat OTOIA · (c) aucun mount → artefact suspect")
elif _bad_m:
bad(f"Chat OTOIA · (c) mount(s) {_bad_m} persona/capabilities ≠ CLAUDE.md "
f"→ chat monté hors mandat")
elif not man_ok:
bad("Chat OTOIA · (c) MANIFEST persona/capabilities ≠ CLAUDE.md")
else:
good(f"Chat OTOIA · (c) {len(chat_mounts)} mounts + MANIFEST portent "
f"persona/capabilities == CLAUDE.md")
# (d) README cite l'ancre §Architecture cible + nomme persona + chaque capability
CHAT_RD = f"{CHAT}/README.md"
chat_rd = read_norm(CHAT_RD)
if chat_rd is not None:
chat_rd_n = _nfc(chat_rd)
_mp_rd = re.search(r"Persona.*?§Architecture cible", chat_rd_n)
_mc_rd = re.search(r"Capabilities.*?§Architecture cible", chat_rd_n)
if not _mp_rd:
bad(f"Chat OTOIA · (d) ligne « Persona … §Architecture cible » INTROUVABLE "
f"dans {CHAT_RD} (claim absent = régression #6)")
elif cm_persona["nom"] not in _mp_rd.group(0) or cm_persona["voix"] not in _mp_rd.group(0):
bad(f"Chat OTOIA · (d) README ligne persona omet {cm_persona['nom']!r}/"
f"{cm_persona['voix']!r}")
elif not _mc_rd:
bad(f"Chat OTOIA · (d) ligne « Capabilities … §Architecture cible » INTROUVABLE "
f"dans {CHAT_RD} (claim absent = régression #6)")
else:
_stems = [c[:-3] if c.endswith(".py") else c for c in cm_caps]
_miss = [s for s in _stems
if not re.search(r"(?<![\w.])" + re.escape(s) + r"(?![\w])", _mc_rd.group(0))]
if _miss:
bad(f"Chat OTOIA · (d) README ligne capabilities omet {_miss}")
else:
good(f"Chat OTOIA · (d) README cite §Architecture cible + persona + "
f"{len(cm_caps)} capabilities")
# (e) l'ORACLE du test == CLAUDE.md (sinon la copie de copie survit à un renommage)
CHAT_TEST = f"{CHAT}/tests/test_chat_otoia.py"
try:
_t = open(CHAT_TEST, encoding="utf-8").read()
except OSError as e:
bad(f"Chat OTOIA · (e) {CHAT_TEST} illisible : {e}"); _t = None
if _t is not None:
_mtp = re.search(r'spec\["persona"\]\["nom"\],\s*"([^"]+)"', _t)
_mtv = re.search(r'spec\["persona"\]\["voix"\],\s*"([^"]+)"', _t)
_mtc = re.search(r"caps,\s*\[([^\]]*)\]", _t)
if not (_mtp and _mtv and _mtc):
bad(f"Chat OTOIA · (e) oracle persona/capabilities INTROUVABLE dans {CHAT_TEST} "
f"(claim absent = régression #6)")
else:
t_caps = [x.strip().strip("\"'") for x in _mtc.group(1).split(",") if x.strip()]
if _nfc(_mtp.group(1)) != cm_persona["nom"] or _nfc(_mtv.group(1)) != cm_persona["voix"]:
bad(f"Chat OTOIA · (e) oracle du test persona "
f"{{{_mtp.group(1)!r},{_mtv.group(1)!r}}} ≠ CLAUDE.md {cm_persona} "
f"→ copie de copie périmée")
elif t_caps != cm_caps:
bad(f"Chat OTOIA · (e) oracle du test capabilities {t_caps} ≠ CLAUDE.md "
f"{cm_caps}")
else:
good("Chat OTOIA · (e) oracle du test persona/capabilities == CLAUDE.md")
sys.exit(1 if FAIL else 0)
PY
rc=$?