From 082b05763989ec15f5faf408a6ee643a43767b9b Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Fri, 31 Jul 2026 23:11:18 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20CRM/workflow=5Fvente=20(3e=20surface)=20:=20la=20TABLE?= =?UTF-8?q?=20=C2=AB=20R=C3=B4les=20=3D=20contrat=20RBAC=20=C2=BB=20?= =?UTF-8?q?=E2=80=94=20le=20R=C3=94LE=20RBAC=20qui=20GARDE=20chaque=20tran?= =?UTF-8?q?sition=20du=20pipeline=20vente,=20C=C5=92UR=20s=C3=A9curit?= =?UTF-8?q?=C3=A9=20du=20workflow=20=E2=80=94=20=C3=A9tait=20transcrite=20?= =?UTF-8?q?EN=20PROSE=20(README:55-62)=20SANS=20AUCUN=20gate=20d'IDENTIT?= =?UTF-8?q?=C3=89.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les deux blocs CRM/workflow existants gatent le COMPTE (« 9 états · 11 transitions ») ET l'énumération des transitions à séparation des pouvoirs (allow_self_approval=0), jamais l'identité des GARDES. Sources faisant autorité (byte-gatées par check_artifacts) : out/workflow.json (chaque transition porte `allowed` = le rôle gardien) + out/MANIFEST.json.roles_rbac_utilises[].erpnext_role_name (recomputé du contrat rbac_50_roles.json à chaque build). PIÈGE : les blocs COMPTE/séparation sont AVEUGLES à l'identité des gardes → RÉAFFECTER un pas monétaire (« Confirmer réservation » Réservations→Conseiller = élévation de privilège) · RENOMMER un rôle · AJOUTER un fantôme · en OUBLIER un laissait le README périmé pendant que l'artefact dit autre chose → l'agent ERPNext Backend câblerait le mauvais garde (le risque même que la table veut prévenir) — « vert trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS de graphe/résolution RBAC, pas la prose) n'attrape. Même patron d'IDENTITÉ que la cross-cohérence PERMISSIONS Legal/CONFOTUR déjà gatée. Nouveau bloc « CRM gardes » dans ci/check_readme_claims.sh (après le bloc Démo scénarios) : (1) identité d'ensemble — colonne « Rôle » de la table == {allowed} de workflow.json (set-diff · NFKD/casefold) ; (2) cohérence artefacts — {allowed} == roles_rbac_utilises du MANIFEST (aucun garde hors manifeste ni l'inverse) · ensemble NON VIDE ; (3) cross-cohérence par pas — les gardes des étapes SENSIBLES et uniques Confirmer réservation/Signer contrat/Approuver CONFOTUR (recomputés `allowed`, jamais figés) nommés EXACTEMENT dans leur ligne. Le (3) mord la RÉAFFECTATION vers un rôle DÉJÀ présent (rôle servant deux transitions) que le set-diff seul manquerait. Un claim absent échoue AUSSI. 7 morsures vérifiées : README réaffecte Confirmer réservation→Conseiller (absents=[réservations] + pas mordu) · artefact réaffecte Signer contrat→Conseiller (en trop=[contrats] + incohérence MANIFEST + pas mordu) · README rôle fantôme (en trop=[oto fantome]) · README retire un rôle (absents=[direction commerciale]) · artefact MANIFEST perd un garde (garde pas dans MANIFEST=[réservations]) · table supprimée (INTROUVABLE) · artefact réaffecte Approuver CONFOTUR→Directeur (rôle déjà présent, set INCHANGÉ, captée UNIQUEMENT par la cross-cohérence par pas) ; restauré = green : 7 rôles · colonne == {allowed} == roles_rbac_utilises · 3 étapes sensibles pinnées · exit 0. État courant : aucun garde périmé (anti-invention #6, rien à réécrire) — le défaut était la surface ungated. ci/README.md (table + détail « 3e surface workflow_vente ») mis à jour · working tree byte-restauré (git checkout --, JAMAIS git clean) · 7 gates re-verts. Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-07-31.md | 58 ++++++++++++++++++ ci/README.md | 25 +++++++- ci/check_readme_claims.sh | 109 ++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 575cb0d..f3e2ae9 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,63 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_230154` · Buffer S8 · Domaine CRM/workflow_vente (3e surface) : la **table « Rôles = contrat RBAC »** — le **rôle RBAC qui GARDE chaque transition** du pipeline vente, CŒUR sécurité du workflow (qui a le droit d'exécuter quel pas) — était transcrite EN PROSE (README:55-62) SANS AUCUN gate d'IDENTITÉ. Les deux blocs CRM/workflow existants gatent le COMPTE (« 9 états · 11 transitions ») ET l'énumération des transitions à séparation des pouvoirs (`allow_self_approval=0`), jamais l'identité des gardes. + +**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 cross-cohérence PERMISSIONS Legal/CONFOTUR (`role_id`→actions) ou le catalogue +RBAC `fixtures_gen` — appliqué à la **3ᵉ surface data-derived du même README +`crm/workflow_vente`**, jamais gatée. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/crm/workflow_vente/README.md:55-62` — table « Étape → Rôle + qui garde la transition » : nomme, PAR pas, le rôle RBAC autorisé (`Planifier + visite · Émettre devis`→`OTO Ventes Conseiller` · `Confirmer réservation`→`OTO + Ventes Réservations` · `Signer contrat`→`OTO Ventes Contrats` · `Déposer/Approuver + CONFOTUR`→`OTO Ventes CONFOTUR` · `Annuler/Résilier`→`OTO Ventes Directeur · OTO + Direction Commerciale` · etc.). +- Sources faisant autorité (byte-gatées par `check_artifacts`) : + `out/workflow.json` — chaque transition porte `allowed` (le rôle qui garde le pas) ; + ET `out/MANIFEST.json.roles_rbac_utilises[].erpnext_role_name` — l'ensemble des + rôles employés, RECOMPUTÉ du contrat `rbac_50_roles.json` à chaque build. +- Piège : les blocs de COMPTE/séparation sont AVEUGLES à l'identité des gardes. + RÉAFFECTER un pas monétaire (« Confirmer réservation » — soumission d'argent — + `Réservations`→`Conseiller` : **élévation de privilège**), RENOMMER un rôle, + AJOUTER un fantôme dans la prose ou en OUBLIER un laisse le README périmé pendant + que l'artefact dit autre chose → l'agent ERPNext Backend câblerait le mauvais garde + (le risque même que la table veut prévenir). Aucune suite `tests/` (qui teste des + FONCTIONS de graphe/résolution RBAC, pas la prose) n'attrape ce « vert trompeur ». +- **État courant** : **aucun garde périmé** — les 7 rôles de la table 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 « CRM gardes » après le +bloc Démo scénarios) : (1) **identité d'ensemble** — colonne « Rôle » de la table == +`{allowed}` de `workflow.json` (set-diff · NFKD/casefold) ; (2) **cohérence +artefacts** — `{allowed}` == `roles_rbac_utilises` du MANIFEST (aucun garde hors +manifeste, ni l'inverse) · ensemble NON VIDE ; (3) **cross-cohérence par pas** — les +gardes des étapes SENSIBLES et uniques `Confirmer réservation`/`Signer contrat`/ +`Approuver CONFOTUR` (recomputés `allowed`, jamais figés) doivent être nommés +EXACTEMENT dans leur ligne. Le (3) mord la **réaffectation vers un rôle DÉJÀ +présent** que le set-diff seul manquerait (un rôle servant deux transitions). Un +claim absent échoue AUSSI (traçabilité). + +**7 morsures vérifiées** : README réaffecte `Confirmer réservation`→`Conseiller` +(absents=[réservations] + cross-cohérence par pas mordue) · artefact réaffecte +`Signer contrat`→`Conseiller` (en trop=[contrats] + incohérence MANIFEST + pas +mordu) · README ajoute rôle fantôme (en trop=[oto fantome]) · README retire un rôle +(absents=[direction commerciale]) · artefact MANIFEST perd un garde (garde pas dans +MANIFEST=[réservations]) · table supprimée (INTROUVABLE) · **artefact réaffecte +`Approuver CONFOTUR`→`Directeur` — rôle déjà présent, set INCHANGÉ, captée UNIQUEMENT +par la cross-cohérence par pas** ; restauré = green : 7 rôles · colonne == `{allowed}` +== `roles_rbac_utilises` · 3 étapes sensibles pinnées · exit 0. Working tree +byte-restauré (`git checkout --`, JAMAIS `git clean`) · **7 gates re-verts**. + +- `ci/README.md` (table récap + détail « 3ᵉ surface du même README workflow_vente ») + 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_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 diff --git a/ci/README.md b/ci/README.md index 63af57b..1827fa8 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) == 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) == 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 @@ -291,6 +291,29 @@ vérifiées (déclassement `Signer contrat` capté là où le compte reste 9/11 promotion `Déposer CONFOTUR` captée · échange prose `signer contrat`→`signer bail` capté · énumération supprimée = INTROUVABLE ; restauré = green). +**3ᵉ surface du même README** — le CŒUR sécurité du workflow : la table « Rôles = +contrat RBAC » (l.55-62) nomme, **par pas**, le **rôle RBAC qui garde** chaque +transition. Les deux blocs ci-dessus gatent le COMPTE et la séparation des pouvoirs +mais restent **aveugles à l'identité** de ces gardes — data-derived de deux +artefacts byte-gatés : `workflow.json` (chaque transition porte `allowed`) et +`MANIFEST.roles_rbac_utilises[].erpnext_role_name` (recomputé du contrat +`rbac_50_roles.json`). **Réaffecter** un pas monétaire (« Confirmer réservation » +`Réservations`→`Conseiller` : élévation de privilège), **renommer** un rôle, +**ajouter** un rôle fantôme ou en **oublier** un laisse la table périmée pendant que +l'artefact dit autre chose → l'agent ERPNext Backend câblerait le mauvais garde. On +exige : (1) **identité d'ensemble** — colonne « Rôle » == `{allowed}` de +`workflow.json` (absent **et** en trop) ; (2) **cohérence artefacts** — `{allowed}` +== `roles_rbac_utilises` (aucun garde hors manifeste, ni l'inverse) · ensemble non +vide ; (3) **cross-cohérence par pas** — les gardes des étapes SENSIBLES et uniques +`Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` (recomputés `allowed`, +jamais figés) doivent être nommés EXACTEMENT dans leur ligne — ce qui mord la +**réaffectation vers un rôle déjà présent** que le seul set-diff manquerait. 7 +morsures vérifiées (réaffectation prose `Confirmer réservation` · réaffectation +artefact `Signer contrat` · rôle fantôme · rôle manquant · incohérence MANIFEST · +table supprimée = INTROUVABLE · réaffectation artefact `Approuver CONFOTUR` vers un +rôle déjà présent — set inchangé, captée par la cross-cohérence par pas ; restauré = +green). + Même classe encore sur l'**app mobile Expo** : la ligne récap du README du module `mobile/app_config` (« **5 onglets** · **44 rôles** couverts · **3 langues** · **13 identifiants `a_confirmer`** ») transcrivait **à la main** un quadruplet dont diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index c7695d9..c963d9b 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2754,6 +2754,115 @@ if d_rs is not None and _proj_map is not None: bad(f"Démo scénarios · table — lignes {sorted(row_ids)} MAIS run_sheet " f"{sorted(want_ids)} (counts.scenarios={d_n}){detail} → régénérer le README") +# ============================================================================ +# CRM/workflow_vente (3e surface du MÊME README) · la TABLE « Rôles = contrat +# RBAC » — le RÔLE RBAC qui GARDE chaque transition du pipeline vente. C'est le +# CŒUR sécurité du workflow (qui a le droit d'exécuter quel pas), transcrit À LA +# MAIN sans AUCUN gate d'IDENTITÉ. +# ---------------------------------------------------------------------------- +# Les deux blocs CRM/workflow existants gatent (1) le COUPLE de COMPTES du pipeline +# (« 9 états · 11 transitions ») et (2) l'ÉNUMÉRATION des transitions à SÉPARATION +# DES POUVOIRS (allow_self_approval=0). Restait, dans le MÊME README (l.55-62), une +# 3e surface data-derived : la table « Étape → Rôle qui garde la transition » qui +# nomme, PAR pas, le rôle RBAC autorisé. Sources faisant autorité (byte-gatées par +# check_artifacts) : crm/workflow_vente/out/workflow.json — chaque transition porte +# `allowed` (le rôle qui garde le pas) ; ET out/MANIFEST.json.roles_rbac_utilises[] +# — l'ensemble des rôles employés, RECOMPUTÉ du contrat rbac_50_roles.json à chaque +# build. Les deux blocs de COMPTE/séparation sont AVEUGLES à l'identité de ces rôles : +# RÉAFFECTER un pas (« Confirmer réservation » — soumission d'argent — passe de +# `OTO Ventes Réservations` à `OTO Ventes Conseiller` : élévation de privilège sur +# une étape monétaire), RENOMMER un rôle, en AJOUTER un fantôme dans la prose ou en +# OUBLIER un laisse le README périmé pendant que l'artefact dit autre chose → +# l'agent ERPNext Backend câblerait le mauvais garde (le risque même que la table +# veut prévenir). « Vert trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS +# de graphe/résolution RBAC, 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 artefact INTERNEMENT incohérent) : {allowed} de workflow.json +# == {erpnext_role_name} de MANIFEST.roles_rbac_utilises (aucun rôle gardant une +# transition sans être listé dans le manifeste, ni l'inverse) · ensemble NON VIDE. +# Cross-cohérence par pas (mord la réaffectation que le set-diff seul manque quand +# un rôle sert DEUX transitions) : pour les pas monétaires/de clôture SENSIBLES et +# UNIQUES dans le graphe (recomputés `allowed`, jamais figés), la ligne README qui +# les nomme doit citer EXACTEMENT ce rôle. +# État courant : aucun garde périmé (anti-invention #6) — le défaut est la surface +# ungated. Un claim absent échoue AUSSI (traçabilité). +# ============================================================================ +def _fold(s): + s = unicodedata.normalize("NFKD", s) + return "".join(c for c in s if not unicodedata.combining(c)).strip().casefold() +try: + wfv_trans = load("crm/workflow_vente/out/workflow.json")[0]["transitions"] + wfv_roles = {t["allowed"] for t in wfv_trans} # gardes du graphe + wfv_man = load("crm/workflow_vente/out/MANIFEST.json")["roles_rbac_utilises"] + wfv_roles_man = {r["erpnext_role_name"] for r in wfv_man} + # rôle par action, UNIQUEMENT quand l'action garde 1 seul rôle (déterministe) + wfv_by_act = {} + for t in wfv_trans: + wfv_by_act.setdefault(t["action"], set()).add(t["allowed"]) + wfv_uniq = {a: next(iter(v)) for a, v in wfv_by_act.items() if len(v) == 1} +except (OSError, KeyError, ValueError, IndexError, TypeError) as e: + bad(f"CRM gardes · workflow.json/MANIFEST illisible/inattendu : {e}"); wfv_roles = None +if wfv_roles is not None: + # bonus 1 : ensemble non vide + if not wfv_roles: + bad("CRM gardes · aucun rôle ne garde de transition (ensemble VIDE) → contrat cassé") + else: + good(f"CRM gardes · {len(wfv_roles)} rôles gardent le pipeline (ensemble non vide)") + # bonus 2 : workflow.json ⇄ MANIFEST d'accord sur l'ensemble des rôles employés + if wfv_roles == wfv_roles_man: + good(f"CRM gardes · {{allowed}} workflow.json == roles_rbac_utilises MANIFEST " + f"({len(wfv_roles)} rôles · aucun fantôme/manquant entre artefacts)") + else: + miss = sorted(wfv_roles_man - wfv_roles); extra = sorted(wfv_roles - wfv_roles_man) + detail = (f" · dans MANIFEST pas gardien={miss}" if miss else "") + \ + (f" · gardien pas dans MANIFEST={extra}" if extra else "") + bad(f"CRM gardes · incohérence artefacts : gardes workflow.json {sorted(wfv_roles)} " + f"MAIS roles_rbac_utilises {sorted(wfv_roles_man)}{detail} → régénérer") + wfv_txt = read_norm(WF_RD) + if wfv_txt is not None: + seg = re.search(r"Rôle qui garde la transition \|(.+?)## Séparation", wfv_txt) + if seg is None: + bad(f"CRM gardes · {WF_RD} — table « Rôle qui garde la transition » INTROUVABLE " + f"(attendu {sorted(wfv_roles)})") + else: + rows = [(l, r) for l, r in re.findall(r"\| ([^|]+?) \| ([^|]+?) \|", seg.group(1)) + if set(l.strip()) != {"-"}] + # identité d'ensemble : colonne « Rôle » de la table == gardes du graphe + rd_roles = set() + for _lbl, cell in rows: + for tok in cell.split("·"): + if tok.strip(): + rd_roles.add(tok.strip()) + rd_fold = {_fold(x) for x in rd_roles}; wf_fold = {_fold(x) for x in wfv_roles} + if rd_fold == wf_fold: + good(f"CRM gardes · {WF_RD} — colonne Rôle {sorted(rd_roles)} == " + f"{{allowed}} workflow.json ({len(wfv_roles)} rôles · ni fantôme ni manquant)") + else: + miss = sorted(wf_fold - rd_fold); extra = sorted(rd_fold - wf_fold) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"CRM gardes · {WF_RD} — colonne Rôle {sorted(rd_fold)} MAIS " + f"{{allowed}} workflow.json {sorted(wf_fold)}{detail} → régénérer le README") + # cross-cohérence par pas : les gardes des étapes SENSIBLES (argent/clôture), + # uniques dans le graphe, recomputés depuis workflow.json — jamais figés. + for act in ("Confirmer réservation", "Signer contrat", "Approuver CONFOTUR"): + exp = wfv_uniq.get(act) + if exp is None: + bad(f"CRM gardes · action sensible « {act} » absente/non-déterministe " + f"dans workflow.json → contrat changé de forme, vérifier") + continue + row = next((c for l, c in rows if _fold(act) in _fold(l)), None) + if row is None: + bad(f"CRM gardes · {WF_RD} — ligne de l'étape sensible « {act} » " + f"INTROUVABLE (attendu garde {exp})") + elif _fold(exp) in {_fold(t) for t in row.split("·") if t.strip()}: + good(f"CRM gardes · étape sensible « {act} » gardée par « {exp} » " + f"== workflow.json (séparation des pouvoirs · #6)") + else: + bad(f"CRM gardes · {WF_RD} — étape sensible « {act} » : README dit " + f"« {row.strip()} » MAIS workflow.json dit garde « {exp} » → " + f"réaffectation de privilège non répercutée, régénérer") + sys.exit(1 if FAIL else 0) PY rc=$?