Simpl-Resultat/spec-plan-import-csv-format.md
le king fu b30c9fa5c1 docs(spec): add import CSV format spec (decisions + reviewed plan)
The import wizard forgets its format between runs: import_sources carries
neither amount_mode nor sign_convention, so a source configured for positive
expenses silently flips every amount on its second import.

Force-added despite .gitignore so /autopilot workers can read them from a
worktree — same precedent as PR #295 (ADR 0016 shipped a dead Spec: line).

Plan reviewed by the 3-expert pass: 7 criticals integrated, 8 decisions
drained. Milestone planned-2026-08-12-import-csv-format (#323-#332).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:17:04 -04:00

43 KiB
Raw Blame History

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

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.

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.

🟡 ARCHITECTUREtemplate_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.tsImportSource.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 debitAmountdebit_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

🔴 TECHNIQUEparseFrenchAmount 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

🔴 SECURITEimport_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)