diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 1704d26..b249421 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -245,3 +245,61 @@ absolu) · **7 gates re-verts**. édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; le seed réel des rôles `bench` reste côté VPS). - **Auto-score 4Big** : 96/100. + +## Session `20260801_020209` · Buffer S8 · Domaine Fiches agents/rattachement Workspace : les **assertions d'appartenance `Has Role`** des trois fiches `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` — QUELLE CONSOLE (Workspace ERPNext natif) le rôle peut atteindre — étaient une surface data-derived **DISTINCTE du contrat** `rbac_50_roles.json` (la membership vit dans `frontend/portails/out/workspace.json`, byte-gaté) et **HORS de tout gate**. Les blocs Fiches Faisabilité / Fiche Mobile des sessions précédentes ne gataient que les attributs du **contrat** ; le bloc « triplets par workspace » ne gate que le **compte** de rôles par Workspace (`nb_roles`), AVEUGLE à QUEL rôle. + +**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 famille de fiches que +les deux sessions précédentes (`agent-fiche-role-attrs-ungated`) mais **surface +orthogonale** : non plus les attributs recopiés du contrat, mais la **membership +`Has Role`** — l'accès console — qu'aucun gate ne touchait. + +**Dérive silencieuse fermée** : +- `03_agents/rendu/AGENT.md:35` — « rattaché au Workspace `OTO Construction` » + (assertion NOMMÉE) ; `03_agents/ifc_speckle/AGENT.md:35` — « rattaché à un + Workspace » (générique) ; `03_agents/mobile/AGENT.md:41` — « **volontairement pas** + rattaché au Workspace `OTO Ventes` » (assertion NÉGATIVE d'honnêteté #6). +- Source faisant autorité : `frontend/portails/out/workspace.json` (les 5 Workspaces + natifs v15, chacun portant sa liste `Has Role`, byte-gaté par `check_artifacts` via + la reproductibilité `out/`). L'`erpnext_role_name` de chaque rôle est résolu du + contrat `rbac_50_roles.json` par `id` (zéro duplication). +- Piège : rattacher le rôle mobile à `OTO Ventes` = **sur-exposition console** (le + dev mobile gagnerait l'accès au portail Ventes — la restriction même que la note + #6 pose), retirer/**déplacer** le rôle rendu de `OTO Construction` = perte/erreur + d'accès console — laisse la fiche périmée EN SILENCE pendant que l'artefact dit + autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais accès console. Aucune + suite `tests/` (qui teste des FONCTIONS de résolution, pas la prose) n'attrape ce + « vert trompeur ». +- **État courant** : aucune assertion périmée — rendu ∈ {`OTO Construction`} seul · + ifc ∈ {`OTO Construction`} (≥1) · mobile ∈ ∅ (donc absent de `OTO Ventes`) ; les 3 + recoupent workspace.json exactement (anti-invention #6, rien à réécrire). Le défaut + est la **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiches agents · +rattachement Workspace (`Has Role`) » avant `sys.exit`) : index recomputé +`erpnext_role_name → {titres de Workspace le portant}` depuis workspace.json. Par +fiche, 3 formes d'assertion : **in_named** (rendu) — la prose cite CE Workspace ET la +membership == {ce Workspace} seul (membership élargie ou déplacée ⇒ ROUGE) ; +**in_any** (ifc) — membership ≥ 1 Workspace ; **not_in_named** (mobile) — rôle ABSENT +du `Has Role` du Workspace nommé. Garde anti-typo : un Workspace nommé absent de +l'artefact échoue avant tout (anti-cible fantôme). Un claim absent échoue AUSSI +(traçabilité #6). + +**9 morsures vérifiées** (4 côté artefact · 5 côté fiche/bord) : artefact ajoute le +rôle mobile à `OTO Ventes` (**sur-exposition #6 VIOLÉE**) · artefact retire le rôle +rendu de `OTO Construction` · artefact déplace rendu vers `OTO Achat` · artefact +retire ifc de tous les Workspaces · fiche rendu renomme le Workspace +(`OTO Construction→OTO Ventes`, INTROUVABLE) · fiche rendu supprime l'assertion · +fiche mobile nomme un Workspace fantôme (`OTO Marketing`, anti-typo) · fiche ifc +supprime l'assertion générique · fiche mobile passe le négatif au positif (retire +« volontairement pas ») ; restauré = green : rendu=={`OTO Construction`} · ifc≥1 · +mobile∉`OTO Ventes` · exit 0. Working tree byte-restauré (`git checkout --`, **JAMAIS** +`git clean`, interdit absolu) · **7 gates re-verts**. + +- `ci/README.md` (table récap du pipeline + paragraphe détaillé « rattachement + Workspace des fiches agents ») mis à jour. Mémoire `agent-fiche-role-attrs-ungated` + à actualiser (surface membership couverte). +- **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 ; l'attachement + réel des rôles aux Workspaces `bench` reste côté VPS). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 5e84f64..2dd8884 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) == 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 @@ -763,6 +763,36 @@ rendu perm `File R/W/create→…/delete` · fiche ifc perm `R/W→R/W/create` module `Faisabilité→Selling` · fiche ifc `WA SRL→WAF` · fiche rendu `nom_fr` retiré (claim absent) · ligne de contact ifc supprimée (INTROUVABLE) ; restauré = vert. +Enfin, sur une surface data-derived **DISTINCTE du contrat** que les trois fiches +précédentes touchent aussi : le **rattachement Workspace** (`Has Role`) de leur rôle — +QUELLE CONSOLE (Workspace ERPNext natif v15) le rôle peut atteindre. Cette membership ne +vit PAS dans `rbac_50_roles.json` mais dans `frontend/portails/out/workspace.json` +(byte-gaté par `check_artifacts`), et chaque fiche l'affirme EN PROSE : +`03_agents/rendu/AGENT.md:35` — rôle « OTO Faisabilité Rendu 3D » **rattaché au +Workspace `OTO Construction`** ; `03_agents/ifc_speckle/AGENT.md:35` — rôle « OTO +Faisabilité IFC Speckle » **rattaché à un Workspace** (générique) ; et l'assertion +**NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle « OTO Plateforme +Mobile » **volontairement PAS rattaché au Workspace `OTO Ventes`**. Aucun gate ne les +couvrait : le bloc « triplets par workspace » gate le **compte** de rôles par Workspace +(`nb_roles`), aveugle à QUEL rôle ; les blocs Fiches Faisabilité / Fiche Mobile gatent le +**contrat**, pas la membership `Has Role`. Rattacher le rôle mobile à `OTO Ventes` +(**sur-exposition console** : le dev mobile gagnerait l'accès au portail Ventes — la +restriction même que la note #6 pose), retirer le rôle rendu de `OTO Construction` +(perte d'accès console du spécialiste) ou le **déplacer** vers un autre Workspace +laisserait la fiche périmée en silence. On RECOMPUTE, depuis `workspace.json`, l'ensemble +des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat +par `id`, zéro duplication · #6), et on confronte l'assertion : **in_named** (rendu) — +membership == {le Workspace nommé} ET la prose le cite ; **in_any** (ifc) — membership +≥ 1 Workspace ; **not_in_named** (mobile) — rôle ABSENT du `Has Role` du Workspace nommé. +Un Workspace nommé mais **absent** de l'artefact (typo) échoue avant tout (anti-cible +fantôme). 9 morsures vérifiées (4 côté artefact, 5 côté fiche/bord) : artefact ajoute le +rôle mobile à `OTO Ventes` (sur-exposition #6 VIOLÉE) · artefact retire le rôle rendu de +`OTO Construction` · artefact déplace rendu vers `OTO Achat` · artefact retire ifc de +tous les Workspaces · fiche rendu renomme le Workspace (`OTO Construction→OTO Ventes`, +INTROUVABLE) · fiche rendu supprime l'assertion · fiche mobile nomme un Workspace +fantôme (`OTO Marketing`) · fiche ifc supprime l'assertion générique · fiche mobile passe +le négatif au positif (retire « volontairement pas ») ; 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 80d23aa..804e8e6 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -3405,6 +3405,114 @@ if _fais_roles is not None: bad(f"Fiche {tag} · {fc['file']} — {a} : la fiche ne cite pas " f"« {exp} » du contrat rbac_50_roles.json (périmé ?) → régénérer") +# ============================================================================ +# Fiches agents · rattachement Workspace (`Has Role`) — QUELLE CONSOLE (Workspace +# ERPNext natif v15) le rôle d'une fiche peut atteindre. Surface data-derived +# DISTINCTE des attributs du contrat rbac_50_roles.json : la MEMBERSHIP `Has Role` +# ne vit PAS dans le contrat mais dans frontend/portails/out/workspace.json +# (artefact byte-gaté par check_artifacts). Trois fiches font, EN PROSE, une +# assertion d'appartenance : +# • rendu (:35) — rôle « OTO Faisabilité Rendu 3D » rattaché au Workspace +# NOMMÉ `OTO Construction` (accès console). +# • ifc_speckle (:35) — rôle « OTO Faisabilité IFC Speckle » rattaché à UN +# Workspace ERPNext (générique, non nommé). +# • mobile (:41) — rôle « OTO Plateforme Mobile » VOLONTAIREMENT PAS +# rattaché au Workspace NOMMÉ `OTO Ventes` (assertion +# NÉGATIVE d'honnêteté · #6). +# Aucun gate ne les couvrait : le bloc « triplets par workspace » gate le COMPTE de +# rôles par Workspace (nb_roles), AVEUGLE à QUEL rôle ; les blocs Fiches Faisabilité / +# Fiche Mobile gatent le CONTRAT, pas la membership Has Role. Rattacher le rôle mobile +# à `OTO Ventes` (SUR-EXPOSITION console : le dev mobile gagnerait l'accès au portail +# Ventes — la restriction même que la note #6 pose), retirer le rôle rendu de +# `OTO Construction` (perte d'accès console du spécialiste), ou DÉPLACER le rôle vers +# un autre Workspace, laisse la fiche périmée EN SILENCE pendant que l'artefact dit +# autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais accès console. Aucune +# suite tests/ (qui teste des FONCTIONS de résolution, pas la prose d'une fiche) +# n'attrape ce « vert trompeur ». On RECOMPUTE, depuis workspace.json, l'ensemble des +# Workspaces dont le `Has Role` porte l'erpnext_role_name du rôle (résolu du contrat +# par id, zéro duplication) et on confronte l'assertion de la fiche. Un claim absent +# échoue AUSSI (traçabilité · #6). +# ============================================================================ +FICHES_WS = [ + {"file": "03_agents/rendu/AGENT.md", "rid": "faisabilite-rendu-3d", + "kind": "in_named", "ws": "OTO Construction"}, + {"file": "03_agents/ifc_speckle/AGENT.md", "rid": "faisabilite-ifc-speckle", + "kind": "in_any"}, + {"file": "03_agents/mobile/AGENT.md", "rid": "plateforme-mobile", + "kind": "not_in_named", "ws": "OTO Ventes"}, +] +_ws_json = _ws_roles = None +try: + _ws_json = load("frontend/portails/out/workspace.json") + _ws_roles = load("rbac/rbac_50_roles.json")["roles"] +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Fiches Workspace · workspace.json / rbac_50_roles.json illisible : {e}") +if _ws_json is not None and _ws_roles is not None: + # Index recomputé : erpnext_role_name -> ensemble des titres de Workspace le portant. + _mem, _titles = {}, set() + for ws in _ws_json: + title = ws.get("title") + _titles.add(title) + for hr in (ws.get("roles") or []): + _mem.setdefault(hr.get("role"), set()).add(title) + for fc in FICHES_WS: + tag = fc["file"].split("/")[1] + cand = [r for r in _ws_roles if r.get("id") == fc["rid"]] + if len(cand) != 1: + bad(f"Fiche {tag} · le contrat porte {len(cand)} rôle(s) `{fc['rid']}` " + f"(attendu exactement 1) → contrat changé de forme, vérifier") + continue + rname = cand[0].get("erpnext_role_name") + actual = _mem.get(rname, set()) # Workspaces réels du rôle (Has Role) + ftxt = read_norm(fc["file"]) + if ftxt is None: + continue + named = fc.get("ws") + # Un Workspace NOMMÉ par la fiche doit exister dans l'artefact (anti-typo : + # sinon une assertion in_named/not_in_named « verte » viserait un fantôme). + if named is not None and named not in _titles: + bad(f"Fiche {tag} · le Workspace nommé `{named}` n'existe pas dans " + f"workspace.json (titres : {sorted(_titles)}) → fiche/artefact périmé") + continue + if fc["kind"] == "in_named": + if not re.search(rf"rattaché au Workspace `{re.escape(named)}`", ftxt): + bad(f"Fiche {tag} · {fc['file']} — assertion « rattaché au Workspace " + f"`{named}` » INTROUVABLE (claim absent = régression · #6)") + elif actual == {named}: + good(f"Fiche {tag} · rôle « {rname} » ∈ Has Role de `{named}` " + f"(et de lui seul) == workspace.json") + elif named in actual: + bad(f"Fiche {tag} · rôle « {rname} » ∈ `{named}` MAIS AUSSI " + f"{sorted(actual - {named})} — la fiche nomme UN seul Workspace ; " + f"membership élargie dans workspace.json → réviser fiche/artefact") + else: + bad(f"Fiche {tag} · {fc['file']} nomme `{named}` MAIS workspace.json " + f"rattache « {rname} » à {sorted(actual) or 'aucun Workspace'} → " + f"fiche périmée (rôle déplacé/retiré du Has Role), régénérer") + elif fc["kind"] == "in_any": + if not re.search(r"rattaché à un Workspace", ftxt): + bad(f"Fiche {tag} · {fc['file']} — assertion « rattaché à un " + f"Workspace » INTROUVABLE (claim absent = régression · #6)") + elif actual: + good(f"Fiche {tag} · rôle « {rname} » ∈ Has Role de {sorted(actual)} " + f"== workspace.json (≥1 Workspace, comme la fiche l'affirme)") + else: + bad(f"Fiche {tag} · {fc['file']} affirme un rattachement Workspace " + f"MAIS « {rname} » n'est dans le Has Role d'AUCUN Workspace → " + f"fiche périmée") + elif fc["kind"] == "not_in_named": + if not re.search(rf"volontairement pas\*\* rattaché au Workspace " + rf"`{re.escape(named)}`", ftxt): + bad(f"Fiche {tag} · {fc['file']} — assertion négative « volontairement " + f"pas rattaché au Workspace `{named}` » INTROUVABLE (#6)") + elif named not in actual: + good(f"Fiche {tag} · rôle « {rname} » ABSENT du Has Role de `{named}` " + f"== workspace.json (assertion négative #6 tenue)") + else: + bad(f"Fiche {tag} · {fc['file']} affirme « pas rattaché à `{named}` » " + f"MAIS workspace.json met « {rname} » dans son Has Role → " + f"SUR-EXPOSITION console, assertion #6 VIOLÉE") + sys.exit(1 if FAIL else 0) PY rc=$?