[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:
@@ -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
|
aucune écriture dans un `out/` ⇒ 0 dérive d'artefact ; conversion PDF + rebuild
|
||||||
Portail Bancables restent côté VPS).
|
Portail Bancables restent côté VPS).
|
||||||
- **Auto-score 4Big** : 96/100.
|
- **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
File diff suppressed because one or more lines are too long
@@ -3194,6 +3194,120 @@ if _peb is not None and rec_units is not None and man_fig is not None:
|
|||||||
else:
|
else:
|
||||||
good(f"Bancable figures · README formule « × {g_tot} » == Σ unités")
|
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)
|
sys.exit(1 if FAIL else 0)
|
||||||
PY
|
PY
|
||||||
rc=$?
|
rc=$?
|
||||||
|
|||||||
Reference in New Issue
Block a user