# The Checklist — audit auto par site + vue d'ensemble (tranche 1)

**Date :** 2026-06-11 · **Epic :** ELEVA-93 · **Sous-projets :** B (moteur d'audit) + C (vue d'ensemble).

Décisions brainstorm (2026-06-11) : audit = **sous-ensemble auto-vérifiable automatisé + reste en checklist manuelle** ; MCP = remote hébergé/clé API compte (sous-projet D, **plus tard**) ; **on commence par B+C** ; gating add-on = entitlement org + flag (sous-projet E, s'applique au MCP, **pas** à cette tranche).

## Périmètre de la tranche 1
Audit automatique d'un site contre le sous-ensemble **fetchable** des 135 items de `src/lib/spec/checklist-data.ts`, résultats persistés **par site**, vue par site + **vue d'ensemble cross-sites**. Pas de MCP, pas de gating ici (l'audit est cœur produit ; le MCP est l'add-on).

Hors scope tranche 1 : checks de jugement (WCAG a11y, qualité éditoriale) → restent en checklist manuelle ; headless/axe ; serveur MCP ; billing.

## B — Moteur d'audit (`src/lib/spec/audit/`)

### Snapshot (I/O, côté Next server)
`fetchSiteSnapshot(baseUrl)` : fetch tolérant aux erreurs (timeout ~8s chacun, `AbortSignal.timeout`), retourne :
- `home`: { status, headers (Map normalisée lowercase), html (string), finalUrl } de la home.
- `robotsTxt`, `sitemapXml`, `securityTxt` (`/.well-known/security.txt`), `llmsTxt` (`/llms.txt`) : { ok, status, text } chacun.
Aucune dépendance navigateur — fetch natif + parsing string/regex. Erreurs réseau → champs `ok:false` (jamais throw).

### Registry de checks
`type CheckResult = { status: "pass" | "fail" | "warn" | "na"; detail: string }`.
`CHECKS: Record<string /* itemId */, (s: SiteSnapshot) => CheckResult>`. Un item de `CHECKLIST` **sans** entrée registry → statut `manual` (reste dans la checklist manuelle).

Ensemble initial (≈20 checks, extensible — couvre foundations/seo/security/agent-readiness/well-known/i18n) :
- **foundations** : `doctype` (html commence par `<!doctype html>`), `html-lang` (attribut lang présent+valide), `meta-charset` (utf-8 dans le `<head>`), `meta-viewport` (présent, sans `user-scalable=no`), `title` (1 `<title>` non vide), `meta-description` (présent → pass, absent → warn).
- **seo** : `meta-robots` (lit la policy ; noindex sur la home → fail), `robots-txt` (200 + parse), `canonical-url` (link rel=canonical présent), `xml-sitemaps` (sitemap.xml 200 ou référencé dans robots.txt), `open-graph` (og:title + og:image présents).
- **security** : `https` (finalUrl en https), `hsts` (header Strict-Transport-Security), `csp` (Content-Security-Policy présent → pass, absent → warn), `x-content-type-options` (`nosniff`), `referrer-policy` (header présent), `x-frame-options` (header OU CSP frame-ancestors).
- **agent-readiness** : `llms-txt` (/llms.txt 200 → pass, sinon na/warn).
- **well-known** : `security-txt` (/.well-known/security.txt 200 → pass, sinon warn).
- **i18n** : `hreflang` (si le site a >1 langue cible → présence de link rel=alternate hreflang ; sinon `na`).

`runSiteAudit(snapshot, items=CHECKLIST)` : applique le registry, renvoie `AuditResult { ranAt, results: {itemId, status, detail}[], score }`. **Score** = round(100 × pass / (pass+fail+warn)) sur les items auto-checkés (les `na`/`manual` exclus du dénominateur). Pondération required > recommended : v1 simple non pondéré ; noter la pondération comme amélioration.

### Données (migration Drizzle, ~0025)
Table `site_audits` : `id` (pk), `site_id` (fk sites, onDelete cascade, **index**), `ran_at` (timestamptz), `score` (int), `results` (jsonb : `{itemId, status, detail}[]`). On garde le **dernier** audit par site (upsert sur site_id, ou insert + lecture du plus récent — choisir : insert append + `getLatest` ordonné par ran_at desc, garde un historique léger). Tenancy via `site_id`.

### Actions serveur (`src/lib/spec/audit/actions.ts`, "use server")
- `runSiteAuditAction(siteId)` : `assertSiteInActiveOrg(siteId)` → résout l'URL du site → `fetchSiteSnapshot` → `runSiteAudit` → persiste → `revalidatePath`. Retourne l'AuditResult.
- `getLatestAuditAction(siteId)` : `assertSiteInActiveOrg` → dernier audit (ou null).

## C — UI

### Par site (`/flight-checker`)
- Bouton « Lancer l'audit » (par site actif) → `runSiteAuditAction`, spinner, toast.
- Anneau de score + date du dernier audit.
- Résultats par catégorie : chaque item auto-checké montre un badge pass/fail/warn + `detail` ; les items `manual` gardent la checklist cochable existante (`use-checklist-state`, mais **par site** — migrer la clé localStorage `flight-checker:checked` → `flight-checker:checked:<siteId>` pour ne pas mélanger les sites).
- Réutilise le langage visuel design (panel, badges) ; pas de nouveau design lourd.

### Vue d'ensemble cross-sites (`/flight-checker` onglet/section « Vue d'ensemble » ou en tête)
- Pour chaque site de l'org : nom + score du dernier audit + compteurs (pass/fail/warn) + date, lien vers l'audit du site. C'est la « vue d'ensemble de tout leur site ».
- Lecture serveur agrégée : `getOrgAuditOverview()` (sites de l'org + dernier audit chacun, en une requête ou Promise.all borné).

## Tests
- DB : `runSiteAuditAction` persiste + `getLatestAuditAction` renvoie le dernier ; tenancy (site hors org → throw).
- Engine (pur, sans réseau) : `runSiteAudit` sur des snapshots fixtures (home avec/sans doctype, headers présents/absents, robots/sitemap ok/ko) → statuts attendus + score. C'est le gros des tests (déterministe, rapide).
- Pas de test réseau live (fetchSiteSnapshot mocké ou testé via injection d'un fetcher).

## Notes archi
- Le fetch est I/O-bound → TS côté Next (le principe « Rust maximal » vise le CPU-heavy batch ; ici peu de gain Rust, et wasm ne peut pas fetch). Le moteur de checks pourra être porté en crate Rust/wasm plus tard s'il est partagé avec le MCP — non requis maintenant (le MCP lira les résultats stockés en Postgres, il ne re-checke pas).
- Extensibilité : ajouter un check = une entrée dans `CHECKS`. Cible à terme : ~90 items auto-checkables.
