# Rapport de session · 2026-07-30 · session 18 ## Tâche **Sprint 6 · SEO** — Générateur **SEO trilingue** (mots-clés FR/EN/ES + schema.org + hreflang). Roadmap L60 : « Refactor mission `seo_autonome/` → **200+ mots-clés FR/EN/ES · schema.org · hreflang** ». Sprint 5 étant clos en repo (ONAPI/Legal session 16 · QA Audit 5D session 17, Mobile hors périmètre VPS · #8), c'est le premier volet Sprint 6 réalisable dans le repo. Les deux autres volets Sprint 6 (OTOIA voice Amélie pilote AEC · chat OTOIA embarqué) dépendent d'API externes / du desk VPS → hors périmètre worker. ## Décision d'architecture Le SEO est un livrable de **second niveau** : sa matière première est **`projets_master.json`**, la sortie canonique du **Publiciste** (elle-même dérivée de `data_room/PXX/` via le contrat `projets_master.schema.json`). Le générateur **ne fabrique aucun fait de projet** — il compose du SEO à partir de données déjà sourcées + un lexique éditorial générique non chiffré. Ce choix respecte #6 (zéro invention) et #5 (zéro duplication) : le générateur **réutilise** le validateur maison et les tokens de marque du Publiciste (`lib/validator.py`, `lib/branding.py` → `STATUTS_SANS_PRIX`, devise USD), sans les réimplémenter. ## Fichiers créés — `05_deliverables_mvp/seo/` - `seo_spec.json` — config site (base_url, langs FR/EN/ES, préfixes, x-default) + **lexique éditorial générique** (immobilier / à vendre / résidence / pays / régime CONFOTUR) + org schema.org + cibles. **Zéro donnée de projet, zéro chiffre.** - `seolib/{__init__,deps,keywords,schemaorg,hreflang,builder}.py` - `deps` réutilise validateur + branding Publiciste ; `slugify`/`digits` déterministes. - `keywords` : génération déterministe, chaque mot-clé porte `scope`, `projet`, `intent`, `category` et une liste `sources` non vide. - `schemaorg` : graphe JSON-LD (`Organization` + une `Residence`/projet), `offers` **uniquement** si projet disponible + prix sourcé (USD). - `hreflang` : `alternate` FR/EN/ES + `x-default` par page. - `seo_gen.py` — CLI `build`/`validate` · **15 invariants** de cross-cohérence et d'anti-invention. - `seo.schema.json` — contrat de sortie draft-07 (bundle keywords + schema.org + hreflang + manifest). - `fixtures/projets_master.json` — entrée **de test** : 9 projets P01..P09 (noms sourcés de `CLAUDE.md §Projets`), tous `en_developpement`, **sans aucun chiffre**. - `out/{seo_keywords,seo_schema_org,seo_hreflang,MANIFEST}.json` (hand-off) · `tests/test_seo.py` (**36 tests** dont 8 injections négatives) · `README.md` · `.gitignore`. ## Fichiers modifiés - `.gitea/workflows/ci.yml` : job `seo-tests` + ajout au `gate`. ## Anti-invention (cœur · #6) - Un **mot-clé** = composition de *tokens factuels* (`projet:.nom` / `.localisation`, sourçables) et de *lexique éditorial* (`lexicon:*`, générique non chiffré). Un invariant vérifie que chaque mot-clé est **sourcé et résoluble**. - Un mot-clé **ne peut porter que les chiffres présents dans son champ projet source** : « 1069 Crisfer » (P09) passe ; un prix injecté (`… 250000`) est **refusé** par l'invariant #7 (test négatif dédié). - **schema.org** n'émet une `offers`/`price` **que** pour un projet `disponible` dont une typologie porte un `prix_depuis_usd` **numérique sourcé** ; devise **USD** (#10). Pour tout statut sans prix : **aucun chiffre** (invariant #11 + tests). La fixture livrée (9 projets `en_developpement`) produit donc **0 offre** — aucune valeur inventée dans le hand-off committé. ## Résultat - **258 mots-clés** — fr=87 · en=87 · es=84 (cible roadmap 200 dépassée) ; - schema.org : **10 nœuds** (1 Organization + 9 Residence), 0 offre ; - hreflang : **10 pages** (accueil + 9 projets), 4 `alternate`/page (FR/EN/ES + x-default). ## Vérifs - **36/36 tests** SEO verts (dont oracle `jsonschema` si présent) ; - gate CI local vert : guard contraintes · JSON bien formés · docs (liens + score) · YAML valide ; - **régression 377 tests verts** au total (341 → +36), 0 module en échec ; - build déterministe (test dédié : deux builds identiques). ## Hors périmètre worker (VPS · #8) - Injection des balises `` + `