# Spec Plan — Import CSV et reconnaissance de format

> Date: 2026-08-12
> Projet: simpl-resultat
> Statut: Draft
> Slug: import-csv-format
> Decisions: [spec-decisions-import-csv-format.md](./spec-decisions-import-csv-format.md)

## Design

### UX / Interface

Le wizard passe de six étapes rendues à sept. `ImportWizardStep` déclare déjà `file-preview` (`types/index.ts:543`) sans que `ImportPage.tsx` ne la rende jamais — le modal optionnel l'avait supplantée. L'étape est restaurée et devient obligatoire.

**Étape configuration.** À l'ouverture d'une source jamais configurée, la détection se lance seule ; le bouton baguette magique reste pour la rejouer. Le panneau gagne un bandeau de résultat : soit une banque reconnue par signature (« Format Desjardins reconnu »), soit un score générique (« Format reconnu — 147 des 150 lignes lues »), soit un avertissement sous le seuil. Le sélecteur de convention de signe, aujourd'hui affiché en permanence alors que le parsing ne l'applique qu'en mode simple (`useImportWizard.ts:514`), n'apparaît plus qu'en mode montant unique.

**Étape aperçu.** Nouvelle étape traversée à chaque import. Au-dessus du tableau des vingt premières lignes, un récapitulatif signé : nombre de sorties et total, nombre d'entrées et total, nombre de lignes en erreur. C'est le contrôle qui rattrape visuellement toute erreur de convention, quelle qu'en soit la cause. Un bouton *Inverser les signes* bascule `sign_convention` et relance le parsing — il agit sur la configuration, jamais sur les données seules, pour que la correction soit mémorisée.

**Étape confirmation.** Le récapitulatif des réglages passe de quatre entrées à sept : le mode de montant, la convention de signe et le mapping des colonnes rejoignent délimiteur, encodage, format de date et lignes ignorées.

> **🟡 SECURITE** — « Inverser les signes » est inerte en mode débit/crédit. `sign_convention` n'est appliqué que dans la branche montant unique (`useImportWizard.ts:514`), et l'issue 6 masque son sélecteur en mode débit/crédit : le bouton présenté comme rattrapant « toute erreur de convention, quelle qu'en soit la cause » ne fait rien sur un fichier dont les deux colonnes sont mappées à l'envers.
> **Resolution :** En mode débit/crédit, le même bouton permute `debitAmount` et `creditAmount` dans le mapping puis relance le parsing.

**Dérive de format.** Au ré-import d'une source connue dont l'en-tête ne correspond plus à la signature mémorisée, un panneau présente l'écart colonne par colonne (« Montant : 3 → 4 ») avec deux issues : adopter la configuration re-détectée, ou conserver l'ancienne.

### Donnees

**Migration v17** — additive, `v1` à `v16` intactes.

```sql
ALTER TABLE import_sources ADD COLUMN amount_mode TEXT NOT NULL DEFAULT 'single'
  CHECK (amount_mode IN ('single','debit_credit','absolute_indicator'));
ALTER TABLE import_sources ADD COLUMN sign_convention TEXT NOT NULL DEFAULT 'negative_expense'
  CHECK (sign_convention IN ('negative_expense','positive_expense'));
ALTER TABLE import_sources ADD COLUMN header_signature TEXT;
ALTER TABLE import_sources ADD COLUMN template_id INTEGER REFERENCES import_config_templates(id) ON DELETE SET NULL;
UPDATE import_sources SET amount_mode = 'debit_credit'
  WHERE column_mapping LIKE '%debitAmount%';
```

Le backfill reproduit exactement la règle appliquée aujourd'hui à la volée (`useImportWizard.ts:321`), donc aucune source ne change de comportement à la migration. Le test `LIKE` est préféré à `json_extract` pour ne dépendre d'aucune extension JSON1 dans le SQLite embarqué.

Les deux colonnes d'énumération portent un `CHECK`, conformément au pattern de la v15 sur `balance_accounts.kind`. Celui d'`amount_mode` accepte d'emblée `absolute_indicator`, la valeur du troisième mode laissé hors scope : la contrainte protège dès aujourd'hui contre une valeur corrompue, et le mode pourra être implémenté plus tard sans migration. La liste blanche applicative de la frontière SREF reste nécessaire — elle produit un message lisible là où la base ne rendrait qu'une erreur de contrainte.

> **🟢 ARCHITECTURE** — L'objectif (ajouter le 3e mode sans migration) s'atteint sans renoncer à la contrainte : `CHECK (amount_mode IN ('single','debit_credit','absolute_indicator'))` tient dès aujourd'hui et accepte déjà la valeur future.
> **Resolution :** Écrire le `CHECK` élargi en v17 plutôt que de l'omettre.

> **🟡 SECURITE** — Sans `CHECK`, une valeur inconnue restaurée depuis une sauvegarde SREF éditable retombe en silence sur un défaut : `useImportWizard.ts:501` branche `if (amountMode === "debit_credit") … else`, donc toute autre valeur lit la mauvaise colonne, et toute valeur autre que `positive_expense` signifie `negative_expense`. C'est exactement la classe d'erreur silencieuse que le chantier existe pour tuer.
> **Resolution :** Valider les deux champs contre une liste blanche à la frontière d'import SREF et à la lecture dans `importSourceService`, avec erreur visible plutôt que repli.
> *Ref : CWE-20*

`consolidated_schema.sql` reçoit les quatre colonnes, non pour alimenter les nouveaux profils — ils les reçoivent de la v17, puisque le script consolidé s'exécute après toutes les migrations et n'utilise que `CREATE TABLE IF NOT EXISTS` — mais pour rester la définition de référence testée. Une constante `V17_SQL` miroir est ajoutée côté tests, conformément au pattern `V13_SQL` à `V16_SQL`, avec un test de parité sur le modèle de `consolidated_schema_has_holdings_tables_and_kind_at_parity` (`lib.rs:2824`) : sans lui, les `DEFAULT` et les `CHECK` peuvent diverger entre les deux définitions.

> **🟢 SECURITE + TECHNIQUE** — Le rationale est inversé, vérification faite. `get_new_profile_init_sql` s'exécute **après** que tauri-plugin-sql a appliqué toutes les migrations, et le script consolidé utilise `CREATE TABLE IF NOT EXISTS` : les quatre colonnes qui y sont ajoutées sont inertes en production. Les nouveaux profils les reçoivent de la v17 seule. Le miroir sert au test de parité, pas au chemin de production.
> **Resolution :** Corriger la formulation et exiger un test de parité sur le modèle de `consolidated_schema_has_holdings_tables_and_kind_at_parity` (`lib.rs:2824`), sans quoi le `DEFAULT` de `sign_convention` peut diverger entre les deux définitions.

Après la v17, `import_sources` et `import_config_templates` portent les mêmes huit champs de format. L'asymétrie qui a causé le bug racine disparaît structurellement.

`template_id` est une **étiquette de provenance** : elle enregistre le modèle depuis lequel la source a été configurée et n'est **jamais relue comme format**. Le format en vigueur est toujours celui des huit colonnes de la source. La colonne sert à l'affichage (« configurée depuis le modèle Desjardins ») et au signalement de divergence ; éditer un modèle ne modifie aucune source liée, ce qu'un critère d'acceptation vérifie.

> **🟡 ARCHITECTURE** — `template_id` réintroduit une seconde source de vérité que la migration venait d'éliminer. Les huit champs sont déjà **copiés** sur la source ; or `updateConfigTemplate` modifie un modèle en place (`useImportWizard.ts:965`), donc le format d'une source liée diverge silencieusement de son modèle, sans qu'aucune règle ne dise lequel fait foi.
> **Resolution :** Soit retirer `template_id` (YAGNI — le format est déjà copié), soit énoncer dans le plan et l'ADR qu'il est une **étiquette de provenance**, jamais relue comme format, avec un critère d'acceptation prouvant qu'éditer un modèle ne modifie aucune source liée.

**Format SREF.** Le fichier d'export gagne deux tableaux, `import_sources` et `import_config_templates`. À l'import, les sources sont restaurées au lieu d'être détruites ; la source factice « Data Import » n'est créée que pour rattacher des transactions orphelines. Une sauvegarde produite avant ce changement ne porte aucun de ces tableaux : l'import doit alors conserver le comportement actuel plutôt qu'échouer.

### Architecture

Deux types et un codec. `ImportFormatRow` est la forme persistée — snake_case, mapping en JSON, `has_header` normalisé — partagée par `import_sources` et `import_config_templates`. `ImportFormat` est la forme de domaine — camelCase, mapping parsé — manipulée par le wizard et la détection. Une paire `formatToRow` / `formatFromRow` est le **seul point de conversion** entre les deux.

La garantie recherchée ne vient donc pas de la structure des types, qui ne peut pas être commune (les porteurs actuels mélangent les casses et typent `has_header` en `boolean` d'un côté, `number` de l'autre), mais du codec et de son test : un champ ajouté au format sans être traversé par le codec fait échouer le test de complétude. C'est ce qui ferme la classe d'erreur à l'origine du chantier.

> **🔴 ARCHITECTURE + TECHNIQUE** — La composition est structurellement impossible telle qu'écrite : les porteurs ont des formes incompatibles, vérifiées dans `types/index.ts` — `ImportSource.has_header: boolean` (`:12`) et `column_mapping: string` (`:10`), `ImportConfigTemplate.has_header: number` (`:164`), `SourceConfig.hasHeader: boolean` en camelCase avec `columnMapping: ColumnMapping` objet (`:225`), plus `AutoDetectResult` qui ne porte que 7 des 8 champs (pas d'`encoding`). Un type unique composé dans les quatre ne peut pas exister sans trancher une casse et une représentation du mapping.
> **Resolution :** Deux types et un codec : `ImportFormatRow` (persisté, snake_case, mapping en JSON, `has_header` normalisé) et `ImportFormat` (domaine, camelCase, mapping parsé), reliés par une paire `formatToRow`/`formatFromRow` **unique point de conversion**. La garantie de complétude vient alors du codec et de son test, pas de la structure du type.

La détection gagne une couche lexicale en amont de l'heuristique existante. `normalizeHeaderCell` et `matchHeaderColumn` (`csvAutoDetect.ts:633-666`) sont déjà au niveau module et sont réutilisés tels quels ; c'est le **dictionnaire** qui est nouveau, et il vit dans son propre module plutôt que dans `csvAutoDetect.ts`, déjà long de 856 lignes pour deux flux sans rapport. La séparation n'est pas cosmétique : `montant` est un token d'**exclusion** pour les titres (`VALUE_HEADER_KEYWORDS`, `:630`) et le mot-clé de montant **principal** pour les transactions — deux tables distinctes, deux flux qui ne se marchent pas dessus.

> **🟡 ARCHITECTURE** — Le « remontage » est un no-op : les deux fonctions sont déjà au niveau module du même fichier que `autoDetectConfig` (`:633`, `:646`). La mitigation du tableau des risques couvre donc un risque inexistant, tandis que le vrai couplage n'est pas traité — les tables de mots-clés sont partagées, et `montant` est un **token d'exclusion** pour les holdings (`VALUE_HEADER_KEYWORDS`, `:630`) alors qu'il est le mot-clé de montant principal pour les transactions.
> **Resolution :** Retirer la tâche de remontage ; placer le dictionnaire transactions dans son propre module à côté de `bankSignatures.ts`, important les deux helpers et laissant les tables holdings intactes. `csvAutoDetect.ts` fait déjà 856 lignes pour deux flux sans rapport. Le dictionnaire FR/EN couvre date, description (libellé, détail), montant, débit (retrait, déboursé), crédit (dépôt, encaissement) et solde. Quand les libellés tranchent, ils priment ; quand ils sont muets — fichier sans en-tête, libellés inconnus — l'heuristique de forme actuelle reprend la main. C'est ce qui résout d'un coup l'ordre débit/crédit deviné par position (`csvAutoDetect.ts:461-470`), la détection d'en-tête aveugle aux nombres et le choix de la colonne description par longueur moyenne.

La règle de mapping d'une ligne — date, montant, application du signe — est extraite de `parseFilesInternal` en une fonction pure exportée `mapRow(raw, format): ParsedRow`. Elle devient le point unique consommé par le wizard, par le calcul de score et par le récapitulatif d'aperçu. Sans cette extraction, le score réimplémenterait la règle et pourrait afficher 100 % pendant que l'import écrit de mauvais signes — la divergence même que ce chantier combat. Elle rend au passage la règle testable unitairement, le dépôt n'ayant pas de jsdom (d'où le pattern d'export des reducers de `useSnapshotEditor.ts`).

`autoDetectConfig` retourne désormais un score : la configuration détectée est rejouée via `mapRow` sur les lignes de l'échantillon et le taux de lignes parsées sans erreur est remonté à l'appelant.

> **🔴 ARCHITECTURE + TECHNIQUE** — Rejouer la configuration exige la règle de mapping de ligne (date + montant + signe), qui vit dans `parseFilesInternal`, un `useCallback` de `useImportWizard.ts:461-557`. `autoDetectConfig` est un util pur qui ne reçoit que `rawContent` : calculer le score dans le module de détection en écrirait une **seconde implémentation**. Deux mappers, c'est précisément la classe de divergence que ce chantier existe pour tuer — le score pourrait afficher 100 % pendant que l'import écrit de mauvais signes.
> **Resolution :** Extraire en issue 3 une fonction pure exportée `mapRow(raw, format): ParsedRow` dans `src/utils/`, consommée par le hook, le calcul de score et le récapitulatif d'aperçu. Elle rend au passage possibles les tests unitaires promis par l'issue 3 — le dépôt n'a pas de jsdom, d'où le pattern d'export des reducers et builders de `useSnapshotEditor.ts`.

Les signatures de banques sont un tableau de règles déclaratives — ensemble de libellés d'en-tête normalisés, délimiteur, particularités de préambule. Elles sont évaluées avant le dictionnaire générique et n'ajoutent aucune surface d'interface.

Le point d'écriture de la configuration en base migre de `checkDuplicatesInternal` (`useImportWizard.ts:588-606`) vers `executeImport`, pour qu'un import abandonné ne laisse plus de configuration derrière lui.

## Plan de travail

### Issue 1 — Migration v17 : le format complet sur les sources [type:schema]
Dependances : Issue 4
- [ ] Migration v17 dans `lib.rs` : quatre colonnes + backfill `amount_mode`
- [ ] Miroir dans `consolidated_schema.sql`
- [ ] Constante `V17_SQL` + test d'application sur une base v16
- [ ] Test : le backfill reproduit la règle actuelle (source avec `debitAmount` → `debit_credit`, sans → `single`)
- [ ] Test de parité entre `consolidated_schema.sql` et la chaîne v1→v17 (colonnes, `DEFAULT`, `CHECK`)
- [ ] Test de non-régression : les chaînes SQL `v1` à `v16` absentes du diff

> **🟡 TECHNIQUE** — « Checksums intacts » n'est pas une assertion testable ici : aucun harnais de checksum n'existe. Les constantes `V10_SQL` à `V16_SQL` sont des copies manuelles appliquées via `execute_batch`, et les checksums ne vivent qu'au runtime dans `_sqlx_migrations` (réparés par `profile_commands.rs:223-275`). Un worker autonome va soit bâcler la case, soit y brûler un cycle.
> **Resolution :** Remplacer par ce que le harnais vérifie réellement — les chaînes SQL v1 à v16 absentes du diff, plus l'application de `V17_SQL` sur une base v16 peuplée — et retirer le mot « checksums ». Ajouter le test de parité du schéma consolidé.

### Issue 2 — Persister et restaurer le format d'import [type:bug]
Dependances : Issue 1
- [ ] Type `ImportFormat` partagé ; `ImportSource` et `ImportConfigTemplate` le composent
- [ ] `importSourceService` : les quatre nouvelles colonnes en création, mise à jour et lecture
- [ ] `useImportWizard.selectSource` : lire `amount_mode` et `sign_convention` ; supprimer la valeur en dur (`:323`) et la ré-inférence du mode (`:321`)
- [ ] `ColumnMappingEditor.onAmountModeChange` nettoie les colonnes du mode abandonné
- [ ] Déplacer l'écriture de la config de `checkDuplicatesInternal` vers `executeImport`
- [ ] Lier la source au modèle appliqué (`template_id`), persisté et restauré
- [ ] Test de cycle : configurer → sauvegarder → recharger → le format est identique au bit près

### Issue 3 — Règle débit/crédit et parsing des montants [type:bug]
Dependances : Issue 2
- [ ] `amount = crédit − débit` sur magnitudes, en remplacement de la comparaison de nullité (`:509`)
- [ ] Colonne inutilisée à `0,00` traitée comme absente
- [ ] Supprimer les fallbacks `?? 0` (`:504-512`) au profit d'une erreur de ligne « colonne de montant non mappée »
- [ ] `parseFrenchAmount` : parenthèses comptables `(50,00)` et signe suffixe `50,00-`
- [ ] Séparateur décimal arbitré au niveau de la colonne et non de la cellule
- [ ] **Validation ancrée** : `parseFrenchAmount` rend `NaN` sur tout caractère résiduel après normalisation — aujourd'hui `"50,00-"` rend 5000, `"1 234,56 CR"` rend 123456 et `"100,00 CAD"` rend 10000, erreurs de facteur 100 qui passent `isNaN`
- [ ] Durcissement appliqué **partout** (décision tranchée) : vérifier les 11 sites d'appel — 8 dans `csvAutoDetect.ts` (`:224`, `:289`, `:323`, `:489`, `:517`, `:543-544`, `:712`) et 3 dans `useSnapshotEditor.ts:191-202` (import CSV de titres, #245)
- [ ] Extraire `mapRow(raw, format): ParsedRow` pur et exporté depuis `parseFilesInternal`, consommé par le wizard, le score et l'aperçu
- [ ] Tests unitaires sur chaque cas, dans le nouveau `src/utils/amountParser.test.ts`
- [ ] Régression : les tests holdings existants restent verts

> **🔴 SECURITE** — Le défaut est plus grave que décrit, vérifié en exécutant la fonction : `parseFrenchAmount` termine sur `parseFloat`, qui s'arrête au premier caractère invalide au lieu de rejeter. `"50,00-"` rend **5000**, `"1 234,56 CR"` rend **123456**, `"100,00 CAD"` rend **10000** — une erreur de facteur 100, pas un signe perdu. Chacune passe `isNaN` et sera donc comptée comme ligne **valide** dans le nouveau récapitulatif signé, ce qui neutralise le filet de sécurité principal du chantier. Ajouter la forme `50,00-` traite un suffixe et laisse passer tous les autres.
> **Resolution :** Valider la chaîne entière par une regex ancrée après normalisation et rendre `NaN` sur tout caractère résiduel. Ajouter ces trois chaînes aux tests unitaires.
> *Ref : CWE-1284*

> **🔴 TECHNIQUE** — `parseFrenchAmount` a 11 sites d'appel que l'issue ne liste pas : 8 dans `csvAutoDetect.ts` (`:224` `detectHeader`, `:289`, `:323`, `:489`, `:517` `detectSingleAmount`, `:543-544` `isSparseComplementary`, `:712` holdings) et 3 dans `useSnapshotEditor.ts:191-202` — l'import CSV de titres (#245). `useSnapshotEditor.ts` est absent du tableau « Fichiers concernés ». Changer le parser déplace silencieusement `hasNumber`, `negCount` et les quantités de titres.
> **Resolution :** Ajouter `csvAutoDetect.ts` et `useSnapshotEditor.ts` au périmètre de l'issue, avec une case de régression sur les tests holdings, et trancher explicitement si les nouvelles formes sont globales ou opt-in.

### Issue 4 — Corpus de fixtures CSV [type:feature]
Dependances : aucune — **premier maillon de la pile**

> **🔴 TECHNIQUE + ARCHITECTURE** — Le corpus censé « figer le comportement avant la refonte » arrive **après** le changement qu'il doit servir de référence. L'issue 3 modifie `parseFrenchAmount`, dont dépendent `detectHeader`, `detectSingleAmount`, `pickBestAmountColumn` et `isSparseComplementary` : la ligne de base enregistrerait donc un comportement déjà modifié.
> **Resolution :** Déplacer cette issue en première position — elle ne dépend de rien, ni migration ni persistance. Nouvel ordre : 4 → 1 → 2 → 3 → 5 → … Les issues 3, 5 et 6 se mesurent alors toutes à un contrat préexistant. Étendre les assertions aux sites d'appel holdings.
- [ ] Fixtures synthétiques : montant signé, débit/crédit, débit/crédit inversés, colonne à `0,00`, préambule, en-tête contenant un nombre, sans en-tête, tout-positif, ligne entière entre guillemets
- [ ] Tests de contrat figeant le comportement de `autoDetectConfig` avant sa refonte
- [ ] Tests de bout en bout du parsing sur chaque fixture

### Issue 5 — Détection par libellé d'en-tête [type:feature]
Dependances : Issue 3
- [ ] Remonter `normalizeHeaderCell` et `matchHeaderColumn` en helpers partagés du module
- [ ] Dictionnaire FR/EN : date, description, montant, débit, crédit, solde
- [ ] Ordre débit/crédit résolu par libellé plutôt que par position
- [ ] `detectHeader` : signal lexical en complément de la forme
- [ ] Repli sur l'heuristique actuelle quand les libellés sont muets
- [ ] Dictionnaire dans son propre module, hors de `csvAutoDetect.ts` — les tables holdings restent intactes (`montant` y est un token d'exclusion)
- [ ] **Refus du troisième format** : détecter montants tous positifs + colonne voisine à une seule lettre `D`/`C`, et refuser explicitement avec un message dédié plutôt que d'importer chaque débit comme un revenu
- [ ] Tests sur les fixtures, dont l'inversion crédit-avant-débit et le fichier à indicateur refusé

> **🟡 SECURITE** — Le document de décisions promet qu'un fichier au format « montant absolu + indicateur `D`/`C` » sera « refusé explicitement plutôt qu'importé de travers », mais aucune issue du plan ne le détecte ni ne le refuse. En l'état, un tel fichier présente une colonne de montants tous positifs, `detectSingleAmount` rend `positive_expense`, et chaque débit est importé comme un revenu.
> **Resolution :** Ajouter ici une règle de détection (colonne de montants tous positifs plus une colonne voisine à une seule lettre `D`/`C`) et un message de refus explicite, avec un critère d'acceptation et une fixture dans l'issue de corpus.

### Issue 6 — Score de confiance et détection lancée d'office [type:feature]
Dependances : Issue 5
- [ ] `autoDetectConfig` rejoue sa configuration sur l'échantillon et retourne un taux
- [ ] Détection déclenchée seule à l'ouverture d'une source non configurée
- [ ] Bandeau de résultat dans `SourceConfigPanel` : reconnu, score, ou avertissement
- [ ] Convention de signe masquée en mode débit/crédit
- [ ] Clés i18n FR et EN

### Issue 7 — Aperçu obligatoire et récapitulatif signé [type:feature]
Dependances : Issue 6
- [ ] Rendre l'étape `file-preview` dans `ImportPage`, traversée à chaque import
- [ ] Récapitulatif : sorties et total, entrées et total, lignes en erreur
- [ ] Bouton *Inverser les signes* agissant sur la configuration, avec re-parsing
- [ ] `ImportConfirmation` affiche mode, convention et mapping
- [ ] `useImportWizard` : nouvelle transition vers `file-preview` dans le reducer, recâblage de `checkDuplicates` (aujourd'hui code mort) et de `parseAndCheckDuplicates` qui saute l'étape
- [ ] `ImportPage` : remplacer la paire de boutons Aperçu / Vérifier-doublons par `WizardNavigation`
- [ ] Retirer `FilePreviewModal`
- [ ] En mode débit/crédit, *Inverser les signes* permute `debitAmount` et `creditAmount`
- [ ] Clés i18n FR et EN

> **🟡 TECHNIQUE** — L'étape est un changement de machine à états, pas un rendu, et le périmètre annoncé est trop étroit. `file-preview` n'a **aucune transition** dans le reducer (`SET_STEP` ne la vise jamais), `parseAndCheckDuplicates` saute de `source-config` à `duplicate-check` (`:719`, commentaire « skips preview step »), `ImportPage` porte encore la paire de boutons Aperçu / Vérifier-doublons (`:126-140`) et le `FilePreviewModal` (`:196-202`) ; enfin `checkDuplicates` (`:685`, exporté `:1006`) est du code mort à recâbler. Modifier `FilePreviewTable`, seul fichier listé, mute aussi le modal encore vivant.
> **Resolution :** Étendre le périmètre à `useImportWizard` (nouvelle transition + recâblage de `checkDuplicates`), `ImportPage` (remplacer la paire de boutons par `WizardNavigation`) et `FilePreviewModal` (le retirer).

### Issue 8 — Signatures de banques et dérive de format [type:feature]
Dependances : Issue 7
- [ ] Signatures déclaratives Desjardins, RBC, BNC, Tangerine, évaluées avant le dictionnaire générique
- [ ] `header_signature` mémorisée à l'import réussi
- [ ] Au ré-import : comparaison, re-détection, panneau d'écart avec adopter ou conserver
- [ ] Clés i18n FR et EN
- [ ] Le panneau de dérive et l'aperçu énoncent le seul chemin de réparation sûr : supprimer l'import fautif via l'historique avant de le rejouer — un ré-import corrigé ne s'apparie pas aux lignes déjà écrites et les double
- [ ] Tests sur fixtures par banque et sur un cas de dérive

### Issue 9 — Sources et modèles dans l'export de données [type:bug]
Dependances : Issue 8
- [ ] Sérialiser `import_sources` et `import_config_templates` à l'export
- [ ] Restaurer les sources à l'import au lieu du `DELETE` suivi d'une source factice (`:265`, `:362`)
- [ ] **Envelopper purge et restauration dans `withTransaction`** — les deux fonctions n'en ont aucune aujourd'hui, une violation de contrainte à mi-course détruit l'historique sans retour arrière
- [ ] Ordre de restauration : modèles avant sources ; stratégie d'identifiants explicite (upsert par nom + remap de `template_id`), `import_config_templates` n'étant dans aucune liste de purge
- [ ] Liste blanche sur `amount_mode` et `sign_convention` à la frontière d'import, avec erreur lisible
- [ ] Rétrocompatibilité : une sauvegarde antérieure sans ces tableaux s'importe comme aujourd'hui
- [ ] Tests de cycle export/import préservant les configurations
- [ ] Test : une restauration qui échoue à la ligne N laisse le profil intact

> **🔴 SECURITE** — La restauration SREF n'est enveloppée dans **aucune transaction** : `dataExportService.ts` ne contient pas une seule occurrence de `withTransaction` (vérifié), et enchaîne `DELETE FROM transactions / imported_files / import_sources / keywords / suppliers / categories` (`:263-268`, `:360-362`) puis des `db.execute` d'insertion. Cette issue ajoute deux boucles d'insertion de plus, sur des tables à contrainte `UNIQUE(name)` et une clé étrangère `template_id` : la moindre violation abandonne la restauration à mi-course et l'historique financier de l'utilisateur est perdu sans retour arrière.
> **Resolution :** Envelopper l'ensemble purge + restauration des deux fonctions dans `withTransaction`, et ajouter un critère d'acceptation : une restauration qui échoue à la ligne N laisse le profil intact.
> *Ref : CWE-460*

> **🔴 SECURITE** — `import_config_templates` n'apparaît dans aucune des deux listes de purge (vérifié) : réinsérer des modèles restaurés dans un profil qui en possède déjà échoue sur `UNIQUE constraint failed: import_config_templates.name`. Ce n'est pas un cas théorique — restaurer dans un profil existant est le chemin normal. L'ordre de restauration n'est pas non plus spécifié alors que `template_id` porte désormais une clé étrangère vers cette table.
> **Resolution :** Spécifier l'ordre (modèles avant sources) et la stratégie d'identifiants : soit purger aussi `import_config_templates`, soit faire un upsert par nom et remapper `import_sources.template_id` vers les identifiants résolus.

### Issue 10 — Documentation [type:feature]
Dependances : Issue 9
- [ ] `docs/architecture.md` : migration v17, quatre colonnes, couche de détection lexicale, nouvelle étape du wizard
- [ ] ADR 0019 — le format d'import est une donnée persistée intégralement, jamais re-devinée

> **🟢 ARCHITECTURE** — `.gitignore:71-72` ignore `spec-decisions-*.md` et `spec-plan-*.md` : l'ADR 0016 a livré une ligne `Spec:` morte pour cette raison exacte, corrigée par un force-add (PR #295).
> **Resolution :** Ajouter `git add -f spec-decisions-import-csv-format.md spec-plan-import-csv-format.md` à la checklist, avant d'écrire la ligne `Spec:`.
- [ ] `docs/guide-utilisateur.md` + clés `docs.*` FR et EN
- [ ] `CHANGELOG.md` et `CHANGELOG.fr.md` sous `[Unreleased]`

### Ordre d'execution

```
4 → 1 → 2 → 3 → 5 → 6 → 7 → 8 → 9 → 10
```

Pile strictement linéaire. Chaque maillon touche `useImportWizard.ts`, `csvAutoDetect.ts` ou les deux ; une exécution en vagues parallèles produirait des conflits sur ces deux fichiers à chaque étage. Le CHANGELOG est centralisé dans l'issue 10 pour la même raison.

> **🔴 TECHNIQUE + ARCHITECTURE** — L'ordre place le corpus de contrat (issue 4) après le changement de parser qu'il doit servir de référence (issue 3). Ordre corrigé :
> ```
> 4 → 1 → 2 → 3 → 5 → 6 → 7 → 8 → 9 → 10
> ```
> **Resolution :** L'issue 4 ne dépend de rien et passe en tête ; la chaîne reste linéaire. Les issues Forgejo #323-#332 doivent être renumérotées en conséquence dans leurs lignes `Depends on`.

## Fichiers concernes

| Fichier | Action | Raison |
|---------|--------|--------|
| `src-tauri/src/lib.rs` | Modifier | Migration v17 + constante `V17_SQL` + tests |
| `src-tauri/src/database/consolidated_schema.sql` | Modifier | Quatre colonnes sur `import_sources` pour les nouveaux profils |
| `src/shared/types/index.ts` | Modifier | Type `ImportFormat`, composition dans `ImportSource` et `ImportConfigTemplate` |
| `src/services/importSourceService.ts` | Modifier | Persistance des quatre colonnes en création, mise à jour, lecture |
| `src/services/importConfigTemplateService.ts` | Modifier | Alignement sur `ImportFormat` |
| `src/hooks/useImportWizard.ts` | Modifier | Restauration réelle, règle débit/crédit, point d'écriture, score, étape aperçu |
| `src/utils/csvAutoDetect.ts` | Modifier | Couche lexicale, helpers partagés, score, signatures de banques |
| `src/utils/amountParser.ts` | Modifier | Parenthèses comptables, signe suffixe, arbitrage par colonne |
| `src/hooks/useSnapshotEditor.ts` | Modifier | **Manquant** — 3 appels à `parseFrenchAmount` (`:191-202`), import CSV de titres (#245) |
| `src/utils/amountParser.test.ts` | Créer | **Manquant** — n'existe pas aujourd'hui, requis par l'issue de parsing |
| `src/components/import/ColumnMappingEditor.tsx` | Modifier | Le mode nettoie le mapping opposé |
| `src/components/import/SourceConfigPanel.tsx` | Modifier | Bandeau de détection, convention masquée en débit/crédit |
| `src/components/import/FilePreviewTable.tsx` | Modifier | Récapitulatif signé et bascule de convention |
| `src/components/import/ImportConfirmation.tsx` | Modifier | Mode, convention et mapping au récapitulatif |
| `src/components/import/FormatDriftPanel.tsx` | Créer | Panneau d'écart au ré-import |
| `src/pages/ImportPage.tsx` | Modifier | Étape `file-preview` rendue, panneau de dérive |
| `src/services/dataExportService.ts` | Modifier | Sources et modèles sérialisés et restaurés |
| `src/utils/bankSignatures.ts` | Créer | Signatures déclaratives par banque |
| `src/utils/csvAutoDetect.test.ts` | Modifier | Tests du flux transactions, aujourd'hui absents |
| `src/__fixtures__/csv/` | Créer | Corpus synthétique |

> **🟡 TECHNIQUE + ARCHITECTURE** — Le chemin `src/test/fixtures/csv/` inventait une troisième convention : `src/test/` n'existe pas (vérifié). Les tests sont colocalisés (`src/utils/*.test.ts`, `src/services/*.test.ts`), les fixtures vivent dans `src/__fixtures__/` et les tests d'intégration dans `src/__integration__/`. Corrigé dans le tableau ci-dessus.
> **Resolution :** Corpus dans `src/__fixtures__/csv/`, tests de contrat dans `src/utils/csvAutoDetect.test.ts` et le nouveau `src/utils/amountParser.test.ts`.
| `src/i18n/locales/{fr,en}.json` | Modifier | Clés de détection, aperçu, dérive, aide |
| `docs/architecture.md` | Modifier | v17, détection lexicale, étape du wizard |
| `docs/adr/0019-format-import-persiste.md` | Créer | Décision structurante |
| `docs/guide-utilisateur.md` | Modifier | Nouveau parcours d'import |
| `CHANGELOG.md`, `CHANGELOG.fr.md` | Modifier | Entrées sous `[Unreleased]` |

## Plan de tests

### Tests unitaires
`parseFrenchAmount` sur chaque forme de montant, dont parenthèses et signe suffixe. `parseDate` inchangé, couvert en régression. La couche lexicale : chaque libellé du dictionnaire, l'ordre débit/crédit inversé, les libellés muets qui déclenchent le repli. `autoDetectConfig` sur chaque fixture, contrat complet — délimiteur, en-tête, lignes ignorées, format de date, mode, convention, mapping, score. Les signatures de banques, une par banque plus un fichier inconnu qui doit retomber sur le générique.

### Tests d'integration
Le cycle de format configurer → sauvegarder → recharger, qui est le test qui aurait attrapé le bug racine. Le cycle export → import de données préservant sources et modèles, plus l'import d'une sauvegarde antérieure sans ces tableaux. L'application de la migration v17 sur une base v16 peuplée, avec vérification du backfill. Le parsing de bout en bout sur chaque fixture, du fichier brut aux montants signés.

### Tests de regression
Les tests existants restent verts — 871 vitest et 106 Rust au dernier relevé. Le corpus de fixtures de l'issue 4 fige le comportement de `autoDetectConfig` avant sa refonte, de sorte que les issues 5 et 6 se mesurent à un contrat établi plutôt qu'à une intention. Les migrations v1 à v16 conservent leurs checksums.

## Criteres d'acceptation

- [ ] Une source réglée en montants positifs conserve sa convention au deuxième import, et à tous les suivants
- [ ] Un fichier `Date;Description;Crédit;Débit` est mappé dans le bon ordre sans intervention
- [ ] Un fichier dont la colonne inutilisée porte `0,00` produit les bons montants, aucun n'est nul
- [ ] Un mapping incomplet produit une erreur de ligne explicite, jamais une lecture de la colonne 0
- [ ] Basculer de mode puis recharger la source restitue le mode choisi
- [ ] Une source jamais configurée déclenche la détection sans action de l'utilisateur
- [ ] L'aperçu est traversé à chaque import et affiche sorties, entrées et totaux
- [ ] L'écran de confirmation affiche mode, convention et mapping
- [ ] Un import abandonné ne laisse aucune configuration en base
- [ ] Un fichier Desjardins est reconnu par signature et annoncé comme tel
- [ ] Un en-tête modifié depuis le dernier import déclenche le panneau d'écart
- [ ] Exporter puis réimporter ses données préserve toutes les configurations de sources
- [ ] Une sauvegarde produite avant ce chantier s'importe sans erreur
- [ ] Les migrations v1 à v16 sont inchangées ; la v17 s'applique sur une base v16 peuplée

## Edge cases et risques

| Cas | Mitigation |
|-----|------------|
| Le backfill v17 ne peut pas deviner la convention passée d'une source existante | Aucune source ne change de comportement — le défaut restitue exactement ce que le code appliquait déjà. Une source qui souffrait du bug continue de souffrir jusqu'au prochain import, où le score de confiance et l'aperçu obligatoire exposent l'écart. C'est une amélioration franche sans mutation silencieuse, cohérente avec la décision d'exclure toute correction rétroactive. |
| Un ré-import corrigé double-compte les lignes déjà importées de travers | `findDuplicates` (`transactionService.ts:164`) apparie sur `date AND description AND amount`. Les issues 2 et 3 changent les montants produits par une source — signe inversé, débits qui ne valent plus 0 : le ré-import que la ligne précédente invite à faire **ne reconnaîtra pas** les lignes fautives et les ajoutera en double, une inversion de signe produisant en prime des paires miroir qui se compensent à ~0 au lieu de sauter aux yeux. Le seul chemin de réparation sûr est `deleteImportWithTransactions` sur l'import fautif **avant** de le rejouer — à énoncer dans l'interface de dérive et d'aperçu, pas seulement ici. |
| Un fichier sans ligne d'en-tête ne bénéficie d'aucun signal lexical | Repli explicite sur l'heuristique de forme actuelle, qui reste testée par le corpus. Le score de confiance sera mécaniquement plus bas, ce qui est l'information juste. |
| Un fichier sans en-tête n'a pas de signature à mémoriser | `header_signature` reste nulle et la détection de dérive est inopérante sur ces sources. Documenté plutôt que contourné : inventer une signature sur les données produirait de faux positifs à chaque changement de contenu. |
| `json_extract` indisponible dans le SQLite embarqué | Backfill écrit en `LIKE '%debitAmount%'`, sans dépendance à l'extension JSON1. |
| L'aperçu obligatoire ajoute un clic à chaque import mensuel | Coût accepté par décision explicite, contre la recommandation d'un aperçu conditionné à la confiance. |
| Les signatures de banques sont écrites sans relevés réels | Elles reposent sur les formats documentés et sur `preprocessQuotedCSV`, qui prouve déjà le cas Desjardins. Un fichier non reconnu retombe sur le dictionnaire générique — l'échec d'une signature dégrade, il ne casse pas. |
| Modifier `csvAutoDetect.ts` risque de régresser le flux holdings (#245) | Les helpers sont remontés sans changer leur comportement ; les tests holdings existants font foi et doivent rester verts à chaque maillon. |
| Une pile de dix issues sur deux fichiers centraux dérive au rebase | Ordre strictement linéaire, CHANGELOG centralisé dans le dernier maillon, chaque PR basée sur la précédente. |

## Revision — Synthese

> Date: 2026-08-12 | Experts: Securite, Architecture, Technique

### Verdict

🟡 **CRITIQUES ADRESSEES — plan corrige le 2026-08-13** — A la revue, deux fondations du plan etaient fausses telles qu'ecrites (le type `ImportFormat` compose, l'ordre des issues) et trois defauts du code se sont reveles plus graves que ce que le plan enoncait. Les 7 critiques et les 9 ameliorations sont desormais integrees au corps du document ; les annotations restent en trace de revue. Decisions tranchees : 4 (voir ci-dessous).

### Resume

| Expert | 🔴 | 🟡 | 🟢 | Points cles |
|--------|-----|-----|-----|-------------|
| Securite | 3 | 4 | 1 | Restauration SREF hors transaction ; `parseFloat` accepte les suffixes et rend une magnitude ×100 ; modeles absents de la liste de purge |
| Architecture | 3 | 3 | 2 | `ImportFormat` compose est structurellement impossible ; le score dupliquerait la regle de parsing ; `template_id` recree une seconde source de verite |
| Technique | 4 | 3 | 1 | Le corpus de contrat arrive apres le changement qu'il fige ; 11 sites d'appel non listes ; l'etape apercu est un changement de machine a etats |

Trois constats ont ete verifies a la main avant annotation, et un constat d'agent a ete durci plutot que repris : `parseFrenchAmount("50,00-")` rend **5000**, pas 50 — la revue d'implementation initiale sous-estimait ce defaut.

### Actions requises

1. 🔴 **`ImportFormat` compose** — remplacer par `ImportFormatRow` + `ImportFormat` relies par un codec unique ; les quatre porteurs ont des casses et des types incompatibles.
2. 🔴 **Ordre des issues** — le corpus de contrat passe en tete : `4 → 1 → 2 → 3 → 5 → …`.
3. 🔴 **`parseFrenchAmount`** — validation ancree rendant `NaN` sur tout residu ; les suffixes produisent aujourd'hui une erreur de facteur 100 qui passe `isNaN`.
4. 🔴 **Sites d'appel du parser** — ajouter `csvAutoDetect.ts` et `useSnapshotEditor.ts` au perimetre ; 11 appels non listes, dont le flux holdings.
5. 🔴 **Score de confiance** — extraire `mapRow(raw, format)` pur en amont, sinon le score reimplemente la regle de parsing.
6. 🔴 **Restauration SREF** — envelopper purge et restauration dans `withTransaction` ; aujourd'hui aucune.
7. 🔴 **Modeles a la restauration** — ordre et strategie d'identifiants a specifier ; `import_config_templates` n'est dans aucune liste de purge.
8. 🟡 Whitelist des valeurs `amount_mode` / `sign_convention` a la frontiere SREF.
9. 🟡 `template_id` — le retirer ou le declarer etiquette de provenance.
10. 🟡 Bouton d'inversion inerte en mode debit/credit.
11. 🟡 Le refus du troisieme mode de montant est promis sans livrable.
12. 🟡 Un re-import corrige double-compte les lignes fautives (`findDuplicates` apparie sur le montant).
13. 🟡 Perimetre de l'etape apercu — machine a etats, pas rendu.
14. 🟡 Le remontage des helpers est un no-op ; le vrai couplage est le dictionnaire partage.
15. 🟡 « Checksums intacts » n'est pas testable ; chemin des fixtures corrige en `src/__fixtures__/csv/`.


### Decisions tranchees (2026-08-13)

| Decision | Retenu | Rationale |
|----------|--------|-----------|
| Contrainte `CHECK` sur `amount_mode` | `CHECK` elargi a `absolute_indicator` | Revise la decision de cadrage « pas de CHECK ». L'objectif d'origine — ajouter le 3e mode sans migration — est atteint par la valeur future deja admise dans la contrainte, sans renoncer a la garantie en base. `sign_convention` recoit le meme traitement. |
| Role de `template_id` | Etiquette de provenance | La colonne enregistre d'ou vient la configuration et n'est jamais relue comme format ; les huit colonnes de la source font foi. Un critere d'acceptation verifie qu'editer un modele ne modifie aucune source liee. |
| Troisieme format de montant | Detecte et refuse explicitement | Le document de decisions promettait un refus explicite sans qu'aucune issue ne le livre. Sans la regle, un tel fichier importe chaque debit comme un revenu — l'echec silencieux que le chantier combat. |
| Portee du durcissement de `parseFrenchAmount` | Global, tous les sites d'appel | Un `"100,00 CAD"` qui rend 10000 est un bug partout, y compris a l'import de titres. Une seule fonction a raisonner, au prix d'une passe de regression sur les tests holdings existants. |

### Corrections integrees sans arbitrage

`ImportFormatRow` + `ImportFormat` relies par un codec unique (la composition d'un type unique etait structurellement impossible) ; ordre des issues `4 → 1 → 2 → 3 → 5 → …` ; extraction de `mapRow` avant le calcul de score ; `withTransaction` sur la purge et la restauration SREF ; ordre et strategie d'identifiants a la restauration des modeles ; perimetre de l'etape apercu etendu au reducer, a `ImportPage` et au modal ; dictionnaire lexical dans son propre module ; `useSnapshotEditor.ts` et `src/utils/amountParser.test.ts` ajoutes au tableau des fichiers ; chemin des fixtures corrige en `src/__fixtures__/csv/` ; assertion « checksums » remplacee par ce que le harnais verifie reellement ; chemin de reparation sur re-import enonce dans l'interface.


### Decisions de planification (2026-08-13, seance /plan-run)

| Question | Decision | Portee |
|----------|----------|--------|
| Les specs sont gitignorees, les workers ne les verraient pas | Force-add et commit des deux fichiers (precedent PR #295) **et** bodies d'issues auto-suffisants | Toutes les issues ; debloque la ligne `Spec:` de l'ADR 0019 |
| Seuil de confiance non chiffre | **90 %** de lignes lues. En dessous : bandeau d'avertissement + score detaille (« 132/150 lignes »). Au-dessus : bandeau neutre. L'apercu obligatoire reste le filet reel quel que soit le score | Issues 6 et 7 (#328, #329) |
| Contenu de `header_signature` | Tableau JSON des libelles normalises par `normalizeHeaderCell`, ex. `["date","description","montant","solde"]`. Un hash rendrait impossible l'ecart colonne par colonne promis par le panneau de derive | Issue 8 (#330) |
| Compatibilite du format SREF | Champ de version explicite dans le fichier exporte. A l'import, son absence signifie « format anterieur » et les tableaux manquants sont traites comme vides | Issue 9 (#331) |
| Emplacement du codec et de `mapRow` | `src/utils/importFormat.ts` — meme dossier que `amountParser`, `dateParser` et `csvAutoDetect`, qui portent deja la logique pure du domaine. Les types restent dans `src/shared/types/` | Issues 2 et 3 (#324, #325) |
