diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 6a03948..50d45f1 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,65 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_183124` · Buffer S8 · Domaine CRM/dossier_vente (2ᵉ surface) : le **catalogue de projets** du DocType porteur (« Projet (`Select` **P01..P09** ancré sur CLAUDE.md) ») était **ancré à CLAUDE.md EN PROSE** sans AUCUN gate d'identité — le bloc CRM pipeline existant ne recompute que le COMPTE d'états (« les **9 états** du pipeline ») du même README. + +**Tâche** : **Sprint 8 · buffer L60** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive (CLAUDE.md #6). Cette session cible une +**2ᵉ surface data-derived du README `crm/dossier_vente`** — le champ `Select` +`projet` du DocType porteur, dont les 9 options (`P01 Structure` … `P09 1069 +Crisfer`) sont une transcription de la section « ## Projets » de CLAUDE.md, jamais +ANCRÉE par aucun gate. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/crm/dossier_vente/README.md:46` : « Projet (`Select` + **P01..P09** ancré sur CLAUDE.md) » ; `doctype_spec.json` `_comment` : « les + Select `projet`/`devise` sont ancrés sur CLAUDE.md … pas inventés ». +- Source faisant autorité : la section **« ## Projets » de CLAUDE.md** (contraintes + #6/#7 · liste canonique des projets). Le DocType (`out/doctype_oto_dossier_vente.json`, + byte-gaté par `check_artifacts`) porte l'ensemble ORDONNÉ des 9 options. +- `check_artifacts` ne prouve QUE _DocType == spec_ (byte-for-byte) ; la spec recopie + elle-même le catalogue → **toute la chaîne (spec → DocType) peut DÉRIVER de + CLAUDE.md en restant byte-verte** : **RENOMMER** un projet (`P03 Nakua` → `P03 + Nakua Bay`), en **AJOUTER** (`P10`) ou en **RETIRER** un laisse le Select périmé, + contredisant silencieusement CLAUDE.md pendant que la prose « ancré sur CLAUDE.md » + devient FAUSSE — « vert trompeur » qu'aucune suite `tests/` (qui teste des FONCTIONS + de builder, pas l'ancrage à CLAUDE.md) n'attrape. +- **État courant** : **aucun projet périmé** — les 9 options recoupent CLAUDE.md + 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 +« ## Projets » de CLAUDE.md (zéro duplication · source unique · même esprit que le +**roadmap-anchor** INV11) et exige que les options du `Select` `projet` l'égalent +**exactement** (set-diff absent ET en trop, **puis ordre** · NFC), plus la plage +`P01..P` + le mot d'ancrage dans le README. Cohérences croisées en bonus +(mordent un catalogue CLAUDE.md internement incohérent) : catalogue **non vide, +sans doublon et contigu** depuis `P01`. Un claim absent échoue AUSSI. + +### Preuve (6 morsures vérifiées + restauration verte) + +| Injection | Résultat | +|---|---| +| CLAUDE.md `P03 Nakua → P03 Nakua Bay` (DocType stale) | ✗ Select DÉRIVE : absents=`[P03 Nakua Bay]` en trop=`[P03 Nakua]` · exit 1 | +| CLAUDE.md `+P10` (catalogue grandit) | ✗ Select absents=`[P10 …]` + README dit P01..P09 MAIS catalogue P01..P10 · exit 1 | +| DocType Select réordonné (bon ensemble, ordre ≠) | ✗ bon ENSEMBLE mais ORDRE ≠ CLAUDE.md · exit 1 | +| DocType renomme 1 libellé (`P07 Aqua Terra Las Terrenas → P07 Aqua Terra`) | ✗ Select DÉRIVE : absents/en trop · exit 1 | +| README plage `P01..P09 → P01..P08` | ✗ README dit P01..P08 MAIS catalogue P01..P09 · exit 1 | +| README ancrage supprimé (reformulé) | ✗ claim « Projet (`Select` P01..P09 ancré sur CLAUDE.md) » INTROUVABLE · exit 1 | +| restauré | ✓ Select `projet` == CLAUDE.md (9 projets, ordre inclus) · plage P01..P09 + ancrage · catalogue contigu · exit 0 | + +- **`ci/README.md`** — surface ajoutée à la table récap du pipeline + paragraphe + descriptif (« catalogue de projets ancré sur CLAUDE.md »). + +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_180119` · Buffer S8 · Domaine SEO (3e surface) : la **carte hreflang** (« carte `alternate` **FR/EN/ES** + `x-default` par page ») était décrite EN PROSE dans le README du module `seo` sans AUCUN gate d'IDENTITÉ — les deux blocs SEO existants gatent le COMPTE de mots-clés (artefact `seo_keywords`) et les `@type` du graphe schema.org (artefact `seo_schema_org`), jamais le jeu de langues hreflang qui vit dans un **troisième** artefact. **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement diff --git a/ci/README.md b/ci/README.md index 01eedae..5192802 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 ») **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) == 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`) **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) == 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 @@ -218,6 +218,24 @@ contenu (ni vacante ni étrangère), et pour chaque page `x-default`.href == défaut = canonical » du README). Un claim supprimé (« carte INTROUVABLE ») échoue **aussi**. +**Catalogue de projets ancré sur CLAUDE.md** (2ᵉ surface du README +`crm/dossier_vente`) : le champ `Select` `projet` du DocType porteur +(`out/doctype_oto_dossier_vente.json`, byte-gaté par `check_artifacts`) énumère les +**9 projets OTO** (`P01 Structure` … `P09 1069 Crisfer`) que le README résume +« Projet (`Select` **P01..P09** ancré sur CLAUDE.md) ». Ce catalogue est une +**transcription** : la section « ## Projets » de **CLAUDE.md** est la source +faisant autorité (#6/#7), mais `check_artifacts` ne prouve que _DocType == spec_ +(byte-for-byte) — la spec recopie elle-même la liste, donc toute la chaîne peut +**dériver** de CLAUDE.md en restant byte-verte : **renommer** un projet +(`P03 Nakua` → `P03 Nakua Bay`), en **ajouter** (`P10`) ou en **retirer** un laisse +le Select périmé tandis que la prose « ancré sur CLAUDE.md » devient fausse. Même +esprit que le **roadmap-anchor** (INV11 · `roadmap_line` ancré au fichier roadmap) : +on recompute le catalogue depuis « ## Projets » de CLAUDE.md et on exige que les +options du Select l'égalent **exactement** (set-diff absent ET en trop, **puis +ordre**), plus la plage `P01..P` + le mot d'ancrage dans le README. En +bonus, le catalogue CLAUDE.md est vérifié **non vide, sans doublon et contigu** +depuis `P01` (byte-déterminisme du contrat). Un claim supprimé échoue **aussi**. + Même classe enfin sur le **pipeline vente CRM** : le couple « **9 états · 11 transitions** » (titre de section du README du module `crm/workflow_vente`) et le **même** compte d'états « les **9 états** du pipeline » (README du DocType porteur diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 45bf226..39cf62e 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -1991,6 +1991,95 @@ if hl_pages: f"{'+x-default' if has_xdef else ''} MAIS carte " f"{sorted(content_langs)}+x-default{detail} → régénérer le README") +# ============================================================================ +# CRM/dossier_vente (2e surface) · CATALOGUE DE PROJETS « P01..P09 ancré sur +# CLAUDE.md » — le Select `projet` du DocType porteur. +# ---------------------------------------------------------------------------- +# Le bloc CRM pipeline ci-dessus recompute le COMPTE d'états du même README +# (« les 9 états du pipeline »). Restait, dans le MÊME README, une surface +# data-derived distincte et jamais gatée : la liste des PROJETS. Le champ Select +# `projet` du DocType (out/doctype_oto_dossier_vente.json, byte-gaté par +# check_artifacts) porte l'ENSEMBLE ORDONNÉ des 9 projets OTO +# (« P01 Structure », « P02 Coral del Sur », …). Le README:46 le résume +# « Projet (`Select` **P01..P09** ancré sur CLAUDE.md) » et le `_comment` du +# doctype_spec.json le déclare « ancré sur CLAUDE.md … pas inventé ». Or ce +# catalogue est une TRANSCRIPTION MANUELLE : la section « ## Projets » de +# CLAUDE.md est la SOURCE FAISANT AUTORITÉ (contraintes #6/#7 · liste des +# projets), mais AUCUN gate ne compare les options du DocType à CLAUDE.md. +# check_artifacts ne prouve que DocType == spec (byte-for-byte) ; la spec +# elle-même recopie la liste — donc TOUTE la chaîne (spec → DocType) peut +# DÉRIVER de CLAUDE.md en restant byte-verte : RENOMMER un projet dans CLAUDE.md +# (« P03 Nakua » → « P03 Nakua Bay »), en AJOUTER un (P10) ou en RETIRER un +# laisse le Select périmé, contredisant silencieusement CLAUDE.md pendant que la +# prose « ancré sur CLAUDE.md » devient FAUSSE — « vert trompeur » qu'aucune +# suite tests/ (qui teste des FONCTIONS de builder, pas l'ancrage à CLAUDE.md) +# n'attrape. Même esprit que le roadmap-anchor (INV11) qui ancre les +# `roadmap_line` de l'artefact au fichier roadmap : ici on ANCRE le catalogue +# projet du DocType à CLAUDE.md. On RECOMPUTE le catalogue depuis « ## Projets » +# de CLAUDE.md (zéro duplication · source unique) et on exige que les options du +# Select l'égalent EXACTEMENT (absent ET en trop via set-diff, puis ORDRE), et +# que la prose porte la bonne plage P01..P + le mot d'ancrage. Un claim +# absent échoue AUSSI. Cohérences croisées en bonus (mordent un catalogue +# CLAUDE.md internement incohérent) : catalogue NON VIDE, codes SANS DOUBLON et +# CONTIGUS depuis P01 (P01,P02,…,P0N — byte-déterminisme du contrat). +# ============================================================================ +def _nfc(s): + return unicodedata.normalize("NFC", s) +try: + cm_raw = open("CLAUDE.md", encoding="utf-8").read() + mcm = re.search(r"^## Projets\s*\n(.+)$", cm_raw, re.M) + if mcm is None: + raise ValueError("section « ## Projets » INTROUVABLE") + # catalogue faisant autorité = tokens « PNN Label » séparés par « · » + cat = [_nfc(t.strip()) for t in mcm.group(1).split("·") if t.strip()] + cat_codes = [re.match(r"(P\d{2})\b", p).group(1) for p in cat] +except (OSError, ValueError, AttributeError) as e: + bad(f"CRM projets · CLAUDE.md « ## Projets » illisible/inattendu : {e}"); cat = None +if cat is not None: + # bonus : catalogue non vide · codes sans doublon · contigus depuis P01 + contig = cat_codes == [f"P{i:02d}" for i in range(1, len(cat_codes) + 1)] + if not cat or len(set(cat_codes)) != len(cat_codes) or not contig: + bad(f"CRM projets · catalogue CLAUDE.md dégénéré : codes={cat_codes} " + f"(attendu P01..P{len(cat_codes):02d} contigus, sans doublon, non vide)") + else: + good(f"CRM projets · catalogue CLAUDE.md sain : {len(cat)} projets " + f"P01..{cat_codes[-1]} contigus") + # 1) ANCRAGE : options du Select `projet` du DocType == catalogue CLAUDE.md + try: + dv_dt = load("crm/dossier_vente/out/doctype_oto_dossier_vente.json") + pf = next(f for f in dv_dt["fields"] if f.get("fieldname") == "projet") + dt_opts = [_nfc(o.strip()) for o in pf["options"].split("\n") if o.strip()] + except (OSError, KeyError, ValueError, StopIteration) as e: + bad(f"CRM projets · DocType Select `projet` illisible/inattendu : {e}"); dt_opts = None + if dt_opts is not None: + if dt_opts == cat: + good(f"CRM projets · Select `projet` du DocType == CLAUDE.md " + f"({len(cat)} projets, ordre inclus)") + elif set(dt_opts) == set(cat): + bad(f"CRM projets · Select `projet` porte le bon ENSEMBLE mais un ORDRE " + f"≠ CLAUDE.md : DocType={dt_opts} vs {cat} → régénérer le DocType") + else: + miss = [p for p in cat if p not in dt_opts] + extra = [p for p in dt_opts if p not in cat] + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"CRM projets · Select `projet` du DocType DÉRIVE de CLAUDE.md" + f"{detail} → régénérer le DocType (ou aligner CLAUDE.md)") + # 2) PROSE : README résume la plage P01..P + ancrage CLAUDE.md + dv_rd2 = read_norm(f"{D}/crm/dossier_vente/README.md") + if dv_rd2 is not None: + mp = re.search(r"Projet \(`Select` \*\*(P\d{2})\.\.(P\d{2})\*\* " + r"ancré sur CLAUDE\.md\)", dv_rd2) + if mp is None: + bad(f"CRM projets · {D}/crm/dossier_vente/README.md — claim « Projet " + f"(`Select` P01..P{cat_codes[-1][1:]} ancré sur CLAUDE.md) » INTROUVABLE") + elif (mp.group(1), mp.group(2)) == (cat_codes[0], cat_codes[-1]): + good(f"CRM projets · README plage {mp.group(1)}..{mp.group(2)} " + f"+ ancrage CLAUDE.md == catalogue") + else: + bad(f"CRM projets · README dit {mp.group(1)}..{mp.group(2)} MAIS catalogue " + f"CLAUDE.md va de {cat_codes[0]} à {cat_codes[-1]} → régénérer le README") + sys.exit(1 if FAIL else 0) PY rc=$?