[DTP-Worker] Sprint 8 · buffer · Fiches agents/rattachement Workspace : les ASSERTIONS D'APPARTENANCE Has Role des trois fiches 03_agents/{rendu,ifc_speckle,mobile}/AGENT.md — QUELLE CONSOLE (Workspace ERPNext natif v15) le rôle peut atteindre — étaient une surface data-derived DISTINCTE du contrat rbac_50_roles.json (la membership vit dans frontend/portails/out/workspace.json, byte-gaté par check_artifacts) et HORS de tout gate.

Les blocs Fiches Faisabilité / Fiche Mobile ne gataient que les attributs du CONTRAT ; le bloc « triplets par workspace » ne gate que le COMPTE de rôles par Workspace (`nb_roles`), AVEUGLE à QUEL rôle. Rattacher le rôle mobile à `OTO Ventes` (SUR-EXPOSITION console : le dev mobile gagnerait l'accès au portail Ventes — la restriction même que la note #6 pose), retirer/déplacer le rôle rendu de `OTO Construction`, laissait la fiche périmée en silence pendant que l'artefact dit autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais accès console.

Gate ajouté (nouveau bloc « Fiches agents · rattachement Workspace ») : index recomputé `erpnext_role_name → {titres de Workspace le portant}` depuis workspace.json (nom résolu du contrat par id, zéro duplication). 3 formes d'assertion : in_named (rendu — prose cite CE Workspace ET membership == {lui seul}) · in_any (ifc — membership ≥1) · not_in_named (mobile — assertion NÉGATIVE #6, rôle ABSENT du Has Role du Workspace nommé). Garde anti-typo : Workspace nommé absent de l'artefact ⇒ ROUGE. Claim absent ⇒ ROUGE (traçabilité).

9 morsures vérifiées (4 artefact + 5 fiche/bord) ; restauré vert ; 7 gates re-verts. ci/README.md (récap + paragraphe détaillé) + activity log + mémoire mis à jour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 02:10:19 +00:00
parent bc58809e7d
commit 3a91c9d52f
3 changed files with 197 additions and 1 deletions
+108
View File
@@ -3405,6 +3405,114 @@ if _fais_roles is not None:
bad(f"Fiche {tag} · {fc['file']} — {a} : la fiche ne cite pas "
f"« {exp} » du contrat rbac_50_roles.json (périmé ?) → régénérer")
# ============================================================================
# Fiches agents · rattachement Workspace (`Has Role`) — QUELLE CONSOLE (Workspace
# ERPNext natif v15) le rôle d'une fiche peut atteindre. Surface data-derived
# DISTINCTE des attributs du contrat rbac_50_roles.json : la MEMBERSHIP `Has Role`
# ne vit PAS dans le contrat mais dans frontend/portails/out/workspace.json
# (artefact byte-gaté par check_artifacts). Trois fiches font, EN PROSE, une
# assertion d'appartenance :
# • rendu (:35) — rôle « OTO Faisabilité Rendu 3D » rattaché au Workspace
# NOMMÉ `OTO Construction` (accès console).
# • ifc_speckle (:35) — rôle « OTO Faisabilité IFC Speckle » rattaché à UN
# Workspace ERPNext (générique, non nommé).
# • mobile (:41) — rôle « OTO Plateforme Mobile » VOLONTAIREMENT PAS
# rattaché au Workspace NOMMÉ `OTO Ventes` (assertion
# NÉGATIVE d'honnêteté · #6).
# Aucun gate ne les couvrait : le bloc « triplets par workspace » gate le COMPTE de
# rôles par Workspace (nb_roles), AVEUGLE à QUEL rôle ; les blocs Fiches Faisabilité /
# Fiche Mobile gatent le CONTRAT, pas la membership Has Role. Rattacher le rôle mobile
# à `OTO Ventes` (SUR-EXPOSITION console : le dev mobile gagnerait l'accès au portail
# Ventes — la restriction même que la note #6 pose), retirer le rôle rendu de
# `OTO Construction` (perte d'accès console du spécialiste), ou DÉPLACER le rôle vers
# un autre Workspace, laisse la fiche périmée EN SILENCE pendant que l'artefact dit
# autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais accès console. Aucune
# suite tests/ (qui teste des FONCTIONS de résolution, pas la prose d'une fiche)
# n'attrape ce « vert trompeur ». On RECOMPUTE, depuis workspace.json, l'ensemble des
# Workspaces dont le `Has Role` porte l'erpnext_role_name du rôle (résolu du contrat
# par id, zéro duplication) et on confronte l'assertion de la fiche. Un claim absent
# échoue AUSSI (traçabilité · #6).
# ============================================================================
FICHES_WS = [
{"file": "03_agents/rendu/AGENT.md", "rid": "faisabilite-rendu-3d",
"kind": "in_named", "ws": "OTO Construction"},
{"file": "03_agents/ifc_speckle/AGENT.md", "rid": "faisabilite-ifc-speckle",
"kind": "in_any"},
{"file": "03_agents/mobile/AGENT.md", "rid": "plateforme-mobile",
"kind": "not_in_named", "ws": "OTO Ventes"},
]
_ws_json = _ws_roles = None
try:
_ws_json = load("frontend/portails/out/workspace.json")
_ws_roles = load("rbac/rbac_50_roles.json")["roles"]
except (OSError, KeyError, ValueError, TypeError) as e:
bad(f"Fiches Workspace · workspace.json / rbac_50_roles.json illisible : {e}")
if _ws_json is not None and _ws_roles is not None:
# Index recomputé : erpnext_role_name -> ensemble des titres de Workspace le portant.
_mem, _titles = {}, set()
for ws in _ws_json:
title = ws.get("title")
_titles.add(title)
for hr in (ws.get("roles") or []):
_mem.setdefault(hr.get("role"), set()).add(title)
for fc in FICHES_WS:
tag = fc["file"].split("/")[1]
cand = [r for r in _ws_roles if r.get("id") == fc["rid"]]
if len(cand) != 1:
bad(f"Fiche {tag} · le contrat porte {len(cand)} rôle(s) `{fc['rid']}` "
f"(attendu exactement 1) → contrat changé de forme, vérifier")
continue
rname = cand[0].get("erpnext_role_name")
actual = _mem.get(rname, set()) # Workspaces réels du rôle (Has Role)
ftxt = read_norm(fc["file"])
if ftxt is None:
continue
named = fc.get("ws")
# Un Workspace NOMMÉ par la fiche doit exister dans l'artefact (anti-typo :
# sinon une assertion in_named/not_in_named « verte » viserait un fantôme).
if named is not None and named not in _titles:
bad(f"Fiche {tag} · le Workspace nommé `{named}` n'existe pas dans "
f"workspace.json (titres : {sorted(_titles)}) → fiche/artefact périmé")
continue
if fc["kind"] == "in_named":
if not re.search(rf"rattaché au Workspace `{re.escape(named)}`", ftxt):
bad(f"Fiche {tag} · {fc['file']} — assertion « rattaché au Workspace "
f"`{named}` » INTROUVABLE (claim absent = régression · #6)")
elif actual == {named}:
good(f"Fiche {tag} · rôle « {rname} » ∈ Has Role de `{named}` "
f"(et de lui seul) == workspace.json")
elif named in actual:
bad(f"Fiche {tag} · rôle « {rname} » ∈ `{named}` MAIS AUSSI "
f"{sorted(actual - {named})} — la fiche nomme UN seul Workspace ; "
f"membership élargie dans workspace.json → réviser fiche/artefact")
else:
bad(f"Fiche {tag} · {fc['file']} nomme `{named}` MAIS workspace.json "
f"rattache « {rname} » à {sorted(actual) or 'aucun Workspace'} → "
f"fiche périmée (rôle déplacé/retiré du Has Role), régénérer")
elif fc["kind"] == "in_any":
if not re.search(r"rattaché à un Workspace", ftxt):
bad(f"Fiche {tag} · {fc['file']} — assertion « rattaché à un "
f"Workspace » INTROUVABLE (claim absent = régression · #6)")
elif actual:
good(f"Fiche {tag} · rôle « {rname} » ∈ Has Role de {sorted(actual)} "
f"== workspace.json (≥1 Workspace, comme la fiche l'affirme)")
else:
bad(f"Fiche {tag} · {fc['file']} affirme un rattachement Workspace "
f"MAIS « {rname} » n'est dans le Has Role d'AUCUN Workspace → "
f"fiche périmée")
elif fc["kind"] == "not_in_named":
if not re.search(rf"volontairement pas\*\* rattaché au Workspace "
rf"`{re.escape(named)}`", ftxt):
bad(f"Fiche {tag} · {fc['file']} — assertion négative « volontairement "
f"pas rattaché au Workspace `{named}` » INTROUVABLE (#6)")
elif named not in actual:
good(f"Fiche {tag} · rôle « {rname} » ABSENT du Has Role de `{named}` "
f"== workspace.json (assertion négative #6 tenue)")
else:
bad(f"Fiche {tag} · {fc['file']} affirme « pas rattaché à `{named}` » "
f"MAIS workspace.json met « {rname} » dans son Has Role → "
f"SUR-EXPOSITION console, assertion #6 VIOLÉE")
sys.exit(1 if FAIL else 0)
PY
rc=$?