[DTP-Worker] Sprint 8 · buffer · Mobile/fiche agent : la FICHE D'IDENTITÉ du rôle RBAC plateforme-mobile (03_agents/mobile/AGENT.md:36) — le SEUL ancrage in-repo réellement vérifiable de l'agent mobile (builds/stores hors-repo #8) — recopiait EN PROSE, DEPUIS le contrat rbac_50_roles.json, TOUS les attributs data-derived du rôle (erpnext_role_name · nom_fr · famille/portail · entite_principale · niveau · scope_donnees · modules · perm API Access custom R/W · description) SANS AUCUN gate d'IDENTITÉ. Le README mobile/app_config n'est gaté que sur des COMPTES agrégés (« 5 onglets · 44 rôles »), aveugles à l'identité de CE rôle — la carte que l'agent ERPNext Backend lit pour seeder le rôle. Gate ajouté : chaque attribut recomputé du contrat, prose EXACTE par attribut (sigle de perm canonique ordonné C/R/W/D/S/X/A — élargir/rétrécir change le sigle), description verbatim (emphase/point neutralisés), cohérences croisées famille==portail · perm custom · role_id singleton ; 12 morsures (6 contrat + 6 fiche) vérifiées, restauré vert, 7 gates re-verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3194,6 +3194,120 @@ if _peb is not None and rec_units is not None and man_fig is not None:
|
||||
else:
|
||||
good(f"Bancable figures · README formule « × {g_tot} » == Σ unités")
|
||||
|
||||
# ============================================================================
|
||||
# Fiche Mobile (03_agents/mobile/AGENT.md) · la FICHE D'IDENTITÉ du rôle RBAC
|
||||
# `plateforme-mobile` — le SEUL ancrage in-repo réellement vérifiable de l'agent
|
||||
# mobile (les builds/stores étant hors-repo, #8), transcrit À LA MAIN sans gate.
|
||||
# ----------------------------------------------------------------------------
|
||||
# La ligne « Points de contact réellement commités » (AGENT.md:36) recopie, DEPUIS
|
||||
# le contrat rbac_50_roles.json (source de vérité RBAC, schéma-validé · byte-gaté
|
||||
# par les 3 MANIFEST recomputés), TOUS les attributs data-derived du rôle : son
|
||||
# `erpnext_role_name` (« OTO Plateforme Mobile »), son `nom_fr`, sa `famille`/
|
||||
# `portail`, son `entite_principale`, son `niveau`, son `scope_donnees`, ses
|
||||
# `modules` et sa `permission_cible` (`API Access` custom R/W) + sa `description`.
|
||||
# C'est la carte que l'agent ERPNext Backend lit pour seeder le rôle. AUCUN gate ne
|
||||
# comparait ces valeurs au contrat : le README mobile/app_config est gaté sur des
|
||||
# COMPTES agrégés (« 5 onglets · 44 rôles »), AVEUGLES à l'identité de CE rôle. Éditer
|
||||
# le contrat — élever le `scope_donnees` (`groupe`→`entite` : sur-portée), changer
|
||||
# le `niveau`, l'`entite_principale`, renommer le rôle, ajouter/retirer un module ou
|
||||
# élargir la permission (R/W→+delete) — laisse la fiche périmée en silence pendant
|
||||
# que le contrat dit autre chose ⇒ l'agent câblerait le mauvais rôle (l'invention
|
||||
# même que #6 interdit). Aucune suite tests/ (qui teste des FONCTIONS de résolution
|
||||
# RBAC, pas la prose d'une fiche) n'attrape ce « vert trompeur » — même patron
|
||||
# d'IDENTITÉ que la table gardes CRM/workflow_vente ou le mapping RBAC userperm_gen.
|
||||
# On RECOMPUTE chaque attribut depuis le contrat (zéro duplication) et on exige que
|
||||
# la fiche les énonce EXACTEMENT. Cohérences croisées bonus (mordent un contrat
|
||||
# INTERNEMENT incohérent) : `famille` == `portail` (invariant de bijection portail) ·
|
||||
# la permission est bien `custom` · l'ensemble des rôles porte CE role_id UNE fois.
|
||||
# Un claim absent échoue AUSSI (traçabilité). La description (data-derived) est
|
||||
# recoupée verbatim, emphase markdown et point final neutralisés.
|
||||
# ============================================================================
|
||||
MOB = "03_agents/mobile/AGENT.md"
|
||||
RID = "plateforme-mobile"
|
||||
try:
|
||||
roles = load("rbac/rbac_50_roles.json")["roles"]
|
||||
mob_role = [r for r in roles if r.get("id") == RID]
|
||||
except (OSError, KeyError, ValueError, TypeError) as e:
|
||||
bad(f"Fiche Mobile · rbac_50_roles.json illisible/inattendu : {e}"); mob_role = None
|
||||
mob_txt = read_norm(MOB)
|
||||
if mob_role is not None and mob_txt is not None:
|
||||
if len(mob_role) != 1:
|
||||
bad(f"Fiche Mobile · le contrat porte {len(mob_role)} rôle(s) `{RID}` "
|
||||
f"(attendu exactement 1) → contrat changé de forme, vérifier")
|
||||
else:
|
||||
r = mob_role[0]
|
||||
# cellule « Ce qu'il fixe » de la ligne plateforme-mobile (sans le `|` de fin)
|
||||
seg = re.search(rf"→ rôle `{re.escape(RID)}` \|(.+?)\|", mob_txt)
|
||||
if seg is None:
|
||||
bad(f"Fiche Mobile · {MOB} — ligne de contact du rôle `{RID}` INTROUVABLE "
|
||||
f"(un claim absent est une régression · #6)")
|
||||
else:
|
||||
cell = seg.group(1)
|
||||
# cohérence croisée : famille == portail (bijection portail du contrat)
|
||||
if r.get("famille") != r.get("portail"):
|
||||
bad(f"Fiche Mobile · contrat incohérent : famille={r.get('famille')} "
|
||||
f"≠ portail={r.get('portail')} → bijection portail cassée")
|
||||
# permission_cible : doctype + flag custom + verbes → « R/W »
|
||||
perms = r.get("permissions_cibles") or []
|
||||
perm = perms[0] if len(perms) == 1 else None
|
||||
if perm is None:
|
||||
bad(f"Fiche Mobile · rôle `{RID}` — attendu 1 permission_cible, "
|
||||
f"trouvé {len(perms)} → contrat changé, vérifier la fiche")
|
||||
elif not perm.get("custom", False):
|
||||
bad(f"Fiche Mobile · rôle `{RID}` — permission « {perm.get('doctype')} » "
|
||||
f"non `custom` dans le contrat MAIS la fiche la dit custom → aligner")
|
||||
acts = set(perm.get("actions", [])) if perm else set()
|
||||
# verbes → sigle canonique ordonné : élargir la perm (ex. +delete) change
|
||||
# le sigle attendu ⇒ la fiche « R/W » périme et le gate mord.
|
||||
_VERB = [("create", "C"), ("read", "R"), ("write", "W"), ("delete", "D"),
|
||||
("submit", "S"), ("cancel", "X"), ("amend", "A")]
|
||||
rw = "/".join(sig for k, sig in _VERB if k in acts)
|
||||
if acts - {k for k, _ in _VERB}:
|
||||
bad(f"Fiche Mobile · rôle `{RID}` — verbe(s) inconnu(s) "
|
||||
f"{sorted(acts - {k for k, _ in _VERB})} dans le contrat → étendre le gate")
|
||||
mods = "+".join(f"`{m}`" for m in (r.get("modules") or []))
|
||||
# chaque attribut data-derived recomputé du contrat → prose EXACTE exigée
|
||||
attrs = [
|
||||
("nom d'affichage ERPNext",
|
||||
rf"rôle ERPNext \*\*`{re.escape(r['erpnext_role_name'])}`\*\*",
|
||||
r["erpnext_role_name"]),
|
||||
("nom_fr", rf"« {re.escape(r['nom_fr'])} »", r["nom_fr"]),
|
||||
("famille/portail",
|
||||
rf"famille/portail \*\*`{re.escape(r['portail'])}`\*\*", r["portail"]),
|
||||
("entité principale",
|
||||
rf"entité \*\*{re.escape(r['entite_principale'])}\*\*",
|
||||
r["entite_principale"]),
|
||||
("niveau", rf"niveau {r['niveau']}(?!\d)", str(r["niveau"])),
|
||||
("scope_donnees",
|
||||
rf"scope `{re.escape(r['scope_donnees'])}`", r["scope_donnees"]),
|
||||
("modules", rf"modules {re.escape(mods)}(?![+`])",
|
||||
"+".join(r.get("modules") or [])),
|
||||
]
|
||||
if perm is not None:
|
||||
attrs.append(("permission cible",
|
||||
rf"perm `{re.escape(perm['doctype'])}` custom {re.escape(rw)}(?![\w/])",
|
||||
f"{perm['doctype']} custom {rw}"))
|
||||
for label, pat, exp in attrs:
|
||||
if re.search(pat, cell):
|
||||
good(f"Fiche Mobile · rôle `{RID}` — {label} « {exp} » == contrat")
|
||||
else:
|
||||
bad(f"Fiche Mobile · {MOB} — {label} : la fiche ne cite pas "
|
||||
f"« {exp} » du contrat rbac_50_roles.json (périmé ?) → régénérer")
|
||||
# description data-derived : recoupée verbatim (emphase + point final neutralisés)
|
||||
def _plain(s):
|
||||
return re.sub(r"\s+", " ", s.replace("*", "")).strip().rstrip(".").strip()
|
||||
md = re.search(r"description « (.+?) »\)", cell)
|
||||
exp_desc = _plain(r.get("description", ""))
|
||||
if md is None:
|
||||
bad(f"Fiche Mobile · {MOB} — la description « … » du rôle `{RID}` "
|
||||
f"INTROUVABLE (attendu « {exp_desc} »)")
|
||||
elif _plain(md.group(1)) == exp_desc:
|
||||
good(f"Fiche Mobile · rôle `{RID}` — description == contrat "
|
||||
f"(emphase/point neutralisés)")
|
||||
else:
|
||||
bad(f"Fiche Mobile · {MOB} — description « {_plain(md.group(1))} » "
|
||||
f"MAIS contrat « {exp_desc} » → description périmée, régénérer")
|
||||
|
||||
sys.exit(1 if FAIL else 0)
|
||||
PY
|
||||
rc=$?
|
||||
|
||||
Reference in New Issue
Block a user