feat(import): score the detected format and run detection on its own #338

Closed
maximus wants to merge 1 commit from issue-328-confidence-score into issue-327-lexical-header-detection
Owner

Link 6 of the ten-link import-format stack. Based on issue-327-lexical-header-detection, not on main — review the top commit only.

Resolves #328

What was wrong

Detection returned a configuration it had never tested on the data, and only ever ran behind the magic-wand button. A source opened for the first time therefore started on defaultConfig (;, DD/MM/YYYY, columns 0/1/2) — plausible enough to import a whole file wrong rather than fail visibly. And the sign-convention selector was shown in debit/credit mode, where parsing ignores it.

What this does

1. A measured score, computed by the production row rule.
detectImportFormat replays the configuration it just decided over every data row of the file and returns a DetectionScore (readRows, totalRows, ratio, confident). The replay calls mapRow — the same pure function parseFilesInternal runs at import time — under the same column-level decimal arbitration (detectAmountSeparators). Per the /review-spec revision on the issue: two mappers would be the exact divergence class this chantier removes, so a test asserts the score and an end-to-end parse agree row for row on every fixture of the corpus.

The score runs over the whole file, not the 20-row detection sample: the sample decides a shape cheaply, the score is what the user reads ("147 of 150 rows").

2. The 90 % threshold colours, it does not block.
Below it, a warning banner with the detailed count and a hint to check the mapping and the preview; at or above it, a neutral banner. Nothing is disabled, and there is a test spelling out why: a perfect score says nothing about the sign of what was read. all-positive scores 100 % while every credit imports as an expense (#329's known defect) — the preview step is the real net, traversed at every import whatever the score says.

3. Detection fires on its own, guarded on !existing.
A source with no stored format is detected the moment it is opened; the wand button stays, to replay it. A source that has one is never re-detected — the stored format wins, which is the whole point of the chantier.

The guard is deliberately !existing, not !restored: a stored format that fails to decode already dispatches its own explicit error (unsupportedAmountMode and friends), and detecting over it would replace that message with a silent guess. Pinned by static guards, since the hook is untestable without jsdom:

  • detection appears in selectSource,
  • it is guarded on the absence of a stored source,
  • the restore arm contains no detection call,
  • exactly one detector, two callers (button + automatic run).

4. Sign convention hidden in debit/credit mode.
mapRow computes credit - debit on magnitudes there and never reads signConvention, so the control changed nothing. Hidden, not reset — the stored value is untouched.

5. Score invalidation. Cleared when the selected source changes (reducer-level, so no source can inherit another's confidence), when the format is edited by hand (compared through formatToRow, so renaming the source keeps the banner) and when a template overwrites the format. A banner vouching for a configuration nobody measured is the misinformation the score exists to remove.

Notable decisions

Decision Why Rejected
Score on the {status:"ok"} arm of detectImportFormat Full-fidelity entry point (link 5); autoDetectConfig stays AutoDetectResult | null for its ~25 config-only call sites Extending autoDetectConfig
One shared detectFormatForFile(filePath, base) selectSource cannot call the button callback: it closes over state that selectSource has just dispatched and cannot yet read. Everything is a parameter, so no stale closure A useEffect on step change (StrictMode double-fire + "already detected?" flag)
Button merges via {...base, ...outcome.config} Naming the seven fields one by one is how sign_convention got dropped in #324 Keeping the explicit assignments
Detection failure raises the banner on the automatic path too Silence where information is owed is the defect the issue names in its first paragraph; the user is sitting on defaultConfig Staying silent when the user did not click
--accent for the warning banner No --warning variable exists in styles.css; --negative is reserved for the page error banner Adding a CSS variable

Scope

No CHANGELOG and no docs/ change: spec-plan-import-csv-format.md centralizes both in link 10 of the stack, explicitly to avoid conflicts down a linear pile. Links 1-5 touched neither. No DB migration (v1→v17 unchanged). No Rust change.

Link 3's source-reading guards in importFormat.test.ts were not weakened: every change to selectSource lands either before the let restored marker or after if (restored) {, and the new helper is declared before selectSource, so the sliced windows are identical.

Verification

  • npm test1054 passed (baseline after link 5: 1034, +20)
  • npm run build — green (tsc + vite)
  • cd src-tauri && cargo check — green

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

Link 6 of the ten-link import-format stack. **Based on `issue-327-lexical-header-detection`, not on `main`** — review the top commit only. Resolves #328 ## What was wrong Detection returned a configuration it had **never tested** on the data, and only ever ran behind the magic-wand button. A source opened for the first time therefore started on `defaultConfig` (`;`, `DD/MM/YYYY`, columns 0/1/2) — plausible enough to import a whole file wrong rather than fail visibly. And the sign-convention selector was shown in debit/credit mode, where parsing ignores it. ## What this does **1. A measured score, computed by the production row rule.** `detectImportFormat` replays the configuration it just decided over every data row of the file and returns a `DetectionScore` (`readRows`, `totalRows`, `ratio`, `confident`). The replay calls `mapRow` — the same pure function `parseFilesInternal` runs at import time — under the same column-level decimal arbitration (`detectAmountSeparators`). Per the `/review-spec` revision on the issue: two mappers would be the exact divergence class this chantier removes, so a test asserts the score and an end-to-end parse agree **row for row on every fixture of the corpus**. The score runs over the whole file, not the 20-row detection sample: the sample decides a shape cheaply, the score is what the user reads ("147 of 150 rows"). **2. The 90 % threshold colours, it does not block.** Below it, a warning banner with the detailed count and a hint to check the mapping and the preview; at or above it, a neutral banner. Nothing is disabled, and there is a test spelling out why: a perfect score says nothing about the **sign** of what was read. `all-positive` scores 100 % while every credit imports as an expense (#329's known defect) — the preview step is the real net, traversed at every import whatever the score says. **3. Detection fires on its own, guarded on `!existing`.** A source with **no stored format** is detected the moment it is opened; the wand button stays, to replay it. A source that **has** one is never re-detected — the stored format wins, which is the whole point of the chantier. The guard is deliberately `!existing`, not `!restored`: a stored format that fails to decode already dispatches its own explicit error (`unsupportedAmountMode` and friends), and detecting over it would replace that message with a silent guess. Pinned by static guards, since the hook is untestable without jsdom: - detection appears in `selectSource`, - it is guarded on the absence of a stored source, - the restore arm contains **no** detection call, - exactly one detector, two callers (button + automatic run). **4. Sign convention hidden in debit/credit mode.** `mapRow` computes `credit - debit` on magnitudes there and never reads `signConvention`, so the control changed nothing. Hidden, **not reset** — the stored value is untouched. **5. Score invalidation.** Cleared when the selected source changes (reducer-level, so no source can inherit another's confidence), when the format is edited by hand (compared through `formatToRow`, so renaming the source keeps the banner) and when a template overwrites the format. A banner vouching for a configuration nobody measured is the misinformation the score exists to remove. ## Notable decisions | Decision | Why | Rejected | |---|---|---| | Score on the `{status:"ok"}` arm of `detectImportFormat` | Full-fidelity entry point (link 5); `autoDetectConfig` stays `AutoDetectResult \| null` for its ~25 config-only call sites | Extending `autoDetectConfig` | | One shared `detectFormatForFile(filePath, base)` | `selectSource` cannot call the button callback: it closes over `state` that `selectSource` has just dispatched and cannot yet read. Everything is a parameter, so no stale closure | A `useEffect` on step change (StrictMode double-fire + "already detected?" flag) | | Button merges via `{...base, ...outcome.config}` | Naming the seven fields one by one is how `sign_convention` got dropped in #324 | Keeping the explicit assignments | | Detection failure raises the banner on the automatic path too | Silence where information is owed is the defect the issue names in its first paragraph; the user is sitting on `defaultConfig` | Staying silent when the user did not click | | `--accent` for the warning banner | No `--warning` variable exists in `styles.css`; `--negative` is reserved for the page error banner | Adding a CSS variable | ## Scope No `CHANGELOG` and no `docs/` change: `spec-plan-import-csv-format.md` centralizes both in link 10 of the stack, explicitly to avoid conflicts down a linear pile. Links 1-5 touched neither. No DB migration (v1→v17 unchanged). No Rust change. Link 3's source-reading guards in `importFormat.test.ts` were **not weakened**: every change to `selectSource` lands either before the `let restored` marker or after `if (restored) {`, and the new helper is declared before `selectSource`, so the sliced windows are identical. ## Verification - `npm test` — **1054 passed** (baseline after link 5: 1034, +20) - `npm run build` — green (tsc + vite) - `cd src-tauri && cargo check` — green --- Generated autonomously by /autopilot run of 2026-08-13
maximus added 1 commit 2026-08-13 18:16:01 +00:00
feat(import): score the detected format and run detection on its own
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s
bf608b9d67
Detection handed back a configuration it had never tested, and only ever
ran behind the magic-wand button. A source opened for the first time
therefore started on `defaultConfig` — `;`, `DD/MM/YYYY`, columns 0/1/2 —
plausible enough to import a whole file wrong rather than fail visibly.

`detectImportFormat` now REPLAYS what it just decided over every data row
of the file and returns the rate as a `DetectionScore`. The replay runs
`mapRow`, the same pure function `parseFilesInternal` runs at import time,
under the same column-level decimal arbitration: two mappers would be the
exact divergence this chantier removes — a score reading 100 % while the
import wrote different amounts. A test asserts the two agree row for row
on every fixture of the corpus.

The threshold is 90 %, and it only colours the banner. Below it the panel
warns and prints the detailed count ("132 of 150 rows read"); at or above
it the banner is neutral. Nothing is blocked, because a perfect score says
nothing about the SIGN of what was read — `all-positive` scores 100 %
while every credit imports as an expense — and the preview step (#329) is
the real net, traversed at every import.

Detection now also fires on its own, guarded on `!existing`: a source that
has never been configured. A source that HAS one is never re-detected, the
stored format wins. The condition is deliberately not `!restored` — a
stored format that fails to decode already reports its own error, and
detecting over it would replace that message with a silent guess.

The button and the automatic run share one `detectFormatForFile`, so the
button replays detection instead of running a second, drifting variant of
it. The score is cleared when its source changes, when the format is
edited by hand (compared through the codec, so a rename keeps it) and when
a template overwrites the format — a banner vouching for a configuration
nobody measured is the misinformation it exists to remove.

Finally, the sign-convention selector is hidden in debit/credit mode.
`mapRow` computes `credit - debit` on magnitudes there and never reads
`signConvention`, so the control changed nothing. Hidden, not reset: the
stored value is left untouched.

Tests: 1054 vitest (+20), build and cargo check green. No DB migration.
CHANGELOG and docs are centralized in link 10 of the stack per the plan.

Resolves #328
maximus added the
autopilot:pending-human
label 2026-08-13 18:16:12 +00:00
Author
Owner

Revue adversariale — PR #338

Verdict : APPROVE — aucun blocage. 6 suggestions, toutes non bloquantes.

Résumé

Les cinq affirmations du corps ont été vérifiées dans le code, pas sur parole.

Le point 4 (« une source déjà configurée n'est jamais re-détectée ») tient. Le seul site de détection du hook est useImportWizard.ts:446, à l'intérieur de la branche else de if (restored) (l.411) et gardé par !existing. restored non nul implique existing non nul, donc les deux conditions se renforcent au lieu de se remplacer. Vérifié sur les trois entrées possibles :

  • application d'un modèleapplyConfigTemplate (l.1000) ne fait que SET_SOURCE_CONFIG + SET_SELECTED_TEMPLATE_ID + SET_DETECTION_SCORE: null + rechargement des en-têtes ; aucune détection ;
  • re-sélection de fichierstoggleFile (l.548) et selectAllFiles (l.570) ne touchent ni la config ni la détection ;
  • StrictModeselectSource n'est atteint que par onSelectSource={selectSource} (ImportPage.tsx:82), un gestionnaire d'événement. React ne double-invoque que le rendu et les effets ; aucun useEffect n'appelle selectSource. Le double-montage n'expose rien.

Le choix !existing plutôt que !restored est le bon : un format stocké indécodable dispatche déjà son erreur explicite (l.407), et la branche else ouvre alors sur defaultConfig sans écraser ce message par une devinette.

Le score consomme bien mapRow, lu à la source et non déduit du corps : csvAutoDetect.ts:343 appelle mapRow(raw, format, { decimalSeparators }) avec l'arbitrage detectAmountSeparators (l.338). La sélection des lignes de scoreConfig (l.331-336) est identique caractère pour caractère à celle de parseFilesInternal (useImportWizard.ts:603, 616-620) : même startIdx, même saut de la cellule vide isolée, même Papa.parse(preprocessQuotedCSV(...), { delimiter, skipEmptyLines: true }). Pas de second mappeur.

Le seuil ne bloque rien. detectionScore.confident n'apparaît que dans SourceConfigPanel.tsx:95/100/111/120 — bordure, icône, message. nextDisabled (ImportPage.tsx:48) ne dépend que de selectedFiles.length et de sourceConfig.name. Aucun bouton, aucun contrôle n'est désactivé par le score.

Les gardes statiques du maillon 3 sont intactes. importFormat.test.ts n'est pas touché, et ses fenêtres découpent toujours le même texte : restoreBranch() va de let restored (l.398) à if (restored) { (l.411) et ne contient aucun des ajouts ; callbackBody("selectSource", "loadHeadersWithConfig") borne 353→479 et contient toujours existing?.template_id ?? null. detectFormatForFile est déclaré avant selectSource (l.322), donc aucune fenêtre ne se déplace. Les trois not.toContain globaux (mapping.debitAmount !== undefined, columnMapping.amount ?? 0, isNaN(credit)) restent à zéro occurrence, et le garde du maillon 1 expect(SRC).toContain("mapRow(raw, config, {") matche toujours (l.626). Aucun compte trafiqué : runAutoDetect( = 1 occurrence, await detectFormatForFile( = 2.

Reste : i18n dans les deux fichiers avec interpolation testée des deux côtés, aucune migration touchée (v1→v17), aucun skip/only, --accent existe bien (styles.css:49 et :70, clair et sombre), CI frontend verte.

Blocages

Aucun.

Suggestions

1. Une lecture de fichier en échec rend la source inouvrable — useImportWizard.ts:446-452.
detectFormatForFile appelle invoke("read_file_content"), qui renvoie Err dès que fs::read échoue (fs_commands.rs:113) : fichier disparu entre le scan et le clic, partage réseau, permission. Le rejet remonte au catch de selectSource (l.472), donc le SET_STEP: "source-config" de la l.471 n'est jamais atteint : l'utilisateur reste sur la liste des sources avec un message d'erreur, et ne peut plus ouvrir cette source du tout — or la sélection des fichiers vit dans le panneau de configuration, donc il ne peut même pas écarter le fichier fautif pour importer les autres. Avant la PR, le même clic ouvrait le panneau (get_file_preview avale ses erreurs, l.510). Un try/catch autour du seul appel detectFormatForFile, sur le modèle de celui qui entoure detect_encoding juste au-dessus (l.420-426), rendrait la panne informative sans être bloquante.

2. Score périmé quand la sélection de fichiers change — useImportWizard.ts:548 et :570.
Le score est mesuré sur un seul fichier, mais ni toggleFile ni selectAllFiles ne le vident. Un dossier contenant deux exports de formes différentes affiche donc « Format reconnu — 50 des 50 lignes lues » pendant que le fichier retenu pour l'import n'est pas celui qui a été mesuré. C'est le quatrième trou de la liste d'invalidation du corps (source / édition manuelle / modèle), et le seul qui reste ouvert. Probabilité faible — un dossier vaut une source, donc des formes hétérogènes sont l'exception — mais l'invalidation coûte une ligne dans le reducer.

3. Le fichier mesuré n'est pas nommé — useImportWizard.ts:447 vs :964.
Le tir automatique mesure source.files[0] (ordre du scan), le bouton baguette mesure state.selectedFiles[0] (ordre trié, nouveaux fichiers d'abord). Les deux peuvent désigner des fichiers différents dans le même dossier. Porter le nom du fichier dans la bannière lèverait l'ambiguïté et rendrait la suggestion 2 sans objet.

4. La justification du seuil ne décrit pas l'app d'aujourd'hui — ImportPage.tsx:132-146.
Le corps s'appuie sur « l'aperçu est traversé à chaque import, quoi que dise le score ». Aujourd'hui c'est faux : « Aperçu » (handlePreview) et « Vérifier les doublons » (parseAndCheckDuplicates) sont deux boutons frères également activés, et l'aperçu est un modal facultatif. La décision reste la bonne — un seuil qui bloque serait le défaut — mais l'argument ne tiendra qu'une fois #329 livrée. En attendant, la bannière est le seul avertissement avant l'import, ce qui donne un peu plus de poids à la suggestion 2.

5. Le garde « delegates to mapRow » est fragile, et la comparaison bout-en-bout prouve moins qu'annoncé — csvAutoDetect.test.ts:947-955 et :186-207.
Le garde assert le texte exact de la ligne d'import ('import { detectAmountSeparators, mapRow } from "./importFormat"') : un reformat prettier ou un réordonnancement des named imports le casse sans qu'aucune divergence n'existe. Par ailleurs parseFixtureEndToEnd est lui-même un miroir de parseFilesInternal — il recopie la sélection des lignes et l'arbitrage des séparateurs — donc le test « agrees row for row » oppose deux miroirs qui appellent le même mapRow ; il valide la sélection des lignes, pas l'égalité avec le chemin d'import. Ce qui rattache réellement les deux est le garde expect(SRC).toContain("mapRow(raw, config, {") (l.248). Rien à corriger ici, mais le commentaire du test surestime ce que le cas démontre.

6. Course entre deux sources, fenêtre élargie — useImportWizard.ts:353.
selectSource attend désormais une lecture et un parse complets du fichier. Deux clics rapprochés A puis B laissent la détection de A résoudre après que B a été dispatchée, écrasant à la fois sourceConfig et detectionScore de B. La classe est pré-existante (getSourceByName et detect_encoding étaient déjà await), mais la fenêtre passe de quelques millisecondes à la durée d'un parse complet. Un jeton de génération fermerait le trou ; à défaut, poser isLoading pendant la détection automatique — il n'y a aujourd'hui aucun retour visuel pendant ce parse, et la liste des sources reste cliquable.

## Revue adversariale — PR #338 **Verdict : APPROVE** — aucun blocage. 6 suggestions, toutes non bloquantes. ### Résumé Les cinq affirmations du corps ont été vérifiées dans le code, pas sur parole. **Le point 4 (« une source déjà configurée n'est jamais re-détectée ») tient.** Le seul site de détection du hook est `useImportWizard.ts:446`, à l'intérieur de la branche `else` de `if (restored)` (l.411) *et* gardé par `!existing`. `restored` non nul implique `existing` non nul, donc les deux conditions se renforcent au lieu de se remplacer. Vérifié sur les trois entrées possibles : - **application d'un modèle** — `applyConfigTemplate` (l.1000) ne fait que `SET_SOURCE_CONFIG` + `SET_SELECTED_TEMPLATE_ID` + `SET_DETECTION_SCORE: null` + rechargement des en-têtes ; aucune détection ; - **re-sélection de fichiers** — `toggleFile` (l.548) et `selectAllFiles` (l.570) ne touchent ni la config ni la détection ; - **StrictMode** — `selectSource` n'est atteint que par `onSelectSource={selectSource}` (`ImportPage.tsx:82`), un gestionnaire d'événement. React ne double-invoque que le rendu et les effets ; aucun `useEffect` n'appelle `selectSource`. Le double-montage n'expose rien. Le choix `!existing` plutôt que `!restored` est le bon : un format stocké indécodable dispatche déjà son erreur explicite (l.407), et la branche `else` ouvre alors sur `defaultConfig` sans écraser ce message par une devinette. **Le score consomme bien `mapRow`**, lu à la source et non déduit du corps : `csvAutoDetect.ts:343` appelle `mapRow(raw, format, { decimalSeparators })` avec l'arbitrage `detectAmountSeparators` (l.338). La sélection des lignes de `scoreConfig` (l.331-336) est identique caractère pour caractère à celle de `parseFilesInternal` (`useImportWizard.ts:603, 616-620`) : même `startIdx`, même saut de la cellule vide isolée, même `Papa.parse(preprocessQuotedCSV(...), { delimiter, skipEmptyLines: true })`. Pas de second mappeur. **Le seuil ne bloque rien.** `detectionScore.confident` n'apparaît que dans `SourceConfigPanel.tsx:95/100/111/120` — bordure, icône, message. `nextDisabled` (`ImportPage.tsx:48`) ne dépend que de `selectedFiles.length` et de `sourceConfig.name`. Aucun bouton, aucun contrôle n'est désactivé par le score. **Les gardes statiques du maillon 3 sont intactes.** `importFormat.test.ts` n'est pas touché, et ses fenêtres découpent toujours le même texte : `restoreBranch()` va de `let restored` (l.398) à `if (restored) {` (l.411) et ne contient aucun des ajouts ; `callbackBody("selectSource", "loadHeadersWithConfig")` borne 353→479 et contient toujours `existing?.template_id ?? null`. `detectFormatForFile` est déclaré **avant** `selectSource` (l.322), donc aucune fenêtre ne se déplace. Les trois `not.toContain` globaux (`mapping.debitAmount !== undefined`, `columnMapping.amount ?? 0`, `isNaN(credit)`) restent à zéro occurrence, et le garde du maillon 1 `expect(SRC).toContain("mapRow(raw, config, {")` matche toujours (l.626). Aucun compte trafiqué : `runAutoDetect(` = 1 occurrence, `await detectFormatForFile(` = 2. Reste : i18n dans les deux fichiers avec interpolation testée des deux côtés, aucune migration touchée (v1→v17), aucun `skip`/`only`, `--accent` existe bien (`styles.css:49` et `:70`, clair et sombre), CI frontend verte. ### Blocages Aucun. ### Suggestions **1. Une lecture de fichier en échec rend la source inouvrable — `useImportWizard.ts:446-452`.** `detectFormatForFile` appelle `invoke("read_file_content")`, qui renvoie `Err` dès que `fs::read` échoue (`fs_commands.rs:113`) : fichier disparu entre le scan et le clic, partage réseau, permission. Le rejet remonte au `catch` de `selectSource` (l.472), donc le `SET_STEP: "source-config"` de la l.471 n'est jamais atteint : l'utilisateur reste sur la liste des sources avec un message d'erreur, et **ne peut plus ouvrir cette source du tout** — or la sélection des fichiers vit dans le panneau de configuration, donc il ne peut même pas écarter le fichier fautif pour importer les autres. Avant la PR, le même clic ouvrait le panneau (`get_file_preview` avale ses erreurs, l.510). Un `try/catch` autour du seul appel `detectFormatForFile`, sur le modèle de celui qui entoure `detect_encoding` juste au-dessus (l.420-426), rendrait la panne informative sans être bloquante. **2. Score périmé quand la sélection de fichiers change — `useImportWizard.ts:548` et `:570`.** Le score est mesuré sur un seul fichier, mais ni `toggleFile` ni `selectAllFiles` ne le vident. Un dossier contenant deux exports de formes différentes affiche donc « Format reconnu — 50 des 50 lignes lues » pendant que le fichier retenu pour l'import n'est pas celui qui a été mesuré. C'est le quatrième trou de la liste d'invalidation du corps (source / édition manuelle / modèle), et le seul qui reste ouvert. Probabilité faible — un dossier vaut une source, donc des formes hétérogènes sont l'exception — mais l'invalidation coûte une ligne dans le reducer. **3. Le fichier mesuré n'est pas nommé — `useImportWizard.ts:447` vs `:964`.** Le tir automatique mesure `source.files[0]` (ordre du scan), le bouton baguette mesure `state.selectedFiles[0]` (ordre trié, nouveaux fichiers d'abord). Les deux peuvent désigner des fichiers différents dans le même dossier. Porter le nom du fichier dans la bannière lèverait l'ambiguïté et rendrait la suggestion 2 sans objet. **4. La justification du seuil ne décrit pas l'app d'aujourd'hui — `ImportPage.tsx:132-146`.** Le corps s'appuie sur « l'aperçu est traversé à chaque import, quoi que dise le score ». Aujourd'hui c'est faux : « Aperçu » (`handlePreview`) et « Vérifier les doublons » (`parseAndCheckDuplicates`) sont deux boutons frères également activés, et l'aperçu est un modal facultatif. La décision reste la bonne — un seuil qui bloque serait le défaut — mais l'argument ne tiendra qu'une fois #329 livrée. En attendant, la bannière est le seul avertissement avant l'import, ce qui donne un peu plus de poids à la suggestion 2. **5. Le garde « delegates to mapRow » est fragile, et la comparaison bout-en-bout prouve moins qu'annoncé — `csvAutoDetect.test.ts:947-955` et `:186-207`.** Le garde assert le texte exact de la ligne d'import (`'import { detectAmountSeparators, mapRow } from "./importFormat"'`) : un reformat prettier ou un réordonnancement des named imports le casse sans qu'aucune divergence n'existe. Par ailleurs `parseFixtureEndToEnd` est lui-même un miroir de `parseFilesInternal` — il recopie la sélection des lignes et l'arbitrage des séparateurs — donc le test « agrees row for row » oppose deux miroirs qui appellent le *même* `mapRow` ; il valide la sélection des lignes, pas l'égalité avec le chemin d'import. Ce qui rattache réellement les deux est le garde `expect(SRC).toContain("mapRow(raw, config, {")` (l.248). Rien à corriger ici, mais le commentaire du test surestime ce que le cas démontre. **6. Course entre deux sources, fenêtre élargie — `useImportWizard.ts:353`.** `selectSource` attend désormais une lecture *et* un parse complets du fichier. Deux clics rapprochés A puis B laissent la détection de A résoudre après que B a été dispatchée, écrasant à la fois `sourceConfig` et `detectionScore` de B. La classe est pré-existante (`getSourceByName` et `detect_encoding` étaient déjà `await`), mais la fenêtre passe de quelques millisecondes à la durée d'un parse complet. Un jeton de génération fermerait le trou ; à défaut, poser `isLoading` pendant la détection automatique — il n'y a aujourd'hui aucun retour visuel pendant ce parse, et la liste des sources reste cliquable.
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:13:59 +00:00
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s

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#338
No description provided.