# Spec Plan — Refonte du seed de catégories vers IPC Statistique Canada

> Date: 2026-04-19
> Projet: simpl-resultat
> Statut: Draft
> Slug: refonte-seed-categories-ipc
> Decisions: [spec-decisions-refonte-seed-categories-ipc.md](./spec-decisions-refonte-seed-categories-ipc.md)

## Design

### UX / Interface

#### Livraison 1 — Découverte (Option E)

**Bannière dashboard** (première ouverture post-MAJ)
- Position : haut du dashboard, dismissable
- Texte : "Découvrez la nouvelle structure standard des catégories — inspirée de Statistique Canada"
- CTA : *Voir le guide* → navigue vers `/paramètres/categories/standard`
- Une fois dismiss, ne réapparaît plus (flag `user_preferences.categories_v1_banner_dismissed = true`)

**Page `/paramètres/categories/standard`** (lecture seule)
- Bloc pédagogique en haut : pourquoi cette structure, lien vers source Statistique Canada
- Arbre navigable avec expand/collapse des branches
- Hover sur catégorie : tooltip avec description + exemples de fournisseurs (ex: "Metro, IGA, Maxi, Loblaws...")
- Compteur global : "9 catégories racines, ~40 sous-catégories, ~90 feuilles"
- Bouton recherche full-text
- Bouton export PDF
- **Aucune action destructive** — lecture pure

**Entrée Paramètres**
- Dans `Paramètres > Gestion des catégories`, ajouter lien *Explorer la structure standard*

#### Livraison 2 — Migration (Option B)

Page 3 étapes séquentielles à `/paramètres/categories/migrer` :

**Étape 1 — Découvrir** (reprend la page de Livraison 1 en lecture)
- CTA : *Continuer vers l'aperçu de migration*

**Étape 2 — Simuler** (dry-run)
- Résumé impact : X catégories, Y transactions, Z règles, W budgets
- Table 3 colonnes : *Actuelle* | *Correspondance* | *v1 proposée*
  - Badges confiance 🟢/🟡/🟠/🔴
  - Panneau latéral cliquable par ligne : liste des transactions affectées + possibilité de changer la cible
- Compteur "N décisions à prendre" + bouton suivant désactivé tant que toutes les 🟠 ne sont pas résolues
- Les choix sont persistés en mémoire (pas encore BDD)

**Étape 3 — Consentir**
- Checklist explicite : "Je comprends que cette opération modifie mes catégories / Une sauvegarde sera créée avant tout changement / Je peux rétablir à tout moment"
- Bouton *Créer la sauvegarde et migrer* désactivé tant que checklist non cochée
- Pendant exécution : loader avec 4 étapes affichées (backup créé → vérifié → migration SQL → commit)
- Écran succès avec chemin du fichier SREF + CTA *Aller au tableau de bord*
- Écran échec backup : abort complet, aucune écriture, message clair + options (changer dossier / réessayer / annuler)

**Bannière post-migration** (Paramètres > Catégories, 90 jours)
- "Migration appliquée le <date>. Sauvegarde : <chemin>"
- Bouton *Rétablir la sauvegarde* → modale confirmation → `importFullProfile()` en mode replace
- Bouton *Ne plus afficher* → flag dans `user_preferences.categories_migration_banner_dismissed`

### Données

#### Nouvelle migration SQL v8 (additive)
Nom : `v8__category_schema_version.sql`

Ajoute :
- Colonne `categories.i18n_key TEXT NULL` — clé i18n technique pour les catégories seedées (ex: `seed.categories.alimentation.epicerie`). NULL pour les catégories custom → le renderer utilise `name` brut.
- Entrée dans `user_preferences` : `categories_schema_version` = `'v1'` ou `'v2'` (détermine quelle taxonomie le profil utilise).

**Important** : cette migration **n'écrase pas** le seed v2 des profils existants. Elle ajoute juste la colonne `i18n_key` (NULL par défaut) et pose `categories_schema_version='v2'` pour tous les profils existants.

#### `consolidated_schema.sql` mis à jour
- Contient le seed v1 complet (issu de `spike-archived/seed-standard/code/seed-proposal-v1.sql`)
- Pose `categories_schema_version='v1'` par défaut
- Les i18n_key sont peuplées au seed

#### Clés i18n ajoutées
Dans `src/i18n/locales/fr.json` et `en.json`, nouveau namespace `categoriesSeed` :

```json
{
  "categoriesSeed": {
    "revenus": { "root": "Revenus", "emploi": { "root": "Emploi", "paie": "Paie régulière", ... } },
    "alimentation": { "root": "Alimentation", "epicerie": { "root": "Épicerie & marché", "reguliere": "Épicerie régulière", ... } },
    ...
  }
}
```

Les `i18n_key` dans la BDD pointent vers ces clés : ex `categoriesSeed.alimentation.epicerie.reguliere`.

#### Rien de neuf en schéma v2 : pas de colonnes `frequency` ni `essentiality`
Ces attributs sont hors scope (cf. spec-decisions).

### Architecture

#### Composants React (nouveaux)

| Fichier | Rôle |
|---------|------|
| `src/pages/CategoriesStandardGuidePage.tsx` | Page read-only de Livraison 1 |
| `src/pages/CategoriesMigrationPage.tsx` | Page 3-étapes de Livraison 2 (routeur interne par étape) |
| `src/components/categories-migration/StepDiscover.tsx` | Étape 1 |
| `src/components/categories-migration/StepSimulate.tsx` | Étape 2 avec table 3 colonnes |
| `src/components/categories-migration/StepConsent.tsx` | Étape 3 avec checklist et loader |
| `src/components/categories-migration/MappingRow.tsx` | Ligne de table avec badge confiance + panneau latéral |
| `src/components/categories-migration/TransactionPreviewPanel.tsx` | Panneau latéral montrant transactions impactées |
| `src/components/dashboard/CategoriesV1DiscoveryBanner.tsx` | Bannière dashboard one-shot |
| `src/components/settings/CategoriesMigrationBackupBanner.tsx` | Bannière post-migration (90j) |

#### Services (nouveaux)

| Fichier | Rôle |
|---------|------|
| `src/services/categoryTaxonomyService.ts` | Source de vérité de la taxonomie v1 (structure hardcodée en TS, utilisée par StepDiscover + StepSimulate pour afficher l'arbre cible). Import depuis JSON bundle. |
| `src/services/categoryMappingService.ts` | Calcule le mapping v2→v1 avec badge de confiance. Implémente l'algo 4-passes (keyword → supplier → défaut → revue). Retourne une structure `MigrationPlan` en mémoire, sans écriture BDD. |
| `src/services/categoryMigrationService.ts` | Orchestre la migration : prend un `MigrationPlan` + `BackupResult` validé, exécute la transaction SQL atomique (INSERT catégories v1 → UPDATE transactions/budgets/keywords → DELETE catégories v2 non mappées → création parent "Catégories personnalisées (migration)" si custom détectées). |
| `src/services/categoryBackupService.ts` | Wrapper autour de `dataExportService` pour le flow pre-migration : crée un fichier SREF nommé `<ProfileName>_avant-migration-<ISO8601>.sref` dans `~/Documents/Simpl-Resultat/backups/`, vérifie l'intégrité (read-back + checksum), retourne `BackupResult` ou lève une erreur claire. |

#### Hooks (nouveaux)

| Fichier | Rôle |
|---------|------|
| `src/hooks/useCategoryTaxonomy.ts` | Charge la taxonomie v1 depuis le service (useMemo). |
| `src/hooks/useCategoryMigration.ts` | useReducer pour l'état de la page de migration (étape courante, mapping plan, backup result, erreurs). |

#### Fichier JSON de taxonomie
`src/data/categoryTaxonomyV1.json` — structure hiérarchique de la taxonomie v1 utilisée par `categoryTaxonomyService.ts`. Régénéré depuis `spec-plan-*/code/seed-proposal-v1.sql` (source de vérité = le SQL, le JSON est dérivé pour l'UI).

#### Flow d'intégration

```
Utilisateur clique "Créer la sauvegarde et migrer"
    ↓
useCategoryMigration dispatches START_MIGRATION
    ↓
categoryBackupService.createAndVerify()
    → Tauri: pick_save_file + write_export_file + read_import_file
    → Si échec : dispatch BACKUP_FAILED, abort, aucune écriture BDD
    → Si succès : dispatch BACKUP_VERIFIED, retourne BackupResult
    ↓
categoryMigrationService.applyMigration(plan, backup)
    → BEGIN TRANSACTION
    → INSERT catégories v1 (IDs 1000+, i18n_key peuplées, is_inputable calculé)
    → UPDATE transactions SET category_id = <mapping[old_id]>
    → UPDATE budgets SET category_id = <mapping[old_id]>
    → UPDATE keywords SET category_id = <mapping[old_id]>
    → Si custom détectées : INSERT parent "Catégories personnalisées (migration)" + re-parent
    → DELETE FROM categories WHERE id IN (<v2 non mappées, non custom>)
    → UPDATE user_preferences SET value='v1' WHERE key='categories_schema_version'
    → INSERT INTO user_preferences (key='last_categories_migration', value=<JSON métadonnées>)
    → COMMIT
    → Si erreur : ROLLBACK, backup reste disponible pour rétablissement
    ↓
dispatch MIGRATION_SUCCESS, affiche écran succès
```

## Plan de travail

### Issue 1 — Seed v1 + i18n keys pour nouveaux profils [type:task]
Dépendances : aucune
- [ ] Ajouter migration SQL v8 : colonne `categories.i18n_key TEXT NULL` + `user_preferences('categories_schema_version', 'v2')` pour profils existants
- [ ] Mettre à jour `consolidated_schema.sql` avec le seed v1 complet (issu de `spike-archived/seed-standard/code/seed-proposal-v1.sql`) et poser `categories_schema_version='v1'` par défaut
- [ ] Créer `src/data/categoryTaxonomyV1.json` dérivé du SQL seed v1
- [ ] Ajouter les clés i18n FR et EN dans `src/i18n/locales/{fr,en}.json` sous `categoriesSeed.*`
- [ ] Adapter le renderer CategoryTree/CategoryCombobox pour utiliser `i18n_key` si présent, fallback sur `name`
- [ ] Tests : création d'un nouveau profil → vérifier que le seed v1 est appliqué, que `categories_schema_version='v1'`, et que les noms s'affichent traduits FR/EN

### Issue 2 — Service categoryTaxonomyService (source taxonomie v1 en TS) [type:task]
Dépendances : Issue 1
- [ ] Créer `src/services/categoryTaxonomyService.ts` avec `getTaxonomyV1()` retournant l'arbre typé depuis le JSON
- [ ] Créer `src/hooks/useCategoryTaxonomy.ts`
- [ ] Exposer des helpers : `findByPath(path)`, `getLeaves()`, `getParentById(id)`

### Issue 3 — Page "Guide des catégories standard" (Livraison 1) [type:feature]
Dépendances : Issue 2
- [ ] Créer route `/paramètres/categories/standard` dans `src/App.tsx`
- [ ] Créer `src/pages/CategoriesStandardGuidePage.tsx`
- [ ] Implémenter l'arbre navigable avec expand/collapse, tooltips, compteur global
- [ ] Bouton recherche full-text
- [ ] Bouton export PDF (via `window.print()` avec feuille style dédiée, ou lib PDF léger)
- [ ] Ajouter lien dans `src/components/settings/CategoriesCard.tsx` (ou équivalent)

### Issue 4 — Bannière dashboard one-shot + découverte [type:feature]
Dépendances : Issue 3
- [ ] Créer `src/components/dashboard/CategoriesV1DiscoveryBanner.tsx`
- [ ] Ajouter flag `categories_v1_banner_dismissed` dans `user_preferences`
- [ ] Intégrer au `Dashboard.tsx` : affichée si `categories_schema_version='v2'` AND flag non-dismiss
- [ ] CTA dismissable vers la page Guide

### Issue 5 — Service categoryMappingService (algo ventillage 4 passes) [type:task]
Dépendances : Issue 2
- [ ] Créer `src/services/categoryMappingService.ts`
- [ ] Implémenter l'algo 4 passes (keyword → supplier → défaut → revue)
- [ ] Types : `MigrationPlan`, `MappingRow`, `ConfidenceBadge`
- [ ] Fonction `computeMigrationPlan(profileData): MigrationPlan` — pure, sans effet de bord BDD
- [ ] Mapping table encodée depuis `spike-archived/seed-standard/code/mapping-old-to-new.md`
- [ ] Détection des catégories custom (non présentes dans le seed v2)

### Issue 6 — Service categoryBackupService + wrapper SREF pre-migration [type:task]
Dépendances : aucune (peut aller en parallèle avec Issue 5)
- [ ] Créer `src/services/categoryBackupService.ts`
- [ ] Fonction `createPreMigrationBackup(profile): Promise<BackupResult>` :
  - Génère nom fichier `<ProfileName>_avant-migration-<ISO8601>.sref`
  - Emplacement par défaut `~/Documents/Simpl-Resultat/backups/`
  - Appelle `dataExportService.performExport('transactions_with_categories', 'json', password)`
  - Écrit via `write_export_file` (commande Tauri existante)
  - Vérifie intégrité via `read_import_file` + checksum SHA-256
  - Retourne `BackupResult { path, size, checksum, verifiedAt }` ou throw
- [ ] Gérer erreurs : espace disque, permissions, chiffrement si profil a un PIN

### Issue 7 — Page de migration 3-étapes (Livraison 2) [type:feature]
Dépendances : Issue 5, Issue 6
- [ ] Créer route `/paramètres/categories/migrer`
- [ ] Créer `src/pages/CategoriesMigrationPage.tsx` avec routeur interne (step 1/2/3)
- [ ] Créer `src/components/categories-migration/` avec StepDiscover, StepSimulate, StepConsent, MappingRow, TransactionPreviewPanel
- [ ] Créer `src/hooks/useCategoryMigration.ts` (useReducer)
- [ ] Créer `src/services/categoryMigrationService.ts` avec `applyMigration(plan, backup)` :
  - Transaction SQL atomique (BEGIN/COMMIT/ROLLBACK)
  - INSERT v1 + UPDATE transactions/budgets/keywords + création parent custom + DELETE v2 non mappées
  - Journal dans `user_preferences.last_categories_migration`
- [ ] Écran succès/échec avec chemin backup et options de rollback

### Issue 8 — Bouton "Rétablir la sauvegarde" (90 jours) [type:feature]
Dépendances : Issue 6, Issue 7
- [ ] Créer `src/components/settings/CategoriesMigrationBackupBanner.tsx`
- [ ] Afficher dans `Paramètres > Catégories` si `last_categories_migration.timestamp` < 90 jours et flag `banner_dismissed=false`
- [ ] Modale de confirmation
- [ ] Appel à `dataImportService.importFullProfile(path, { mode: 'replace' })`
- [ ] Post-rollback : mettre à jour `categories_schema_version='v2'` et `last_categories_migration.reverted_at`

### Issue 9 — Tests complets [type:test]
Dépendances : Issues 1, 2, 5, 6, 7
- [ ] Tests unitaires `categoryMappingService` (algo 4 passes, badges confiance, détection custom)
- [ ] Tests unitaires `categoryBackupService` (création, vérification, erreurs)
- [ ] Tests intégration : flow complet `plan → backup → migrate → verify → rollback`
- [ ] Tests régression : transactions/budgets/keywords préservés post-migration avec IDs remappés
- [ ] Tests régression : fixtures paramétrées (ancien seed v2 ET nouveau seed v1) sur budget, transactions, splits, auto-catégorisation
- [ ] QA manuelle : checklist dans `docs/qa-refonte-seed-categories-ipc.md` couvrant les 3 étapes UI, les cas nominal/échec/rollback

### Ordre d'exécution

```
Livraison 1 (E read-only + seed nouveaux profils):
  Issue 1 → Issue 2 → Issue 3 → Issue 4

Livraison 2 (B migration profils existants):
  Issue 2 → Issue 5 ─┐
                     ├→ Issue 7 → Issue 8
  Issue 6 ───────────┘

Tests:
  Issues 1,2,5,6,7 → Issue 9
```

Livraison 1 = Issues 1-4 (PR #1). Livraison 2 = Issues 5-9 (PR #2 ou series).

## Fichiers concernés

| Fichier | Action | Raison |
|---------|--------|--------|
| `src-tauri/src/lib.rs` | Modifier | Ajouter migration v8 (colonne `i18n_key` + pref `categories_schema_version`) |
| `src-tauri/src/database/migrations/v8__category_schema_version.sql` | Créer | Migration additive |
| `src-tauri/src/database/consolidated_schema.sql` | Modifier | Seed v1 complet pour nouveaux profils |
| `src/i18n/locales/fr.json` | Modifier | Nouveau namespace `categoriesSeed` + clés UI migration |
| `src/i18n/locales/en.json` | Modifier | Nouveau namespace `categoriesSeed` + clés UI migration |
| `src/data/categoryTaxonomyV1.json` | Créer | Dérivé du SQL seed v1 |
| `src/services/categoryTaxonomyService.ts` | Créer | Source taxonomie v1 côté TS |
| `src/services/categoryMappingService.ts` | Créer | Algo 4 passes |
| `src/services/categoryBackupService.ts` | Créer | Wrapper SREF pre-migration |
| `src/services/categoryMigrationService.ts` | Créer | Orchestration migration SQL atomique |
| `src/hooks/useCategoryTaxonomy.ts` | Créer | |
| `src/hooks/useCategoryMigration.ts` | Créer | useReducer état page migration |
| `src/pages/CategoriesStandardGuidePage.tsx` | Créer | Livraison 1 |
| `src/pages/CategoriesMigrationPage.tsx` | Créer | Livraison 2 |
| `src/components/categories-migration/*` | Créer | 5 composants (step 1/2/3 + mapping row + preview panel) |
| `src/components/dashboard/CategoriesV1DiscoveryBanner.tsx` | Créer | Bannière one-shot |
| `src/components/settings/CategoriesMigrationBackupBanner.tsx` | Créer | Bannière post-migration 90j |
| `src/components/settings/CategoriesCard.tsx` | Modifier | Ajouter lien vers page Guide + page Migrer |
| `src/pages/Dashboard.tsx` | Modifier | Intégrer la bannière découverte |
| `src/App.tsx` | Modifier | Nouvelles routes |
| `src/components/categories/CategoryTree.tsx` | Modifier | Support `i18n_key` fallback `name` |
| `src/components/categories/CategoryCombobox.tsx` | Modifier | Idem |
| `docs/architecture.md` | Modifier | Documenter nouveaux services, pages, migration v8 |
| `docs/adr/NNNN-refonte-seed-categories-ipc.md` | Créer | ADR pour le choix IPC + pattern prévisualisation-consentement |
| `docs/qa-refonte-seed-categories-ipc.md` | Créer | Checklist QA manuelle |
| `CHANGELOG.md` | Modifier | Entrée sous `[Unreleased]` — Added/Changed |
| `CHANGELOG.fr.md` | Modifier | Idem FR |

## Plan de tests

### Tests unitaires
- `categoryMappingService.computeMigrationPlan()` : chaque règle de mapping v2→v1 (18 haute / 12 moyenne / 3 basse / 1 aucune) retourne le bon badge et la bonne cible.
- Algo 4 passes :
  - Pass 1 (keyword match) avec diverses combinaisons
  - Pass 2 (supplier propagation)
  - Pass 3 (fallback défaut)
  - Pass 4 (flag "à réviser")
- Détection des catégories custom (absentes du seed v2) → bucket `preserved`.
- Détection des splits (ex: Transport en commun 28 v2 → Bus 1521 OR Train 1522 v1).
- `categoryBackupService.createPreMigrationBackup()` avec mocks Tauri :
  - Succès normal : retourne BackupResult valide
  - Échec write_export_file : throw erreur "Impossible de créer la sauvegarde"
  - Échec integrity check : throw erreur "Sauvegarde corrompue"
  - Profil avec PIN : chiffrement appliqué

### Tests d'intégration
- Flow complet `plan → backup → migrate → verify` sur profil fixture v2 réaliste :
  - Catégories v2 mappées correctement
  - Transactions re-liées aux nouvelles catégories v1
  - Keywords re-liés
  - Budgets re-liés
  - Catégories custom regroupées sous "Catégories personnalisées (migration)"
- Flow `rollback` après migration : import SREF restaure l'état v2 exact (transactions, keywords, budgets, categories).
- Échec backup → abort → aucune écriture BDD (profil v2 intact).
- Échec migration SQL → ROLLBACK → profil v2 intact, backup reste disponible.

### Tests de régression
Fixtures paramétrées v2 ET v1 pour couvrir :
- Auto-catégorisation (`categorizationService.applyKeywordToTransaction`)
- Budgets mensuels et agrégation parent/enfant (`budgetService.getBudgetVsActual`)
- Splits de transactions sur catégories multiples (`transactionService.splitTransaction`)
- Import CSV avec matching supplier/keyword
- Export/Import SREF (pas de régression sur le format)
- UI : `CategoryTree` et `CategoryCombobox` rendent correctement v2 et v1

## Critères d'acceptation

### Livraison 1
- [ ] Tout nouveau profil créé après la MAJ a le seed v1 appliqué (vérifié par SELECT sur la BDD d'un fresh profile).
- [ ] La bannière dashboard s'affiche sur les profils v2 existants au premier lancement post-MAJ.
- [ ] La bannière disparaît après dismiss et ne réapparaît plus (persistant).
- [ ] La page `/paramètres/categories/standard` affiche correctement l'arbre complet v1 avec FR/EN.
- [ ] Recherche full-text trouve les catégories par nom ou par mot-clé associé.
- [ ] Export PDF produit un document lisible de la taxonomie.
- [ ] Aucun changement aux données des profils v2 existants (test : avant/après MAJ, `SELECT * FROM categories` identique).

### Livraison 2
- [ ] Page `/paramètres/categories/migrer` est accessible depuis la page Guide et depuis Paramètres.
- [ ] Étape 2 affiche le bon badge de confiance pour chaque catégorie (validation sur fixture).
- [ ] Toutes les catégories 🟠 "split requis" bloquent l'avancement tant que non résolues.
- [ ] Backup SREF est créé et vérifié AVANT toute écriture BDD.
- [ ] Échec backup → abort → aucune écriture BDD (profil v2 intact).
- [ ] Migration succès → transactions, budgets, keywords tous re-liés correctement.
- [ ] Catégories custom préservées sous "Catégories personnalisées (migration)".
- [ ] Bannière post-migration visible pendant 90 jours, dismissable.
- [ ] Bouton *Rétablir la sauvegarde* fonctionne : restaure exactement l'état v2.

### Global
- [ ] Tous les tests unitaires et intégration passent (`npm test`, `cargo test`).
- [ ] Type-check clean (`npm run build`).
- [ ] CHANGELOG mis à jour FR et EN.
- [ ] `docs/architecture.md` mis à jour.
- [ ] ADR rédigé.
- [ ] QA manuelle exécutée selon checklist `docs/qa-refonte-seed-categories-ipc.md`.

## Edge cases et risques

| Cas | Mitigation |
|-----|------------|
| Profil v2 avec 0 catégorie custom → mapping simple | Testé par fixture minimale |
| Profil v2 avec ≥50 catégories custom (utilisateur power) | UI pagine la liste dans l'étape 2 ; parent "Catégories personnalisées (migration)" absorbe tout |
| Utilisateur abandonne étape 2 en plein milieu | Aucune écriture BDD, le plan en mémoire est perdu — OK, aucun effet secondaire |
| Utilisateur abandonne étape 3 après checklist cochée mais avant backup | Bouton *Annuler* abort propre, aucune écriture |
| Espace disque insuffisant pour backup | `categoryBackupService` lève erreur claire → UI montre écran d'échec avec options "changer dossier / réessayer / annuler" |
| Migration SQL échoue au milieu | `ROLLBACK` automatique, backup reste disponible, UI affiche erreur + invite à rétablir |
| Utilisateur lance 2 instances de Simpl'Résultat en parallèle pendant la migration | SQLite lock naturel ; la 2e instance attend ; bas risque (app desktop mono-fenêtre en pratique) |
| Profil protégé par PIN : backup doit être chiffré | `categoryBackupService` récupère le PIN depuis le ProfileContext et passe en password à `write_export_file` |
| Utilisateur renomme/déplace le fichier SREF après migration | Le bouton *Rétablir* affiche un file picker si le chemin enregistré n'est plus valide |
| Déjà-migré : utilisateur re-lance la page de migration | Détection `categories_schema_version='v1'` → message "Votre profil utilise déjà la taxonomie v1" avec option "revoir la sauvegarde" uniquement |
| Utilisateur veut migrer APRÈS les 90 jours (bannière disparue) | Entrée permanente dans Paramètres > Catégories reste disponible → bouton *Explorer / Migrer* |
| Clé i18n manquante (typo dans le JSON) | Fallback sur le nom brut de la catégorie — pas de crash |
| Seed v1 manque une feuille qu'un utilisateur a en v2 (ex: "Projets") | Mapping badge 🔴 + prompt obligatoire étape 2, ou préservé en custom |
| Compatibilité forward : une future v2 du seed (refinement v1.1, v1.2) | `categories_schema_version` permet de détecter et ajouter des colonnes plus tard. Pattern réutilisable. |
| Performance : 139 catégories + 100+ keywords au seed pour un nouveau profil | < 200 ms sur SSD moderne, négligeable |
| Performance : migration de 5000 transactions | Transaction unique, < 2 s sur SSD moderne, loader visible pendant ce temps |
