diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 51de60e..ef58af8 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,60 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_220140` · Buffer S8 · Domaine Legal/CONFOTUR (2e surface) : la **cross-cohérence PERMISSIONS** du DocType — le CŒUR sécurité du livrable — énumérait EN PROSE, PAR rôle, son **portail** ET son **jeu d'actions RBAC** (`ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report) SANS AUCUN gate d'IDENTITÉ. Le bloc CONFOTUR existant ne gate QUE le COMPTE (« 3 rôles »). + +**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 catalogue RBAC `fixtures_gen` (séparation des pouvoirs `set_user_permissions`) ou +la cross-cohérence e-CF DGII — appliqué à la 2ᵉ surface data-derived du README +`legal/confotur`, jamais gatée. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/legal/confotur/README.md` §« Cœur du livrable : + cross-cohérence » →Permissions (l.28-30) énumère, PAR rôle, `role_id` + portail + + jeu d'actions, annoncé « **mot pour mot** les `permissions_cibles` RBAC … ni ajout + ni retrait ». +- Sources faisant autorité (byte-gatées par `check_artifacts`) : + `out/MANIFEST.json.roles_rbac_utilises[]` (`role_id` · `portail` · `permissions`, + recomputé du contrat `rbac_50_roles.json` à chaque build) + sa projection + `out/doctype_confotur_application.json.permissions[]` (par nom de rôle ERPNext). +- Piège : le compte « 3 rôles » est AVEUGLE à leur identité. PROMOUVOIR + `ventes-confotur` à `submit` (élévation de privilège cassant la séparation des + pouvoirs dont `is_submittable` est déduit), RETIRER une action, RÉAFFECTER un + portail, RENOMMER un rôle ou AJOUTER une ligne fantôme laissait la prose périmée + pendant que les artefacts disent autre chose → l'agent ERPNext Backend câblerait le + mauvais jeu de permissions (le risque même que la cross-cohérence veut prévenir). + Aucune suite `tests/` (qui teste des FONCTIONS de mapping RBAC, pas la prose) + n'attrape ce « vert trompeur ». +- **État courant** : **aucun jeu d'actions périmé** — les 3 lignes recoupent les + artefacts exactement (anti-invention #6, rien à réécrire). Le défaut est la + **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Legal · CONFOTUR +cross-cohérence PERMISSIONS » après le bloc de comptes CONFOTUR) : (1) jeu d'actions ++ portail de CHAQUE rôle recomputés de `roles_rbac_utilises[]`, prose exigée EXACTE +(set-diff · casse normalisée) ; (2) identité d'ensemble README ⇔ MANIFEST (aucun rôle +fantôme NI manquant). Cohérences croisées en bonus (mordent un artefact INTERNEMENT +incohérent) : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation +des pouvoirs — `submit` porté par EXACTEMENT un rôle (`legal-directeur`) et +`is_submittable == True` déduit de sa présence (README:31). Un claim absent échoue +AUSSI (traçabilité). + +**7 morsures vérifiées** : README promeut `ventes-confotur` à `submit` (en +trop=[submit]) · README réaffecte `legal-onapi` Direction→Ventes (portail) · README +retire `report` de `legal-directeur` (absents=[report]) · README renomme un `role_id` +(fantôme=[legal-conseil] + manquant=[legal-onapi]) · énumération supprimée +(INTROUVABLE) · artefact MANIFEST promeut `ventes-confotur` à `submit` (README périmé ++ séparation cassée `[legal-directeur,ventes-confotur]` + incohérence MANIFEST⇄DocType) +· artefact DocType `is_submittable=false` alors qu'un rôle porte `submit` (incohérence +interne) ; restauré = green : 3 rôles portail+actions == MANIFEST · identité d'ensemble +· `submit` singleton · `is_submittable=True` · exit 0. Working tree byte-restauré +(`git checkout --`, JAMAIS `git clean`) · **7 gates re-verts**. + +- `ci/README.md` (table récap + détail « 2ᵉ surface du même README CONFOTUR ») 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. + ## Session `20260731_210134` · Buffer S8 · **CI ROUGE sur `main` — régression réelle réparée** : le gate `guard_constraints.sh` (lancé par `ci.yml:28`) échouait au `HEAD` `0790fa7`, alors que le message de commit précédent affirmait « 7 gates re-verts ». **Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Contrairement aux sessions diff --git a/ci/README.md b/ci/README.md index 71b9252..e7c7d78 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}`) == 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) == 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 @@ -306,7 +306,35 @@ spec du DocType. Les cinq nombres sont recomputés depuis depot_events}`, recomptés du `confotur_spec.json` à chaque build, byte-gaté par `check_artifacts`) et comparés aux **deux** docs. Recomputer **les cinq** volets mord une dérive de n'importe lequel (ajouter un champ/une section/un rôle au spec) là où un -seul nombre resterait aveugle. +seul nombre resterait aveugle. **2ᵉ surface du même README CONFOTUR** — le CŒUR +sécurité du livrable : la section « Cross-cohérence » §Permissions énumère **à la +main**, PAR rôle, son **portail** ET son **jeu d'actions** RBAC (annoncé « mot pour +mot les `permissions_cibles` … ni ajout ni retrait ») — `ventes-confotur` (Ventes) +→ read/write/create/print · `legal-onapi` (Direction) → read/write/create · +`legal-directeur` (Direction) → read/write/**submit**/report. Le compte « 3 rôles » +est **aveugle** à leur identité : **promouvoir** `ventes-confotur` à `submit` +(élévation de privilège cassant la séparation des pouvoirs dont `is_submittable` est +déduit), **retirer** une action, **réaffecter** un portail, **renommer** un rôle ou +**ajouter** une ligne fantôme laisse la prose périmée pendant que les artefacts +byte-gatés (`out/MANIFEST.json.roles_rbac_utilises[]` + `out/doctype_confotur_ +application.json.permissions[]`) disent autre chose → l'agent ERPNext Backend +câblerait le mauvais jeu de permissions (le risque même que la cross-cohérence veut +prévenir), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de mapping RBAC, +pas la prose) n'attrape. On RECOMPUTE le jeu d'actions + le portail de chaque rôle +depuis le MANIFEST et on exige que la prose l'encode **exactement** (set-diff : +absent **et** en trop · casse normalisée), plus l'**identité d'ensemble** (README ⇔ +MANIFEST — aucun rôle fantôme ni manquant). Cohérences croisées en bonus (mordent un +artefact **internement incohérent**) : MANIFEST ⇄ DocType d'accord sur le jeu +d'actions par rôle · **séparation des pouvoirs** — `submit` porté par **exactement +un** rôle (`legal-directeur`, comme le singleton `set_user_permissions` du catalogue +RBAC) et `is_submittable == True` déduit de sa présence (README:31). 7 morsures +vérifiées : README promeut `ventes-confotur` à `submit` (en trop) · README réaffecte +`legal-onapi` Direction→Ventes · README retire `report` de `legal-directeur` +(sous-ensemble) · README renomme un `role_id` (fantôme + manquant) · énumération +supprimée (INTROUVABLE) · MANIFEST promeut `ventes-confotur` à `submit` (README +périmé + séparation cassée + incohérence MANIFEST⇄DocType) · DocType +`is_submittable=false` alors qu'un rôle porte `submit` (incohérence interne) ; +restauré = vert. Même classe enfin sur la **synthèse de l'audit 5D** (livrable `qa/audit_5d`) : la phrase d'ouverture du README du module (« **17 contrôles** répartis en **5 dimensions diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 6af05c7..aa73b40 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -1136,6 +1136,117 @@ if conf_exp is not None: bad(f"Legal confotur · {path_f} — dit {'/'.join(m.groups())} MAIS " f"MANIFEST dit {'/'.join(conf_exp)} ({conf_src}) → régénérer le doc") +# ============================================================================ +# Legal · CONFOTUR — cross-cohérence PERMISSIONS (2e surface du MÊME README) : +# l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact — le CŒUR sécurité du livrable. +# ---------------------------------------------------------------------------- +# Le bloc CONFOTUR ci-dessus ne gate que le COMPTE (« 3 rôles »). La section +# « Cœur du livrable : cross-cohérence » du README (05_deliverables_mvp/legal/ +# confotur/README.md §Permissions) énumère À LA MAIN, PAR rôle, son portail ET son +# jeu d'actions RBAC — annoncé « mot pour mot les permissions_cibles RBAC … ni +# ajout ni retrait » : +# - `ventes-confotur` (portail Ventes) → read/write/create/print +# - `legal-onapi` (portail Direction) → read/write/create +# - `legal-directeur` (portail Direction) → read/write/**submit**/report +# Ces trois lignes sont DATA-DERIVED d'out/MANIFEST.json (`roles_rbac_utilises[]` : +# role_id · portail · `permissions`, recomputé du contrat rbac_50_roles.json à +# chaque build) et projetées dans out/doctype_confotur_application.json +# (`permissions[]` par nom de rôle) — les DEUX byte-gatés par check_artifacts. Le +# compte « 3 rôles » est AVEUGLE à leur identité : PROMOUVOIR `ventes-confotur` à +# `submit` (élévation de privilège — casse la séparation des pouvoirs dont +# is_submittable est déduit), RETIRER une action, RÉAFFECTER un portail, RENOMMER +# un rôle ou AJOUTER une ligne fantôme laisse la prose périmée pendant que les +# artefacts disent autre chose → l'agent ERPNext Backend câblerait le mauvais jeu +# de permissions (le risque même que la cross-cohérence veut prévenir) — « vert +# trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS de mapping RBAC, pas +# la prose) n'attrape. Même patron d'IDENTITÉ que le catalogue RBAC fixtures_gen +# (séparation des pouvoirs `set_user_permissions`) ou la cross-cohérence e-CF. On +# RECOMPUTE chaque jeu d'actions + portail depuis le MANIFEST (zéro duplication du +# contrat #6) et on exige que la prose l'encode EXACTEMENT (set-diff : absent ET en +# trop · casse normalisée), + l'identité d'ensemble (README ⇔ MANIFEST, aucun rôle +# fantôme ni manquant). Cohérences croisées en bonus (mordent un artefact +# INTERNEMENT incohérent) : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par +# rôle · séparation des pouvoirs : `submit` porté par EXACTEMENT un rôle et +# is_submittable=True déduit de sa présence (README:31). Un claim absent échoue +# AUSSI (traçabilité). +# ============================================================================ +CONF_RD2 = f"{D}/legal/confotur/README.md" +_CONF_PERM_FLAGS = ("read", "write", "create", "submit", "cancel", "amend", + "delete", "report", "export", "import", "print", "email", "share") +try: + conf_roles = load("legal/confotur/out/MANIFEST.json")["roles_rbac_utilises"] + conf_perm_by_id = {r["role_id"]: set(r["permissions"]) for r in conf_roles} + conf_portail_by_id = {r["role_id"]: r["portail"] for r in conf_roles} + conf_name_by_id = {r["role_id"]: r["erpnext_role_name"] for r in conf_roles} + conf_dt = load("legal/confotur/out/doctype_confotur_application.json") + conf_dt_perm_by_name = {p["role"]: {f for f in _CONF_PERM_FLAGS if p.get(f)} + for p in conf_dt["permissions"]} + conf_submittable = bool(conf_dt.get("is_submittable")) +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Legal confotur perms · artefact illisible/inattendu : {e}"); conf_roles = None +if conf_roles: + # bonus 1 : les DEUX artefacts (MANIFEST ⇄ DocType) d'accord sur le jeu d'actions + for rid, perms in conf_perm_by_id.items(): + nm = conf_name_by_id[rid] + dt_perms = conf_dt_perm_by_name.get(nm) + if dt_perms is None: + bad(f"Legal confotur perms · rôle `{rid}` ({nm}) ABSENT des permissions du DocType") + elif dt_perms != perms: + bad(f"Legal confotur perms · incohérence artefacts pour `{rid}` : MANIFEST " + f"{sorted(perms)} MAIS DocType {sorted(dt_perms)}") + else: + good(f"Legal confotur perms · `{rid}` — MANIFEST {sorted(perms)} == DocType (accord)") + # bonus 2 : séparation des pouvoirs — `submit` porté par EXACTEMENT un rôle, et + # is_submittable DÉDUIT de sa présence (README:31). Singleton sécurité (cf. RBAC Admin). + conf_submitters = {rid for rid, p in conf_perm_by_id.items() if "submit" in p} + if len(conf_submitters) != 1: + bad(f"Legal confotur perms · séparation des pouvoirs : action `submit` portée par " + f"{sorted(conf_submitters)} (attendu 1 — ni vacant, ni élévation de privilège)") + elif conf_submittable is not True: + bad(f"Legal confotur perms · un rôle porte `submit` MAIS is_submittable=" + f"{conf_submittable} → DocType incohérent") + else: + good(f"Legal confotur perms · séparation des pouvoirs — `submit` singleton " + f"{sorted(conf_submitters)} · is_submittable=True (déduit · README:31)") + conf_rd2 = read_norm(CONF_RD2) + if conf_rd2 is not None: + for rid in sorted(conf_perm_by_id): + exp_perms = conf_perm_by_id[rid] + exp_portail = conf_portail_by_id[rid] + m = re.search(rf"`{re.escape(rid)}` \(portail ([^)]+)\) → ([\w/*]+)", conf_rd2) + if m is None: + bad(f"Legal confotur perms · {CONF_RD2} — ligne du rôle `{rid}` INTROUVABLE " + f"(attendu portail {exp_portail} · {sorted(exp_perms)})") + continue + got_portail = m.group(1).strip().lower() + got_perms = {t.replace("*", "").strip().lower() + for t in m.group(2).split("/") if t.replace("*", "").strip()} + if got_portail != exp_portail: + bad(f"Legal confotur perms · `{rid}` — {CONF_RD2} dit portail " + f"« {m.group(1).strip()} » MAIS MANIFEST dit « {exp_portail} » → régénérer") + elif got_perms != exp_perms: + miss = sorted(exp_perms - got_perms); extra = sorted(got_perms - exp_perms) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"Legal confotur perms · `{rid}` — {CONF_RD2} énumère {sorted(got_perms)} " + f"MAIS MANIFEST {sorted(exp_perms)}{detail} → régénérer le README") + else: + good(f"Legal confotur perms · `{rid}` — portail {got_portail} · actions " + f"{sorted(got_perms)} == MANIFEST") + # identité d'ensemble : README ⇔ MANIFEST (aucun rôle fantôme NI manquant) — + # rd_ids = rôles réellement écrits dans la prose §Permissions (patron identique). + rd_ids = set(re.findall(r"`([a-z][a-z-]+)` \(portail [^)]+\) → [\w/*]+", conf_rd2)) + exp_ids = set(conf_perm_by_id) + if rd_ids == exp_ids: + good(f"Legal confotur perms · identité d'ensemble — {sorted(rd_ids)} == " + f"roles_rbac_utilises (aucun rôle fantôme/manquant)") + else: + miss = sorted(exp_ids - rd_ids); extra = sorted(rd_ids - exp_ids) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop (fantôme)={extra}" if extra else "") + bad(f"Legal confotur perms · {CONF_RD2} — rôles énumérés {sorted(rd_ids)} MAIS " + f"MANIFEST {sorted(exp_ids)}{detail} → régénérer le README") + # ============================================================================ # Audit 5D · synthèse de l'audit de conformité (contrôles / dimensions / verdict # pass-fail-à confirmer) — même classe de « vert trompeur », transcrite À LA MAIN