diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 3193ff4..ff84444 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -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. diff --git a/ci/README.md b/ci/README.md index dcede04..6b61aa5 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -704,6 +704,37 @@ README périmés) · model.py marketing `8.5 %→9 %` · CLAUDE.md #10 `Cardnet périmé) · README retire `Letter US` (absent) · README ajoute un token `IBAN` (en trop) · énumération README supprimée (INTROUVABLE) · model.py `USD + DOP → USD` ; restauré = vert. +Même classe enfin sur la **fiche d'identité du rôle RBAC mobile** — la fiche agent +`03_agents/mobile/AGENT.md`. Les builds/soumissions stores étant **hors-repo** (#8), +l'agent mobile le reconnaît et déclare que son **seul ancrage in-repo vérifiable** est +le rôle `plateforme-mobile` du contrat `rbac_50_roles.json` ; sa ligne « Points de +contact réellement commités » en recopie donc **à la main** TOUS les attributs +data-derived : `erpnext_role_name` (« OTO Plateforme Mobile »), `nom_fr` +(« Développeur Mobile »), `famille`/`portail` (`plateforme`), `entite_principale` +(`9060 QC`), `niveau` (`2`), `scope_donnees` (`groupe`), `modules` (`Core`+`Website`), +la **permission** `API Access` custom R/W et la `description`. C'est la carte que +l'agent ERPNext Backend lit pour **seeder le rôle** (`seed_mobile_rbac.py`). Aucun gate +ne comparait ces valeurs au contrat : 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. +Élever le `scope_donnees` (`groupe→entite` : sur-portée inter-entités), changer le +`niveau`/l'`entite_principale`, renommer le rôle, ajouter/retirer un module ou élargir +la permission (`R/W→R/W/D`) laisserait 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), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de résolution RBAC, +pas la prose d'une fiche) n'attrape. On RECOMPUTE chaque attribut depuis le contrat +(zéro duplication · #6) et on exige la prose EXACTE par attribut ; le sigle de +permission est **canonique et ordonné** (C/R/W/D/S/X/A) de sorte qu'élargir OU rétrécir +les verbes change le sigle attendu (un préfixe `R/W` ne satisfait plus `R/W/D`). La +`description` est recoupée **verbatim**, emphase markdown et point final neutralisés. +Cohérences croisées : `famille == portail` (bijection portail du contrat), la +permission est bien `custom`, et le `role_id` figure **une seule fois** dans le contrat. +12 morsures vérifiées (6 côté contrat, 6 côté fiche) : contrat `scope groupe→entite` · +contrat `niveau 2→3` · contrat `+delete` (sigle `R/W→R/W/D`) · contrat description +réécrite · contrat rôle renommé · fiche `modules Website→Selling` · fiche +`entité 9060 QC→WAF` · fiche perm `R/W→R/W/D` (sur-revendication) et `R/W→R` +(sous-revendication) · fiche modules over/under-claim · ligne de rôle supprimée +(INTROUVABLE) ; restauré = vert. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 29f999f..53d5de8 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -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=$?