[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:
@@ -735,3 +735,64 @@ gates re-verts**.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_063234` · Buffer S8 · Chat OTOIA/persona+capabilities : l'**IDENTITÉ de la persona** (`Amélie` / voix `multilingual_v2`) et des **4 CAPABILITIES OTOIA** (`aec.py` + `knowledge.py` + `prompt_engine.py` + `chat.py`) du montage Chat par portail `frontend/chat_otoia` — portées par `out/chat_mount.json` (5 configs runtime) + `out/MANIFEST.json`, **byte-gatés pour la reproductibilité** (`check_artifacts` prouve la reconstruction depuis `chat_spec.json`) mais **JAMAIS ANCRÉES à CLAUDE.md §Architecture cible**, leur source faisant autorité (« Voix Amélie QC (multilingual_v2) » l.30 · « OTOIA capabilities : aec.py + knowledge.py + prompt_engine.py + chat.py » l.28).
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||
close ; poursuite de la série anti-dérive d'ANCRAGE à CLAUDE.md (classe
|
||||
[[claude-md-constant-anchor-gate]]). **Même patron EXACT** que les tokens branding
|
||||
`publiciste/branding` ancrés sur CLAUDE.md #4, la marque SEO `Organization` ancrée sur
|
||||
§Entités, les paramètres canoniques `faisabilite/generator` ancrés sur #9/#10 —
|
||||
appliqué à une **surface distincte** : la persona + les capabilities émises dans CHAQUE
|
||||
portail rôle, dont la reproductibilité byte était gatée mais pas l'ancrage.
|
||||
|
||||
**Dérive silencieuse fermée** :
|
||||
- `05_deliverables_mvp/frontend/chat_otoia/out/chat_mount.json` — les 5 mounts,
|
||||
chacun `persona: {nom: "Amélie", voix: "multilingual_v2"}` +
|
||||
`capabilities: [aec.py, knowledge.py, prompt_engine.py, chat.py]` ; idem
|
||||
`out/MANIFEST.json` ; l'INPUT `chat_spec.json` (persona + `capabilities[].module`) ;
|
||||
la table README:28-29 (« Persona `Amélie` / `multilingual_v2` » · « Capabilities
|
||||
`aec/knowledge/prompt_engine/chat.py` » toutes deux annotées « `CLAUDE.md
|
||||
§Architecture cible` »).
|
||||
- Source faisant autorité : **CLAUDE.md §Architecture cible** (l.28 capabilities ·
|
||||
l.30 « Voix Amélie QC (multilingual_v2) »).
|
||||
- Piège #6 : le SEUL contrôle d'identité vit dans `tests/`
|
||||
(`test_persona_amelie_sourcee` / `test_capabilities_sourcees_sans_ajout`) mais son
|
||||
**ORACLE est HARDCODÉ** (`self.assertEqual(spec["persona"]["nom"], "Amélie")` ·
|
||||
`caps == ["aec.py", …]`) — une **copie de plus, jamais comparée à CLAUDE.md**.
|
||||
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 du test (tests VERTS) → le chat
|
||||
monté dans CHAQUE portail annoncerait une persona/voix/capabilities qui CONTREDIT le
|
||||
mandat — « vert trompeur » qu'aucune suite `tests/` (FONCTIONS de génération, jamais
|
||||
l'ancre à CLAUDE.md) n'attrape.
|
||||
- **État courant** : **aucune valeur périmée** — persona + capabilities recoupent
|
||||
CLAUDE.md exactement (anti-invention #6, rien à réécrire). Le défaut est l'**ancrage
|
||||
ungated**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Chat OTOIA · persona +
|
||||
capabilities ancrées CLAUDE.md » avant `sys.exit`) : persona+capabilities re-dérivées
|
||||
de CLAUDE.md §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
|
||||
portent persona/capabilities == CLAUDE.md ; **(d)** README cite l'ancre §Architecture
|
||||
cible + nomme persona (`Amélie`+`multilingual_v2`) + chaque capability (notation
|
||||
compacte matchée par radical) ; **(e)** l'ORACLE du test == CLAUDE.md (set-diff). Un
|
||||
claim absent échoue AUSSI (traçabilité #6).
|
||||
|
||||
**8 morsures vérifiées** : CLAUDE.md `Amélie→Sophie` (mord SIMULTANÉMENT
|
||||
spec/mount/README/oracle — 5 fails, l'ancre est vive) · CLAUDE.md retire `chat.py`
|
||||
(3 fails) · 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 : 5 sous-checks (a/b/c/d/e) verts ·
|
||||
exit 0. Working tree byte-restauré (`git checkout --`, **JAMAIS** `git clean`, interdit
|
||||
absolu) · **7 gates re-verts** · unittest module `chat_otoia` re-vert.
|
||||
|
||||
- `ci/README.md` (table récap du pipeline + paragraphe détaillé « Persona &
|
||||
capabilities du Chat OTOIA ancrées sur CLAUDE.md §Architecture cible ») mis à jour.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'import réel des
|
||||
`Custom Block` + le câblage de l'endpoint OTOIA restent côté agent ERPNext Backend / VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
+33
-1
File diff suppressed because one or more lines are too long
@@ -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=$?
|
||||
|
||||
Reference in New Issue
Block a user