feat(import): detect transaction columns by header label #337

Closed
maximus wants to merge 1 commit from issue-327-lexical-header-detection into issue-325-amount-parsing
Owner

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

Resolves #327. Link 5 of the import-format stack — based on issue-325-amount-parsing, not on main.

What changed

A lexical layer in front of the shape heuristics. Detection reasoned on the shape of the data alone, so it could not tell a debit column from a credit one: Date;Description;Credit;Debit was mapped by position and every sign came out inverted — silently, since the total is merely negated and no aggregate check notices.

  • src/utils/headerDictionary.ts (new) — the FR/EN transaction dictionary (date ; description/libellé/détail/transaction ; montant/amount ; débit/retrait/déboursé/withdrawal ; crédit/dépôt/encaissement/deposit ; solde/balance), the header-role resolver, and the two matching helpers normalizeHeaderCell / matchHeaderColumn moved out of csvAutoDetect.ts (bodies unchanged). Its own module because montant is an exclusion token for holdings (VALUE_HEADER_KEYWORDS) and the primary amount keyword for transactions — two tables that must not collide. The helpers were moved rather than exported so the dependency stays one-way; exporting them and importing them back would make csvAutoDetectheaderDictionary circular.
  • Debit/credit order by label. The pair is still found by shape (sparse and complementary); which half is the debit now comes from the dictionary. Knowing one of the two is enough — the other column is the other role.
  • detectHeader gains a lexical signal — a row the shape test rejects only because of a number is a header when it names ≥ 2 roles. The date test stays absolute, which is what keeps a transaction like DEPOT PAIE EMPLOYEUR (it contains depot) from being swallowed as a header.
  • Labels also feed the date, description and single-amount column choices; each hint is a preference the data can veto.
  • Explicit fall-back — no header row, or labels the dictionary does not know, and every step runs on shape exactly as before.
  • Third format refused — an amount column with no negatives plus an adjacent column of D/C (or DB/CR) tokens carrying ≥ 2 distinct values is detected and refused with a dedicated message, instead of being configured positive_expense and importing every deposit as an expense. detectImportFormat reports the reason; autoDetectConfig keeps its old shape for callers that only need the config.
  • i18nimport.errors.absoluteIndicatorFormat in FR + EN, and the pre-existing English literal "Auto-detection failed…" (rendered whatever the interface language) became import.errors.autoDetectFailed.

KNOWN DEFECT blocks flipped

Link 1 tagged the detection defects #328; they are this issue's. Both are now fixed, markers dropped, tags corrected to #327 — no test deleted:

  • debit-credit-reversed — was "maps Credit to debitAmount"; now mapped 3/2, parses to REFERENCE_AMOUNTS, and reads identically to its Debit-first twin. The "still nets to the same total, which is why the bug hides" case now asserts the reference total.
  • absolute-indicator — was "indistinguishable from all-positive"; now refused with import.errors.absoluteIndicatorFormat, produces no config, and refuses on the values alone with no header row to help.
  • header-numeric-label (#325's) — comment retagged to #327; the residual bare-number header case (a column titled 2025) is covered by an inline CSV rather than a 12th fixture, so #326's frozen corpus and its toHaveLength(11) stay intact.
  • all-positive still belongs to #329 and passes unchanged.

corpus integrity's "none returns null" had to move: absolute-indicator is refused by design now. The test is kept, scoped to the other ten, and joined by "never fails silently: every fixture is a config or a stated reason".

Checks

  • npm test1034 vitest green (989 after link 4; +26 in the new headerDictionary.test.ts, +19 in csvAutoDetect.test.ts)
  • npm run build — tsc + vite green
  • cd src-tauri && cargo check — green (no Rust work)
  • Holdings tests (#245) untouched and green: the shared helpers moved without a behaviour change

Notes for the reviewer

  • No CHANGELOG / docs entry on purpose — the plan centralizes both in issue 10 (#332) to avoid a conflict at every stage of the linear stack, as links 1-4 did.
  • A lexical exclusion is dropped whenever it would empty the amount-candidate list (narrowCandidates): the layer must never turn "detected, possibly wrong" into "detected nothing".
  • The refusal requires adjacency and ≥ 2 distinct indicator tokens on purpose. A file-wide scan, or a single constant flag value, would refuse files that import correctly today — and a refusal the user cannot work around is worse than a mapping they can fix.
Generated autonomously by /autopilot run of 2026-08-13 Resolves #327. Link 5 of the import-format stack — **based on `issue-325-amount-parsing`**, not on `main`. ## What changed **A lexical layer in front of the shape heuristics.** Detection reasoned on the shape of the data alone, so it could not tell a debit column from a credit one: `Date;Description;Credit;Debit` was mapped by position and every sign came out inverted — silently, since the total is merely negated and no aggregate check notices. - **`src/utils/headerDictionary.ts` (new)** — the FR/EN transaction dictionary (date ; description/libellé/détail/transaction ; montant/amount ; débit/retrait/déboursé/withdrawal ; crédit/dépôt/encaissement/deposit ; solde/balance), the header-role resolver, and the two matching helpers `normalizeHeaderCell` / `matchHeaderColumn` **moved** out of `csvAutoDetect.ts` (bodies unchanged). Its own module because `montant` is an *exclusion* token for holdings (`VALUE_HEADER_KEYWORDS`) and the *primary* amount keyword for transactions — two tables that must not collide. The helpers were moved rather than exported so the dependency stays one-way; exporting them and importing them back would make `csvAutoDetect` ↔ `headerDictionary` circular. - **Debit/credit order by label.** The pair is still found by shape (sparse and complementary); which half is the debit now comes from the dictionary. Knowing one of the two is enough — the other column is the other role. - **`detectHeader` gains a lexical signal** — a row the shape test rejects *only* because of a number is a header when it names ≥ 2 roles. The date test stays absolute, which is what keeps a transaction like `DEPOT PAIE EMPLOYEUR` (it contains `depot`) from being swallowed as a header. - **Labels also feed** the date, description and single-amount column choices; each hint is a preference the data can veto. - **Explicit fall-back** — no header row, or labels the dictionary does not know, and every step runs on shape exactly as before. - **Third format refused** — an amount column with no negatives plus an *adjacent* column of D/C (or DB/CR) tokens carrying ≥ 2 distinct values is detected and refused with a dedicated message, instead of being configured `positive_expense` and importing every deposit as an expense. `detectImportFormat` reports the reason; `autoDetectConfig` keeps its old shape for callers that only need the config. - **i18n** — `import.errors.absoluteIndicatorFormat` in FR + EN, and the pre-existing English literal `"Auto-detection failed…"` (rendered whatever the interface language) became `import.errors.autoDetectFailed`. ## KNOWN DEFECT blocks flipped Link 1 tagged the detection defects `#328`; they are this issue's. Both are now **fixed, markers dropped, tags corrected to #327** — no test deleted: - `debit-credit-reversed` — was "maps Credit to debitAmount"; now mapped 3/2, parses to `REFERENCE_AMOUNTS`, and reads identically to its Debit-first twin. The "still nets to the same total, which is why the bug hides" case now asserts the *reference* total. - `absolute-indicator` — was "indistinguishable from all-positive"; now refused with `import.errors.absoluteIndicatorFormat`, produces no config, and refuses on the values alone with no header row to help. - `header-numeric-label` (#325's) — comment retagged to #327; the residual bare-number header case (a column titled `2025`) is covered by an inline CSV rather than a 12th fixture, so #326's frozen corpus and its `toHaveLength(11)` stay intact. - `all-positive` still belongs to #329 and passes unchanged. `corpus integrity`'s "none returns null" had to move: `absolute-indicator` is refused by design now. The test is kept, scoped to the other ten, and joined by "never fails silently: every fixture is a config or a stated reason". ## Checks - `npm test` — **1034 vitest** green (989 after link 4; +26 in the new `headerDictionary.test.ts`, +19 in `csvAutoDetect.test.ts`) - `npm run build` — tsc + vite green - `cd src-tauri && cargo check` — green (no Rust work) - Holdings tests (#245) untouched and green: the shared helpers moved without a behaviour change ## Notes for the reviewer - No `CHANGELOG` / `docs` entry on purpose — the plan centralizes both in issue 10 (#332) to avoid a conflict at every stage of the linear stack, as links 1-4 did. - A lexical exclusion is dropped whenever it would empty the amount-candidate list (`narrowCandidates`): the layer must never turn "detected, possibly wrong" into "detected nothing". - The refusal requires adjacency and ≥ 2 distinct indicator tokens on purpose. A file-wide scan, or a single constant flag value, would refuse files that import correctly today — and a refusal the user cannot work around is worse than a mapping they can fix.
maximus added 1 commit 2026-08-13 17:59:16 +00:00
feat(import): detect transaction columns by header label
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
6f64fc5c1a
Detection reasoned on the shape of the data alone, so it could not tell a debit
column from a credit one: a file laid out `Date;Description;Credit;Debit` was
mapped by position and every sign of the import came out inverted — silently,
since the total is merely negated and no aggregate check notices.

A new `headerDictionary.ts` carries the FR/EN transaction dictionary (date,
description, amount, debit, credit, balance) plus the two matching helpers,
moved out of `csvAutoDetect.ts` whose holdings tables stay untouched: `montant`
is an exclusion token there and the primary amount keyword here, so the two
tables cannot be merged. Moving the generic helpers rather than exporting them
keeps the dependency one-way.

`csvAutoDetect` puts that layer in front of the shape heuristics. Labels resolve
the debit/credit order, the date, description and single-amount columns, and
give `detectHeader` a second signal for a header row carrying a bare number.
Every hint is a preference the data can veto — a labelled date column must still
parse, a labelled balance column is never excluded if it would leave nothing to
map — and a mute file (no header row, unknown labels) falls back to the shape
heuristics unchanged.

Files pairing unsigned amounts with an adjacent D/C indicator column are now
detected and REFUSED with a dedicated message, instead of being configured as
`positive_expense` and importing every deposit as an expense. Detection reports
that reason through `detectImportFormat`; `autoDetectConfig` keeps its previous
shape for the callers that only need the configuration.

Resolves #327
maximus added the
autopilot:pending-human
label 2026-08-13 17:59:27 +00:00
Author
Owner

/pr-review — REQUEST_CHANGES

Résumé

Le cœur de la PR tient. J'ai vérifié les claims plutôt que de les croire :

  • Le déplacement des helpers est byte-identique. diff de la région holdings entre issue-325-amount-parsing et la tête : les seuls écarts sont le retrait de normalizeHeaderCell / matchHeaderColumn et l'ajout du commentaire NOTE. Les corps sont inchangés au caractère près, et SYMBOL/QUANTITY/PRICE/BOOKCOST/VALUE_HEADER_KEYWORDS sont intacts. La justification du cycle d'import tient : csvAutoDetect importe headerDictionary, l'inverse aurait fermé la boucle.
  • Pas de collision avec le flux titres. matchTransactionHeaders(["Symbole","Quantite","Prix","Montant"]) rend roleCount: 1 (< MIN_HEADER_ROLE_MATCHES), et 0 pour les en-têtes EN et « Valeur marchande ». La clause lexicale de detectHeader ne peut pas se déclencher là. detectHeader reste par ailleurs strictement plus permissif (hasDate → data ; !hasNumber → header ; le reste seul est nouveau).
  • narrowCandidates ne peut pas vider la liste (csvAutoDetect.ts:275-281), et j'ai repassé chaque sortie anticipée : aucun chemin nouveau ne transforme un config en failed. Le seul changement d'issue est le rejected voulu.
  • Le wrapper est fidèle : autoDetectConfig = ok ? config : null. Un seul site de production consommait la détection (useImportWizard.ts:871), il est passé à detectImportFormat ; la raison n'est donc perdue nulle part. Le banner résout bien la clé (ImportPage.tsx:67, t(state.error, { defaultValue: state.error }), posé au lien 4).
  • Aucun test supprimé, aucun garde affaibli : 45 → 64 it, tous les describe de la base sont là, toHaveLength(11) et le garde statique useImportWizard intacts, all-positive reste en KNOWN DEFECT pour #329, zéro skip/only. Le retag #328#327 est correct : #328 est « score de confiance et détection lancée d'office », rien à voir.
  • i18n dans les deux langues, aucune migration touchée, CI frontend verte sur 6f64fc5.

Un blocage : la couche lexicale régresse le choix de la colonne description sur un format réel.


Blocages

1. src/utils/headerDictionary.ts:33 + src/utils/csvAutoDetect.ts:571-579 — l'indice lexical de description n'a aucun veto par les données, et transaction se déclenche sur une colonne de TYPE

Contrairement à la date (rejouée à 0,8 de taux de parse, csvAutoDetect.ts:395-405) et au montant (contraint à amountCandidates.includes(preferred), :648-651), la description retourne preferred dès qu'elle n'est ni la date ni numérique. Il n'y a rien derrière. Le corps de la PR dit « each hint is a preference the data can veto » — pour la description c'est faux.

Différentiel base vs tête sur un relevé Tangerine (Date,Transaction,Name,Memo,Amount, format d'export réel d'une banque canadienne personnelle, donc dans la cible de la spec) :

old={"date":0,"description":2,"amount":4}   <- Name (le marchand)
new={"date":0,"description":1,"amount":4}   <- Transaction ("DEBIT"/"CREDIT")

Aucune colonne ne matche description/libelle/detail, donc transaction gagne, et chaque transaction importée porte la description « DEBIT » ou « CREDIT » au lieu du nom du marchand. La catégorisation automatique par mots-clés — la fonction qui vit de ce champ — ne trouve plus rien, et la liste de transactions devient illisible. L'heuristique de forme faisait le bon choix avant cette PR.

Même mécanisme, seconde manifestation : une colonne libellée mais vide est mappée quand même (Date;Libelle;Detail;Montant avec Libelle vide → description: 1, base → 2).

C'est exactement ce que le commentaire de headerDictionary.ts:25-27 annonce vouloir éviter (« a keyword that fires on the wrong column is worse than a keyword that never fires »).

Correctif proposé — un veto par cardinalité plutôt que par longueur : calculer d'abord le gagnant de forme, puis ne garder preferred que s'il a du contenu non vide et que ses valeurs distinctes ne le trahissent pas comme un énuméré (p. ex. distinct >= rows.length / 2 quand rows.length >= 4). Sur Tangerine, Transaction a 2 valeurs distinctes sur 6 lignes → forme reprend la main ; sur le test Libelle vs Note de la PR (csvAutoDetect.test.ts:793-806), Libelle a 3 distinctes sur 3 → le libellé gagne toujours. Attention : un veto par ratio de longueur casserait ce test-là (14 vs 45 caractères), c'est pourquoi je propose la cardinalité. Alternative plus économique mais qui sort du texte de #327 : retirer ou déclasser transaction du dictionnaire de description.


Suggestions (non bloquantes)

a. headerDictionary.ts:175DR manque à l'alphabet indicateur. ["d","db"] / ["c","cr"] suit la spec à la lettre, mais Dr/Cr est la paire d'abréviation comptable standard. Vérifié : un fichier Date;Description;Montant;Sens avec DR/CR en colonne adjacente échappe au refus et repart en positive_expense — le défaut que #327 doit fermer, à un mot près. Un "dr" de plus coûte une ligne.

b. csvAutoDetect.ts:720 — la borne d'adjacence laisse passer les indicateurs placés ailleurs. Date;Sens;Description;Montant (indicateur en 1, montant en 3) → status: ok, positive_expense, chaque C importé en dépense. Le compromis est assumé et conforme à la spec, mais le garde-fou pourrait s'élargir sans ouvrir de fausse alarme : « toute colonne non numérique dont toutes les valeurs sont dans l'alphabet D/C avec ≥ 2 tokens distincts » est déjà la condition acceptée pour la voisine. À noter dans #328 ou #332 si vous le laissez tel quel.

c. Faux positif possible, et le message oriente alors mal. Une colonne adjacente sans rapport tenant ≥ 2 lettres de l'alphabet (un code titulaire C/D, par exemple) fait refuser le fichier. C'est récupérable — le panneau de configuration manuelle reste ouvert, l'auto-détection est un bouton — mais import.errors.absoluteIndicatorFormat dit alors « réexportez votre relevé », ce qui est un mauvais conseil pour ce fichier-là. Ajouter « ou configurez les colonnes manuellement » couvrirait les deux cas.

d. csvAutoDetect.ts:900\u2014 littéral dans un commentaire // au lieu du tiret cadratin.

e. headerDictionary.ts:28-46 — six tables exportées, importées nulle part (ni code, ni tests). Seuls matchTransactionHeaders, les deux helpers, MIN_HEADER_ROLE_MATCHES et les *_INDICATOR_TOKENS sont consommés. Les repasser en const interne jusqu'à ce que #330 en ait besoin.

f. csvAutoDetect.ts:88-95 — la doc du wrapper décrit des appelants qui n'existent plus. autoDetectConfig n'a aucun appelant de production depuis cette PR ; ses 34 sites d'appel sont tous des tests. C'est un bon choix (ne pas churner le corpus gelé de #326), mais autant l'écrire : « surface conservée pour les tests de corpus » plutôt que « callers that only need the configuration ».

## `/pr-review` — REQUEST_CHANGES ### Résumé Le cœur de la PR tient. J'ai vérifié les claims plutôt que de les croire : - **Le déplacement des helpers est byte-identique.** `diff` de la région holdings entre `issue-325-amount-parsing` et la tête : les seuls écarts sont le retrait de `normalizeHeaderCell` / `matchHeaderColumn` et l'ajout du commentaire NOTE. Les corps sont inchangés au caractère près, et `SYMBOL/QUANTITY/PRICE/BOOKCOST/VALUE_HEADER_KEYWORDS` sont intacts. La justification du cycle d'import tient : `csvAutoDetect` importe `headerDictionary`, l'inverse aurait fermé la boucle. - **Pas de collision avec le flux titres.** `matchTransactionHeaders(["Symbole","Quantite","Prix","Montant"])` rend `roleCount: 1` (< `MIN_HEADER_ROLE_MATCHES`), et `0` pour les en-têtes EN et « Valeur marchande ». La clause lexicale de `detectHeader` ne peut pas se déclencher là. `detectHeader` reste par ailleurs strictement plus permissif (`hasDate` → data ; `!hasNumber` → header ; le reste seul est nouveau). - **`narrowCandidates` ne peut pas vider la liste** (`csvAutoDetect.ts:275-281`), et j'ai repassé chaque sortie anticipée : aucun chemin nouveau ne transforme un `config` en `failed`. Le seul changement d'issue est le `rejected` voulu. - **Le wrapper est fidèle** : `autoDetectConfig` = `ok ? config : null`. Un seul site de production consommait la détection (`useImportWizard.ts:871`), il est passé à `detectImportFormat` ; la raison n'est donc perdue nulle part. Le banner résout bien la clé (`ImportPage.tsx:67`, `t(state.error, { defaultValue: state.error })`, posé au lien 4). - **Aucun test supprimé, aucun garde affaibli** : 45 → 64 `it`, tous les `describe` de la base sont là, `toHaveLength(11)` et le garde statique `useImportWizard` intacts, `all-positive` reste en KNOWN DEFECT pour #329, zéro `skip`/`only`. Le retag #328 → #327 est **correct** : #328 est « score de confiance et détection lancée d'office », rien à voir. - i18n dans les deux langues, aucune migration touchée, CI frontend verte sur `6f64fc5`. Un blocage : la couche lexicale **régresse** le choix de la colonne description sur un format réel. --- ### Blocages **1. `src/utils/headerDictionary.ts:33` + `src/utils/csvAutoDetect.ts:571-579` — l'indice lexical de description n'a aucun veto par les données, et `transaction` se déclenche sur une colonne de TYPE** Contrairement à la date (rejouée à 0,8 de taux de parse, `csvAutoDetect.ts:395-405`) et au montant (contraint à `amountCandidates.includes(preferred)`, `:648-651`), la description retourne `preferred` dès qu'elle n'est ni la date ni numérique. Il n'y a **rien** derrière. Le corps de la PR dit « each hint is a preference the data can veto » — pour la description c'est faux. Différentiel base vs tête sur un relevé Tangerine (`Date,Transaction,Name,Memo,Amount`, format d'export réel d'une banque canadienne personnelle, donc dans la cible de la spec) : ``` old={"date":0,"description":2,"amount":4} <- Name (le marchand) new={"date":0,"description":1,"amount":4} <- Transaction ("DEBIT"/"CREDIT") ``` Aucune colonne ne matche `description`/`libelle`/`detail`, donc `transaction` gagne, et **chaque transaction importée porte la description « DEBIT » ou « CREDIT »** au lieu du nom du marchand. La catégorisation automatique par mots-clés — la fonction qui vit de ce champ — ne trouve plus rien, et la liste de transactions devient illisible. L'heuristique de forme faisait le bon choix avant cette PR. Même mécanisme, seconde manifestation : une colonne libellée mais **vide** est mappée quand même (`Date;Libelle;Detail;Montant` avec `Libelle` vide → `description: 1`, base → `2`). C'est exactement ce que le commentaire de `headerDictionary.ts:25-27` annonce vouloir éviter (« a keyword that fires on the wrong column is worse than a keyword that never fires »). **Correctif proposé** — un veto par cardinalité plutôt que par longueur : calculer d'abord le gagnant de forme, puis ne garder `preferred` que s'il a du contenu non vide **et** que ses valeurs distinctes ne le trahissent pas comme un énuméré (p. ex. `distinct >= rows.length / 2` quand `rows.length >= 4`). Sur Tangerine, `Transaction` a 2 valeurs distinctes sur 6 lignes → forme reprend la main ; sur le test `Libelle` vs `Note` de la PR (`csvAutoDetect.test.ts:793-806`), `Libelle` a 3 distinctes sur 3 → le libellé gagne toujours. Attention : un veto par **ratio de longueur** casserait ce test-là (14 vs 45 caractères), c'est pourquoi je propose la cardinalité. Alternative plus économique mais qui sort du texte de #327 : retirer ou déclasser `transaction` du dictionnaire de description. --- ### Suggestions (non bloquantes) **a. `headerDictionary.ts:175` — `DR` manque à l'alphabet indicateur.** `["d","db"]` / `["c","cr"]` suit la spec à la lettre, mais `Dr`/`Cr` est la paire d'abréviation comptable standard. Vérifié : un fichier `Date;Description;Montant;Sens` avec `DR`/`CR` en colonne adjacente **échappe au refus** et repart en `positive_expense` — le défaut que #327 doit fermer, à un mot près. Un `"dr"` de plus coûte une ligne. **b. `csvAutoDetect.ts:720` — la borne d'adjacence laisse passer les indicateurs placés ailleurs.** `Date;Sens;Description;Montant` (indicateur en 1, montant en 3) → `status: ok`, `positive_expense`, chaque `C` importé en dépense. Le compromis est assumé et conforme à la spec, mais le garde-fou pourrait s'élargir sans ouvrir de fausse alarme : « toute colonne non numérique dont **toutes** les valeurs sont dans l'alphabet D/C avec ≥ 2 tokens distincts » est déjà la condition acceptée pour la voisine. À noter dans #328 ou #332 si vous le laissez tel quel. **c. Faux positif possible, et le message oriente alors mal.** Une colonne adjacente sans rapport tenant ≥ 2 lettres de l'alphabet (un code titulaire `C`/`D`, par exemple) fait refuser le fichier. C'est récupérable — le panneau de configuration manuelle reste ouvert, l'auto-détection est un bouton — mais `import.errors.absoluteIndicatorFormat` dit alors « réexportez votre relevé », ce qui est un mauvais conseil pour ce fichier-là. Ajouter « ou configurez les colonnes manuellement » couvrirait les deux cas. **d. `csvAutoDetect.ts:900` — `\u2014` littéral dans un commentaire `//`** au lieu du tiret cadratin. **e. `headerDictionary.ts:28-46` — six tables exportées, importées nulle part** (ni code, ni tests). Seuls `matchTransactionHeaders`, les deux helpers, `MIN_HEADER_ROLE_MATCHES` et les `*_INDICATOR_TOKENS` sont consommés. Les repasser en `const` interne jusqu'à ce que #330 en ait besoin. **f. `csvAutoDetect.ts:88-95` — la doc du wrapper décrit des appelants qui n'existent plus.** `autoDetectConfig` n'a **aucun** appelant de production depuis cette PR ; ses 34 sites d'appel sont tous des tests. C'est un bon choix (ne pas churner le corpus gelé de #326), mais autant l'écrire : « surface conservée pour les tests de corpus » plutôt que « callers that only need the configuration ».
Author
Owner

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

detectDescriptionColumn oppose maintenant un veto par cardinalité à la colonne préférée : une description ne répète presque rien, un énuméré répète presque tout. Le veto par longueur a été écarté — Note et Libellé sont courts tous les deux.

Le correctif est le commit ef7de3c 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 #327.

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 `detectDescriptionColumn` oppose maintenant un veto par **cardinalité** à la colonne préférée : une description ne répète presque rien, un énuméré répète presque tout. Le veto par longueur a été écarté — `Note` et `Libellé` sont courts tous les deux. Le correctif est le commit `ef7de3c` 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 #327`. **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:13:59 +00:00
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s

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