diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md new file mode 100644 index 0000000..ab82c24 --- /dev/null +++ b/05_activity_log/2026-08-01.md @@ -0,0 +1,64 @@ +# Activity Log · 2026-08-01 · Claude Code DTP + +## Session `20260801_000201` · Buffer S8 · Domaine RBAC/userperm_gen : la **table « Mapping `scope_donnees` → mécanisme »** — la **FONCTION d'enforcement ROW-LEVEL** (quel mécanisme Frappe natif applique CHAQUE portée de données + si un `User Permission` template est émis), CŒUR sécurité du module — était transcrite EN PROSE (README:33-38) SANS AUCUN gate d'IDENTITÉ. Le bloc RBAC « 3 volets » existant ne gate QUE la **ventilation** par mécanisme (« 28 `entite` · 16 `groupe` · 2 `own` · 4 `equipe` »), un COMPTE aveugle à QUEL mécanisme applique QUELLE portée. + +**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 +le mapping RBAC `fixtures_gen` (séparation des pouvoirs `set_user_permissions`) ou la +cross-cohérence PERMISSIONS Legal/CONFOTUR (`role_id`→actions) — appliqué à la +**surface data-derived distincte du même README `rbac/userperm_gen`** : la table +qui associe chaque portée de données à son mécanisme d'enforcement, jamais gatée hors +son compte agrégé. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/rbac/userperm_gen/README.md:33-38` — table « Mapping + `scope_donnees` → mécanisme » : nomme, PAR portée, le `mechanism` Frappe natif + (`own`→`docperm_if_owner` · `entite`→`user_permission_company` · + `groupe`→`none_consolidated` · `equipe`→`vps_confirm_team`) ET le verdict + « Template émis ? » (`oui` pour `entite` seul, sinon `non (null)`). +- Source faisant autorité (byte-gatée par `check_artifacts`) : + `out/user_permission_plan.json` — chaque entrée porte `scope_donnees` + + `mechanism` + `user_permission_template` (null ou objet). Le mapping est une + **fonction** : chaque portée → exactement un mécanisme ; template émis SSI + `entite`/`user_permission_company`. +- Piège : le bloc de ventilation est AVEUGLE à l'identité de ces couples. + RÉAFFECTER une portée à un mécanisme plus permissif (`entite` → + `none_consolidated` : le **row-level enforcement ABANDONNÉ**, sur-exposition des + données inter-entités — la restriction même que le module pose), RENOMMER un + mécanisme ou BASCULER le verdict « Template émis ? » laisse le README périmé + pendant que l'artefact dit autre chose → l'agent ERPNext Backend câblerait le + mauvais mécanisme (le risque même que la table veut prévenir). Aucune suite + `tests/` (qui teste des FONCTIONS de mapping/résolution, pas la prose) n'attrape + ce « vert trompeur ». +- **État courant** : **aucune ligne périmée** — les 4 couples portée→mécanisme et + leurs verdicts recoupent l'artefact exactement (anti-invention #6, rien à + réécrire). Le défaut est la **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « RBAC/userperm_gen +Mapping » après le bloc CRM gardes) : (1) **par portée** — le `mechanism` (1er token +backtické de la colonne) ET le verdict « Template émis ? » (`**oui**` / `non (null)`) +recomputés de `user_permission_plan.json`, prose exigée EXACTE ; (2) **identité +d'ensemble** — portées de la table == portées de l'artefact (set-diff : ni fantôme +ni manquante). Cohérences croisées en bonus (mordent un plan INTERNEMENT incohérent) : +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` » du README:36/61) · +ensemble des portées NON VIDE. Un claim absent échoue AUSSI (traçabilité). + +**7 morsures vérifiées** : README réaffecte `entite`→`none_consolidated` +(mécanisme périmé · élévation de portée) · README bascule `entite` « Template +émis » oui→non (verdict périmé) · README échange une portée `groupe`→`famille` +(absents=[groupe] + en trop=[famille] + ligne INTROUVABLE) · README retire la +ligne `own` (absents=[own]) · table entière supprimée (les 4 absents + toutes +lignes INTROUVABLES) · **artefact** réaffecte toutes les entrées `entite`→ +`none_consolidated` (README périmé sur mécanisme ET verdict) · **artefact** rend +une entrée `own` porteuse d'un template (« Template émis » NON uniforme +[False,True] · invariant SSI cassé) ; restauré = green : 4 portées · table == +artefact · par-ligne exact · fonction/uniforme/SSI verts · exit 0. Working tree +byte-restauré (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**. + +- `ci/README.md` (table récap du pipeline + paragraphe détaillé « 2ᵉ surface + `rbac/userperm_gen` ») 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). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 45a157b..edd6dc5 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) == 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 ») == 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 @@ -199,7 +199,28 @@ et que le bloc de comptes ignore. On recompute `#`/`responsable`/`deps` depuis == étapes de l'artefact) mord toute ligne fantôme ou étape manquante, et trois cohérences croisées mordent un plan **internement** incohérent : ordres contigus `1..N` sans doublon · `responsable` ∈ {`worker`, `vps`, `worker+vps`} · toute -dépendance pointe en **arrière** (n° < n° de l'étape). +dépendance pointe en **arrière** (n° < n° de l'étape). **2ᵉ surface +`rbac/userperm_gen`** : le bloc des comptes ne gatait que la **ventilation** par +mécanisme (« 28 `entite` · 16 `groupe` · 2 `own` · 4 `equipe` ») — aveugle à +**quel** mécanisme applique **quelle** portée. La table « Mapping `scope_donnees` +→ mécanisme » est la **fonction d'enforcement row-level** (cœur sécurité du +module) : chaque portée → son `mechanism` Frappe natif + le verdict « Template +émis ? ». Ces trois sont data-derived de `out/user_permission_plan.json` +(byte-gaté par `check_artifacts`, chaque entrée portant `scope_donnees` + +`mechanism` + `user_permission_template`). **Réaffecter** une portée à un +mécanisme plus permissif (`entite` → `none_consolidated` : le row-level enforcement +**abandonné**, sur-exposition des données inter-entités), **renommer** un mécanisme +ou **basculer** le verdict « Template émis ? » laisse la table figée pendant que +l'artefact dit autre chose — hazard réel qu'aucune suite `tests/` (qui teste des +fonctions de mapping, pas la table commitée) n'attrape et que le bloc de comptes +ignore. On recompute par portée le `mechanism` (1er token backtické de la colonne) +et le verdict « Template émis ? » et on exige la table **exacte** ; l'**identité +d'ensemble** (portées de la table == portées de l'artefact) mord toute ligne +fantôme ou portée manquante, et trois cohérences croisées mordent un plan +**internement** incohérent : chaque portée mappe **un seul** mécanisme (fonction, +pas relation) · le verdict « Template émis » est **uniforme** sur les entrées +d'une portée · le template est émis **exactement** pour `user_permission_company` +(l'invariant « template SSI `entite` »). Même classe enfin côté **SEO** : le total et la ventilation par langue des mots-clés (« **258** mots-clés (fr=87 · en=87 · es=84) ») étaient transcrits **à diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 742ae7a..8a86669 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2959,6 +2959,125 @@ if wfv_roles is not None: f"« {row.strip()} » MAIS workflow.json dit garde « {exp} » → " f"réaffectation de privilège non répercutée, régénérer") +# ============================================================================ +# RBAC/userperm_gen · la TABLE « Mapping scope_donnees → mécanisme » — la +# FONCTION d'enforcement ROW-LEVEL : quel mécanisme Frappe natif applique CHAQUE +# portée de données, ET si un `User Permission` template est émis. CŒUR sécurité +# du module, transcrit À LA MAIN sans AUCUN gate d'IDENTITÉ. +# ---------------------------------------------------------------------------- +# Le bloc RBAC existant (« 3 volets ») ne gate QUE la VENTILATION par mécanisme +# (« 28 entite · 16 groupe · 2 own · 4 equipe » — un COMPTE, aveugle à QUEL +# mécanisme applique QUELLE portée). Restait, dans le MÊME README (l.33-38), une +# surface data-derived distincte : la table qui énonce, PAR portée, le `mechanism` +# Frappe natif ET le verdict « Template émis ? ». Source faisant autorité +# (byte-gatée par check_artifacts) : rbac/userperm_gen/out/user_permission_plan.json +# — chaque entrée porte `scope_donnees` + `mechanism` + `user_permission_template` +# (null ou objet). Le mapping est une FONCTION : chaque portée → exactement un +# mécanisme, template émis SSI `entite`/`user_permission_company`. RÉAFFECTER une +# portée à un mécanisme plus permissif (`entite` → `none_consolidated` : le +# row-level enforcement ABANDONNÉ, sur-exposition des données inter-entités — la +# restriction même que le module pose), RENOMMER un mécanisme ou basculer le +# verdict « Template émis ? » laisse le README périmé pendant que l'artefact dit +# autre chose → l'agent ERPNext câblerait le mauvais mécanisme (le risque même que +# la table veut prévenir). « Vert trompeur » qu'aucune suite tests/ (qui teste des +# FONCTIONS de mapping/résolution, pas la prose) n'attrape — même patron +# d'IDENTITÉ que la cross-cohérence PERMISSIONS Legal/CONFOTUR ou le catalogue RBAC +# fixtures_gen. +# Bonus (mordent un plan INTERNEMENT incohérent) : 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 » du README:36/61) · ensemble des portées NON VIDE. +# État courant : aucune ligne périmée (anti-invention #6) — le défaut est la surface +# ungated. Un claim absent échoue AUSSI (traçabilité). +# ============================================================================ +try: + up_plan = load("rbac/userperm_gen/out/user_permission_plan.json") + up_scopes, up_tmpl = {}, {} # portée -> set(mécanisme) / set(template non-null?) + for e in up_plan: + s = e["scope_donnees"] + up_scopes.setdefault(s, set()).add(e["mechanism"]) + up_tmpl.setdefault(s, set()).add(e["user_permission_template"] is not None) +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"RBAC userperm map · user_permission_plan.json illisible/inattendu : {e}") + up_scopes = None +if up_scopes is not None: + # bonus 1 : ensemble des portées non vide + if not up_scopes: + bad("RBAC userperm map · plan VIDE (aucune portée) → contrat cassé") + else: + good(f"RBAC userperm map · {len(up_scopes)} portées dans le plan (ensemble non vide)") + # bonus 2 : chaque portée → UN SEUL mécanisme (fonction) + « Template émis » uniforme + scope_mech = {} + for s in sorted(up_scopes): + if len(up_scopes[s]) != 1: + bad(f"RBAC userperm map · portée « {s} » mappe {sorted(up_scopes[s])} " + f"(attendu 1 mécanisme) → plan incohérent (relation, pas fonction)") + continue + scope_mech[s] = next(iter(up_scopes[s])) + if len(up_tmpl[s]) != 1: + bad(f"RBAC userperm map · portée « {s} » — « Template émis ? » NON uniforme " + f"sur ses entrées {sorted(up_tmpl[s])} → plan incohérent") + # bonus 3 : template émis EXACTEMENT pour user_permission_company (l'invariant SSI) + for s, mech in scope_mech.items(): + emitted = up_tmpl[s] == {True} + if emitted != (mech == "user_permission_company"): + bad(f"RBAC userperm map · portée « {s} » (mécanisme={mech}) émet " + f"template={emitted} MAIS l'invariant est « template SSI " + f"user_permission_company » → plan incohérent") + # identité + par-ligne vs README (lignes RAW : la table Mapping est la seule à 3 + # cellules dont col1 = une portée backtickée `[a-z_]+` et col3 = le verdict + # « Template émis ? » **oui** / non (`null`)). + UPM_RD = f"{D}/rbac/userperm_gen/README.md" + try: + upm_lines = open(UPM_RD, encoding="utf-8").read().splitlines() + except OSError as e: + bad(f"RBAC userperm map · {UPM_RD} illisible : {e}"); upm_lines = [] + map_rows = {} + for ln in upm_lines: + cells = [c.strip() for c in ln.strip().strip("|").split("|")] + if len(cells) != 3: + continue + ms = re.fullmatch(r"`([a-z_]+)`", cells[0]) + if ms and re.search(r"\*\*oui\*\*|non \(`null`\)", cells[2]): + map_rows[ms.group(1)] = cells + # identité d'ensemble : portées de la table == portées de l'artefact + tbl, art = set(map_rows), set(scope_mech) + if tbl == art: + good(f"RBAC userperm map · table {sorted(tbl)} == portées de l'artefact " + f"({len(art)} · ni fantôme ni manquante)") + else: + miss = sorted(art - tbl); extra = sorted(tbl - art) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"RBAC userperm map · {UPM_RD} table {sorted(tbl)} MAIS artefact " + f"{sorted(art)}{detail} → régénérer le README") + # par portée : mécanisme (1er token backtické de col2) + verdict « Template émis ? » + for s in sorted(art): + exp_mech = scope_mech[s] + exp_emit = up_tmpl[s] == {True} + row = map_rows.get(s) + if row is None: + bad(f"RBAC userperm map · portée « {s} » — ligne INTROUVABLE dans {UPM_RD} " + f"(attendu mécanisme={exp_mech} · template=" + f"{'oui' if exp_emit else 'non'})") + continue + mm = re.match(r"`([^`]+)`", row[1]) + got_mech = mm.group(1) if mm else "?" + if got_mech == exp_mech: + good(f"RBAC userperm map · portée « {s} » — mécanisme « {got_mech} » == artefact") + else: + bad(f"RBAC userperm map · portée « {s} » — {UPM_RD} dit mécanisme " + f"« {got_mech} » MAIS artefact dit « {exp_mech} » → réaffectation " + f"d'enforcement non répercutée, régénérer") + got_emit = row[2].strip() == "**oui**" + if got_emit == exp_emit: + good(f"RBAC userperm map · portée « {s} » — Template émis=" + f"{'oui' if got_emit else 'non'} == artefact") + else: + bad(f"RBAC userperm map · portée « {s} » — {UPM_RD} dit Template émis=" + f"{'oui' if got_emit else 'non'} MAIS artefact dit " + f"{'oui' if exp_emit else 'non'} → régénérer le README") + sys.exit(1 if FAIL else 0) PY rc=$?