feat(import): recognise known bank layouts and report format drift #340

Closed
maximus wants to merge 1 commit from issue-330-bank-signatures-drift into issue-329-mandatory-preview
Owner

Link 8 of 10 of the import-format chantier (spec-plan-import-csv-format.md).
Stacked on issue-329-mandatory-preview — review only the top commit.

What it does

Two failure modes, both anchored on the header row.

A known bank is read by name

src/utils/bankSignatures.ts declares the documented export layout of Desjardins, RBC, Banque Nationale and Tangerine as a set of normalized header labels plus a delimiter and a preamble quirk. The table is evaluated before the generic dictionary; an unknown file falls straight through to it, unchanged. A recognised file is announced — « Format Desjardins reconnu — 6 des 6 lignes lues ».

The signatures are not decorative. headerDictionary.ts matches keywords as substrings, one role at a time, and reads two of these four layouts wrong — both frozen as counterfactual test pairs (same rows, header renamed):

Layout Generic dictionary With the signature
Tangerine Date,Transaction,Name,Memo,Amount description = Transaction — every row labelled DEBIT / CREDIT description = Name
RBC …,Cheque Number,Description 1,CAD$,USD$ CAD$ matches no amount keyword; it is sparse-complementary with the near-empty Cheque Number, so the pair is read as debit/credit and the cheque row imports as -247.95 instead of -6.95 single signed amount on CAD$

A signature stays a set of preferences: they are written from documented layouts, without real statements, so every hint is dropped the moment the data contradicts it. The single exception is the amount mode, which outranks the sparse-complementary scan — nothing inside an RBC file distinguishes that pair from a genuine one — and even that is refused unless the declared columns are candidates the shape scan itself proposed. Failing degrades to the generic path; it never breaks. Three preconditions guard every match (delimiter, preamble length, whole-line quoting), each tested.

Format drift is reported instead of imported

Every successful import records the normalized labels of its header row in import_sources.header_signature — a JSON array, not a hash: the panel has to be able to name the columns that moved. On the next import, a header that normalizes differently opens a FormatDriftPanel above the preview, column by column (Montant : 3 → 4), with the two outcomes that exist: adopt the re-detected format, or keep the stored one. A cosmetic rename (MontantMONTANT ($)) normalizes identically and says nothing.

A source whose file has no header row keeps header_signature NULL and drift detection is inoperative on it — documented, not worked around: a signature invented from the data would fire on every import.

The repair path is in the interface

RepairPathNotice, rendered in the drift panel and beside the preview's sign-flip button. findDuplicates (transactionService.ts:164) matches on date AND description AND amount, so re-importing a file "now that it reads right" does not correct the rows already written — it doubles them, and a flipped sign produces mirror pairs that net to ~0 in every report. The only safe path is deleting the faulty import from the history (deleteImportWithTransactions) before replaying it.

Points a reviewer should check

  • The await detectFormatForFile( guard moves 2 → 3, deliberately. The drift re-detection goes through the single detection path rather than around it; runAutoDetect( stays at 1. The test comment says why.
  • That re-detection then clears the score and the bank badge. It measures the format the panel offers, not the one in use — leaving it would show « Format reconnu — 6 des 6 lignes lues » beside the stored mapping as soon as the user steps back. Same rule as the sign flip.
  • MIN_SIGNATURE_LABELS = 4. Date;Description;Montant is the shape of half the synthetic corpus, so a 3-label variant would put a bank's name over files nobody can attribute to it. Consequence, accepted: the existing 3-column desjardins-quoted fixture stays unrecognised and reads through the generic path exactly as before.
  • header_signature stays out of the format codec (link 3's separation), and is written only at executeImport — the single config write point of link 5. A test asserts importFormat.ts never names it.
  • No CHANGELOG, no docs/: the spec plan centralises both in link 10 ("Le CHANGELOG est centralisé dans l'issue 10"), and no link of the stack has touched either file.

Verification

  • npm test — 59 files, 1140 passed (1088 after link 7; +52 in bankSignatures.test.ts).
  • npm run build — tsc + vite clean.
  • cargo check — clean (no Rust work).
  • Every counterfactual was measured before being asserted, never assumed.

Resolves #330


Generated autonomously by /autopilot run of 2026-08-13

Link **8 of 10** of the import-format chantier (`spec-plan-import-csv-format.md`). Stacked on `issue-329-mandatory-preview` — review only the top commit. ## What it does Two failure modes, both anchored on the header row. ### A known bank is read by name `src/utils/bankSignatures.ts` declares the documented export layout of **Desjardins, RBC, Banque Nationale and Tangerine** as a set of normalized header labels plus a delimiter and a preamble quirk. The table is evaluated **before** the generic dictionary; an unknown file falls straight through to it, unchanged. A recognised file is announced — *« Format Desjardins reconnu — 6 des 6 lignes lues »*. The signatures are not decorative. `headerDictionary.ts` matches keywords as substrings, one role at a time, and reads **two of these four layouts wrong** — both frozen as counterfactual test pairs (same rows, header renamed): | Layout | Generic dictionary | With the signature | |---|---|---| | Tangerine `Date,Transaction,Name,Memo,Amount` | description = `Transaction` — every row labelled `DEBIT` / `CREDIT` | description = `Name` | | RBC `…,Cheque Number,Description 1,CAD$,USD$` | `CAD$` matches no amount keyword; it is sparse-complementary with the near-empty `Cheque Number`, so the pair is read as debit/credit and the cheque row imports as **-247.95** instead of -6.95 | single signed amount on `CAD$` | A signature stays a set of **preferences**: they are written from documented layouts, without real statements, so every hint is dropped the moment the data contradicts it. The single exception is the amount mode, which outranks the sparse-complementary scan — nothing inside an RBC file distinguishes that pair from a genuine one — and even that is refused unless the declared columns are candidates the shape scan itself proposed. **Failing degrades to the generic path; it never breaks.** Three preconditions guard every match (delimiter, preamble length, whole-line quoting), each tested. ### Format drift is reported instead of imported Every successful import records the normalized labels of its header row in `import_sources.header_signature` — a **JSON array, not a hash**: the panel has to be able to name the columns that moved. On the next import, a header that normalizes differently opens a `FormatDriftPanel` above the preview, column by column (`Montant : 3 → 4`), with the two outcomes that exist: **adopt** the re-detected format, or **keep** the stored one. A cosmetic rename (`Montant` → `MONTANT ($)`) normalizes identically and says nothing. A source whose file has **no header row keeps `header_signature` NULL** and drift detection is inoperative on it — documented, not worked around: a signature invented from the data would fire on every import. ### The repair path is in the interface `RepairPathNotice`, rendered in the drift panel **and** beside the preview's sign-flip button. `findDuplicates` (`transactionService.ts:164`) matches on `date AND description AND amount`, so re-importing a file "now that it reads right" does not correct the rows already written — it doubles them, and a flipped sign produces mirror pairs that net to ~0 in every report. The only safe path is deleting the faulty import from the history (`deleteImportWithTransactions`) before replaying it. ## Points a reviewer should check - **The `await detectFormatForFile(` guard moves 2 → 3, deliberately.** The drift re-detection goes through the single detection path rather than around it; `runAutoDetect(` stays at 1. The test comment says why. - **That re-detection then clears the score and the bank badge.** It measures the format the panel *offers*, not the one in use — leaving it would show « Format reconnu — 6 des 6 lignes lues » beside the stored mapping as soon as the user steps back. Same rule as the sign flip. - **`MIN_SIGNATURE_LABELS = 4`.** `Date;Description;Montant` is the shape of half the synthetic corpus, so a 3-label variant would put a bank's name over files nobody can attribute to it. Consequence, accepted: the existing 3-column `desjardins-quoted` fixture stays unrecognised and reads through the generic path exactly as before. - **`header_signature` stays out of the format codec** (link 3's separation), and is written only at `executeImport` — the single config write point of link 5. A test asserts `importFormat.ts` never names it. - **No CHANGELOG, no `docs/`**: the spec plan centralises both in link 10 ("Le CHANGELOG est centralisé dans l'issue 10"), and no link of the stack has touched either file. ## Verification - `npm test` — 59 files, **1140 passed** (1088 after link 7; +52 in `bankSignatures.test.ts`). - `npm run build` — tsc + vite clean. - `cargo check` — clean (no Rust work). - Every counterfactual was measured before being asserted, never assumed. Resolves #330 --- Generated autonomously by /autopilot run of 2026-08-13
maximus added 1 commit 2026-08-13 18:55:24 +00:00
feat(import): recognise known bank layouts and report format drift
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m41s
c9872fc36b
Two failure modes, both anchored on the header row.

A KNOWN BANK IS NOW READ BY NAME. `bankSignatures.ts` declares the
documented export layout of Desjardins, RBC, Banque Nationale and
Tangerine as a set of normalized header labels plus a delimiter and a
preamble quirk. The table is evaluated BEFORE the generic dictionary and
an unknown file falls straight through to it, unchanged.

The signatures are not decorative. The generic dictionary matches
keywords as substrings, one role at a time, which reads two of these
four layouts wrong — both frozen as counterfactual test pairs, same
rows, header renamed:

  - Tangerine writes `Date,Transaction,Name,Memo,Amount`. `Transaction`
    is a description keyword, so every row of the file was labelled with
    its direction word instead of the merchant.
  - RBC writes its amount column `CAD$`, which no amount keyword
    matches, next to a nearly empty `Cheque Number`. Those two are
    sparse-complementary, so the shape scan paired them as debit/credit
    and the one row carrying a cheque number imported as -247.95 instead
    of -6.95.

A signature stays a set of PREFERENCES all the same: they are written
from documented layouts, without real statements, so every hint is
dropped the moment the data contradicts it. The single exception is the
amount mode, which outranks the sparse-complementary scan — nothing
inside an RBC file can tell that pair from a genuine one — and even that
is refused unless the declared columns are candidates the shape scan
proposed. Failing degrades to the generic path; it never breaks.

FORMAT DRIFT IS NOW REPORTED INSTEAD OF IMPORTED. Every successful
import records the normalized labels of its header row in
`import_sources.header_signature`, as a JSON array and not a hash: the
panel has to be able to name the columns that moved. On the next import,
a header that normalizes differently opens a `FormatDriftPanel` above
the preview — column by column, `Montant : 3 -> 4` — with the two
outcomes that exist: adopt the re-detected format, or keep the stored
one. A cosmetic rename (`Montant` -> `MONTANT ($)`) normalizes
identically and says nothing.

A source whose file has no header row keeps `header_signature` NULL and
drift detection is inoperative on it. Documented, not worked around:
a signature invented from the data would fire on every import.

THE REPAIR PATH IS NOW IN THE INTERFACE, in the drift panel and beside
the preview's sign flip. `findDuplicates` matches on date AND
description AND amount, so re-importing a file "now that it reads right"
does not correct the rows already written — it doubles them, and a
flipped sign produces mirror pairs that net to zero in every report. The
only safe path is deleting the faulty import from the history first.

The drift re-detection reuses `detectFormatForFile`, so there is still
exactly one detector; the static guard on its caller count moves from
two to three deliberately. Its score and bank badge are dropped straight
after: they measure the format the panel offers, not the one in use.

Resolves #330
maximus added the
autopilot:pending-human
label 2026-08-13 18:55:29 +00:00
Author
Owner

/pr-review #340REQUEST_CHANGES

Résumé

Revue adversariale, faits mesurés en rejouant la détection base-vs-branche hors du worktree (esbuild + node sur origin/issue-329-mandatory-preview et origin/issue-330-bank-signatures-drift), pas en lisant le diff.

Ce qui tient, vérifié :

  • Zéro régression sur le corpus. Les 15 fixtures passées dans les deux versions de detectImportFormat : 13 configurations identiques au caractère près, 2 différences — bank-rbc et bank-tangerine, exactement les deux que les signatures visent. bank-desjardins et bank-bnc sortent la même config qu'avant : la signature n'y ajoute que le badge.
  • La mesure RBC est réelle. Sous un en-tête anonymisé, la paire Cheque Number (col 3) / CAD$ (col 6) est bien lue debit_credit{date:2, description:4, debitAmount:3, creditAmount:6} — et la ligne au chèque sort fausse. La justification porteuse de l'override tient.
  • npm test : 1140 passés, 59 fichiers, chiffre du corps confirmé. Aucun skip/only.
  • Codec : importFormat.ts ne nomme ni header_signature ni headerSignature (0 occurrence). La séparation du lien 3 tient.
  • Guards mis à jour délibérément, comptés : runAutoDetect( = 1, await detectFormatForFile( = 3 (3 sites réels : selectSource, parseAndPreview, autoDetectConfig), CSV_FIXTURE_NAMES = 15. Aucun affaibli.
  • Dérive stable : casse, accents, espaces et cellule vide de fin (délimiteur traînant) ne déclenchent rien ; déplacement, ajout, suppression et permutation d'étiquettes sont tous rapportés. Aucune dérive visible dans l'en-tête ne m'a échappé.
  • Adopter/conserver : executeImport lit state.sourceConfig, donc le format adopté est bien persisté et le format conservé reste intact ; le score et le badge sont mis à null avant le panneau, rien ne cautionne un format non mesuré.
  • i18n : parité fr.json/en.json parfaite (0 clé orpheline dans les deux sens), 10 clés drift de chaque côté, nom de banque interpolé. Aucune migration ni fichier src-tauri touché. SQL paramétré.

Ce qui bloque tient à une seule racine : l'empreinte Desjardins est faite de 4 mots génériques, et une signature qui matche obtient aussi le droit de passer devant le scan sparse-complementary.


Blocages

1. src/utils/bankSignatures.ts:107 — l'empreinte Desjardins réclame n'importe quel Date;Description;Montant;Solde, et MIN_SIGNATURE_LABELS = 4 ne l'en empêche pas.

Mesuré. Ce fichier, qui n'a rien de Desjardins, sort bank = "desjardins" et affiche « Format Desjardins reconnu — 4 des 4 lignes lues » :

Date;Description;Montant;Solde
05/01/2025;EPICERIE;-84,32;915,68
15/01/2025;PAIE;1250,00;2165,68
18/01/2025;HYDRO;-142,18;2023,50
22/01/2025;RESTO;-56,75;1966,75

Idem en virgules (delimiters: [";", ","]), idem sur la variante anglaise Date;Description;Amount;Balance, idem sur un fichier où Montant est en texte (la config retombe correctement sur le générique — mais le badge, lui, reste).

C'est exactement ce que le commentaire de MIN_SIGNATURE_LABELS (bankSignatures.ts:86) déclare inacceptable : « poser "Format Desjardins reconnu" sur des fichiers que personne ne peut attribuer à Desjardins. Annoncer la mauvaise banque est pire que n'en annoncer aucune. » Le seuil de 4 ne protège pas, parce que la variante Desjardins est composée des 4 étiquettes les plus banales d'un relevé québécois — signed-amount.csv du corpus en est à une colonne près. Les trois autres banques ne posent pas le problème : 6, 6 et 5 étiquettes, avec chequenumber, categorie, memo pour les rendre distinctives. Desjardins est la seule entrée sans un seul mot discriminant, et c'est la première de la table donc la première essayée.

Un utilisateur de la Banque Nationale, de Tangerine FR ou d'un export fait main se fera annoncer une banque qui n'est pas la sienne, dès le premier import.

2. src/utils/csvAutoDetect.ts:790-802 — combiné au point 1, l'override du mode montant peut rendre la lecture PIRE que le chemin générique.

Le corps affirme « Failing degrades to the generic path; it never breaks. » Contre-exemple mesuré :

Date;Description;Débit;Crédit;Montant;Solde
05/01/2025;EPICERIE;84,32;;84,32;915,68
15/01/2025;PAIE;;1250,00;1250,00;2165,68
18/01/2025;HYDRO;142,18;;142,18;2023,50
22/01/2025;RESTO;56,75;;56,75;1966,75
27/01/2025;VIREMENT;;300,00;300,00;2266,75
  • avant #330 : debit_credit sur {debitAmount:2, creditAmount:3} → lecture correcte.
  • avec #330 : faux positif Desjardins → l'arm signature de detectAmountMode prend la main sur le scan sparse-complementary et impose amount = Montant (col 4, magnitudes non signées) → amountMode: "single", signConvention: "positive_expense". Toute la paie et tout virement reçu entrent en dépense.

Le garde-fou amountCandidates.includes(amount) ne mord pas ici : Montant est un candidat parfaitement valide, il est juste le mauvais. L'étape 7b ne rattrape pas non plus — les voisins de la colonne montant sont Description et Solde, aucun jeton D/C.

La forme est étroite, je l'assume — mais c'est un contre-exemple à l'invariant central de la PR, dans la classe de panne exacte que le chantier existe pour supprimer (montants lus à l'envers, score à 100 %, aucun signal). L'override a été justifié par un cas précis (CAD$ face à Cheque Number) ; il est en pratique accordé à toute signature, y compris à celle dont l'empreinte ne prouve rien.

Piste de correction pour les deux — deux gestes indépendants, chacun tue le cas de données à lui seul :

  • (a) Scoper l'override : ne laisser l'amount d'une signature déplacer une paire sparse-complementary que si cette paire contient la colonne montant déclarée. Le gain RBC est préservé (la paire est (chequenumber, cad), elle contient cad) ; le cas ci-dessus retombe sur le générique (la paire (debit, credit) ne contient pas montant).
  • (b) Rendre l'empreinte Desjardins distinctive, ou retirer la variante à 4 étiquettes génériques en assumant que Desjardins n'est pas reconnu — c'est déjà la conséquence acceptée pour desjardins-quoted, et elle est cohérente : mieux vaut aucun nom qu'un nom faux.

Suggestions (non bloquantes)

  1. Le chiffre -247.95 est faux (corps de PR, message de commit, et commentaire bankSignatures.test.ts du cas RBC). mapRow calcule |crédit| − |débit| (importFormat.ts:307-309), donc la ligne au chèque 241 sort à -234.05, pas -247.95. Le phénomène est confirmé, seul le montant cité est à corriger — dommage sur une PR dont l'argument est « chaque contrefactuel a été mesuré avant d'être affirmé ».
  2. Colonne ajoutée en fin = dérive rapportée (mesuré : ["date","description","montant"]+ "devise" en position 3 ouvre le panneau). Rien n'a bougé, la table du dessous est parfaitement lisible, et l'intro dit pourtant « Les colonnes mémorisées ne pointent peut-être plus sur les bonnes données ». Un ajout en queue qui ne déplace aucune colonne existante pourrait être signalé plus doucement, ou pas du tout.
  3. Indices 0-based affichés à l'utilisateur : columnMoved interpole entry.previousIndex/currentIndex bruts, donc la 1re colonne s'affiche « colonne 0 ». Le corps de PR écrit « Montant : 3 → 4 », ce que le code ne produit pas. +1 à l'affichage.
  4. RepairPathNotice est inconditionnel dans l'aperçu (FilePreviewTable.tsx:131) : il s'affiche au tout premier import d'une source neuve, où il n'y a rien à réparer, et même quand onFlipSigns est absent. Sa propre docstring dit « à côté du bouton d'inversion » — un {onFlipSigns && …} respecterait l'intention et éviterait un bloc de bruit sur le chemin le plus fréquent.
  5. Multi-fichiers : parseFilesInternal écrase headers à chaque fichier, donc la dérive est mesurée sur l'en-tête du dernier fichier, alors que la re-détection tourne sur state.selectedFiles[0] (useImportWizard.ts:759). Cohérent avec ce que executeImport mémorise, mais les deux moitiés du panneau décrivent alors deux fichiers différents.
  6. La re-détection de dérive peut poser une erreur de page : detectFormatForFile dispatche SET_ERROR quand elle échoue ou refuse, et parseAndPreview continue jusqu'à file-preview. L'utilisateur atterrit sur un aperçu correct surmonté d'un bandeau « détection échouée » — ou pire, du message absoluteIndicatorFormat qui parle d'un format que ce fichier n'utilise pas sous la config en cours. Le panneau dit déjà « La détection n'a pas su lire cette nouvelle forme » ; le bandeau fait doublon et contredit ce qui est à l'écran.

Base issue-329-mandatory-preview (ce19efd), tip c9872fc. Aucun fichier du worktree modifié pendant la revue.

## `/pr-review` #340 — **REQUEST_CHANGES** ### Résumé Revue adversariale, faits mesurés en rejouant la détection base-vs-branche hors du worktree (esbuild + node sur `origin/issue-329-mandatory-preview` et `origin/issue-330-bank-signatures-drift`), pas en lisant le diff. Ce qui tient, vérifié : - **Zéro régression sur le corpus.** Les 15 fixtures passées dans les deux versions de `detectImportFormat` : 13 configurations **identiques au caractère près**, 2 différences — `bank-rbc` et `bank-tangerine`, exactement les deux que les signatures visent. `bank-desjardins` et `bank-bnc` sortent la même config qu'avant : la signature n'y ajoute que le badge. - **La mesure RBC est réelle.** Sous un en-tête anonymisé, la paire `Cheque Number` (col 3) / `CAD$` (col 6) est bien lue `debit_credit` — `{date:2, description:4, debitAmount:3, creditAmount:6}` — et la ligne au chèque sort fausse. La justification porteuse de l'override tient. - `npm test` : **1140 passés, 59 fichiers**, chiffre du corps confirmé. Aucun `skip`/`only`. - **Codec** : `importFormat.ts` ne nomme ni `header_signature` ni `headerSignature` (0 occurrence). La séparation du lien 3 tient. - **Guards mis à jour délibérément, comptés** : `runAutoDetect(` = 1, `await detectFormatForFile(` = 3 (3 sites réels : `selectSource`, `parseAndPreview`, `autoDetectConfig`), `CSV_FIXTURE_NAMES` = 15. Aucun affaibli. - **Dérive stable** : casse, accents, espaces et cellule vide de fin (délimiteur traînant) ne déclenchent rien ; déplacement, ajout, suppression et permutation d'étiquettes sont tous rapportés. Aucune dérive visible dans l'en-tête ne m'a échappé. - **Adopter/conserver** : `executeImport` lit `state.sourceConfig`, donc le format adopté est bien persisté et le format conservé reste intact ; le score et le badge sont mis à `null` avant le panneau, rien ne cautionne un format non mesuré. - i18n : parité `fr.json`/`en.json` parfaite (0 clé orpheline dans les deux sens), 10 clés `drift` de chaque côté, nom de banque interpolé. Aucune migration ni fichier `src-tauri` touché. SQL paramétré. Ce qui bloque tient à **une seule racine** : l'empreinte Desjardins est faite de 4 mots génériques, et une signature qui matche obtient aussi le droit de passer devant le scan sparse-complementary. --- ### Blocages **1. `src/utils/bankSignatures.ts:107` — l'empreinte Desjardins réclame n'importe quel `Date;Description;Montant;Solde`, et `MIN_SIGNATURE_LABELS = 4` ne l'en empêche pas.** Mesuré. Ce fichier, qui n'a rien de Desjardins, sort `bank = "desjardins"` et affiche « Format Desjardins reconnu — 4 des 4 lignes lues » : ``` Date;Description;Montant;Solde 05/01/2025;EPICERIE;-84,32;915,68 15/01/2025;PAIE;1250,00;2165,68 18/01/2025;HYDRO;-142,18;2023,50 22/01/2025;RESTO;-56,75;1966,75 ``` Idem en virgules (`delimiters: [";", ","]`), idem sur la variante anglaise `Date;Description;Amount;Balance`, idem sur un fichier où `Montant` est en texte (la config retombe correctement sur le générique — mais le badge, lui, reste). C'est exactement ce que le commentaire de `MIN_SIGNATURE_LABELS` (`bankSignatures.ts:86`) déclare inacceptable : « poser "Format Desjardins reconnu" sur des fichiers que personne ne peut attribuer à Desjardins. Annoncer la mauvaise banque est pire que n'en annoncer aucune. » Le seuil de 4 ne protège pas, parce que la variante Desjardins **est** composée des 4 étiquettes les plus banales d'un relevé québécois — `signed-amount.csv` du corpus en est à une colonne près. Les trois autres banques ne posent pas le problème : 6, 6 et 5 étiquettes, avec `chequenumber`, `categorie`, `memo` pour les rendre distinctives. Desjardins est la seule entrée sans un seul mot discriminant, et c'est la première de la table donc la première essayée. Un utilisateur de la Banque Nationale, de Tangerine FR ou d'un export fait main se fera annoncer une banque qui n'est pas la sienne, dès le premier import. **2. `src/utils/csvAutoDetect.ts:790-802` — combiné au point 1, l'override du mode montant peut rendre la lecture PIRE que le chemin générique.** Le corps affirme « **Failing degrades to the generic path; it never breaks.** » Contre-exemple mesuré : ``` Date;Description;Débit;Crédit;Montant;Solde 05/01/2025;EPICERIE;84,32;;84,32;915,68 15/01/2025;PAIE;;1250,00;1250,00;2165,68 18/01/2025;HYDRO;142,18;;142,18;2023,50 22/01/2025;RESTO;56,75;;56,75;1966,75 27/01/2025;VIREMENT;;300,00;300,00;2266,75 ``` - **avant #330** : `debit_credit` sur `{debitAmount:2, creditAmount:3}` → lecture correcte. - **avec #330** : faux positif Desjardins → l'arm signature de `detectAmountMode` prend la main sur le scan sparse-complementary et impose `amount = Montant` (col 4, magnitudes non signées) → `amountMode: "single"`, **`signConvention: "positive_expense"`**. Toute la paie et tout virement reçu entrent en **dépense**. Le garde-fou `amountCandidates.includes(amount)` ne mord pas ici : `Montant` est un candidat parfaitement valide, il est juste le mauvais. L'étape 7b ne rattrape pas non plus — les voisins de la colonne montant sont `Description` et `Solde`, aucun jeton D/C. La forme est étroite, je l'assume — mais c'est un contre-exemple à l'invariant central de la PR, dans la classe de panne exacte que le chantier existe pour supprimer (montants lus à l'envers, score à 100 %, aucun signal). L'override a été justifié par un cas précis (`CAD$` face à `Cheque Number`) ; il est en pratique accordé à toute signature, y compris à celle dont l'empreinte ne prouve rien. **Piste de correction pour les deux** — deux gestes indépendants, chacun tue le cas de données à lui seul : - (a) **Scoper l'override** : ne laisser l'`amount` d'une signature déplacer une paire sparse-complementary que si cette paire **contient** la colonne montant déclarée. Le gain RBC est préservé (la paire est `(chequenumber, cad)`, elle contient `cad`) ; le cas ci-dessus retombe sur le générique (la paire `(debit, credit)` ne contient pas `montant`). - (b) **Rendre l'empreinte Desjardins distinctive**, ou retirer la variante à 4 étiquettes génériques en assumant que Desjardins n'est pas reconnu — c'est déjà la conséquence acceptée pour `desjardins-quoted`, et elle est cohérente : mieux vaut aucun nom qu'un nom faux. --- ### Suggestions (non bloquantes) 1. **Le chiffre `-247.95` est faux** (corps de PR, message de commit, et commentaire `bankSignatures.test.ts` du cas RBC). `mapRow` calcule `|crédit| − |débit|` (`importFormat.ts:307-309`), donc la ligne au chèque 241 sort à **`-234.05`**, pas `-247.95`. Le phénomène est confirmé, seul le montant cité est à corriger — dommage sur une PR dont l'argument est « chaque contrefactuel a été mesuré avant d'être affirmé ». 2. **Colonne ajoutée en fin = dérive rapportée** (mesuré : `["date","description","montant"]` → `+ "devise"` en position 3 ouvre le panneau). Rien n'a bougé, la table du dessous est parfaitement lisible, et l'intro dit pourtant « Les colonnes mémorisées ne pointent peut-être plus sur les bonnes données ». Un ajout en queue qui ne déplace aucune colonne existante pourrait être signalé plus doucement, ou pas du tout. 3. **Indices 0-based affichés à l'utilisateur** : `columnMoved` interpole `entry.previousIndex`/`currentIndex` bruts, donc la 1re colonne s'affiche « colonne 0 ». Le corps de PR écrit « Montant : 3 → 4 », ce que le code ne produit pas. `+1` à l'affichage. 4. **`RepairPathNotice` est inconditionnel dans l'aperçu** (`FilePreviewTable.tsx:131`) : il s'affiche au tout premier import d'une source neuve, où il n'y a rien à réparer, et même quand `onFlipSigns` est absent. Sa propre docstring dit « à côté du bouton d'inversion » — un `{onFlipSigns && …}` respecterait l'intention et éviterait un bloc de bruit sur le chemin le plus fréquent. 5. **Multi-fichiers** : `parseFilesInternal` écrase `headers` à chaque fichier, donc la dérive est mesurée sur l'en-tête du **dernier** fichier, alors que la re-détection tourne sur `state.selectedFiles[0]` (`useImportWizard.ts:759`). Cohérent avec ce que `executeImport` mémorise, mais les deux moitiés du panneau décrivent alors deux fichiers différents. 6. **La re-détection de dérive peut poser une erreur de page** : `detectFormatForFile` dispatche `SET_ERROR` quand elle échoue ou refuse, et `parseAndPreview` continue jusqu'à `file-preview`. L'utilisateur atterrit sur un aperçu correct surmonté d'un bandeau « détection échouée » — ou pire, du message `absoluteIndicatorFormat` qui parle d'un format que ce fichier n'utilise pas sous la config en cours. Le panneau dit déjà « La détection n'a pas su lire cette nouvelle forme » ; le bandeau fait doublon et contredit ce qui est à l'écran. --- *Base `issue-329-mandatory-preview` (`ce19efd`), tip `c9872fc`. Aucun fichier du worktree modifié pendant la revue.*
Author
Owner

Blocage levé — corrigé sur la tête de pile

Deux correctifs : une variante faite uniquement de libellés génériques doit décrire l'en-tête exactement (Desjardins ne peut plus revendiquer un fichier plus riche) ; et le scan sparse-complementary est évalué avant la signature, dont la colonne de montant ne gagne que si la paire la contient — ce dont le cas RBC a besoin, et qui passe toujours.

Le correctif est le commit 37b832e sur issue-332-docs-adr-changelog, la tête de la pile, plutôt que sur cette branche : la chaîne se merge en --ff-only d'un seul tenant, et corriger ici aurait imposé un rebase en cascade de toute la descendance. L'attribution reste lisible — le commit porte Refs #330.

Vérifié par mutation : annuler le correctif fait rougir un test dédié. Un garde qui ne peut pas échouer ne garde rien.

Suite complète verte : 1181 vitest, build tsc + vite, cargo check.

## Blocage levé — corrigé sur la tête de pile Deux correctifs : une variante faite uniquement de libellés génériques doit décrire l'en-tête **exactement** (Desjardins ne peut plus revendiquer un fichier plus riche) ; et le scan sparse-complementary est évalué **avant** la signature, dont la colonne de montant ne gagne que si la paire la contient — ce dont le cas RBC a besoin, et qui passe toujours. Le correctif est le commit `37b832e` sur `issue-332-docs-adr-changelog`, la tête de la pile, plutôt que sur cette branche : la chaîne se merge en `--ff-only` d'un seul tenant, et corriger ici aurait imposé un rebase en cascade de toute la descendance. L'attribution reste lisible — le commit porte `Refs #330`. **Vérifié par mutation** : annuler le correctif fait rougir un test dédié. Un garde qui ne peut pas échouer ne garde rien. Suite complète verte : 1181 vitest, build tsc + vite, `cargo check`.
Author
Owner

Mergée dans main en fast-forward avec le reste de la pile (tip 37b832e).

Forgejo ne détecte pas un merge local comme merged — la PR est donc fermée à la main, et l'issue liée s'est fermée automatiquement via son Resolves #N.

Tip cumulé validé avant push : 1181 vitest, 111 tests Rust, build tsc + vite. La CI ne tourne pas sur push main, cette validation locale était donc le seul filet.

Mergée dans `main` en fast-forward avec le reste de la pile (tip `37b832e`). Forgejo ne détecte pas un merge local comme *merged* — la PR est donc fermée à la main, et l'issue liée s'est fermée automatiquement via son `Resolves #N`. Tip cumulé validé avant push : **1181 vitest**, **111 tests Rust**, build tsc + vite. La CI ne tourne pas sur push `main`, cette validation locale était donc le seul filet.
maximus closed this pull request 2026-08-14 16:14:01 +00:00
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m41s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#340
No description provided.