feat(import): make the preview mandatory and show what the amounts mean #339
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#339
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-329-mandatory-preview"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Link 7 of the 10-link import-format stack. Based on
issue-328-confidence-score, not onmain.Link 6 shipped a test proving a 100 % confidence score says nothing about sign: the
all-positivefixture scores 1.0 while every credit imports as an expense. The score measures readability, not meaning. This link builds the control that reads meaning.What changed
The
file-previewstep exists. It had been declared inImportWizardStepsince the beginning with no dispatch ever aiming at it — the optional modal had supplanted it. This is a state-machine change:parseAndPreviewparses and stops at the preview. It replacesparsePreviewandparseAndCheckDuplicates(the one whose comment read "skips preview step").checkDuplicates, dead code until now, is the preview's next button — so the rows the user validated are the rows that get checked, with no second parse in between.ImportPagerenders the step; the Aperçu / Vérifier-doublons button pair is aWizardNavigation, andduplicate-checkwalks back through the preview.FilePreviewModalis removed. EditingFilePreviewTablealone would have mutated a still-live copy of it.The signed recap (
summarizeParsedRows, pure and tested): outflows and their total, inflows and theirs, rows in error. Totals stay signed, as the amounts that would reach the ledger — magnitudes would hide the one thing the recap exposes. Computed over the whole file, never over the twenty rows displayed. A zero-amount row is filed under neither direction, deliberately; the row count printed beside the recap keeps that visible."Inverser les signes" (
flipSignFormat) acts on the CONFIGURATION, so the correction is persisted with the source and the next file from that bank reads right on its own. Insinglemode it toggles the convention; indebit_creditit swaps the two column indices instead, becausemapRowcomputescredit - debiton magnitudes and never reads the convention there — a toggle would have been inert (review finding). A half-mapped format swaps too; an unmapped column stays unmapped, never materialised as anundefinedkey.ImportConfirmationnow states the amount mode, the sign convention (only in the mode that applies it) and the column mapping named by header, on top of the four settings it listed.The two inherited test obligations
all-positiveKNOWN DEFECT block: marker dropped, nothing deleted. Its three expectations still hold — an unsigned file carries no direction and detection cannot invent one — and two cases were added: the tell (six outflows, zero inflows, zero errors) and the honest limit, that flipping this file only produces its mirror image. What the flip does repair is asserted ondebit-creditandsigned-amount.Guards
The static guards from links 3 and 6 all still pass unchanged (
mapRow(raw, config, {inparseFilesInternal, onerunAutoDetect(, twoawait detectFormatForFile(, none in the restore arm, thecheckDuplicatesInternal/executeImportcallback slices). New guards pin the step's transition, the absence of any callback that skips it, the flip's re-parse under the flipped config, and the deleted modal.Checks
npx tsc --noEmitandnpm run buildcleancargo checkexit 0 (no Rust work)Deliberately not in scope
CHANGELOG.md/CHANGELOG.fr.md,docs/architecture.md,docs/guide-utilisateur.mdand thedocs.*keys:spec-plan-import-csv-format.mdcentralises them in issue 10 to keep the linear stack conflict-free, and links 1-6 touched none of them. One item for that link:docs/architecture.md:35andCLAUDE.mdboth say "13 composants" forsrc/components/import/, which is 12 now that the modal is gone.Generated autonomously by /autopilot run of 2026-08-13
A confidence score reports how many rows were READ, never what they say: the `all-positive` fixture scores a perfect 100 % while every credit is imported as an expense, because each of those rows is perfectly readable. Nothing between that score and the database looked at the signs — the preview was an optional modal of 20 rows with no totals, and the final confirmation listed the delimiter and the date format but neither the amount mode nor the sign convention. The `file-preview` step had been declared in `ImportWizardStep` since the beginning and no dispatch ever aimed at it. It is a real step now, traversed at every import and gated by nothing — in particular not by the detection score, which is sign-blind by construction. - `useImportWizard`: `parseAndPreview` parses and stops at the preview, replacing `parsePreview` and the `parseAndCheckDuplicates` that jumped straight to the duplicates ("skips preview step"); `checkDuplicates`, dead code until now, is the preview's next button, so the rows the user validated are the rows that get checked. - `summarizeParsedRows`: the recap, pure and tested — outflows and their total, inflows and theirs, rows in error. Totals stay SIGNED, since magnitudes would hide the one thing the recap exists to expose. Computed over the whole file, never over the twenty rows displayed. - `flipSignFormat` + "Inverser les signes": the correction lands on the CONFIGURATION, so it is persisted with the source and the next file from that bank reads right on its own. In debit/credit mode it swaps the two column indices rather than toggling a convention `mapRow` ignores there, where a toggle would have been inert. - `ImportConfirmation` states the amount mode, the sign convention (in the mode that applies it) and the column mapping, named by header. - `FilePreviewModal` removed: it was the redundant surface, and editing the table alone would have mutated a still-live copy of it. The `all-positive` KNOWN DEFECT marker is dropped rather than deleted. Its three original expectations still hold — an unsigned file carries no direction and detection cannot invent one — and two cases were added: the recap tell (six outflows, zero inflows) and the honest limit, that flipping this particular file only produces its mirror image. 1088 vitest (1054 before), tsc and vite build clean. No DB migration. Resolves #329APPROVE
L'étape
file-previewest réellement obligatoire — un seul chemin y mène, un seul en sort, et le retour arrière la retraverse. Le récap est une fonction pure testée, ses totaux sont exacts au centime près, et l'inversion agit bien sur la configuration (swap de colonnes endebit_credit, donc non inerte). Aucun blocage.Ce qui a été vérifié
Non-contournabilité.
SET_STEP: "duplicate-check"n'est dispatché qu'ensrc/hooks/useImportWizard.ts:762, danscheckDuplicatesInternal, appelé depuis un seul site (:776,checkDuplicates), lui-même câblé au seul bouton Suivant de l'aperçu (src/pages/ImportPage.tsx:132).parseAndCheckDuplicatesa disparu de la source. Le seul autregoToStep("duplicate-check")est le retour depuisconfirm(src/pages/ImportPage.tsx:169), donc en aval de l'aperçu, jamais un saut par-dessus. Retour arrière couvert :duplicate-check→file-preview(src/pages/ImportPage.tsx:150). Il n'existe aucun cheminfile-preview→confirm: après une inversion faite depuis l'aperçu, atteindre la confirmation repasse forcément parcheckDuplicates, doncduplicateResultne peut pas rester périmé sur les anciens montants.Totaux.
summarizeParsedRows(src/utils/importFormat.ts:392-422) compte les erreurs sur!row.parsed, qui est exactement l'ensemble des lignes porteuses d'unerror:mapRowne poseerrorque viafail(), qui poseparsed: nulldans le même objet (src/utils/importFormat.ts:269-273), et le chemin nominal ne pose jamaiserror. Le compteur d'erreurs et les montants affichés à côté lisent donc bien les mêmes lignes. Arithmétique recalculée hors test :-84,32 + -56,75 === -141,07exactement — letoEqualdu test n'est pas un faux positif de flottant ;all-positive→ -1840,20 ;signed-amount→ -290,20 / +1550,00. Ligne à montant nul classée dans aucune direction, documenté, testé, et le nombre de lignes affiché à côté garde l'écart visible.Inversion.
flipSignFormat(src/utils/importFormat.ts:450-473) échangedebitAmount/creditAmounten modedebit_credit. CommemapRowcalcule|credit| - |debit|(src/utils/importFormat.ts:306-308), le swap est une négation exacte, y compris pour un format à moitié mappé (la seule colonne mappée change de rôle,-|v|devient+|v|), et une colonne non mappée n'est jamais matérialisée en cléundefined. Le test bout-en-bout sur la fixturedebit-creditl'assert, pas seulement le mapping. La correction est bien mémorisée :executeImportsérialisestate.sourceConfigviaformatToRow(src/hooks/useImportWizard.ts:838) et écrit à l'unique point d'écriture (src/hooks/useImportWizard.ts:842), gardeawait updateSource(×1 toujours verte.Suppression du modal. Aucune référence résiduelle dans
src/, aucune route.import.preview.titleetimport.preview.moreRowssont repris parFilePreviewTable— aucune clé i18n morte. Les 12 clésimport.config.*que la confirmation consomme (amountMode,signConvention,columnMapping, les 5 colonnes, etc.) existent déjà dansfr.jsoneten.json; les 6 nouvelles sont dans les deux fichiers et couvertes par un test.Gardes héritées.
mapRow(raw, config, {×1,runAutoDetect(×1,await detectFormatForFile(×2,await updateSource(×1 : inchangées. Tous lescallbackBody(a, b)restent ordonnés dans la nouvelle source (parseAndPreview:663 →checkDuplicatesInternal:689,checkDuplicatesInternal:689 →checkDuplicates:771,flipSignConvention:798 →executeImport:825,executeImport:825 →goToStep:988). Blocall-positive: les 3itd'origine conservés mot pour mot, marqueur retiré du titre seulement, +2 cas dont la limite honnête (l'inversion ne produit que l'image miroir) — 84 → 89itdanscsvAutoDetect.test.ts, aucune suppression, aucunskip/only. Récap extrait en fonction pure et testé (5 cas dédiés + 3 sur fixtures), conforme à l'absence de jsdom. CI frontend verte surce19efd.Suggestions non bloquantes
src/hooks/useImportWizard.ts:804—SET_SOURCE_CONFIGest dispatché avant leawait parseFilesInternal(flipped)de la ligne 812. Si ce re-parse échoue (fichier déplacé ou verrouillé entre les deux lectures), le wizard reste avec une configuration inversée et unparsedPreviewd'avant l'inversion : « Suivant » importerait les lignes non inversées tout en persistant le format inversé sur la source — exactement le décalage de signe que l'étape existe pour empêcher. Rare, mais silencieux : restaurer la config précédente dans lecatch, ou vider l'aperçu pour bloquer « Suivant ».src/components/import/FilePreviewTable.tsx:23-29— le retour anticipé surrows.length === 0précède le récap et le bouton d'inversion, etsrc/pages/ImportPage.tsx:135laisse « Suivant » actif : un fichier sans ligne de données traverse l'aperçu sans rien montrer, puis crée quand même la source et un enregistrementimported_filesà 0 ligne. Comportement préexistant, mais l'étape désormais obligatoire est l'endroit naturel pour le refuser.src/utils/importFormat.test.ts:796-801— la garde « aucun contournement » est une absence de chaîne sur l'ancien nom. Elle n'attraperait pas un futur callback qui dispatcheraitpayload: "duplicate-check"sous un autre nom. Un compteur positif dans le style de la gardeawait updateSource(—expect(WIZARD_SRC.match(/payload: "duplicate-check"/g)).toHaveLength(1)— épinglerait l'invariant plutôt que le symptôme d'hier.src/utils/importFormat.ts:411-418— un montant non fini tomberait dans aucune des trois cases (ni sortie, ni entrée, ni erreur) : il disparaîtrait du récap tout en restant importable. Inatteignable aujourd'hui,mapRowrejetantNaNavant de produire unparsed. Unelsesur!Number.isFinite(amount)compté en erreur rendrait le dernier contrôle total-safe par construction plutôt que par dépendance.Doc différée au maillon 10, cohérent avec les maillons 1-6 qui n'ont touché ni CHANGELOG ni
docs/— le guide utilisateur dit déjà « assistant d'import multi-étapes avec aperçu des données », rien de périmé n'y traîne. Reste le décompte signalé dans le corps de la PR :docs/architecture.md:35etCLAUDE.mdannoncent « 13 composants » danssrc/components/import/, désormais 12.Mergée dans
mainen fast-forward avec le reste de la pile (tip37b832e).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.Pull request closed