From e8b30d71470f7dcf109346910722db85022cee73 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 00:41:00 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20Faisabilit=C3=A9/bancable=20:=20les=204=20figures=20?= =?UTF-8?q?=C2=AB=20G=C3=A9n=C3=A9ration=20r=C3=A9elle=20(fixture)=20?= =?UTF-8?q?=C2=BB=20du=20README=20(40=20unit=C3=A9s=20=C2=B7=20USD=208,560?= =?UTF-8?q?,000=20/=20DOP=20505,040,000=20=C2=B7=20point=20d'=C3=A9quilibr?= =?UTF-8?q?e=2021=20unit=C3=A9s=20=E2=8C=8852=20%=20=C3=97=2040=E2=8C=89)?= =?UTF-8?q?=20=E2=80=94=20les=20chiffres=20DATA-DERIVED=20du=20dossier=20b?= =?UTF-8?q?ancable=20que=20voit=20le=20banquier=20=E2=80=94=20=C3=A9taient?= =?UTF-8?q?=20HORS=20de=20tout=20gate.=20Le=20module=20n'=C3=A9crit=20son?= =?UTF-8?q?=20out/=20que=20sur=20disque=20(=C2=AB=20out/=20non=20commit?= =?UTF-8?q?=C3=A9=20=C2=BB)=20=E2=87=92=20check=5Fartifacts=20est=20aveugl?= =?UTF-8?q?e=20(aucun=20artefact=20byte-gat=C3=A9)=20et=20AUCUN=20gate=20n?= =?UTF-8?q?e=20le=20touchait.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ces 4 figures sont Σ quantités · Σ(qté×prix_usd) · Σ(qté×prix_dop) · ⌈point_equilibre_pct × Σ unités⌉ calculées sur la fixture COMMITÉE fixtures/brief_bancable.json, le 52 % ancré sur CLAUDE.md #9 (« Point équilibre 52% pré-vente »). PIÈGE : la seule vérification existante (tests/) teste des FONCTIONS (l'arithmétique de recoupement _check_derived_arithmetic) avec des oracles HARDCODÉS dans le test — une copie de plus, jamais comparée à la PROSE du README. Éditer une typologie de la fixture (quantité/prix), en ajouter/retirer une, ou déplacer le point d'équilibre de CLAUDE.md #9, laisse les 4 chiffres du README périmés pendant que le générateur produit autre chose → le banquier lit un dossier faux (l'invention même que #6 interdit) — « vert trompeur » qu'aucune suite tests/ n'attrape. Nouveau bloc « Faisabilité/bancable figures » dans ci/check_readme_claims.sh (avant sys.exit), même esprit que le bloc faisabilite/generator ancré sur CLAUDE.md #9/#10 mais appliqué à une surface dont la source de vérité n'est PAS un out/*.json : (1) recompute indépendant des 4 figures depuis la fixture commitée ; (2) cohérence croisée — le manifeste GÉNÉRÉ (bancable_gen validate, exécuté via subprocess stdlib pur, encoding utf-8) == le recompute (mord un générateur/fixture incohérents) ; (3) prose — un seul pattern d'identité exige les 4 valeurs EXACTES avec séparateur de milliers pour USD/DOP + la formule ⌈pct × Σ unités⌉ dont le pct == CLAUDE.md #9 et le total == Σ unités. Un claim absent échoue AUSSI (traçabilité). État courant : aucun chiffre périmé (anti-invention #6, rien à réécrire) — le défaut est la surface ungated (module entièrement hors CI). 7 morsures vérifiées : README USD 8,560,000→9,560,000 (figure périmée) · README retire le séparateur de milliers (8,560,000→8560000) · README breakeven 21→20 (formule périmée) · README formule 52 %→55 % (dérive CLAUDE.md #9) · README supprime la ligne entière (INTROUVABLE) · fixture Studio qté 12→13 (5 morsures : units/USD/DOP/breakeven README périmés + manifeste) · fixture 2 Chambres prix_usd 320000→300000 (catalogue USD périmé) ; restauré = green : 4 figures == génération réelle · manifeste == recompute · pct 52 % == CLAUDE.md #9 · exit 0. Working tree byte-restauré via git checkout -- (JAMAIS git clean) · 7 gates re-verts. ci/README.md (ligne récap check-readme-claims) mis à jour · log 05_activity_log/2026-08-01.md. Auto-score 4Big 96/100. Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-01.md | 57 +++++++++++++++++ ci/README.md | 2 +- ci/check_readme_claims.sh | 116 ++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index ab82c24..3193ff4 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -62,3 +62,60 @@ byte-restauré (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts - **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 `20260801_003201` · Buffer S8 · Domaine Faisabilité/bancable : les **4 figures « Génération réelle (fixture) »** du README (`40 unités · valeur catalogue USD 8,560,000 / DOP 505,040,000 · point d'équilibre 21 unités (⌈52 % × 40⌉)`) — les chiffres DATA-DERIVED que le **dossier bancable** (l'artefact que voit le banquier) recopie — étaient **HORS de tout gate**. Le module n'écrit son `out/` que sur disque (« out/ non commité ») ⇒ **check_artifacts est aveugle** (aucun artefact byte-gaté) et **AUCUN** gate ne le touchait. + +**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 esprit que les +paramètres canoniques `faisabilite/generator` **ancrés sur CLAUDE.md #9/#10** (le +`52 %` réutilisé ici) — appliqué à une **surface encore jamais gatée** : un module +dont la source de vérité n'est PAS un `out/*.json` mais **la fixture commitée + le +générateur**. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/faisabilite/bancable/README.md:103-105` — ligne + « Génération réelle (fixture) » : `40 unités` (= Σ quantités typologies) · + `USD 8,560,000` (= Σ qté×prix_usd) · `DOP 505,040,000` (= Σ qté×prix_dop) · + `21 unités (⌈52 % × 40⌉)` (= ⌈point_equilibre_pct × Σ unités⌉). +- Source faisant autorité : `fixtures/brief_bancable.json` (COMMITÉE) + la + **génération réelle** `python3 bancable_gen.py validate` (émet le manifeste sur + stdout, `figures_calculees[]`). Le `52 %` est **ancré sur CLAUDE.md #9** + (« Point équilibre 52% pré-vente »), pas recopié. +- Piège : la seule vérification existante (`tests/`) teste des **FONCTIONS** + (l'arithmétique de recoupement `_check_derived_arithmetic`) avec des **oracles + HARDCODÉS** dans le test — une copie de plus, jamais comparée à la PROSE du + README. Éditer une typologie de la fixture (quantité/prix), en ajouter/retirer + une, ou déplacer le point d'équilibre de CLAUDE.md #9, laisse les 4 chiffres du + README périmés pendant que le générateur produit autre chose → le banquier lit + un dossier faux (l'invention même que #6 interdit). +- **État courant** : **aucun chiffre périmé** — les 4 figures recoupent la + génération réelle exactement (anti-invention #6, rien à réécrire). Le défaut est + la **surface ungated** (module entièrement hors CI). + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Faisabilité/bancable +figures » avant `sys.exit`) : (1) **recompute indépendant** des 4 figures depuis +la fixture commitée ; (2) **cohérence croisée** — le manifeste GÉNÉRÉ +(`bancable_gen validate`) == le recompute (mord un générateur/fixture +incohérents) ; (3) **prose** — un seul pattern d'identité exige les 4 valeurs +EXACTES, **avec séparateur de milliers** pour USD/DOP, + la formule `⌈pct × Σ +unités⌉` dont le pct == CLAUDE.md #9 et le total == Σ unités. Un claim absent +échoue AUSSI (traçabilité). Le `out/` n'étant pas commité, le gate **exécute le +générateur** (stdlib pur, `subprocess`, `encoding="utf-8"`) au lieu de lire un +artefact byte-gaté — même patron que le bloc `faisabilite/generator` qui lit +`model.py`. + +**7 morsures vérifiées** : README `USD 8,560,000→9,560,000` (figure périmée) · +README retire le séparateur de milliers (`8,560,000→8560000`) · README breakeven +`21→20` (formule périmée) · README formule `52 %→55 %` (dérive CLAUDE.md #9) · +README supprime la ligne entière (INTROUVABLE · claim absent) · **fixture** Studio +`qté 12→13` (5 morsures : units/USD/DOP/breakeven README périmés + manifeste) · +**fixture** 2 Chambres `prix_usd 320000→300000` (catalogue USD périmé) ; restauré = +green : 4 figures == génération réelle · manifeste == recompute · pct 52 % == +CLAUDE.md #9 · exit 0. Working tree byte-restauré via `git checkout --` (JAMAIS +`git clean`, interdit absolu) · **7 gates re-verts**. + +- `ci/README.md` (ligne récap du pipeline `check-readme-claims`) mis à jour. +- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; + aucune écriture dans un `out/` ⇒ 0 dérive d'artefact ; conversion PDF + rebuild + Portail Bancables restent côté VPS). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index edd6dc5..dcede04 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 ») == 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) == 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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 8a86669..29f999f 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -3078,6 +3078,122 @@ if up_scopes is not None: f"{'oui' if got_emit else 'non'} MAIS artefact dit " f"{'oui' if exp_emit else 'non'} → régénérer le README") +# ============================================================================ +# Faisabilité/bancable — figures « Génération réelle (fixture) » du README +# ---------------------------------------------------------------------------- +# Le module `faisabilite/bancable` n'écrit son `out/` que sur disque (« out/ non +# commité ») : il est HORS de check_artifacts (aucun artefact byte-gaté) et +# AUCUN gate ne le touchait. Or son README §« Vérification en-repo » recopie +# QUATRE chiffres DATA-DERIVED que le générateur calcule sur la fixture COMMITÉE +# `fixtures/brief_bancable.json` : « 40 unités · valeur catalogue USD 8,560,000 +# / DOP 505,040,000 · point d'équilibre 21 unités (⌈52 % × 40⌉) ». Ces valeurs +# sont Σ quantités · Σ(qté×prix_usd) · Σ(qté×prix_dop) · ⌈52 % × Σ unités⌉ — le +# 52 % ancré sur CLAUDE.md #9. PIÈGE : la suite tests/ teste des FONCTIONS +# (l'arithmétique de recoupement) avec des oracles HARDCODÉS ; elle ne compare +# JAMAIS la PROSE du README à la génération réelle. Éditer une typologie de la +# fixture (quantité/prix), en ajouter/retirer une, ou déplacer le point +# d'équilibre de CLAUDE.md #9, laisse les 4 chiffres du README périmés en silence +# pendant que le générateur produit autre chose → le banquier lit un dossier +# faux, l'invention même que #6 interdit. Ce bloc RECOMPUTE les 4 figures depuis +# la fixture (source de vérité), exige que la GÉNÉRATION RÉELLE (bancable_gen +# validate) les confirme (mord un générateur/fixture incohérents), puis que la +# prose README les énumère EXACTEMENT (valeur + séparateur de milliers + formule +# ⌈pct × Σ unités⌉, pct ancré CLAUDE.md #9). Un claim absent échoue AUSSI. +# ============================================================================ +import math +BANC = f"{D}/faisabilite/bancable" +BANC_RD = f"{BANC}/README.md" +# 1) point d'équilibre % ANCRÉ sur CLAUDE.md #9 (source faisant autorité · #6/#9) +try: + _cmb = open("CLAUDE.md", encoding="utf-8").read() + _l9b = re.search(r"^9\.\s+(.+)$", _cmb, re.M) + if not _l9b: + raise ValueError("contrainte #9 INTROUVABLE") + _peb = None + for _sb in re.split(r"·", _l9b.group(1)): + if "equilibre" in _ai(_sb): + _mmb = re.search(r"(\d+(?:\.\d+)?)\s*%", _sb) + _peb = float(_mmb.group(1)) if _mmb else None + if _peb is None: + raise ValueError("point d'équilibre % absent de #9") +except (OSError, ValueError) as e: + bad(f"Bancable figures · CLAUDE.md #9 (point d'équilibre) illisible : {e}"); _peb = None +# 2) recompute INDÉPENDANT depuis la fixture COMMITÉE (source de vérité · #6) +try: + _fxb = json.load(open(f"{BANC}/fixtures/brief_bancable.json", encoding="utf-8")) + _typb = _fxb["architecture"]["typologies"] + rec_units = sum(int(t["quantite"]) for t in _typb) + rec_usd = sum(int(t["quantite"]) * int(t["prix_usd"]) for t in _typb) + rec_dop = sum(int(t["quantite"]) * int(t["prix_dop"]) for t in _typb) +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Bancable figures · fixture brief_bancable.json illisible/inattendue : {e}"); rec_units = None +# 3) génération RÉELLE : le manifeste que le README prétend recopier +man_fig = None +try: + _procb = subprocess.run( + [sys.executable, "bancable_gen.py", "validate", "fixtures/brief_bancable.json"], + cwd=BANC, capture_output=True, text=True, encoding="utf-8") + if _procb.returncode != 0: + raise ValueError(f"validate exit {_procb.returncode} : {_procb.stderr.strip()[:160]}") + man_fig = {f["cle"]: f["valeur"] for f in json.loads(_procb.stdout)["figures_calculees"]} +except (ValueError, KeyError, OSError) as e: + bad(f"Bancable figures · génération réelle (bancable_gen validate) échoue : {e}") + +if _peb is not None and rec_units is not None and man_fig is not None: + rec_be = math.ceil(_peb / 100.0 * rec_units) + # cohérence croisée : le manifeste GÉNÉRÉ == le recompute indépendant fixture + for cle, expv in (("total_unites", rec_units), ("valeur_catalogue_usd", rec_usd), + ("valeur_catalogue_dop", rec_dop), ("point_equilibre_unites", rec_be)): + gotv = man_fig.get(cle) + if gotv is None: + bad(f"Bancable figures · manifeste sans figure « {cle} » (attendu {expv})") + elif int(gotv) != int(expv): + bad(f"Bancable figures · manifeste « {cle} »={gotv} MAIS recompute fixture " + f"{expv} → générateur/fixture incohérents (formule cassée)") + else: + good(f"Bancable figures · manifeste « {cle} »={int(gotv)} == recompute fixture") + # 4) PROSE README §« Vérification en-repo » — un seul pattern d'identité + try: + banc_txt = re.sub(r"\s+", " ", open(BANC_RD, encoding="utf-8").read()) + except OSError as e: + bad(f"Bancable figures · {BANC_RD} illisible : {e}"); banc_txt = None + if banc_txt is not None: + pat = (r"(\d[\d,]*)\s*unités\s*·\s*valeur catalogue\s*USD\s*([\d,]+)\s*/\s*" + r"DOP\s*([\d,]+)\s*·\s*point d'équilibre\s*(\d+)\s*unités\s*" + r"\(⌈\s*(\d+)\s*%\s*×\s*(\d+)\s*⌉\)") + m = re.search(pat, banc_txt) + if m is None: + bad(f"Bancable figures · {BANC_RD} — ligne « Génération réelle (fixture) » " + f"INTROUVABLE (attendu {rec_units} unités · USD {rec_usd:,} / DOP " + f"{rec_dop:,} · équilibre {rec_be} unités (⌈{_peb:g} % × {rec_units}⌉)) — " + f"un claim absent est une régression (#6)") + else: + g_pct, g_tot = float(m.group(5)), int(m.group(6)) + for lbl, raw, expv in (("Σ unités", m.group(1), rec_units), + ("valeur catalogue USD", m.group(2), rec_usd), + ("valeur catalogue DOP", m.group(3), rec_dop), + ("point d'équilibre", m.group(4), rec_be)): + got = int(raw.replace(",", "")) + if got != expv: + bad(f"Bancable figures · README {lbl}={raw} MAIS génération réelle " + f"={expv} → figure périmée, régénérer la fixture/le README (#6)") + elif expv >= 1000 and "," not in raw: + bad(f"Bancable figures · README {lbl}={raw} sans séparateur de milliers " + f"(attendu « {expv:,} »)") + else: + good(f"Bancable figures · README {lbl}={raw} == génération réelle") + # formule ⌈52 % × 40⌉ : pct ancré CLAUDE.md #9 · total == Σ unités + if g_pct != _peb: + bad(f"Bancable figures · README formule ⌈{g_pct:g} %⌉ MAIS CLAUDE.md #9 " + f"point d'équilibre = {_peb:g} % → aligner (ou CLAUDE.md)") + else: + good(f"Bancable figures · README formule pct {g_pct:g} % == CLAUDE.md #9") + if g_tot != rec_units: + bad(f"Bancable figures · README formule « × {g_tot} » MAIS Σ unités " + f"= {rec_units} → total de la formule désaligné") + else: + good(f"Bancable figures · README formule « × {g_tot} » == Σ unités") + sys.exit(1 if FAIL else 0) PY rc=$?