[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:
Claude Code DTP Worker
2026-08-01 01:13:06 +00:00
parent e8b30d7147
commit 3bce40ea80
3 changed files with 210 additions and 1 deletions
+64
View File
@@ -119,3 +119,67 @@ CLAUDE.md #9 · exit 0. Working tree byte-restauré via `git checkout --` (JAMAI
aucune écriture dans un `out/` ⇒ 0 dérive d'artefact ; conversion PDF + rebuild
Portail Bancables restent côté VPS).
- **Auto-score 4Big** : 96/100.
## Session `20260801_010204` · Buffer S8 · Domaine 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/soumissions stores étant 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` · permission `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 couverts · 3 langues · 13 identifiants »), AVEUGLES à l'identité de CE rôle.
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
close ; poursuite de la série anti-dérive (CLAUDE.md #6). Même patron d'IDENTITÉ que
la table gardes CRM/`workflow_vente` (rôle qui garde chaque transition) ou le mapping
RBAC `userperm_gen` (portée→mécanisme) — appliqué à la **surface distincte** de la
fiche agent mobile : la carte que l'agent ERPNext Backend lit pour **seeder le rôle**
(`seed_mobile_rbac.py`), jamais gatée hors les comptes agrégés de `app_config`.
**Dérive silencieuse fermée** :
- `03_agents/mobile/AGENT.md:36` — ligne « Points de contact réellement commités »,
cellule « Ce qu'il fixe » : nomme, PAR attribut, le rôle ERPNext
**`OTO Plateforme Mobile`** (« Développeur Mobile » · famille/portail
**`plateforme`** · entité **9060 QC** · niveau 2 · scope `groupe` · modules
`Core`+`Website` · perm `API Access` custom R/W · description « Maintient
l'app Expo/React Native et l'API mobile ; gère les builds EAS et les
soumissions stores »).
- Source faisant autorité : `05_deliverables_mvp/rbac/rbac_50_roles.json` (contrat
RBAC, schéma-validé · les 3 MANIFEST RBAC recomputés en dérivent et sont byte-gatés
par `check_artifacts`) → le rôle d'`id` `plateforme-mobile` porte chacun de ces
champs. Autres blocs du gate lisent déjà ce contrat directement (userperm,
workflow_vente, confotur).
- Piège : la seule vérification existante (`tests/`) teste des **FONCTIONS** de
résolution RBAC, jamais la PROSE d'une fiche agent. Éditer le contrat — élever le
`scope_donnees` (`groupe``entite` : sur-portée inter-entités, la restriction même
que le rôle pose), changer le `niveau`/l'`entite_principale`, renommer le rôle,
ajouter/retirer un module ou élargir la permission (`R/W``R/W/D`) — laisse la fiche
périmée pendant que le contrat dit autre chose → l'agent ERPNext Backend câblerait
le mauvais rôle (le risque même que la fiche veut prévenir).
- **État courant** : **aucun attribut périmé** — les 9 attributs + la description
recoupent le contrat exactement (anti-invention #6, rien à réécrire). Le défaut est
la **surface ungated**.
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiche Mobile » avant
`sys.exit`) : (1) **par attribut**`erpnext_role_name` · `nom_fr` · `portail` ·
`entite_principale` · `niveau` · `scope_donnees` · `modules` · permission recomputés
du contrat, prose EXACTE exigée (regex par attribut, lookaheads anti-préfixe pour
niveau/modules/perm) ; (2) **permission** — le sigle des verbes est **canonique et
ordonné** (C/R/W/D/S/X/A) : élargir OU rétrécir la perm change le sigle attendu (un
préfixe `R/W` ne satisfait plus `R/W/D`, ni l'inverse) ; verbe inconnu ⇒ ROUGE
(étendre le gate) ; (3) **description** recoupée **verbatim** (emphase markdown `**` et
point final neutralisés). Cohérences croisées bonus (mordent un contrat INTERNEMENT
incohérent) : `famille == portail` (bijection portail) · permission bien `custom` ·
`role_id` présent EXACTEMENT une fois. Un claim absent échoue AUSSI (traçabilité :
ligne de rôle INTROUVABLE).
**12 morsures vérifiées** (6 côté contrat = fiche périmée · 6 côté fiche = fiche
fausse) : contrat `scope groupe→entite` (sur-portée) · contrat `niveau 2→3` · contrat
`+delete` (sigle `R/W→R/W/D`) · contrat description réécrite · contrat rôle renommé
(`OTO Plateforme Mobile→OTO Mobile Dev`) · fiche `modules Website→Selling` · fiche
`entité 9060 QC→WAF` · fiche perm `R/W→R/W/D` (sur-revendication) · fiche perm `R/W→R`
(sous-revendication) · fiche modules over-claim `+Selling` et under-claim `Core` seul ·
ligne de rôle entièrement supprimée (INTROUVABLE) ; restauré = green : 9 attributs +
description == contrat · famille==portail · perm custom · role_id singleton · exit 0.
Working tree byte-restauré (`git checkout --`, **JAMAIS** `git clean`, interdit
absolu) · **7 gates re-verts**.
- `ci/README.md` (table récap du pipeline + paragraphe détaillé « Fiche d'identité
du rôle RBAC mobile ») 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 ; le seed réel
du rôle `bench` reste côté VPS).
- **Auto-score 4Big** : 96/100.
+32 -1
View File
File diff suppressed because one or more lines are too long
+114
View File
@@ -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=$?