diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index bb7d0a3..c30ff50 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,73 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_170112` · Buffer S8 · Domaine RBAC/fixtures_gen : le **catalogue des DocTypes `custom`** à créer sur le VPS (« `Faisabilité`, `Publiciste Log`, `CONFOTUR Application`, `API Access` ») ET la **séparation des pouvoirs** (« `set_user_permissions` n'est émis que pour le rôle **RBAC Admin** ») étaient transcrits À LA MAIN dans le README du module sans AUCUN gate d'IDENTITÉ — le bloc racine ne gate que le COMPTE (« 50 rôles / 116 DocPerm » via l'agrégat RBAC de l'apply_plan). + +**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive (CLAUDE.md #6). Les sessions précédentes ont +gaté les domaines tests, portails, RBAC (3 volets : roleprofile/apply_plan/userperm), +SEO, CRM (pipeline vente + séparation des pouvoirs + commissions), mobile (quadruplet ++ onglets), Legal/CONFOTUR, DevOps, QA (audit 5D ×2 + 4Big ×2), Démo, Frontend/Chat +OTOIA, recette et Fiscal/e-CF. Cette session cible le **module `rbac/fixtures_gen`** — +jamais gaté hors son compte agrégé — dont deux faits data-derived du README pointent +les artefacts générés sans être recomputés par aucun gate. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/rbac/fixtures_gen/README.md:56` : « listés dans + `MANIFEST.custom_doctypes_a_confirmer` (`Faisabilité`, `Publiciste Log`, `CONFOTUR + Application`, `API Access`) » — l'ENSEMBLE des DocTypes `custom: true` à créer. +- `…/README.md:48` : « le flag `set_user_permissions` n'est émis que pour le rôle + **RBAC Admin** » — le SINGLETON `{role | set_user_permissions == 1}`, cœur sécurité + du module (« séparation des pouvoirs · défense en profondeur »). +- Sources faisant autorité (byte-gatées par `check_artifacts`) : + `rbac/fixtures_gen/out/MANIFEST.json.custom_doctypes_a_confirmer` (les DocTypes + `custom` du contrat `rbac_50_roles.json`) et `out/custom_docperm.json` (le seul + rôle porteur du flag = `OTO Plateforme RBAC Admin`). +- Le bloc racine ne gate QUE le **compte** (« 50 rôles / 116 DocPerm » via l'agrégat + RBAC de l'apply_plan). Aucun gate ne comparait ces ENSEMBLES aux artefacts : + **AJOUTER** un DocType `custom` au contrat (le MANIFEST rebâtit 5 entrées), + **RENOMMER/ÉCHANGER** l'un des 4, ou **PROMOUVOIR** un 2ᵉ rôle porteur de + `set_user_permissions` (élévation de privilège) ferait dériver la prose en silence — + « vert trompeur » qu'aucune suite `tests/` (qui teste des FONCTIONS de mapping, pas + la prose) n'attrape. +- **État courant** : **aucun ensemble périmé** — le catalogue (4) et le singleton + recoupent les artefacts exactement (anti-invention #6, rien à réécrire). Le défaut + est la **surface ungated**. + +**Nouveau bloc dans `ci/check_readme_claims.sh`** : recompute le catalogue depuis +`MANIFEST.custom_doctypes_a_confirmer` et le singleton depuis `custom_docperm.json` +(zéro duplication du contrat du générateur · #6) et exige que la prose l'énumère/le +nomme **exactement** — contrôle par ENSEMBLE (absent ET en trop mordus via set-diff · +accents/casse normalisés via `unicodedata`). Cohérences croisées en bonus : le +catalogue est **non vide, sans doublon et trié** (byte-déterminisme du générateur) ; +la séparation des pouvoirs est un **singleton** (ni vide — garde vacante — ni multiple +— élévation de privilège). Un claim absent échoue AUSSI. + +### Preuve (6 morsures vérifiées + restauration verte) + +| Injection | Résultat | +|---|---| +| README échange nom DocType (`Faisabilité → Faisabilite2`) | ✗ absents=`[faisabilite]` · en trop=`[faisabilite2]` · exit 1 | +| README retrait `API Access` (sous-ensemble) | ✗ absents=`[api access]` · exit 1 | +| README rôle nommé faux (`RBAC Admin → Ventes Conseiller`) | ✗ nomme « Ventes Conseiller » MAIS flag porté par `OTO Plateforme RBAC Admin` · exit 1 | +| README énumération du catalogue supprimée | ✗ énumération du catalogue custom INTROUVABLE · exit 1 | +| artefact 2ᵉ rôle promu au flag (`custom_docperm`) | ✗ set_user_permissions porté par `[…RBAC Admin, …Ventes Conseiller]` (attendu 1) · séparation cassée · exit 1 | +| artefact catalogue MANIFEST non trié | ✗ non trié/dédupliqué · exit 1 | +| restauré | ✓ catalogue `[API Access, CONFOTUR Application, Faisabilité, Publiciste Log]` == MANIFEST · singleton `OTO Plateforme RBAC Admin` == custom_docperm · exit 0 | + +- **`ci/README.md`** — surface ajoutée à la table récap du pipeline + paragraphe + descriptif (classe « catalogue RBAC `rbac/fixtures_gen` »). + +Suite CI complète re-verte (7 gates) : `guard_constraints` · `validate_json` · +`check_docs` · `check_artifacts` · `check_regression` · `check_ci_integrity` · +`check_readme_claims`. + +**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_163112` · Buffer S8 · Domaine CRM/Commissions : la **cross-cohérence** du barème vendeurs (« cœur du livrable ») — les états **brouillon exclus** (`lead/visite/devis/abandonné`), les **champs de base** (`montant_reservation`, `montant_contrat`) et l'**invariant 3** « on ne commissionne que des états soumis » — était énumérée À LA MAIN dans le README du module sans AUCUN gate d'IDENTITÉ ; le bloc racine ne gate que le COMPTE de tests (« 25 », via l'agrégat « Total CRM »). **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement diff --git a/ci/README.md b/ci/README.md index f6014a9..4681150 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 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 ») == 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 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`) == 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 @@ -422,6 +422,31 @@ l'**invariant 3** du module, non vide. 5 morsures vérifiées : échange brouill (`reservation→lead`, invariant 3 cassé) · promotion de l'état `abandonne` en soumis dans l'artefact (draft set change, « en trop » dans la prose) ; restauré = vert. +Même classe enfin sur le **catalogue RBAC** (livrable `rbac/fixtures_gen`) — deux +faits data-derived transcrits **à la main** dans le README. (1) La liste des DocTypes +`custom` à créer avant import « (`Faisabilité`, `Publiciste Log`, `CONFOTUR +Application`, `API Access`) » (README §« Application sur VPS ») == l'ensemble +`custom_doctypes_a_confirmer` d'`rbac/fixtures_gen/out/MANIFEST.json` (les DocTypes +`custom: true` du contrat RBAC). (2) La **séparation des pouvoirs** — « le flag +`set_user_permissions` n'est émis que pour le rôle **RBAC Admin** » (README:48) == +l'ensemble `{role | set_user_permissions == 1}` d'`out/custom_docperm.json`. Le bloc +racine ne gate que le **compte** (« 50 rôles / 116 DocPerm » via l'agrégat RBAC de +l'apply_plan) — jamais l'**identité** de ces deux ensembles. AJOUTER un DocType +`custom` au contrat, RENOMMER/ÉCHANGER l'un des 4 ou PROMOUVOIR un 2ᵉ rôle porteur du +flag (élévation de privilège) ferait dériver la prose en silence pendant que +l'artefact byte-gaté dit autre chose — dérive qu'aucune suite `tests/` (qui teste des +FONCTIONS de mapping, pas la prose du README) n'attrape. On RECOMPUTE chaque ensemble +depuis SON artefact (zéro duplication du contrat du générateur · #6) et on exige que +la prose l'énumère/le nomme **exactement** (absent ET en trop mordus via set-diff · +accents/casse normalisés). Cohérences croisées en bonus : le catalogue est **non +vide, sans doublon et trié** (byte-déterminisme du générateur) et la séparation des +pouvoirs est un **singleton** (ni vide — garde vacante — ni multiple — élévation de +privilège). 6 morsures vérifiées : échange d'un nom de DocType (`Faisabilité → +Faisabilite2`) · sous-ensemble (retrait `API Access`) · rôle nommé faux (`RBAC Admin +→ Ventes Conseiller`) · énumération supprimée (INTROUVABLE) · 2ᵉ rôle promu au flag +dans l'artefact (singleton cassé) · catalogue MANIFEST non trié (byte-déterminisme) ; +restauré = vert. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 612dc51..3cd5c57 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -1713,6 +1713,98 @@ if cm_ev is not None: bad(f"CRM commissions · {CM_RD} — énumère {sorted(bs)} MAIS plan " f"{sorted(cm_base)}{detail} → régénérer le README") +# ============================================================================ +# RBAC/fixtures_gen · le CATALOGUE des DocTypes `custom` à créer sur le VPS et la +# SÉPARATION DES POUVOIRS (le rôle unique porteur de `set_user_permissions`) — +# deux faits data-derived transcrits À LA MAIN dans le README sans AUCUN gate +# d'IDENTITÉ. +# ---------------------------------------------------------------------------- +# Le générateur rbac_fixtures_gen émet out/MANIFEST.json et out/custom_docperm.json +# (byte-gatés par check_artifacts ⇒ faisant autorité) : +# • MANIFEST.custom_doctypes_a_confirmer = l'ensemble des DocTypes `custom: true` +# du contrat rbac_50_roles.json (ceux à CRÉER avant import — le reste est natif +# v15). Le README §« Application sur VPS » l'ÉNUMÈRE À LA MAIN, en nommant même +# le champ : « listés dans `MANIFEST.custom_doctypes_a_confirmer` (`Faisabilité`, +# `Publiciste Log`, `CONFOTUR Application`, `API Access`) ». +# • custom_docperm.json porte le flag `set_user_permissions`. Le CŒUR sécurité du +# module (« séparation des pouvoirs · défense en profondeur ») est que ce flag +# n'est émis que pour UN SEUL rôle — le README:48 dit « n'est émis que pour le +# rôle **RBAC Admin** ». C'est l'ensemble {role | set_user_permissions=1}. +# Le bloc racine ne gate QUE le COMPTE de tests fixtures (« 50 rôles / 116 DocPerm » +# via l'agrégat RBAC de l'apply_plan) — jamais l'IDENTITÉ de ces deux ensembles. +# Aucun gate ne les comparait à l'artefact : AJOUTER un DocType `custom` au contrat +# (le MANIFEST rebâtit 5 entrées) OU RENOMMER/ÉCHANGER l'un des 4 laisse la prose à +# ses 4 noms périmés ; PROMOUVOIR un 2ᵉ rôle porteur de `set_user_permissions` (ou +# renommer RBAC Admin) laisse la prose « un seul rôle RBAC Admin » fausse — « vert +# trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS de mapping, pas la +# prose du README) n'attrape. Même patron que l'énumération des onglets Mobile, des +# brouillons CRM ou des open items de l'audit 5D. On RECOMPUTE chaque ensemble +# depuis SON artefact byte-gaté (zéro duplication du contrat du générateur #6) et on +# exige que la prose l'énumère/le nomme EXACTEMENT (absent ET en trop via set-diff · +# accents/casse normalisés). Cohérence croisée en bonus : le catalogue est NON VIDE, +# sans doublon et TRIÉ (byte-déterminisme du générateur) ; la séparation des pouvoirs +# est un SINGLETON (ni vide — la garde serait vacante — ni multiple — élévation de +# privilège). Un claim absent échoue AUSSI (traçabilité). +# ============================================================================ +FX_RD = f"{D}/rbac/fixtures_gen/README.md" +def _fx_acc(s): + return "".join(c for c in unicodedata.normalize("NFKD", s) + if unicodedata.category(c) != "Mn").strip().lower() +try: + fx_cdt = load("rbac/fixtures_gen/out/MANIFEST.json")["custom_doctypes_a_confirmer"] + fx_perm = load("rbac/fixtures_gen/out/custom_docperm.json") + fx_sup = sorted({p["role"] for p in fx_perm if p.get("set_user_permissions")}) +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"RBAC fixtures · artefact illisible/inattendu : {e}"); fx_cdt = None +if fx_cdt is not None: + # bonus : le catalogue est un vrai ENSEMBLE trié non vide (byte-déterminisme) + if not fx_cdt: + bad("RBAC fixtures · custom_doctypes_a_confirmer VIDE → aucun DocType custom " + "à créer ? (contrat suspect)") + elif fx_cdt != sorted(set(fx_cdt)): + bad(f"RBAC fixtures · custom_doctypes_a_confirmer {fx_cdt} non trié/dédupliqué " + f"→ générateur non déterministe (attendu {sorted(set(fx_cdt))})") + else: + good(f"RBAC fixtures · catalogue custom {fx_cdt} — ensemble trié, sans doublon") + # bonus : séparation des pouvoirs — un SEUL rôle porte set_user_permissions + if len(fx_sup) != 1: + bad(f"RBAC fixtures · set_user_permissions porté par {fx_sup} (attendu 1 seul " + f"rôle) → séparation des pouvoirs cassée (élévation de privilège possible)") + else: + good(f"RBAC fixtures · set_user_permissions — singleton {fx_sup[0]!r} " + f"(défense en profondeur intacte)") + fx_rd = read_norm(FX_RD) + if fx_rd is not None: + # 1) README — énumération du catalogue custom (parenthèse après le champ nommé) + me = re.search(r"custom_doctypes_a_confirmer`?\s*\(([^)]*)\)", fx_rd) + if me is None: + bad(f"RBAC fixtures · {FX_RD} — énumération du catalogue custom INTROUVABLE " + f"(attendu {sorted(fx_cdt)})") + else: + got = {_fx_acc(t) for t in re.findall(r"`([^`]+)`", me.group(1))} + exp = {_fx_acc(x) for x in fx_cdt} + if got == exp: + good(f"RBAC fixtures · {FX_RD} — catalogue custom {sorted(got)} == " + f"MANIFEST.custom_doctypes_a_confirmer") + else: + miss = sorted(exp - got); extra = sorted(got - exp) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"RBAC fixtures · {FX_RD} — énumère {sorted(got)} MAIS MANIFEST " + f"{sorted(exp)}{detail} → régénérer le README") + # 2) README:48 — le rôle NOMMÉ porteur de set_user_permissions (séparation) + if len(fx_sup) == 1: + mr = re.search(r"émis que pour le rôle \*\*([^*]+)\*\*", fx_rd) + if mr is None: + bad(f"RBAC fixtures · {FX_RD} — claim « set_user_permissions n'est émis " + f"que pour le rôle ** ** » INTROUVABLE (attendu {fx_sup[0]!r})") + elif _fx_acc(mr.group(1)) in _fx_acc(fx_sup[0]): + good(f"RBAC fixtures · {FX_RD} — rôle nommé « {mr.group(1).strip()} » ⊆ " + f"{fx_sup[0]!r} (le seul porteur du flag)") + else: + bad(f"RBAC fixtures · {FX_RD} — nomme « {mr.group(1).strip()} » MAIS " + f"set_user_permissions porté par {fx_sup[0]!r} → régénérer le README") + sys.exit(1 if FAIL else 0) PY rc=$?