From 9353d7f54ee3a568b03b3d45aa3bd81527325f5b Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 08:09:19 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20SEO/projets=5Fmaster=20:=20IDENTIT=C3=89=20du=20CATALO?= =?UTF-8?q?GUE=20PROJETS=20(`P01`..`P09`)=20ANCR=C3=89E=20sur=20CLAUDE.md?= =?UTF-8?q?=20=C2=A7Projets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La matière première du worker SEO est `05_deliverables_mvp/seo/fixtures/projets_master.json` — les 9 projets (`code`+`nom`) que le bundle expose à Google (un listing `Residence`/projet dans le JSON-LD indexé sous `vente.otov7.com/projets/…`). Chaque entrée DÉCLARE `source.fichiers: ["CLAUDE.md#Projets"]` et le README:80 l'affirme (« 9 projets P01..P09 · noms sourcés de `CLAUDE.md §Projets` }) MAIS AUCUN gate ne le VÉRIFIAIT. check_artifacts prouve QUE les out/ SE RECONSTRUISENT depuis la fixture (byte-for-byte), PAS que ses code/nom == CLAUDE.md §Projets ; les blocs SEO amont gatent les COMPTES de mots-clés + la COMPOSITION du graphe schema.org (bijection un-listing/projet) + l'IDENTITÉ de la marque racine `Helios RD` (§Entités), tous AVEUGLES à l'identité de la LISTE de projets. Piège #6 : RENOMMER un projet dans CLAUDE.md §Projets (« Aqua Terra Las Terrenas »→« … Bay »), le RENOMMER/AJOUTER/RETIRER dans la fixture, puis régénérer, laisse le JSON-LD PUBLIC lister un projet FANTÔME ou en OMETTRE un canonique pendant que la byte-gate reste VERTE (l'artefact se reconstruit fidèlement… depuis une fixture DÉRIVÉE) et que le compte reste 9 — « vert trompeur » de la MÊME classe que la marque org (§Entités), les entités CONFOTUR (§Entités) et les libellés projets du run_sheet démo (§Projets). Aucune suite tests/ (FONCTIONS de génération, jamais l'ancrage à CLAUDE.md) ne l'attrape. Gate ajouté (bloc « SEO · l'IDENTITÉ du CATALOGUE PROJETS ») : RE-DÉRIVE le catalogue de CLAUDE.md §Projets (zéro duplication · source unique) puis exige — (a) identité d'ensemble des codes fixture ⇔ CLAUDE.md (aucun fantôme/manquant · pas de doublon) ; (b) `nom` de chaque entrée == CLAUDE.md §Projets (accent-INSENSIBLE · la fixture normalise « Xamaná »→« Xamana ») ; (c) chaque entrée DÉCLARE l'ancrage `CLAUDE.md#Projets` dans source.fichiers ; (d) les `name` des listings du graphe schema.org PUBLIC == l'ensemble des noms CLAUDE.md §Projets + bijection (1 listing/projet) + MANIFEST counts projects/listings == |§Projets| ; (e) le README cite l'ancrage §Projets. Un claim absent échoue AUSSI (disparition = régression). 13 confirmations vertes ; 7 morsures vérifiées puis restaurées (CLAUDE.md rename P07 = bites b+d simultanés l'ancre est vive · fixture nom drift P03 · fixture drop P09 · fixture add fantôme P10 · fixture strip source.fichiers P01 · artefact schema.org rename Structure→Estructura = le vrai silent-green public · README retire la citation §Projets), restauré vert, 7 gates CI re-verts. Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-01.md | 21 +++++++ ci/check_readme_claims.sh | 115 ++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 6f16077..927d87a 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -916,3 +916,24 @@ check_regression). la confirmation du **TipoCambio** USD→DOP + l'émission réelle via Compupar restent côté agent ERPNext Backend / Compta · VPS). - **Auto-score 4Big** : 96/100. + +--- + +## Session `20260801_080244` · Buffer S8 · SEO/projets_master — IDENTITÉ du CATALOGUE PROJETS (P01..P09) ANCRÉE sur CLAUDE.md §Projets + +**Domaine** : SEO · `05_deliverables_mvp/seo/fixtures/projets_master.json` + artefacts `out/seo_schema_org.json`. + +**Constat (anti-drift #6)** : la matière première du worker SEO est `projets_master.json` — 9 projets (`code` + `nom`) que le bundle SEO expose à Google (un listing `Residence` par projet dans le JSON-LD indexé sous `vente.otov7.com/projets/…`). Chaque entrée **DÉCLARE** `source.fichiers: ["CLAUDE.md#Projets"]` et le README:80 l'affirme (« 9 projets P01..P09 · noms sourcés de `CLAUDE.md §Projets` ») — **MAIS AUCUN gate ne le VÉRIFIAIT**. `check_artifacts` prouve QUE les `out/` se reconstruisent depuis la fixture (byte-for-byte), PAS que ses `code`/`nom` == CLAUDE.md §Projets ; les blocs SEO amont gatent les COMPTES de mots-clés, la COMPOSITION du graphe schema.org (bijection un-listing-par-projet) et l'IDENTITÉ de la marque racine `Helios RD` (§Entités) — tous AVEUGLES à l'identité de la **liste de projets**. + +**Vert trompeur** : RENOMMER un projet dans CLAUDE.md §Projets (« Aqua Terra Las Terrenas » → « … Bay »), le RENOMMER/AJOUTER/RETIRER dans la fixture, puis régénérer, laisse le JSON-LD PUBLIC lister un projet FANTÔME ou en OMETTRE un canonique pendant que la byte-gate reste VERTE (l'artefact se reconstruit fidèlement… depuis une fixture DÉRIVÉE) et que le compte reste 9. Même classe EXACTE que la marque org (§Entités · session 050225), les entités CONFOTUR (§Entités · 053228) et les libellés projets du run_sheet démo (§Projets · déjà gaté). Aucune suite `tests/` (FONCTIONS de génération, jamais l'ancrage à CLAUDE.md) ne l'attrape. + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « SEO · l'IDENTITÉ du CATALOGUE PROJETS ») — RE-DÉRIVE le catalogue de CLAUDE.md §Projets (zéro duplication · source unique), puis exige : +- **(a)** identité d'ensemble des codes fixture ⇔ CLAUDE.md §Projets (aucun fantôme/manquant · pas de doublon) ; +- **(b)** `nom` de chaque entrée == CLAUDE.md §Projets (accent-**insensible** : la fixture normalise « Xamaná »→« Xamana ») ; +- **(c)** chaque entrée DÉCLARE l'ancrage `CLAUDE.md#Projets` dans `source.fichiers` ; +- **(d)** les `name` des listings du graphe schema.org PUBLIC == l'ensemble des noms CLAUDE.md §Projets + bijection (1 listing/projet) + `MANIFEST.counts` projects/listings == |§Projets| ; +- **(e)** le README cite l'ancrage CLAUDE.md §Projets. Un claim absent échoue AUSSI (disparition = régression). + +**Vérif** : 13 confirmations vertes (9 noms + set codes + listings schema.org + MANIFEST + README) ; **7 morsures vérifiées puis restaurées** — CLAUDE.md rename P07 (bite b+d simultanés, l'ancre est vive) · fixture nom drift P03 · fixture drop P09 (set manquant) · fixture add fantôme P10 (set fantôme) · fixture strip `source.fichiers` P01 · artefact schema.org rename `Structure`→`Estructura` (le vrai silent-green public) · README retire la citation §Projets. Restauré vert. **7 gates CI re-verts** (`check_readme_claims`, `check_artifacts`, `check_docs`, `check_ci_integrity`, `guard_constraints`, `check_regression`, `validate_json`). + +- **Auto-score 4Big** : 96/100. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 376a528..10bd7c5 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2666,6 +2666,121 @@ if brand is not None and sog is not None: else: good(f"SEO org · seo_spec.json déclare l'ancrage CLAUDE.md §Entités pour {brand!r}") +# ============================================================================ +# SEO · l'IDENTITÉ du CATALOGUE PROJETS (P01..P09) — les 9 projets (`code` + +# `nom`) que le bundle SEO expose à Google — ANCRÉE sur CLAUDE.md §Projets. +# ---------------------------------------------------------------------------- +# La matière première du worker SEO est `seo/fixtures/projets_master.json` +# (README:22 « sortie canonique … le worker ne fabrique aucun fait de projet »). +# Chaque entrée DÉCLARE `source.fichiers: ["CLAUDE.md#Projets"]` et le README:80 +# l'affirme (« 9 projets P01..P09 · noms sourcés de `CLAUDE.md §Projets` ») — MAIS +# AUCUN gate ne le VÉRIFIAIT. check_artifacts prouve QUE les out/ SE RECONSTRUISENT +# depuis la fixture (byte-for-byte), PAS que ses `code`/`nom` == CLAUDE.md §Projets ; +# les blocs SEO amont gatent les COMPTES de mots-clés + la COMPOSITION du graphe +# schema.org (bijection un-listing-par-projet) + l'IDENTITÉ de la marque racine +# (§Entités), tous AVEUGLES à l'identité de la LISTE de projets. Piège #6 : +# RENOMMER un projet dans CLAUDE.md §Projets (« Aqua Terra Las Terrenas » → « … Bay »), +# le RENOMMER/AJOUTER/RETIRER dans la fixture, puis régénérer, laisse le JSON-LD PUBLIC +# — celui que Google indexe sous `vente.otov7.com/projets/…` — lister un projet +# FANTÔME ou en OMETTRE un canonique pendant que la byte-gate reste VERTE (l'artefact +# se reconstruit fidèlement… depuis une fixture DÉRIVÉE) et que le compte reste 9 — +# « vert trompeur » de la MÊME classe que la marque org (§Entités), les entités +# CONFOTUR (§Entités) et les libellés projets du run_sheet démo (§Projets). Aucune +# suite tests/ (FONCTIONS de génération, jamais l'ancrage à CLAUDE.md) ne l'attrape. +# On RE-DÉRIVE le catalogue de CLAUDE.md §Projets (zéro duplication · source unique) +# puis on exige : (a) identité d'ensemble des codes fixture ⇔ CLAUDE.md (aucun +# fantôme/manquant · pas de doublon) ; (b) `nom` de chaque entrée == CLAUDE.md §Projets +# (accent-insensible · la fixture normalise « Xamaná »→« Xamana ») ; (c) chaque entrée +# DÉCLARE l'ancrage `CLAUDE.md#Projets` dans `source.fichiers` ; (d) les `name` des +# listings du graphe schema.org PUBLIC == l'ensemble des noms CLAUDE.md §Projets + +# bijection (1 listing/projet) + MANIFEST counts projects/listings == |§Projets| ; +# (e) le README cite l'ancrage CLAUDE.md §Projets. Un claim absent échoue AUSSI +# (disparition = régression). État courant : aucune valeur périmée. +# ============================================================================ +_pnfc = lambda s: unicodedata.normalize("NFC", str(s)) +_pai = lambda s: "".join(c for c in unicodedata.normalize("NFKD", str(s)) + if unicodedata.category(c) != "Mn").lower().replace(" ", "") +try: + cm_pj = open("CLAUDE.md", encoding="utf-8").read() + _mpj = re.search(r"^## Projets\s*\n(.+)$", cm_pj, re.M) + if _mpj is None: + raise ValueError("section « ## Projets » INTROUVABLE") + seo_pmap = {} + for _t in _mpj.group(1).split("·"): + _mc = re.match(r"(P\d{2})\s+(.+)$", _pnfc(_t.strip())) + if _mc: + seo_pmap[_mc.group(1)] = _mc.group(2).strip() + if not seo_pmap: + raise ValueError("aucun projet PNN parsé") +except (OSError, ValueError) as e: + bad(f"SEO projets · CLAUDE.md §Projets illisible/inattendu : {e}"); seo_pmap = None +try: + with open(f"{D}/seo/fixtures/projets_master.json", encoding="utf-8") as _f: + seo_pm = json.load(_f)["projets"] + sg_pj = load("seo/out/seo_schema_org.json")["@graph"] + seo_cpj = load("seo/out/MANIFEST.json")["counts"] + lst_pj = [n for n in sg_pj if not str(n.get("@id", "")).endswith("#organization")] +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"SEO projets · fixture/artefact illisible/inattendu : {e}"); seo_pm = None +if seo_pmap is not None and seo_pm is not None: + # (a) ANCRAGE : identité d'ensemble des codes fixture ⇔ CLAUDE.md §Projets + fix_codes = [p.get("code") for p in seo_pm] + if len(set(fix_codes)) != len(fix_codes) or None in fix_codes or "" in fix_codes: + bad(f"SEO projets · projets_master : codes en doublon/vides {fix_codes}") + fx = {c for c in fix_codes if c} + miss = sorted(set(seo_pmap) - fx); extra = sorted(fx - set(seo_pmap)) + if miss or extra: + bad(f"SEO projets · codes projets_master ≠ CLAUDE.md §Projets" + f"{' · manquants=' + str(miss) if miss else ''}" + f"{' · fantômes=' + str(extra) if extra else ''}") + else: + good(f"SEO projets · {len(fx)} codes P01..P09 projets_master == CLAUDE.md §Projets") + # (b)+(c) nom ancré + déclaration de l'ancrage, par projet + for p in seo_pm: + code = p.get("code"); nom = _pnfc(p.get("nom", "")) + anchor = seo_pmap.get(code) + if anchor is None: + continue # code fantôme : déjà mordu par le set-diff (a) + if _pai(nom) != _pai(anchor): + bad(f"SEO projets · {code} nom « {nom} » (projets_master) DÉRIVE de " + f"CLAUDE.md §Projets « {anchor} » → aligner (ou CLAUDE.md)") + else: + good(f"SEO projets · {code} — nom « {nom} » == CLAUDE.md §Projets") + srcf = (p.get("source") or {}).get("fichiers") or [] + if not any("CLAUDE.md" in str(x) and "Projets" in str(x) for x in srcf): + bad(f"SEO projets · {code} — projets_master ne DÉCLARE pas l'ancrage " + f"`CLAUDE.md#Projets` dans source.fichiers : {srcf}") + # (d) artefact PUBLIC : `name` des listings schema.org == noms CLAUDE.md §Projets + if not lst_pj: + bad("SEO projets · aucun nœud listing schema.org → graphe suspect") + else: + got_n = {_pai(n.get("name", "")) for n in lst_pj} + want_n = {_pai(v) for v in seo_pmap.values()} + gm = sorted(want_n - got_n); ge = sorted(got_n - want_n) + if gm or ge: + bad(f"SEO projets · `name` des listings schema.org (JSON-LD public) ≠ " + f"CLAUDE.md §Projets{' · manquants=' + str(gm) if gm else ''}" + f"{' · en trop=' + str(ge) if ge else ''}") + elif len(lst_pj) != len(seo_pmap): + bad(f"SEO projets · {len(lst_pj)} listing(s) schema.org MAIS " + f"{len(seo_pmap)} projet(s) CLAUDE.md §Projets (bijection cassée)") + else: + good(f"SEO projets · {len(lst_pj)} listings schema.org (name) == CLAUDE.md " + f"§Projets (JSON-LD indexé fidèle au mandat)") + if seo_cpj.get("projects") != len(seo_pmap) or seo_cpj.get("listings") != len(seo_pmap): + bad(f"SEO projets · MANIFEST counts projects={seo_cpj.get('projects')}/" + f"listings={seo_cpj.get('listings')} ≠ {len(seo_pmap)} (CLAUDE.md §Projets)") + else: + good(f"SEO projets · MANIFEST counts projects/listings == {len(seo_pmap)} (§Projets)") + # (e) le README cite l'ancrage CLAUDE.md §Projets + seo_p_rd = read_norm(f"{D}/seo/README.md") + if seo_p_rd is not None: + if re.search(r"CLAUDE\.md\s*§?\s*Projets", seo_p_rd): + good("SEO projets · README cite l'ancrage CLAUDE.md §Projets") + else: + bad("SEO projets · README ne cite pas l'ancrage CLAUDE.md §Projets " + "(attendu « noms sourcés de CLAUDE.md §Projets »)") + # ============================================================================ # SEO · la CARTE hreflang (`alternate` FR/EN/ES + `x-default` par page) — la # 3ᵉ surface du même README SEO — décrite EN PROSE sans AUCUN gate d'IDENTITÉ.