diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index ef58af8..575cb0d 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -2502,3 +2502,49 @@ verrouillés) · `guard_constraints` · `validate_json`. `check_regression` · `guard_constraints` · `validate_json`). - **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo). - **Auto-score 4Big** : 96/100. + +## Sprint 8 · buffer · RBAC/apply_plan (2e surface) · la TABLE « Run-book généré (SPEC §7) » · le GRAPHE d'application ordonné (# · Responsable · Dépend de) +- **Constat** : le README `rbac/apply_plan` porte la table « ## Run-book généré + (SPEC §7 · ordre d'application VPS) » (README:38-46) — les **6 étapes ordonnées** + que l'agent ERPNext suit **pas à pas** sur le VPS. Ses trois colonnes d'identité + sont data-derived de `out/apply_plan.json` : `#` = `order`, `Responsable` = + `responsable` (`worker`/`vps`/`worker+vps`), `Dépend de` = les **n°s d'ordre** des + `depends_on`. Le bloc « apply_plan » existant de `check_readme_claims.sh` (bloc 2) + ne gate QUE la ligne AGRÉGÉE « Génération réelle » (8 comptes consolidés) — ces + trois colonnes étaient un **wildcard**. +- **Data-derived** : `out/apply_plan.json` (liste de 6 étapes · `order`/`id`/ + `responsable`/`depends_on`), byte-gaté par `check_artifacts`. Ce dernier ne prouve + QUE `apply_plan==build` (byte-for-byte) ; l'identité des LIGNES de la table (qui + fait quoi · dans quel ordre) restait non gatée. +- **Preuve reproduite** : passer `Responsable` de l'étape 4 (`worker+vps`) à `vps`, + ou retirer une dépendance de l'étape 6 (`4, 5`→`4`) dans le README → + `check_readme_claims` **exit 0** (le bloc 2 ne voyait que les comptes agrégés). Un + responsable périmé (étape VPS attribuée au worker) ou une dépendance périmée + (`Role Profile` importé **avant** les `Role`) est un hazard réel — le risque même + que le run-book veut prévenir — qu'aucune suite `tests/` (qui teste des fonctions, + pas la table commitée) n'attrape. +- **Gate ajouté** (`ci/check_readme_claims.sh`, sous-bloc « 2bis) apply_plan — TABLE + Run-book ») : (1) **IDENTITÉ par étape** — chaque ligne porte EXACTEMENT + `#`=`order` + `Responsable`=`responsable` + `Dépend de`=n°s d'ordre des + `depends_on` (deps extraits par digits ⇒ robuste au séparateur / à l'em-dash) ; + la colonne « Étape » reste libre (paraphrase). (2) **IDENTITÉ d'ensemble** — + {ordres des lignes} == {ordres de l'artefact}, sans doublon (aucune ligne FANTÔME, + aucune étape MANQUANTE). Cohérences croisées en bonus (mordent un plan INTERNEMENT + incohérent) : ordres contigus `1..N` · `responsable` ∈ {worker, vps, worker+vps} · + toute dépendance pointe en **arrière** (n° < n° de l'étape ⇒ pas de cycle/renvoi + avant). Un claim absent échoue AUSSI. +- **8 morsures vérifiées** : README étape4 responsable `worker+vps`→`vps` (câblage) · + README étape6 `Dépend de 4,5`→`4` (dépendance manquante) · README étape3 + `1,2`→`1,2,5` (dépendance fantôme) · ligne FANTÔME `#7` (7≠6) · étape 5 SUPPRIMÉE + (INTROUVABLE + 5≠6) · artefact étape2 responsable `vps`→`worker` (README stale) · + artefact étape2 dep en AVANT (`verify-http-qa` · graphe incohérent) · artefact + étape1 responsable `robot` hors domaine. Restauré = green : 6 lignes == 6 étapes · + responsables/deps == artefact · ordres 1..6 contigus · exit 0. +- **État courant** : aucune valeur périmée (anti-invention #6, rien à réécrire) — le + défaut est la surface **ungated** (colonnes wildcardées). `ci/README.md` (table + + détail « 2ᵉ surface `rbac/apply_plan` ») mis à jour · working tree byte-restauré + (arbre propre · `git clean` cité ici · ci-allow) · **7 gates re-verts** (`check_readme_claims` · `check_artifacts` · + `check_docs` · `check_ci_integrity` · `check_regression` · `guard_constraints` · + `validate_json`). +- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index e7c7d78..63af57b 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) == 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) == 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 @@ -183,7 +183,23 @@ exige que le **nombre de lignes de profil** de la table == `|profiles|` (aucune ligne fantôme d'un profil inventé absent du contrat), et en bonus deux cohérences croisées qui mordent un MANIFEST **internement** incohérent : `portails_metier + portails_techniques == role_profiles` **et** ces comptes -recomptent les flags `metier` réels des profils. +recomptent les flags `metier` réels des profils. **2ᵉ surface `rbac/apply_plan`** : +le bloc ci-dessus ne gatait que la ligne **agrégée** de comptes ; la table +« ## Run-book généré (SPEC §7) » — le **graphe d'application ordonné** que l'agent +ERPNext suit pas à pas — laissait ses colonnes `#`/`Responsable`/`Dépend de` en +**wildcard**. Or ces trois colonnes sont data-derived de `out/apply_plan.json` +(byte-gaté par `check_artifacts`) : le `#` = `order`, `Responsable` = +`responsable` (`worker`/`vps`/`worker+vps`), `Dépend de` = les n°s d'ordre des +`depends_on`. Un responsable périmé (étape VPS attribuée au worker) ou une +dépendance périmée (Role Profile importé **avant** les Role) est un hazard réel +qu'aucune suite `tests/` (qui teste des fonctions, pas la table commitée) n'attrape +et que le bloc de comptes ignore. On recompute `#`/`responsable`/`deps` depuis +`apply_plan.json` et on exige que la table les énonce **exactement** (la colonne +« Étape » reste libre — paraphrase) ; l'**identité d'ensemble** (lignes de la table +== é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). 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 aa73b40..c7695d9 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -780,6 +780,80 @@ if ap_man is not None and ap_rd is not None: bad(f"RBAC apply_plan · {AP_RD} dit {'/'.join(ma.groups())} MAIS MANIFEST " f"dit {'/'.join(ap_exp)} ({ap_src}) → régénérer le README") +# 2bis) apply_plan — IDENTITÉ de la TABLE « Run-book généré » (SPEC §7) ---------- +# Le bloc 2 ci-dessus ne gate que la ligne AGRÉGÉE « Génération réelle » (comptes). +# La table « ## Run-book généré » énumère, PAR étape, trois données data-derived de +# apply_plan.json (byte-gaté par check_artifacts) : le n° d'ordre (#), le +# RESPONSABLE (worker/vps/worker+vps) et la colonne « Dépend de » (les n°s d'ordre +# des `depends_on`). C'est le GRAPHE D'APPLICATION ORDONNÉ que l'agent ERPNext suit +# pas à pas — un responsable ou une dépendance périmés (ex. Role Profile importé +# AVANT les Role, ou une étape VPS attribuée au worker) sont un hazard réel, +# invisible à check_artifacts (qui ne prouve QUE apply_plan==build) ET au bloc de +# comptes (aveugle à l'identité des lignes ; ces colonnes étaient un WILDCARD). +# Même classe que la colonne « Type » de roleprofile_gen et la table demo/scenarios. +# On RECOMPUTE #/responsable/deps depuis apply_plan.json (zéro duplication) et on +# exige que la table les énonce EXACTEMENT. Une étape MANQUANTE ou une ligne FANTÔME +# échoue AUSSI (identité d'ensemble). La colonne « Étape » reste libre (paraphrase). +# Cohérences croisées en bonus (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 ⇒ pas de cycle/renvoi avant). +try: + ap_plan = load("rbac/apply_plan/out/apply_plan.json") +except (OSError, KeyError, ValueError) as e: + bad(f"RBAC apply_plan · run-book illisible : {e}"); ap_plan = None +if ap_plan is not None: + id2order = {s["id"]: s["order"] for s in ap_plan} + ALLOWED_RESP = {"worker", "vps", "worker+vps"} + orders = sorted(s["order"] for s in ap_plan) + if orders == list(range(1, len(ap_plan) + 1)): + good(f"RBAC apply_plan · run-book — ordres contigus 1..{len(ap_plan)} " + f"(aucun trou/doublon)") + else: + bad(f"RBAC apply_plan · run-book — ordres NON contigus {orders} " + f"(attendu 1..{len(ap_plan)}) → plan incohérent") + # Lignes de la table lues en RAW : la colonne 1 ENTIÈRE ⇒ scope la table + # run-book (seule table du README à 1ʳᵉ colonne numérique · vérifié). + try: + ap_lines = open(AP_RD, encoding="utf-8").read().splitlines() + except OSError as e: + bad(f"RBAC apply_plan · {AP_RD} illisible : {e}"); ap_lines = [] + table_orders, rows_by_order = [], {} + for ln in ap_lines: + cells = [c.strip() for c in ln.strip().strip("|").split("|")] + if len(cells) == 4 and re.fullmatch(r"\d+", cells[0]): + o = int(cells[0]); table_orders.append(o); rows_by_order[o] = cells + if sorted(table_orders) == orders and len(table_orders) == len(set(table_orders)): + good(f"RBAC apply_plan · run-book — {len(table_orders)} lignes == " + f"{len(ap_plan)} étapes (aucune FANTÔME/MANQUANTE)") + else: + bad(f"RBAC apply_plan · run-book — lignes table {sorted(table_orders)} ≠ " + f"étapes artefact {orders} (ligne en trop/manquante ou doublon)") + for s in sorted(ap_plan, key=lambda x: x["order"]): + o, resp = s["order"], s["responsable"] + deps = sorted(id2order[x] for x in s["depends_on"]) + if resp not in ALLOWED_RESP: + bad(f"RBAC apply_plan · étape {o} — responsable artefact « {resp} » " + f"hors {sorted(ALLOWED_RESP)} (plan incohérent)") + if any(d >= o for d in deps): + bad(f"RBAC apply_plan · étape {o} — dépendance en AVANT {deps} " + f"(≥ {o}) : graphe incohérent") + row = rows_by_order.get(o) + if row is None: + bad(f"RBAC apply_plan · étape {o} — ligne INTROUVABLE dans {AP_RD} " + f"(attendu responsable={resp} deps={deps or '—'})"); continue + got_resp = row[1] + got_deps = sorted(int(x) for x in re.findall(r"\d+", row[3])) + if got_resp == resp: + good(f"RBAC apply_plan · étape {o} — responsable « {got_resp} » == artefact") + else: + bad(f"RBAC apply_plan · étape {o} — {AP_RD} dit responsable « {got_resp} » " + f"MAIS artefact dit « {resp} » → régénérer le README") + if got_deps == deps: + good(f"RBAC apply_plan · étape {o} — Dépend de {got_deps or '—'} == artefact") + else: + bad(f"RBAC apply_plan · étape {o} — {AP_RD} « Dépend de » = {got_deps or '—'} " + f"MAIS artefact dit {deps or '—'} → régénérer le README") + # 3) userperm — ventilation par mécanisme d'enforcement ------------------------- UP_RD = f"{D}/rbac/userperm_gen/README.md" try: