[DTP-Worker] Sprint 8 · buffer L60 · 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 (seo_hreflang) au contenu distinct. Ce jeu est DATA-DERIVED : langues de contenu = {a.hreflang} − {x-default} de seo/out/seo_hreflang.json (byte-gaté par check_artifacts) = {en,es,fr} · x_default_lang = fr. Aucun gate ne comparait ces CODES à l'artefact : AJOUTER une langue au bundle (pt → 4 langues de contenu) · RETIRER/ÉCHANGER es ferait dériver la prose (README:17) en silence pendant que la carte byte-gatée dit autre chose — « vert trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS de génération, pas la prose du README) n'attrape → nouveau bloc recomputant l'ENSEMBLE des langues de contenu depuis seo_hreflang.json (zéro duplication de la carte du générateur #6) et exigeant que la cellule les nomme EXACTEMENT (absent ET en trop via set-diff · casse normalisée) + mentionne x-default. Cohérences croisées en bonus (mordent un artefact INTERNEMENT incohérent) : carte UNIFORME (toutes les pages portent le même jeu d'alternate) · x-default présent sur CHAQUE page · langue par défaut (x_default_lang) ∈ langues de contenu (ni vacante ni étrangère) · pour chaque page x-default.href == canonical == href de la langue par défaut (l'invariant README:61). Un claim absent échoue AUSSI. 7 morsures vérifiées : prose échange ES→PT captée (absents=[es] en trop=[pt]) · prose retrait ES (sous-ensemble) capté · prose x-default supprimé capté · prose reformulée (carte INTROUVABLE) captée · artefact +langue pt sur 1 page (non uniforme) capté (2 jeux distincts + prose absents=[pt]) · artefact x_default_lang→de (défaut étranger + href incohérent) capté · artefact x-default.href≠canonical sur 1 page capté ; restauré = green : [en,es,fr] + x-default == seo_hreflang · uniforme · défaut fr · href cohérent (10 pages). État courant : aucune langue périmée (anti-invention #6, rien à réécrire) — le défaut est la surface ungated. ci/README.md (table + détail « 3e surface SEO ») mis à jour · 7 gates re-verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1900,6 +1900,97 @@ if sg is not None:
|
||||
bad(f"SEO schema.org · {SEO_SG_RD} — dit « une `{mpp.group(1)}` par projet » "
|
||||
f"MAIS listings sont `{lt}` → régénérer le README")
|
||||
|
||||
# ============================================================================
|
||||
# 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É.
|
||||
# ----------------------------------------------------------------------------
|
||||
# Le générateur SEO émet out/seo_hreflang.json (byte-gaté par check_artifacts ⇒
|
||||
# faisant autorité) : une page par projet + accueil, chacune portant un ENSEMBLE
|
||||
# d'`alternate` (un par langue de contenu) + un `x-default`. Le README:17 décrit
|
||||
# cet ensemble À LA MAIN : « carte `alternate` FR/EN/ES + `x-default` par page ».
|
||||
# Les deux blocs SEO existants gatent le COMPTE de mots-clés (keywords_total +
|
||||
# ventilation par langue, artefact seo_keywords) et la composition du graphe
|
||||
# schema.org (artefact seo_schema_org) — jamais le jeu de langues hreflang, qui
|
||||
# vit dans un TROISIÈME artefact (seo_hreflang) au contenu distinct. Aucun gate
|
||||
# ne comparait ces CODES à l'artefact : AJOUTER une langue au bundle (ex. `pt`
|
||||
# → hreflang à 4 langues de contenu) OU RETIRER/ÉCHANGER `es` ferait dériver la
|
||||
# prose en silence pendant que la carte byte-gatée dit autre chose — « vert
|
||||
# trompeur » qu'aucune suite tests/ (qui teste des FONCTIONS de génération, pas
|
||||
# la prose du README) n'attrape. Même patron que l'énumération des onglets
|
||||
# Mobile, du catalogue custom RBAC ou des @type schema.org. On RECOMPUTE
|
||||
# l'ENSEMBLE des langues de contenu depuis SON artefact byte-gaté (zéro
|
||||
# duplication de la carte du générateur #6) et on exige que la prose les nomme
|
||||
# EXACTEMENT (absent ET en trop via set-diff) + mentionne `x-default`. Cohérences
|
||||
# croisées en bonus (mordent un artefact INTERNEMENT incohérent) : toutes les
|
||||
# pages portent le MÊME jeu d'`alternate` (uniforme) ; `x-default` présent sur
|
||||
# CHAQUE page ; la langue par défaut (`x_default_lang`) ∈ langues de contenu (ni
|
||||
# vacante ni étrangère) ; pour chaque page `x-default`.href == `canonical` ==
|
||||
# href de la langue par défaut (l'invariant « x-default = langue par défaut =
|
||||
# canonical » du README:61). Un claim absent échoue AUSSI.
|
||||
# ============================================================================
|
||||
SEO_HL_RD = f"{D}/seo/README.md"
|
||||
try:
|
||||
hl = load("seo/out/seo_hreflang.json")
|
||||
hl_pages = hl["pages"]
|
||||
x_default_lang = hl["x_default_lang"]
|
||||
page_sets = [frozenset(a["hreflang"] for a in p["alternates"]) for p in hl_pages]
|
||||
all_codes = set().union(*page_sets) if page_sets else set()
|
||||
content_langs = all_codes - {"x-default"} # langues de contenu (hors x-default)
|
||||
except (OSError, KeyError, ValueError, TypeError) as e:
|
||||
bad(f"SEO hreflang · artefact illisible/inattendu : {e}"); hl_pages = None
|
||||
if hl_pages:
|
||||
# bonus 1 : toutes les pages portent le MÊME jeu d'alternate (uniforme)
|
||||
if len(set(page_sets)) != 1:
|
||||
bad(f"SEO hreflang · carte NON uniforme : {len(set(page_sets))} jeux distincts "
|
||||
f"d'`alternate` sur {len(hl_pages)} pages → une carte hétérogène")
|
||||
else:
|
||||
good(f"SEO hreflang · {len(hl_pages)} pages · jeu d'`alternate` uniforme "
|
||||
f"{sorted(all_codes)}")
|
||||
# bonus 2 : x-default sur chaque page + langue par défaut ∈ langues de contenu
|
||||
if "x-default" not in all_codes or any("x-default" not in s for s in page_sets):
|
||||
bad("SEO hreflang · `x-default` absent d'au moins une page → carte incomplète")
|
||||
elif x_default_lang not in content_langs:
|
||||
bad(f"SEO hreflang · x_default_lang={x_default_lang!r} ∉ langues de contenu "
|
||||
f"{sorted(content_langs)} → défaut vacant/étranger")
|
||||
else:
|
||||
good(f"SEO hreflang · `x-default` sur chaque page · défaut {x_default_lang!r} "
|
||||
f"∈ langues de contenu")
|
||||
# bonus 3 : href-cohérence — x-default == canonical == href(langue par défaut)
|
||||
incoh = []
|
||||
for p in hl_pages:
|
||||
alt = {a["hreflang"]: a["href"] for a in p["alternates"]}
|
||||
if not (alt.get("x-default") == p.get("canonical") == alt.get(x_default_lang)):
|
||||
incoh.append(p.get("page"))
|
||||
if incoh:
|
||||
bad(f"SEO hreflang · href incohérent (x-default ≠ canonical ≠ {x_default_lang}) "
|
||||
f"sur {incoh} → artefact interne incohérent")
|
||||
else:
|
||||
good(f"SEO hreflang · `x-default`.href == `canonical` == href({x_default_lang}) "
|
||||
f"(les {len(hl_pages)} pages)")
|
||||
# README:17 — la cellule « carte `alternate` <langues> + `x-default` par page »
|
||||
seo_hl_rd = read_norm(SEO_HL_RD)
|
||||
if seo_hl_rd is not None:
|
||||
m = re.search(r"carte `alternate` (.+?) par page", seo_hl_rd)
|
||||
if m is None:
|
||||
bad(f"SEO hreflang · {SEO_HL_RD} — carte « `alternate` … par page » "
|
||||
f"INTROUVABLE (attendu {sorted(content_langs)} + x-default)")
|
||||
else:
|
||||
seg = m.group(1)
|
||||
prose_langs = {t.lower() for t in re.findall(r"\b[A-Z]{2}\b", seg.split("+")[0])}
|
||||
has_xdef = "x-default" in seg
|
||||
if prose_langs == content_langs and has_xdef:
|
||||
good(f"SEO hreflang · {SEO_HL_RD} — langues {sorted(prose_langs)} + "
|
||||
f"`x-default` == seo_hreflang")
|
||||
else:
|
||||
miss = sorted(content_langs - prose_langs)
|
||||
extra = sorted(prose_langs - content_langs)
|
||||
detail = (f" · absents={miss}" if miss else "") + \
|
||||
(f" · en trop={extra}" if extra else "") + \
|
||||
("" if has_xdef else " · `x-default` non mentionné")
|
||||
bad(f"SEO hreflang · {SEO_HL_RD} — nomme {sorted(prose_langs)}"
|
||||
f"{'+x-default' if has_xdef else ''} MAIS carte "
|
||||
f"{sorted(content_langs)}+x-default{detail} → régénérer le README")
|
||||
|
||||
sys.exit(1 if FAIL else 0)
|
||||
PY
|
||||
rc=$?
|
||||
|
||||
Reference in New Issue
Block a user