diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 61f12b2..e870489 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_090037` · Buffer S8 · Domaine SEO : le total ET la ventilation par langue des mots-clés (« 258 mots-clés (fr=87 · en=87 · es=84) »), transcrits À LA MAIN dans DEUX docs (table du README module + ligne de la fiche agent), n'étaient couverts par AUCUN gate — ajouter un projet ferait dériver les deux docs en silence + +**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap +fonctionnellement close (22/22 modules · 564 tests · 7 gates verts au démarrage). +Poursuite de la série anti-dérive (CLAUDE.md #6). Les sessions précédentes ont gaté +les comptes **data-derived** des domaines tests, portails (structure), RBAC (3 volets). +Cette session cible le **domaine SEO**, dernier grand livrable dont un compte +data-derived cité dans deux docs pointait un artefact sans être recomputé par aucun gate. + +### Défaut trouvé — comptes de mots-clés ungated, transcrits dans DEUX docs + +- **`05_deliverables_mvp/seo/README.md:15`** : table « Livrables » → + « `out/seo_keywords.json` — **258** mots-clés (fr=87 · en=87 · es=84) ». +- **`03_agents/seo/AGENT.md:24`** : **même** ligne, mêmes 4 nombres recopiés. +- **Pourquoi non gaté** : `check_readme_claims` couvrait les comptes tests, les + triplets par workspace des portails et les 3 volets RBAC — jamais le domaine SEO. + `check_docs` ne valide que les liens ; `check_artifacts` prouve que + `seo/out/MANIFEST.json` est **reproductible byte-for-byte** (bloc `counts` + = {keywords_total, keywords_per_lang{fr,en,es}} recomptés de `projets_master.json` + à chaque build) mais **ne compare pas** ce que les DEUX docs en **écrivent**. + Ajouter un projet / du vocabulaire éditorial ferait dériver les deux docs **en + silence** pendant que l'artefact dit autre chose — même « vert trompeur ». +- **État courant** : **aucun nombre périmé** — 258=(87+87+84) recoupe MANIFEST + exactement (anti-invention #6 : rien à réécrire). Le défaut est la **surface + ungated** (faux-vert latent), pas un chiffre faux. + +### Fix — extension de `check_readme_claims.sh` (recompute total + 3 langues depuis MANIFEST) + +Nouveau bloc qui RECOMPUTE le quadruplet `(total, fr, en, es)` depuis +`MANIFEST.counts` (source déjà byte-gatée · zéro duplication du compteur du +générateur) et exige l'égalité avec **les deux** docs. Recomputer les **trois +langues** (pas seulement le total) attrape une **compensation** entre langues +(fr −1 / en +1) qui laisserait le total juste et la ventilation fausse. Un claim +**absent** échoue AUSSI. En-tête du gate + `ci/README.md` (§ table + § détail) mis à jour. + +### Preuves de morsure + +| Injection | Résultat | +|---|---| +| **README total** `258→259` | **exit 1** `dit 259/87/87/84 MAIS MANIFEST 258/87/87/84` | +| **Fiche compensation** `fr=87/en=87 → fr=86/en=88` (total inchangé) | **exit 1** `dit 258/86/88/84 MAIS MANIFEST 258/87/87/84` — mord là où le total seul est aveugle | +| **Claim disparu** (README) | **exit 1** `claim mots-clés INTROUVABLE (attendu total=258 …)` | +| Restauré | **exit 0** (2 docs verts) | + +### Vérifs + +| Contrôle | Résultat | +|---|---| +| 7 gates (`guard`/`json`/`docs`/`artifacts`/`regression`/`ci_integrity`/`readme_claims`) | ✅ exit 0 | +| `check_artifacts` (rebuild + égalité byte des `out/`) | ✅ (édition **hors** `05_deliverables_mvp` ⇒ 0 dérive d'artefact ; aucun README/AGENT.md touché) | +| Matrice de régression | **564/22 · PASS** inchangée (ni `tests/` ni doc-evidence touchés) | +| Diff | 2 fichiers (`ci/check_readme_claims.sh` + `ci/README.md`) · 0 untracked | + +**Anti-invention (#6)** : rien inventé — le gate recompute total + 3 langues depuis +`MANIFEST.counts` au lieu de figer un compteur ; les nombres des deux docs **découlent** de l'artefact. +**Hors périmètre worker (VPS · #8)** : néant (gate python/stdlib en-repo). +**Auto-score 4Big** : 96/100. + +--- + ## Session `20260731_080025` · Buffer S8 · Hors du domaine « tests » : les comptes PAR WORKSPACE (cartes/liens/rôles) des 5 portails frontend, transcrits À LA MAIN dans DEUX docs (table du README module + prose de la fiche agent), n'étaient couverts par AUCUN gate — ajouter un rôle/DocType au contrat RBAC ferait dériver les deux docs en silence **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap diff --git a/ci/README.md b/ci/README.md index 16ec1c9..c4d5eb5 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) == 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) == 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 @@ -174,6 +174,18 @@ du volet (recalculé du contrat `rbac_50_roles.json` à chaque build, byte-gaté `check_artifacts`) : ajouter un rôle/DocType ferait dériver **les trois** READMEs en silence sinon. +Même classe enfin côté **SEO** : le total et la ventilation par langue des +mots-clés (« **258** mots-clés (fr=87 · en=87 · es=84) ») étaient transcrits **à +la main** dans **deux** docs — la table « Livrables » du README de module +(`05_deliverables_mvp/seo/README.md`) **et** la même ligne de la fiche agent +(`03_agents/seo/AGENT.md`) — sans aucun gate. Le quadruplet (total + fr/en/es) est +recomputé depuis `seo/out/MANIFEST.json` (`counts.keywords_total` + +`counts.keywords_per_lang`, recomptés de `projets_master.json` à chaque build, +byte-gaté par `check_artifacts`). Recomputer les **trois** langues, pas seulement +le total, attrape une **compensation** entre langues (fr −1 / en +1) qui laisserait +le total juste et la ventilation fausse — ajouter un projet ferait sinon dériver +**les deux** docs en silence. + 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 574d72d..ffed2df 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -47,6 +47,10 @@ # DEUX docs (table du README + prose « OTO Ventes 4 cartes/11 liens/12 # rôles ») + total « N Workspace natifs » → portails MANIFEST # (tableau `workspaces`, byte-gaté par check_artifacts) +# seo/README.md + 03_agents/seo/AGENT.md (comptes de mots-clés) : +# • total + ventilation « 258 mots-clés (fr=87 · en=87 · es=84) » transcrits À +# LA MAIN dans DEUX docs → seo MANIFEST.counts +# (keywords_total + keywords_per_lang, recomptés de projets_master, byte-gatés) # rbac/{roleprofile_gen,apply_plan,userperm_gen}/README.md (comptes 3 volets) : # • table « Nb rôles » PAR profil + total bijectif → roleprofile MANIFEST # • ligne agrégée « 6 étapes · 50 rôles / 116 DocPerm / 28 UP / 6 Role @@ -584,6 +588,49 @@ if up_bm is not None and up_rd is not None: bad(f"RBAC userperm · {UP_RD} dit {'/'.join(mu.groups())} MAIS MANIFEST " f"dit {'/'.join(up_exp)} ({up_src}) → régénérer le README") +# ============================================================================ +# SEO · comptes de mots-clés (total + ventilation par langue) — +# même classe de « vert trompeur », transcrits À LA MAIN dans DEUX docs. +# ---------------------------------------------------------------------------- +# Le générateur SEO émet out/MANIFEST.json dont le bloc `counts` porte +# keywords_total et keywords_per_lang{fr,en,es}, RECOMPTÉS à chaque build depuis +# `projets_master.json` (byte-gaté par check_artifacts ⇒ source faisant autorité). +# Ce quadruplet est recopié À LA MAIN à DEUX endroits : la table « Livrables » du +# README du module (05_deliverables_mvp/seo/README.md — « **258** mots-clés +# (fr=87 · en=87 · es=84) ») ET la même ligne de la fiche agent +# (03_agents/seo/AGENT.md). Aucun gate ne les couvrait : ajouter un projet ou du +# vocabulaire éditorial ferait dériver LES DEUX docs en silence pendant que +# l'artefact dit autre chose — même piège que les triplets par workspace des +# portails. On RECOMPUTE ici (total + 3 langues) depuis MANIFEST.counts (déjà +# byte-gaté · zéro duplication du compteur du générateur) et on exige l'égalité +# avec les DEUX docs. Un claim absent échoue AUSSI (traçabilité). +# ============================================================================ +SEO_RD = f"{D}/seo/README.md" +SEO_FI = "03_agents/seo/AGENT.md" +try: + seo_c = load("seo/out/MANIFEST.json")["counts"] + seo_exp = (str(seo_c["keywords_total"]), + str(seo_c["keywords_per_lang"]["fr"]), + str(seo_c["keywords_per_lang"]["en"]), + str(seo_c["keywords_per_lang"]["es"])) +except (OSError, KeyError, ValueError) as e: + bad(f"SEO · MANIFEST illisible/inattendu : {e}"); seo_exp = None +if seo_exp is not None: + seo_src = (f"total={seo_exp[0]} fr={seo_exp[1]} en={seo_exp[2]} es={seo_exp[3]}") + seo_pat = r"\*\*(\d+)\*\* mots-clés \(fr=(\d+) · en=(\d+) · es=(\d+)\)" + for path_f in (SEO_RD, SEO_FI): + txt = read_norm(path_f) + if txt is None: + continue + m = re.search(seo_pat, txt) + if m is None: + bad(f"SEO · {path_f} — claim mots-clés INTROUVABLE (attendu {seo_src})") + elif m.groups() == seo_exp: + good(f"SEO · {path_f} — {'/'.join(m.groups())} == MANIFEST ({seo_src})") + else: + bad(f"SEO · {path_f} — dit {'/'.join(m.groups())} MAIS MANIFEST dit " + f"{'/'.join(seo_exp)} ({seo_src}) → régénérer le doc") + sys.exit(1 if FAIL else 0) PY rc=$?