# SERP Semantics Fiabilité (S5-bis) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Rendre le profil sémantique fiable et lisible : garde thin-HTML/faux-200 au runner (CDC §5), `word_count` sur tout le flux principal, formes de surface dans le guide, filtre consensus/outliers, stop-words corrigés.

**Architecture:** Constat QA live (mot-clé « jewelry manufacturers », org eleva-seo) : 2-3 pages-coquilles (20 et 7 mots — murs anti-bot/JS) analysées comme valides → médianes effondrées (317 mots), archétypes indéterminés 63 %, intention mixte ; word_count body-only ment sur pages link-heavy (506 vs ~830) ; stems affichés (« jewelri ») ; « silver 49–49 » d'une seule page dans le guide ; « us » absent de STOP_EN. Cinq corrections : 2 crate-mesure, 1 crate-affichage, 1 runner, 1 câblage TS.

**Tech Stack:** Rust (`crates/semantic-core`), runner TS (`src/lib/semantic/analysis-runner.ts`), contrat TS + prompt + UI.

## Global Constraints

- Branche **`ytg-like/serp-fiabilite`** (empilée sur `ytg-like/serp-ui`). AUCUN merge dev.
- Tout changement Rust → `bun run build:wasm:semantic` AVANT les tests TS qui consomment le wasm.
- Le scoring reste keyé par **stem** (fusion singulier/pluriel intacte) — seule la présentation change.
- Contrat TS = miroir exact du serde Rust (camelCase) ; toute nouvelle clé documentée en JSDoc.
- Seuil thin-HTML : **`MIN_CONTENT_TOKENS = 120`** tokens (somme h1+h2h3+anchor+body stemmés) OU title vide ET body < 30 — constantes nommées, paramétrables via SemanticConfig plus tard si besoin (YAGNI maintenant).
- Flags runner : `js_rendered_suspected:${pos}` (mirror du format `lang_mismatch:${pos}`).
- Conventional Commits en français. Gates récurrentes : `cargo test -p semantic-core`, `bun run test:all`, `bunx tsc --noEmit`.

---

### Task 1: Crate — word_count flux principal + stop-words

**Files:**
- Modify: `crates/semantic-core/src/page.rs` (calcul word_count)
- Modify: `crates/semantic-core/src/tokenize.rs` (STOP_EN)
- Modify: tests des deux modules

**Interfaces:**
- Produces: `PageStructure.word_count` = compte de tokens (`tokenize_keep_stops`) du **flux principal complet** : body_text + h1 + h2h3 + anchors (title/meta/alt/slug exclus — hors flux). Sémantique documentée en doc-comment (« ce que verrait un lecteur du contenu principal »). `body` reste une zone de scoring inchangée.
- `STOP_EN` : ajoute `"us"`. Audit éclair des 7 listes : vérifie la présence de `a, an, the, of, to, in, is, are, it, its, this, that, with, for, on, at, by, from, or, and, be, been, was, were, has, have, had, will, would, can, could` (EN) et l'équivalent minimal FR/ES/DE/IT/PT/NL — ajoute les manquants évidents, ne retire rien.

- [ ] **Step 1: Tests.** `full_page_analysis` : recalibre l'assertion word_count (la fixture a des h1/h2 — le compte doit maintenant les inclure ; valeur exacte attendue calculée à la main dans le test). Nouveau test : page dont le texte est à 80 % dans des ancres (grille) → word_count ≈ total, pas ≈ 0. Test stop-words : `Tokenizer::new(Lang::En).tokenize("contact us today")` ne contient pas `us`.
- [ ] **Step 2: Fail.** `cargo test -p semantic-core`.
- [ ] **Step 3: Implémenter.** Attention : l'archétype (heuristique `word_count >= 600` → Editorial) consomme ce word_count — le nouveau compte (plus haut) est le bon signal ; vérifie que les tests d'archétype restent cohérents (ajuste les fixtures si besoin, en le documentant).
- [ ] **Step 4: Pass** + `cargo clippy -p semantic-core` clean.
- [ ] **Step 5: Commit** — `fix(semantic): word_count sur tout le flux principal + stop-word us (fiabilité QA)`

---

### Task 2: Crate — formes de surface + consensus du guide

**Files:**
- Modify: `crates/semantic-core/src/tokenize.rs` ou `page.rs` (collecte des formes de surface)
- Modify: `crates/semantic-core/src/model.rs` (PageSemantics)
- Modify: `crates/semantic-core/src/scoring.rs` (TermAggregate)
- Modify: `crates/semantic-core/src/profile.rs` (guide)

**Interfaces:**
- Produces:
  - `PageSemantics.surface_forms: BTreeMap<String, BTreeMap<String, u32>>` (stem → forme de surface non stemmée lowercase → count). Collectée pendant la tokenisation des zones (la forme AVANT stemming, APRÈS lowercase).
  - `TermAggregate.display: String` = la forme de surface la plus fréquente toutes pages confondues (tie-break alphabétique) ; fallback = le stem si aucune surface collectée.
  - `TermAggregate.pages_with_term: u32` = nombre de pages analysées où `ZoneCounts::total() > 0`.
  - Guide (`profile.rs`) : `guide.terms` exclut les termes `is_outlier` ET exige `pages_with_term >= 2` (constante `GUIDE_MIN_PAGES = 2`) ; si le filtre vide le guide (petites SERP), repli documenté : garde le top sans le filtre ≥2 mais l'exclusion outlier reste. Les items du guide portent `display` (le prompt/UI l'utiliseront).
  - `terms[]` du profil garde TOUT (outliers flaggés inclus) — seule la sélection du guide filtre.

- [ ] **Step 1: Tests.** Surface : pages avec « jewelry » ×3 et « jewelries » ×1 → display "jewelry" ; guide : terme outlier exclu du guide mais présent dans terms ; terme sur 1 page exclu du guide (≥2), présent dans terms ; petite SERP (1 page) → repli sans filtre pages.
- [ ] **Step 2: Fail.** — [ ] **Step 3: Implémenter.** — [ ] **Step 4: Pass + clippy + `bun run build:wasm:semantic` OK.**
- [ ] **Step 5: Commit** — `feat(semantic): formes de surface + filtre consensus/outliers du guide`

---

### Task 3: Runner — garde thin-HTML / faux-200 (CDC §5)

**Files:**
- Modify: `src/lib/semantic/analysis-runner.ts`
- Modify: `src/lib/semantic/analysis-runner.test.ts`

**Interfaces:**
- Produces : après `extractSemanticPage`, si la page est une coquille — `somme des tokens h1+h2h3+anchor+body < 120` (calculée depuis `termFreqs`… NON : trop indirect — utilise `structure.wordCount` nouveau (< 120) OU (`title` indisponible dans PageSemantics ? vérifie le modèle — si pas de title exposé, le critère est wordCount seul, documente-le) — la page passe `semantics: null, fetchOk: false` + flag `js_rendered_suspected:${pos}` collecté et appendu à `coverage.flags` (exact mirror de la mécanique `lang_mismatch`).
- La constante `MIN_CONTENT_TOKENS = 120` vit dans le runner (commentée : heuristique CDC §5, à affiner avec S7 headless qui rendra ces pages analysables).
- La règle s'applique AVANT la garde langue (une coquille n'a pas de langue fiable).

- [ ] **Step 1: Tests** (harnais DB existant) : une page servie avec 15 mots → exclue, flag `js_rendered_suspected:pos` dans le profil stocké, comptée dans `missing` ; les pages normales inchangées ; le cas « toutes coquilles » → `ok:false` (guard 0-page existant).
- [ ] **Step 2: Fail.** — [ ] **Step 3: Implémenter.** — [ ] **Step 4: Pass** (`DB_TESTS=1 bun run test src/lib/semantic/analysis-runner.test.ts`).
- [ ] **Step 5: Commit** — `fix(semantic): garde thin-HTML/faux-200 au runner (CDC §5) — les coquilles ne polluent plus le profil`

---

### Task 4: Câblage TS — contrat, prompt, UI

**Files:**
- Modify: `src/lib/semantic/types.ts` (miroir : `surfaceForms`, `display`, `pagesWithTerm`)
- Modify: `src/lib/keywords/prompt/build-prompt.ts` (le guide affiche `display`, plus jamais le stem ; **calibrage YTG** — référence : export YTG réel fourni par l'utilisateur — : la liste mixte « Expressions à placer telles quelles » devient DEUX listes séparées « Expressions (2 mots) » et « Expressions (3 mots) », caps relevés : `GUIDE_TERMS_CAP = 20`, `GUIDE_BIGRAMS_CAP = 15`, `GUIDE_TRIGRAMS_CAP = 10` — le crate sépare déjà bigrams/trigrams dans `ngrams`, c'est un changement de rendu uniquement)
- Modify: `src/components/keywords/planning/semantic-analysis-section.tsx` (top termes : `display`)
- Modify: `src/components/keywords/planning/semantic-labels.ts` (+ label FR pour `js_rendered_suspected:N` → « Rendu JS suspecté en position N (page exclue) »)
- Modify: tests concernés (`semantic-labels.test.ts`, tests build-prompt, `engine.test.ts` si les shapes bougent)

**Interfaces:**
- Consumes: Tasks 1-3. Le wasm DOIT être rebuildé avant les tests TS.

- [ ] **Step 1: Tests** (labels + prompt avec `display`). — [ ] **Step 2: Fail.** — [ ] **Step 3: Implémenter.** — [ ] **Step 4: Gates finales du plan :**

Run: `bun run build:wasm:semantic` puis `cargo test -p semantic-core`, `bun run test:all`, `bunx tsc --noEmit`, `bun run build` (next build — le gotcha use-server est derrière nous, on vérifie qu'il le reste).

- [ ] **Step 5: Commit + push** — `git push -u origin ytg-like/serp-fiabilite`. PAS de merge dev.

---

## Self-review (fait à l'écriture)

- Les 5 causes QA couvertes : coquilles ✔ (T3), word_count ✔ (T1), stems affichés ✔ (T2+T4), consensus/outliers ✔ (T2), stop-words ✔ (T1).
- Cohérence : le nouveau word_count sert à la fois l'archétype (T1), la garde thin (T3) et les objectifs structurels (déjà branchés sur les médianes de word_count — elles se corrigent mécaniquement).
- Post-plan : re-run live sur « jewelry manufacturers » pour comparaison avant/après (fait par le contrôleur, pas une tâche).
