# ADR — Routage des imports : crate `import-core` (Rust/WASM) vs `services/analytics` (DuckDB)

- **Date:** 2026-07-21
- **Status:** accepted — acte les phases 1-4 déjà shippées ; §4 et §5 restent ouverts (follow-up produit, non tranché ici)
- **Branch:** n/a — décision transverse actée au fil des phases 1-4 (branches `feature/*` mergées ff-only sur `dev`, supprimées)
- **Related:** `docs/project/2026-07-21-etat-des-lieux-imports.md` (inventaire complet, §5 « écarts doc ↔ code » = origine de cet ADR),
  `docs/bugs/2026-07-21-planning-serp-import.md` (symptôme déclencheur),
  `docs/superpowers/specs/2026-06-04-reservoir-import-design.md` (doctrine « Rust pur → WASM → Next persiste »),
  `docs/superpowers/specs/2026-06-09-duckdb-analytics-pipeline-design.md` et
  `docs/superpowers/specs/2026-06-22-ingest-scalability-adr.md` (doctrine « gros volumes → DuckDB/Hono async »).

## Question

Deux doctrines d'ingestion coexistent sans règle de routage écrite : parsing Rust pur
compilé en WASM et appelé in-process depuis Next (spec 2026-06-04), et pipeline
DuckDB/Hono asynchrone pour les gros volumes (ADR 2026-06-09). Résultat observé :
le module keywords du crate Rust (`detect_source`, `parse_keywords`) est du code
mort — zéro appelant TS, ne gère pas Ahrefs — pendant que `services/analytics`
(DuckDB) est l'autorité de fait pour le CSV keywords. Quelle capacité va où, et
pourquoi ?

## Décision

1. **`import-core` (WASM, in-process, synchrone) est l'autorité de parsing pour** :
   SERP CSV, on-page HTML, backlinks (catalogue + domaine), et `slugify`
   (identité `keywords.id`). Chemins synchrones, payloads plafonnés par les caps
   route handler (≤ 25 Mo — voir §3).
2. **`services/analytics` (DuckDB) est l'autorité pour le CSV keywords lourd**
   (10k+ lignes, ≤ 50 Mo), en asynchrone : upload R2 + queue `analytics_jobs`.
3. **Le module keywords du crate (`detect_source`/`parse_keywords`) est
   supprimé** (phase 1). Motif : code mort dupliquant l'ETL DuckDB — deux
   parseurs pour un même format n'est jamais acceptable. Le seul delta
   fonctionnel qu'il calculait (trend Semrush, `growthPct`) est **perdu** ;
   `processor.ts` fixe `growthPct: null` en dur. Si le trend redevient un
   besoin produit, il se porte dans `processor.ts` — pas de retour du module
   Rust.
4. **Règle générale de routage** (pour trancher les futurs cas) : format/volume
   petit et scopé à une entité (mot-clé, page, domaine) → `import-core`
   synchrone ; format lourd, multi-lignes, nécessitant crunch/dedup avant
   écriture → `services/analytics` asynchrone. Un même format ne doit jamais
   avoir deux parseurs actifs simultanément.

## Contexte / preuves

Constat vérifié par grep croisé (état des lieux §3.1, §5) : 4 implémentations
de détection/parsing CSV keywords coexistaient (Rust WASM mort, DuckDB/TS prod,
2 copies client miroir). La doctrine Rust (2026-06-04) et la doctrine DuckDB
(2026-06-09) ont chacune été correctement appliquées à leur périmètre d'origine,
mais aucune n'a été révisée quand l'autre a pris le pas sur les keywords —
d'où la dérive. Le bug SERP du 21/07 (cul-de-sac Overview ↔ SourcesModal) est
un symptôme du même défaut structurel : absence de chemin d'import unique
factorisé, pas une cause de cet ADR en soi.

## Slug unique

`wasm.slugify` (crate `import-core`) est **la** fonction d'identité pour
`keywords.id = ${siteId}-${slug}`. Le service analytics (`processor.ts`,
qui consommait déjà `parse_page_meta` du même crate) et les scripts de seed
(`scripts/seed-backlinks.ts`) s'y alignent (phase 4), avec test de parité
golden (mêmes entrées → mêmes ids sur les 3 chemins). Toute divergence de
slug casse la clé de merge de tous les imports (ETL vs brief-par-terme vs
GSC vs ajout manuel) — c'était le risque réel identifié en §3.3 de l'état
des lieux.

## Caps upload

État actuel : 1 Mo (Server Actions — brief YTG, catalogue backlinks) /
25 Mo (routes handler — SERP, backlinks domaine) / 50 Mo (route import-heavy
CSV keywords). Décision : **cible = tout import fichier passe par une route
handler, cap 25 Mo minimum** (le plafond 1 Mo est un artefact des Server
Actions, pas une limite fonctionnelle). La migration du brief YTG (et du
catalogue backlinks) hors Server Action vers une route handler est un
**follow-up non fait** — reste à planifier, documenté ici comme tel.

## Rejeté / différé

- **Garder le module keywords du crate en fallback** — rejeté : maintenir deux
  parseurs pour un même format est la source du problème, pas une mitigation.
- **Porter le trend Semrush immédiatement** — différé : aucun consommateur
  actuel ; à faire dans `processor.ts` si/quand le besoin revient (pas de
  résurrection du code Rust).

## Ouvert — non tranché ici

### Quota billing (asymétrie produit)
Le quota (`lockOrgForQuota`) est contrôlé sur les deux imports backlinks
(catalogue + domaine) mais **absent** sur le CSV keywords lourd et sur GSC.
Options, à trancher côté produit :
- (a) étendre `lockOrgForQuota` au chemin `services/analytics` (nécessite un
  hook async — le job tourne hors requête HTTP, donc hors du pattern actuel) ;
- (b) accepter l'asymétrie : keywords/GSC restent hors quota par choix produit
  (volume perçu comme différent des backlinks) — alors le documenter comme
  choix assumé plutôt que dette ;
- (c) quota au niveau du job (rejet à l'enqueue si le quota est dépassé,
  plutôt qu'à l'upload).
Cet ADR n'arbitre pas — il documente l'écart pour que la décision produit
se prenne consciemment.

## Point de vigilance — GSC

Deux clients GSC coexistent et le restent pour l'instant (assumé, hors
périmètre de cet ADR) : TS user-driven (`src/lib/gsc/client.ts`) écrit dans
`keywords.gsc*` (jsonb, sous Drizzle) ; Rust `gsc-worker` planifié écrit en
SQL brut hors Drizzle dans `gsc_query_snapshots`/`gsc_page_snapshots`. Aucun
code partagé entre les deux. Risque : une migration Drizzle sur les colonnes
concernées ne protège pas le chemin Rust (noms de colonnes en dur côté
`sqlx`) — à vérifier manuellement à chaque migration touchant ces tables.

## Next step

Suivre le §4 (quota) comme décision produit à part, et le follow-up caps
(§3) comme item de dette technique planifiable indépendamment — aucun des
deux ne bloque le routage acté en §Décision.
