[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:
@@ -1,5 +1,67 @@
|
|||||||
# Activity Log · 2026-07-31 · Claude Code DTP
|
# Activity Log · 2026-07-31 · Claude Code DTP
|
||||||
|
|
||||||
|
## 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
|
||||||
|
close ; poursuite de la série anti-dérive (CLAUDE.md #6). Cette session cible la
|
||||||
|
**3ᵉ surface data-derived du même README SEO** — la carte hreflang, dont le jeu de
|
||||||
|
langues cité dans la table « Ce que ça fait » pointe l'artefact généré sans être
|
||||||
|
recomputé par aucun gate.
|
||||||
|
|
||||||
|
**Dérive silencieuse fermée** :
|
||||||
|
- `05_deliverables_mvp/seo/README.md:17` : « | hreflang | … carte `alternate`
|
||||||
|
**FR/EN/ES** + `x-default` par page | » — l'ENSEMBLE des langues de contenu +
|
||||||
|
le `x-default`.
|
||||||
|
- Source faisant autorité (byte-gatée par `check_artifacts`) :
|
||||||
|
`seo/out/seo_hreflang.json` — chaque page porte un ensemble d'`alternate` (une
|
||||||
|
langue de contenu par entrée) + un `x-default` ; langues de contenu =
|
||||||
|
`{a.hreflang} − {x-default}` = `{en, es, fr}` · `x_default_lang = fr`.
|
||||||
|
- Les deux blocs SEO existants (mots-clés · schema.org) sont **aveugles** à ces
|
||||||
|
codes : **AJOUTER** une langue au bundle (`pt` → 4 langues), **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) n'attrape.
|
||||||
|
- **État courant** : **aucune langue périmée** — `FR/EN/ES` + `x-default` recoupe
|
||||||
|
l'artefact exactement (anti-invention #6, rien à réécrire). Le défaut est la
|
||||||
|
**surface ungated**.
|
||||||
|
|
||||||
|
**Nouveau bloc dans `ci/check_readme_claims.sh`** : recompute l'ENSEMBLE des
|
||||||
|
langues de contenu depuis `seo_hreflang.json` (zéro duplication de la carte du
|
||||||
|
générateur · #6) et exige 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.
|
||||||
|
|
||||||
|
### Preuve (7 morsures vérifiées + restauration verte)
|
||||||
|
|
||||||
|
| Injection | Résultat |
|
||||||
|
|---|---|
|
||||||
|
| README échange langue (`ES → PT`) | ✗ absents=`[es]` · en trop=`[pt]` · exit 1 |
|
||||||
|
| README retrait `ES` (sous-ensemble) | ✗ absents=`[es]` · exit 1 |
|
||||||
|
| README `x-default` supprimé | ✗ `x-default` non mentionné · exit 1 |
|
||||||
|
| README claim reformulé | ✗ carte « `alternate` … par page » INTROUVABLE · exit 1 |
|
||||||
|
| artefact +langue `pt` sur 1 page (non uniforme) | ✗ carte NON uniforme (2 jeux) + prose absents=`[pt]` · exit 1 |
|
||||||
|
| artefact `x_default_lang → de` (défaut étranger) | ✗ `de` ∉ langues de contenu + href incohérent · exit 1 |
|
||||||
|
| artefact `x-default`.href ≠ canonical (1 page) | ✗ href incohérent sur `[home]` · exit 1 |
|
||||||
|
| restauré | ✓ `[en,es,fr]` + `x-default` == `seo_hreflang` · uniforme · défaut `fr` · href cohérent (10 pages) · exit 0 |
|
||||||
|
|
||||||
|
- **`ci/README.md`** — surface ajoutée à la table récap du pipeline + paragraphe
|
||||||
|
descriptif (« 3ᵉ surface du même README SEO »).
|
||||||
|
|
||||||
|
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_173114` · Buffer S8 · Domaine SEO (2e surface) : la **composition du graphe schema.org** (« graphe JSON-LD (`Organization` + une `Residence` par projet) ») était décrite EN PROSE dans le README du module `seo` sans AUCUN gate d'IDENTITÉ — le bloc SEO existant ne gate que le COMPTE de mots-clés (total + fr/en/es).
|
## Session `20260731_173114` · Buffer S8 · Domaine SEO (2e surface) : la **composition du graphe schema.org** (« graphe JSON-LD (`Organization` + une `Residence` par projet) ») était décrite EN PROSE dans le README du module `seo` sans AUCUN gate d'IDENTITÉ — le bloc SEO existant ne gate que le COMPTE de mots-clés (total + fr/en/es).
|
||||||
|
|
||||||
- **Constat** : les `@type` du `@graph` de `seo/out/seo_schema_org.json` (byte-gaté)
|
- **Constat** : les `@type` du `@graph` de `seo/out/seo_schema_org.json` (byte-gaté)
|
||||||
|
|||||||
+18
-1
@@ -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-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-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-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`) == 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) == artefact cité (#6) | ✅ oui |
|
||||||
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
||||||
|
|
||||||
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||||||
@@ -201,6 +201,23 @@ schema_org_nodes−1` du MANIFEST), et le graphe ne porte **aucune** `offers`
|
|||||||
cohérent avec « aucun prix pour un statut sans prix » (USD · #10). Un claim
|
cohérent avec « aucun prix pour un statut sans prix » (USD · #10). Un claim
|
||||||
supprimé (« composition INTROUVABLE ») échoue **aussi**.
|
supprimé (« composition INTROUVABLE ») échoue **aussi**.
|
||||||
|
|
||||||
|
**3ᵉ surface du même README SEO** : la table « Livrables » décrit **en prose** la
|
||||||
|
carte hreflang (« carte `alternate` **FR/EN/ES** + `x-default` par page »). Les
|
||||||
|
deux blocs SEO précédents (comptes de mots-clés · graphe schema.org) sont
|
||||||
|
**aveugles** à ces codes de langue, qui vivent dans un **troisième** artefact
|
||||||
|
(`seo/out/seo_hreflang.json`, byte-gaté par `check_artifacts`) : **ajouter** une
|
||||||
|
langue au bundle (`pt` → 4 langues de contenu), **retirer** ou **échanger** `es`
|
||||||
|
laisse la prose figée pendant que la carte dit autre chose. On recompute
|
||||||
|
l'**ensemble** des langues de contenu (`alternate` hors `x-default`) et on exige
|
||||||
|
que la cellule les nomme **exactement** (set-diff : absent ET en trop) + mentionne
|
||||||
|
`x-default`. En bonus, quatre cohérences croisées de l'artefact : la carte est
|
||||||
|
**uniforme** (toutes les pages portent le même jeu d'`alternate`), `x-default` est
|
||||||
|
présent sur **chaque** page, la langue par défaut (`x_default_lang`) ∈ langues de
|
||||||
|
contenu (ni vacante ni étrangère), et 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). Un claim supprimé (« carte INTROUVABLE ») échoue
|
||||||
|
**aussi**.
|
||||||
|
|
||||||
Même classe enfin sur le **pipeline vente CRM** : le couple « **9 états · 11
|
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
|
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
|
**même** compte d'états « les **9 états** du pipeline » (README du DocType porteur
|
||||||
|
|||||||
@@ -1900,6 +1900,97 @@ if sg is not None:
|
|||||||
bad(f"SEO schema.org · {SEO_SG_RD} — dit « une `{mpp.group(1)}` par projet » "
|
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")
|
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)
|
sys.exit(1 if FAIL else 0)
|
||||||
PY
|
PY
|
||||||
rc=$?
|
rc=$?
|
||||||
|
|||||||
Reference in New Issue
Block a user