fix: follow-ups of the lot-v018 stack reviews #482

Merged
maximus merged 25 commits from review-followups-lot-v018 into main 2026-10-21 00:31:46 +00:00
Owner

Refs #393, #429, #384, #372, #446, #453, #449, #437
Generated autonomously by /autopilot run of 2026-10-18

Maillon 9 de la pile planned-2026-10-16-lot-v018, au-dessus de #475 : les deux décisions de Max du 2026-10-19 et les suggestions non bloquantes des huit /pr-review indépendantes (#468 à #475). Second tour (1386abd à a17c1b7) : la décision de Max du 2026-10-20 et les suggestions de la revue indépendante de cette PR (commentaire 25921). Troisième tour (13dca87, c50ed53, un test et une ligne de doc) : les suggestions de la revue du second tour (commentaire 25974, APPROVE). Aucune issue propre, aucun mot de fermeture : les huit issues restent à status:approved, inchangées.

Décisions de Max appliquées (2026-10-19 et 2026-10-20)

A1 — #393, revue 25437, point 3. Une catégorie qui porte des transactions directes, un montant de budget non nul (d'une année quelconque dans budget_entries, ou dans budget_template_entries, montants négatifs compris) ou des mots-clés ne peut pas devenir parent.

  • Précision du 2026-10-20 (revue 25921, question d'intention) : « devenir parent » s'entend d'une catégorie encore saisissable (is_inputable = 1, la condition de markAsParent), qui cesserait de l'être. Un parent existant, déjà non saisissable, accepte une nouvelle sous-catégorie ou un rattachement comme dans toutes les versions publiées, quoi qu'il porte : le refus n'y protégerait rien. La règle vaut pour les trois codes, parent_has_transactions venu de #472 compris, et pour les trois points d'entrée. Les lignes de la hiérarchie portent désormais is_inputable, lues par le handle (createCategory les lit dans sa transaction).
  • checkParentContent (categoryService.ts) lit, par le handle de la transaction et avant toute écriture, trois comptes dans cet ordre : transactions, montants de budget, mots-clés actifs. Le premier trouvé fait le refus : parent_has_transactions (inchangé), puis deux nouveaux codes de CategoryHierarchyError, parent_has_budget et parent_has_keywords, traduits en FR et en EN avec ce qu'il faut déplacer d'abord.
  • Trois points d'entrée : updateCategory (parent changé), reattachCategory et désormais createCategory. La création d'une sous-catégorie écrit sa ligne et le is_inputable = 0 du parent dans un seul withTransaction. Sa vérification de profondeur (getCategoryDepth) reste avant la transaction, inchangée.
  • Le formulaire (bandeau de la page, création et édition) et la fenêtre « Vérifier la hiérarchie » affichaient déjà la clé du code : étendus, pas recâblés. Les codes sont exportés (CATEGORY_HIERARCHY_ERROR_CODES), et un test exige un message par code dans les deux langues.
  • Docs : entrées #393 de [Unreleased] (Corrigé et Ajouté, dans les deux fichiers), astuce du guide (section Catégories) et docs.categories.tips FR/EN, architecture.md (validation, createCategory, hors portée du glisser-déposer). Toutes disent « devenir parent » au sens précis, et qu'un parent existant n'est pas vérifié.

A2 — #446, revue 25487, point 4. docs.dashboard.steps[0] (FR et EN) et l'étape 1 du guide (section Tableau de bord) nomment les huit étapes, par les libellés de onboarding.checklist.items.* et dans l'ordre de computeChecklist. Ils disent que les deux étapes de visite se cochent par « J'ai compris » / "Got it" (onboarding.card.acknowledge) ou, pour les rapports, par un clic sur la carte Tendances entourée. Un test de DocsContent.locales.test.ts tient libellés, ordre et libellé du bouton. L'entrée Modifié #446 suit.

Autres décisions de Max, consignées ici.

  • 2026-10-19 (commentaire 25885) : la règle des montants de budget s'applique dans toutes les éditions, Gratuite comprise. Le cas est rare, et la page Budget verrouillée invite à passer à Base pour retirer les montants. Rien à changer dans le code.
  • 2026-10-20, lignes d'ajustement : adjustment_entries ne bloque pas une catégorie qui devient parent. L'effet est limité : seule la page Ajustements lit ces lignes, et une ligne dont la catégorie n'est plus saisissable la garde, avec un champ vide (#429). Aucune donnée n'est perdue.
  • 2026-10-20, avis RustSec : les deux avis relevés au run 601 (RUSTSEC-2026-0382 sur rustls-webpki, RUSTSEC-2026-0383 sur webpki-root-certs) sont suivis par #484, hors de cette pile. Cette PR ne touche ni au lock ni à un fichier Rust de production.

Corrections par commentaire de revue

Revue (PR) Point Fait
25437 (#472, #393) 1 checkHierarchy relit les catégories avant le diagnostic : après un glisser-déposer, SET_TREE les privait des membres d'une boucle. La relecture se fait sans l'état de chargement de la page (voir Décisions), et après les écritures d'un glisser-déposer en cours (constat de l'auto-revue, 273cf36) ; tests du hook « glisser-déposer, puis vérifier » et « écritures en cours ».
25437 2 Les cinq épingles : RM1 (categoryHierarchy.test.ts), RM2 et RM11 (hook et page), RM7 (service, l'exemple du relecteur), RM15 (displaysAsRoot et grille du budget).
25431 (#471, #429) 1 Test de borne de placeListbox à anchorAt(300) / 574 px : R2 rougit.
25431 2 La liste ouverte relit sa position après chaque rendu (useLayoutEffect sans dépendances). keepPlacement (pur) rend le même objet pour une position inchangée. Épinglé par CategoryCombobox.placement.test.ts, sous le harnais.
25431 3 Commentaire de AddKeywordDialog.tsx reformulé.
25592 (#474, #384) 1 Test Rust du relecteur (fichier nommé comme un dossier d'exemple) : RR7 rougit.
25592 2, 3 Bandeau d'erreur effacé par refreshFolder et par browseFolder, une fois un dossier choisi ; entrée Corrigé dans les deux CHANGELOG. Test des vraies locales (i18next réel) : folderExists nomme le dossier en FR et en EN.
25592 5, 6, 7 CLAUDE.md : commands/ recompté depuis generate_handler! (10 modules, 45 commandes). REVIEW.md : piège « Tests vitest qui lisent src-tauri/ », ajouté en dernier. architecture.md : l'erreur du montage de Transactions est effacée vers 300 ms.
25424 (#470, #372) 1, 3 Test du if (!cancelled) du .catch (V1 rougit) ; entrée #372 : « the logs » / « les journaux », noms de la carte dans l'interface.
25409 (#468, #453) 1, 2 Ligne canari not.toBe(fr) (T4 rougit) et fixture « feuilles seulement » (T1 rougit).
25416 (#469, #449) 2, 3 Zone « Release et mise à jour » de REVIEW.md et section release.yml d'architecture.md : le relais -- --locked se reprouve à toute montée de @tauri-apps/cli ou de cargo-xwin. Les six cargo metadata --no-deps viennent de la CLI de Tauri. Aucun workflow ni skill touché.
25443 (#473, #437) 1, 2, 5 architecture.md : la mesure du rattrapage vérifie qu'il aboutit, pas sa durée. Épingles X4 (infobulle), X11 (common.cancel) et X8 (Fragment de la ligne 2). PriceFetchControl.test.tsx ajouté aux utilisateurs du harnais.
25487 (#475, #446) 1, 2, 5, 6, 7 Ordre de browseFolder épinglé (R4 rougit) ; ligne [A, z, Vide] donne z, Vide, A (R13 rougit). Note Ctrl+clic : raison lue dans le code des crates. CLAUDE.md : balance/ 12, categories-migration/ 5, onboarding/ 3. Corps de #475 corrigé : onboarding.card.acknowledge, re-GET intact.
25921 (#482, second tour) question d'intention Décision de Max du 2026-10-20 appliquée (1386abd), voir A1.
25921 1, 2 Montant négatif refusé, dans la grille et dans un modèle (R1 et R2 rouges) ; garde « un chargement plus récent l'emporte » épinglée (R8 rouge).
25921 3 Message parent_has_budget, astuce du guide et docs.categories.tips (FR et EN) : les montants comptent pour toute année, se remettent à zéro dans la grille année par année, et un modèle ne se modifie pas, il se supprime. L'astuce ajoute que remettre à zéro une année passée retire aussi son prévu du rapport Budget vs Réel.
25921 4, 6 En-tête de placeListbox.test.ts corrigé. CLAUDE.md : « 12 modules, dont 10 avec des commandes ». Utilisateurs du harnais présentés par leur nombre et la commande qui les liste (21 au 2026-10-20), plus comme une liste complète.
25921 5 Défaut antérieur (#472) corrigé (bc66101) : après un rattachement, un formulaire ouvert derrière la fenêtre perdait ses changements. La relecture silencieuse devient readCategoriesAgain, partagée par l'ouverture et le rattachement.
25974 (#482, troisième tour) 1 Test d'un échec de la relecture, à l'ouverture puis après un rattachement : la fenêtre affiche le message, le bandeau de la page reste vide, l'enregistrement se termine (13dca87, Q6 rougit).
25974 2 Corps de la PR : la question sur l'édition Gratuite est retirée (tranchée le 2026-10-19), et les décisions du 2026-10-20 sont consignées.
25974 3 Lignes d'ajustement : décision de Max du 2026-10-20, consignée plus haut, rien à changer.
25974 4 architecture.md : la commande qui liste les utilisateurs du harnais filtre les fichiers de test, et rend les 21 annoncés (c50ed53).

Décisions (journal du run, #followups)

  • [MEDIUM] Mots-clés désactivés hors compte. La seule « suppression » d'un mot-clé dans l'app (deactivateKeyword) pose is_active = 0. Les compter laisserait un refus impossible à lever depuis l'interface ; ils ne classent rien et aucun écran ne les montre. La seule réactivation (« Ajouter comme mot-clé ») les déplace vers une catégorie saisissable.
  • [MEDIUM] Ordre des vérifications : transactions, budget, mots-clés ; le premier trouvé fait le refus. Les transactions restent en tête, donc chaque refus d'avant garde son code. Écarté : un refus qui liste tout (nouvelle forme d'erreur).
  • [MEDIUM] createCategory : vérification du contenu et écritures dans un withTransaction ; la vérification de profondeur reste avant, inchangée. Écarté : profondeur recalculée par depthsOf, qui changerait le cas d'un parent supprimé.
  • Remplacée par la décision de Max du 2026-10-20 : la décision MEDIUM du premier tour, « contenu vérifié quel que soit l'état du parent », n'a plus cours ; seul un parent encore saisissable est vérifié. Le constat sur les seeds reste : aucun de leurs mots-clés ne porte sur une catégorie parente.
  • [MEDIUM] Règle du 2026-10-20 par les lignes de la hiérarchie, qui portent désormais is_inputable (une lecture par le handle, celle que les règles lisent déjà). becomesParent lit === 1 || === true, comme isActiveRow ; is_inputable est lié en 1/0 sur main depuis toujours. Écarté : une requête dédiée par vérification.
  • [MEDIUM] Formulaire gardé après un rattachement : readCategoriesAgain est partagé par l'ouverture et le rattachement. Un échec de la relecture après un rattachement s'affiche désormais dans la fenêtre, où se trouve l'utilisateur, au lieu du bandeau caché derrière elle. Limite connue, non corrigée : un formulaire ouvert sur l'orpheline elle-même garde son ancien parent (editingCategory n'est pas relu, comme avec loadCategories avant), qu'un enregistrement tenterait de remettre, sous les mêmes vérifications.
  • [MEDIUM] Tests sur node:sqlite (module intégré à Node, aucun paquet) : les trois requêtes tournent telles qu'écrites, sur les CREATE TABLE de consolidated_schema.sql, si bien qu'une condition retirée (amount <> 0, is_active = 1) rougit. Sous Node 22, node:sqlite est expérimental (avertissement à l'exécution) ; la CI est en Node 24. Décrit dans architecture.md, « SQL réel ».
  • [MEDIUM] Relecture des catégories sans loadCategories : SET_LOADING démonterait le panneau de détail, et CategoryForm repartirait de editingCategory, perdant les modifications non enregistrées d'un formulaire ouvert derrière la fenêtre. Un chargement lancé entre-temps l'emporte (B393-M2 rougit sur SET_LOADING).
  • [MEDIUM] browseFolder efface l'erreur une fois un dossier choisi, pas avant le sélecteur : une annulation ne change rien, et l'erreur qui tient reste. Écarté : effacer dans scanFolderInternal, qui toucherait aussi la lecture au montage.
  • [MEDIUM] Second useLayoutEffect dans CategoryCombobox, le premier inchangé. Le test de placement fait passer useLayoutEffect pour le useEffect du harnais et useId pour une constante, dans ce fichier seulement.
  • [MEDIUM] Commentaire Rust « Prints, never asserts » de profile_db.rs non touché : src-tauri/src/database/** est de niveau haut « toujours », et la consigne limite ce point à architecture.md.
  • [HIGH] Aucune entrée CHANGELOG pour le suivi de la liste du combobox : sa régression vient du portail de #429, non publié, dont l'entrée décrit déjà le comportement voulu.
  • [HIGH] Les autres décisions sont des conventions (libellés, recomptes, phrases) ; détail dans le journal du run.

Preuves

Sonde rc=1 (src/zz_tmp_probe.test.ts, supprimée), npm ci propre dans le worktree. Chaque mutation est appliquée seule, puis la suite vitest complète tourne (Rust : cargo test --locked --all-targets commands::fs_commands) ; le fichier est restauré par git checkout --, et l'arbre est propre après chacune. 51 mutations (42 au premier tour, 8 au second, 1 au troisième), toutes rouges sur le test visé.

# Fichier Mutation Rouges (sur 2526 à 2560) rc
A1-M1 categoryService.ts vérification parent_has_budget retirée 6 1
A1-M2 categoryService.ts vérification parent_has_keywords retirée 5 1
A1-M3 categoryService.ts AND amount <> 0 retiré (budget_entries) 3 1
A1-M4 categoryService.ts budget_template_entries retiré de la requête 1 1
A1-M5 categoryService.ts AND is_active = 1 retiré (mots-clés) 3 1
A1-M6 categoryService.ts mots-clés vérifiés avant le budget 1 1
A1-M7 categoryService.ts createCategory sans vérification du contenu 5 1
A1-M8 categoryService.ts vérification après l'INSERT 3 1
A1-M9 categoryService.ts vérification par getDb() dans la transaction 6 1
A1-M10 categoryService.ts contenu vérifié avant les règles de hiérarchie 1 1
A1-M11 useCategories.ts parent_has_keywords rendu par son message 2 1
A1-M12 fr.json message parent_has_keywords retiré 3 1
B393-M1 useCategories.ts aucune relecture dans checkHierarchy 3 1
B393-M2 useCategories.ts relecture par loadCategories() 1 1
B393-M3 useCategories.ts garde de chargement inversée 1 1
SR-M1 useCategories.ts le contrôle n'attend plus les écritures du glisser-déposer 1 1
SR-M2 useCategories.ts promesse des écritures non gardée 1 1
RM1 categoryHierarchy.ts tête de boucle cherchée sur tout le chemin 1 1
RM2 useCategories.ts START_REATTACH garde l'erreur 1 1
RM7 categoryService.ts règles sur toutes les lignes, inactives comprises 1 1
RM11 CategoriesPage.tsx state.tree passé à la fenêtre 1 1
RM15 categoryHierarchy.ts propre parent affiché en racine 2 1
B429-M1 CategoryCombobox.tsx relecture après chaque rendu retirée 1 1
B429-M2 CategoryCombobox.tsx setPlacement(next) sans keepPlacement 1 1
B429-M3 placeListbox.ts keepPlacement rend toujours next 2 1
R2 placeListbox.ts spaceBelow < maxHeight devient <= 1 1
B384-M1 useImportWizard.ts refreshFolder n'efface plus l'erreur 1 1
B384-M2 useImportWizard.ts browseFolder n'efface plus l'erreur 1 1
B384-M3 useImportWizard.ts effacement avant le sélecteur 1 1
R4 useImportWizard.ts scan, puis SET_IMPORT_FOLDER 1 1
F22-fr / F22-en fr.json / en.json folderExists sans {{name}} 1 / 1 1 / 1
RR7 fs_commands.rs exists() devient is_dir() 1 (11 réussis) 101
T4 locales.keys.test.ts en importé de fr.json 1 1
T1 locales.keys.test.ts missingKeys liste aussi les conteneurs 1 1
V1 ChangelogContent.tsx .catch sans cancelled 1 1
X4 TransactionTable.tsx infobulle sans t 1 1
X11 LicenseKeyForm.tsx common.cancel sans t 1 1
X8 TransactionTable.tsx le clic déplie la ligne 1 1 1
R13 reorderRows.ts sous-total d'un bloc vide en fin de groupe retiré 1 1
A2-M1 / A2-M2 fr.json / en.json deux étapes inversées / « Browse the reports » retirée 1 / 1 1 / 1
D-M1 categoryService.ts contenu vérifié même sur un parent non saisissable 3 1
D-M2 categoryService.ts becomesParent toujours faux (plus aucun refus de contenu) 21 1
D-M3 categoryService.ts createCategory passe des lignes vides au contrôle 7 1
R1 categoryService.ts budget_entries : amount <> 0 réduit à amount > 0 3 1
R2 categoryService.ts budget_template_entries : amount <> 0 réduit à amount > 0 3 1
R8 useCategories.ts garde « un chargement plus récent l'emporte » retirée 1 1
RA-M1 useCategories.ts rattachement relu par loadCategories() 1 1
R9 useCategories.ts relecture sans attendre les écritures du glisser-déposer 1 1
Q6 useCategories.ts un échec de readCategoriesAgain part au bandeau de la page (mutation de la revue 25974) 1 1

Comptes

  • vitest : 156 fichiers, 2560 tests, verts (base : 155 fichiers, 2503 tests ; 2548 à la fin du premier tour, 2559 à la fin du second) ; npm run build vert (tsc + vite).
  • Rust : cargo test --locked --all-targets, 269 réussis, 1 ignoré (base : 268 et 1). Le second tour ne touche pas au Rust.
  • Sonde : rc=1, aux trois tours.

Vérifications manuelles dues (prochaine release)

  1. Refus A1, formulaire de catégorie, création (FR puis EN). Sur une catégorie feuille saisissable qui a un mot-clé actif, « Ajouter une catégorie » avec elle pour parent : le bandeau affiche le message parent_has_keywords traduit, et rien n'est créé. Même chose avec un montant de budget non nul, puis un montant dans un modèle de budget seulement : message parent_has_budget. Avec des montants tous à zéro et des mots-clés tous supprimés, la sous-catégorie est créée et le parent n'accepte plus de saisie.
  2. Refus A1, formulaire de catégorie, édition. Changer le parent d'une catégorie pour l'une des catégories du point 1 : même refus, le formulaire reste ouvert, rien n'est enregistré.
  3. Refus A1, fenêtre « Vérifier la hiérarchie ». Rattacher une orpheline sous une catégorie qui porte des mots-clés : le message s'affiche dans la fenêtre. Puis la racine : le rattachement passe et le message disparaît.
  4. Glisser-déposer, puis « Vérifier la hiérarchie », sur un profil qui a une boucle de parenté. L'orpheline est nommée, pas « — ». Un formulaire d'édition ouvert avec des changements non enregistrés les garde derrière la fenêtre, sans affichage de « Chargement » sur la page.
  5. Liste de catégories qui suit son champ. Ouvrir la liste dès l'ouverture de la fenêtre « Lier transferts » du Bilan (fenêtre centrée qui grandit à l'arrivée des lignes), et de même dans « Nouveau mot-clé » (aperçu qui arrive). La liste reste collée sous son champ, sans clignotement ; une fois la fenêtre stable, l'application ne consomme plus de CPU (pas de boucle de rendu).
  6. Bandeau d'erreur de l'Import. « Créer un dossier d'exemple » refusé (dossier déjà présent) : le bandeau s'affiche. « Actualiser » le retire. « Parcourir » puis Annuler le garde. Le choix d'un autre dossier le retire.
  7. Aide, section Tableau de bord (Paramètres → Guide, FR et EN) : l'étape 1 nomme les huit étapes, dans l'ordre de la liste de démarrage.
  8. Un parent existant avec du contenu accepte une nouvelle sous-catégorie. Sur un parent déjà non saisissable qui garde des transactions, des montants de budget ou des mots-clés (une feuille scindée par « Ajouter une catégorie » dans une version publiée), « Ajouter une catégorie » avec lui pour parent crée la sous-catégorie sans message. Même chose pour un rattachement sous lui dans la fenêtre « Vérifier la hiérarchie ».
  9. Formulaire ouvert derrière la fenêtre, après un rattachement. Modifier une catégorie sans enregistrer, ouvrir « Vérifier la hiérarchie » et rattacher une orpheline : en refermant la fenêtre, le formulaire garde les modifications, et la page n'a pas affiché « Chargement ».

Hors portée (décidé par Max)

Filtres de période et l'astuce « sur 12 mois » (#479), fuite de withoutFolders au Journal (#478), garde des variables d'interpolation (#480), test du CHANGELOG à la release (#481), tests du consentement et de la garde Premium de PriceFetchControl (cours expérimentaux, sans enjeu).

À regarder pour la revue

  • Niveau calculé : normal. Aucun fichier ne remplit une ligne du tableau Haut : aucun appel réseau, aucun consentement touché, aucun fichier ajouté à la racine ni dans src-tauri/. fs_commands.rs ne change que dans son module de tests. REVIEW.md change hors « Niveaux ».
  • Limite connue (antérieure) : un formulaire ouvert sur l'orpheline elle-même garde son ancien parent après son rattachement ; l'enregistrer tenterait de la remettre sous cet ancien parent, sous les mêmes vérifications (une boucle serait refusée).
  • Le second tour est relu (commentaire 25974, APPROVE). Les deux commits du troisième tour (13dca87, un test ; c50ed53, une ligne de doc) ne le sont pas encore.
  • refreshFolder est aussi le onChanged de ImportHistoryPanel : supprimer un import efface désormais un bandeau d'erreur resté affiché.
  • Deux techniques de test nouvelles, chacune dans un seul fichier : node:sqlite (categoryService.test.ts) et useLayoutEffect passé pour le useEffect du harnais (CategoryCombobox.placement.test.ts).
  • Les tests de categoryService lisent consolidated_schema.sql : le nouveau piège de REVIEW.md les cite.

🤖 Generated with Claude Code

Refs #393, #429, #384, #372, #446, #453, #449, #437 Generated autonomously by /autopilot run of 2026-10-18 Maillon 9 de la pile `planned-2026-10-16-lot-v018`, au-dessus de #475 : les deux décisions de Max du 2026-10-19 et les suggestions non bloquantes des huit `/pr-review` indépendantes (#468 à #475). Second tour (`1386abd` à `a17c1b7`) : la décision de Max du 2026-10-20 et les suggestions de la revue indépendante de cette PR (commentaire 25921). Troisième tour (`13dca87`, `c50ed53`, un test et une ligne de doc) : les suggestions de la revue du second tour (commentaire 25974, APPROVE). Aucune issue propre, aucun mot de fermeture : les huit issues restent à `status:approved`, inchangées. ## Décisions de Max appliquées (2026-10-19 et 2026-10-20) **A1 — #393, revue 25437, point 3.** Une catégorie qui porte des transactions directes, un montant de budget non nul (d'une année quelconque dans `budget_entries`, ou dans `budget_template_entries`, montants négatifs compris) ou des mots-clés ne peut pas devenir parent. - **Précision du 2026-10-20 (revue 25921, question d'intention)** : « devenir parent » s'entend d'une catégorie encore saisissable (`is_inputable = 1`, la condition de `markAsParent`), qui cesserait de l'être. Un parent existant, déjà non saisissable, accepte une nouvelle sous-catégorie ou un rattachement comme dans toutes les versions publiées, quoi qu'il porte : le refus n'y protégerait rien. La règle vaut pour les trois codes, `parent_has_transactions` venu de #472 compris, et pour les trois points d'entrée. Les lignes de la hiérarchie portent désormais `is_inputable`, lues par le handle (`createCategory` les lit dans sa transaction). - `checkParentContent` (`categoryService.ts`) lit, par le handle de la transaction et avant toute écriture, trois comptes dans cet ordre : transactions, montants de budget, mots-clés actifs. Le premier trouvé fait le refus : `parent_has_transactions` (inchangé), puis deux nouveaux codes de `CategoryHierarchyError`, `parent_has_budget` et `parent_has_keywords`, traduits en FR et en EN avec ce qu'il faut déplacer d'abord. - Trois points d'entrée : `updateCategory` (parent changé), `reattachCategory` et désormais `createCategory`. La création d'une sous-catégorie écrit sa ligne et le `is_inputable = 0` du parent dans un seul `withTransaction`. Sa vérification de profondeur (`getCategoryDepth`) reste avant la transaction, inchangée. - Le formulaire (bandeau de la page, création et édition) et la fenêtre « Vérifier la hiérarchie » affichaient déjà la clé du code : étendus, pas recâblés. Les codes sont exportés (`CATEGORY_HIERARCHY_ERROR_CODES`), et un test exige un message par code dans les deux langues. - Docs : entrées #393 de `[Unreleased]` (Corrigé et Ajouté, dans les deux fichiers), astuce du guide (section Catégories) et `docs.categories.tips` FR/EN, `architecture.md` (validation, `createCategory`, hors portée du glisser-déposer). Toutes disent « devenir parent » au sens précis, et qu'un parent existant n'est pas vérifié. **A2 — #446, revue 25487, point 4.** `docs.dashboard.steps[0]` (FR et EN) et l'étape 1 du guide (section Tableau de bord) nomment les huit étapes, par les libellés de `onboarding.checklist.items.*` et dans l'ordre de `computeChecklist`. Ils disent que les deux étapes de visite se cochent par « J'ai compris » / "Got it" (`onboarding.card.acknowledge`) ou, pour les rapports, par un clic sur la carte Tendances entourée. Un test de `DocsContent.locales.test.ts` tient libellés, ordre et libellé du bouton. L'entrée Modifié #446 suit. **Autres décisions de Max, consignées ici.** - **2026-10-19 (commentaire 25885)** : la règle des montants de budget s'applique dans toutes les éditions, Gratuite comprise. Le cas est rare, et la page Budget verrouillée invite à passer à Base pour retirer les montants. Rien à changer dans le code. - **2026-10-20, lignes d'ajustement** : `adjustment_entries` ne bloque pas une catégorie qui devient parent. L'effet est limité : seule la page Ajustements lit ces lignes, et une ligne dont la catégorie n'est plus saisissable la garde, avec un champ vide (#429). Aucune donnée n'est perdue. - **2026-10-20, avis RustSec** : les deux avis relevés au run 601 (RUSTSEC-2026-0382 sur `rustls-webpki`, RUSTSEC-2026-0383 sur `webpki-root-certs`) sont suivis par #484, hors de cette pile. Cette PR ne touche ni au lock ni à un fichier Rust de production. ## Corrections par commentaire de revue | Revue (PR) | Point | Fait | |---|---|---| | 25437 (#472, #393) | 1 | `checkHierarchy` relit les catégories avant le diagnostic : après un glisser-déposer, `SET_TREE` les privait des membres d'une boucle. La relecture se fait **sans** l'état de chargement de la page (voir Décisions), et après les écritures d'un glisser-déposer en cours (constat de l'auto-revue, `273cf36`) ; tests du hook « glisser-déposer, puis vérifier » et « écritures en cours ». | | 25437 | 2 | Les cinq épingles : RM1 (`categoryHierarchy.test.ts`), RM2 et RM11 (hook et page), RM7 (service, l'exemple du relecteur), RM15 (`displaysAsRoot` et grille du budget). | | 25431 (#471, #429) | 1 | Test de borne de `placeListbox` à `anchorAt(300)` / 574 px : R2 rougit. | | 25431 | 2 | La liste ouverte relit sa position après chaque rendu (`useLayoutEffect` sans dépendances). `keepPlacement` (pur) rend le même objet pour une position inchangée. Épinglé par `CategoryCombobox.placement.test.ts`, sous le harnais. | | 25431 | 3 | Commentaire de `AddKeywordDialog.tsx` reformulé. | | 25592 (#474, #384) | 1 | Test Rust du relecteur (fichier nommé comme un dossier d'exemple) : RR7 rougit. | | 25592 | 2, 3 | Bandeau d'erreur effacé par `refreshFolder` et par `browseFolder`, une fois un dossier choisi ; entrée Corrigé dans les deux CHANGELOG. Test des vraies locales (i18next réel) : `folderExists` nomme le dossier en FR et en EN. | | 25592 | 5, 6, 7 | `CLAUDE.md` : `commands/` recompté depuis `generate_handler!` (10 modules, 45 commandes). `REVIEW.md` : piège « Tests vitest qui lisent `src-tauri/` », ajouté en dernier. `architecture.md` : l'erreur du montage de Transactions est effacée vers 300 ms. | | 25424 (#470, #372) | 1, 3 | Test du `if (!cancelled)` du `.catch` (V1 rougit) ; entrée #372 : « the logs » / « les journaux », noms de la carte dans l'interface. | | 25409 (#468, #453) | 1, 2 | Ligne canari `not.toBe(fr)` (T4 rougit) et fixture « feuilles seulement » (T1 rougit). | | 25416 (#469, #449) | 2, 3 | Zone « Release et mise à jour » de `REVIEW.md` et section `release.yml` d'`architecture.md` : le relais `-- --locked` se reprouve à toute montée de `@tauri-apps/cli` ou de cargo-xwin. Les six `cargo metadata --no-deps` viennent de la CLI de Tauri. Aucun workflow ni skill touché. | | 25443 (#473, #437) | 1, 2, 5 | `architecture.md` : la mesure du rattrapage vérifie qu'il aboutit, pas sa durée. Épingles X4 (infobulle), X11 (`common.cancel`) et X8 (`Fragment` de la ligne 2). `PriceFetchControl.test.tsx` ajouté aux utilisateurs du harnais. | | 25487 (#475, #446) | 1, 2, 5, 6, 7 | Ordre de `browseFolder` épinglé (R4 rougit) ; ligne `[A, z, Vide]` donne `z, Vide, A` (R13 rougit). Note Ctrl+clic : raison lue dans le code des crates. `CLAUDE.md` : `balance/` 12, `categories-migration/` 5, `onboarding/` 3. Corps de #475 corrigé : `onboarding.card.acknowledge`, re-GET intact. | | 25921 (#482, second tour) | question d'intention | Décision de Max du 2026-10-20 appliquée (`1386abd`), voir A1. | | 25921 | 1, 2 | Montant négatif refusé, dans la grille et dans un modèle (R1 et R2 rouges) ; garde « un chargement plus récent l'emporte » épinglée (R8 rouge). | | 25921 | 3 | Message `parent_has_budget`, astuce du guide et `docs.categories.tips` (FR et EN) : les montants comptent pour toute année, se remettent à zéro dans la grille année par année, et un modèle ne se modifie pas, il se supprime. L'astuce ajoute que remettre à zéro une année passée retire aussi son prévu du rapport Budget vs Réel. | | 25921 | 4, 6 | En-tête de `placeListbox.test.ts` corrigé. `CLAUDE.md` : « 12 modules, dont 10 avec des commandes ». Utilisateurs du harnais présentés par leur nombre et la commande qui les liste (21 au 2026-10-20), plus comme une liste complète. | | 25921 | 5 | Défaut antérieur (#472) corrigé (`bc66101`) : après un rattachement, un formulaire ouvert derrière la fenêtre perdait ses changements. La relecture silencieuse devient `readCategoriesAgain`, partagée par l'ouverture et le rattachement. | | 25974 (#482, troisième tour) | 1 | Test d'un échec de la relecture, à l'ouverture puis après un rattachement : la fenêtre affiche le message, le bandeau de la page reste vide, l'enregistrement se termine (`13dca87`, Q6 rougit). | | 25974 | 2 | Corps de la PR : la question sur l'édition Gratuite est retirée (tranchée le 2026-10-19), et les décisions du 2026-10-20 sont consignées. | | 25974 | 3 | Lignes d'ajustement : décision de Max du 2026-10-20, consignée plus haut, rien à changer. | | 25974 | 4 | `architecture.md` : la commande qui liste les utilisateurs du harnais filtre les fichiers de test, et rend les 21 annoncés (`c50ed53`). | ## Décisions (journal du run, `#followups`) - **[MEDIUM] Mots-clés désactivés hors compte.** La seule « suppression » d'un mot-clé dans l'app (`deactivateKeyword`) pose `is_active = 0`. Les compter laisserait un refus impossible à lever depuis l'interface ; ils ne classent rien et aucun écran ne les montre. La seule réactivation (« Ajouter comme mot-clé ») les déplace vers une catégorie saisissable. - **[MEDIUM] Ordre des vérifications** : transactions, budget, mots-clés ; le premier trouvé fait le refus. Les transactions restent en tête, donc chaque refus d'avant garde son code. Écarté : un refus qui liste tout (nouvelle forme d'erreur). - **[MEDIUM] `createCategory`** : vérification du contenu et écritures dans un `withTransaction` ; la vérification de profondeur reste avant, inchangée. Écarté : profondeur recalculée par `depthsOf`, qui changerait le cas d'un parent supprimé. - **Remplacée par la décision de Max du 2026-10-20** : la décision MEDIUM du premier tour, « contenu vérifié quel que soit l'état du parent », n'a plus cours ; seul un parent encore saisissable est vérifié. Le constat sur les seeds reste : aucun de leurs mots-clés ne porte sur une catégorie parente. - **[MEDIUM] Règle du 2026-10-20 par les lignes de la hiérarchie**, qui portent désormais `is_inputable` (une lecture par le handle, celle que les règles lisent déjà). `becomesParent` lit `=== 1 || === true`, comme `isActiveRow` ; `is_inputable` est lié en `1`/`0` sur `main` depuis toujours. Écarté : une requête dédiée par vérification. - **[MEDIUM] Formulaire gardé après un rattachement** : `readCategoriesAgain` est partagé par l'ouverture et le rattachement. Un échec de la relecture après un rattachement s'affiche désormais dans la fenêtre, où se trouve l'utilisateur, au lieu du bandeau caché derrière elle. Limite connue, non corrigée : un formulaire ouvert sur l'orpheline elle-même garde son ancien parent (`editingCategory` n'est pas relu, comme avec `loadCategories` avant), qu'un enregistrement tenterait de remettre, sous les mêmes vérifications. - **[MEDIUM] Tests sur `node:sqlite`** (module intégré à Node, aucun paquet) : les trois requêtes tournent telles qu'écrites, sur les `CREATE TABLE` de `consolidated_schema.sql`, si bien qu'une condition retirée (`amount <> 0`, `is_active = 1`) rougit. Sous Node 22, `node:sqlite` est expérimental (avertissement à l'exécution) ; la CI est en Node 24. Décrit dans `architecture.md`, « SQL réel ». - **[MEDIUM] Relecture des catégories sans `loadCategories`** : `SET_LOADING` démonterait le panneau de détail, et `CategoryForm` repartirait de `editingCategory`, perdant les modifications non enregistrées d'un formulaire ouvert derrière la fenêtre. Un chargement lancé entre-temps l'emporte (B393-M2 rougit sur `SET_LOADING`). - **[MEDIUM] `browseFolder` efface l'erreur une fois un dossier choisi**, pas avant le sélecteur : une annulation ne change rien, et l'erreur qui tient reste. Écarté : effacer dans `scanFolderInternal`, qui toucherait aussi la lecture au montage. - **[MEDIUM] Second `useLayoutEffect`** dans `CategoryCombobox`, le premier inchangé. Le test de placement fait passer `useLayoutEffect` pour le `useEffect` du harnais et `useId` pour une constante, dans ce fichier seulement. - **[MEDIUM] Commentaire Rust « Prints, never asserts » de `profile_db.rs` non touché** : `src-tauri/src/database/**` est de niveau haut « toujours », et la consigne limite ce point à `architecture.md`. - **[HIGH]** Aucune entrée CHANGELOG pour le suivi de la liste du combobox : sa régression vient du portail de #429, non publié, dont l'entrée décrit déjà le comportement voulu. - **[HIGH]** Les autres décisions sont des conventions (libellés, recomptes, phrases) ; détail dans le journal du run. ## Preuves Sonde rc=1 (`src/zz_tmp_probe.test.ts`, supprimée), `npm ci` propre dans le worktree. Chaque mutation est appliquée seule, puis la suite vitest complète tourne (Rust : `cargo test --locked --all-targets commands::fs_commands`) ; le fichier est restauré par `git checkout --`, et l'arbre est propre après chacune. 51 mutations (42 au premier tour, 8 au second, 1 au troisième), toutes rouges sur le test visé. | # | Fichier | Mutation | Rouges (sur 2526 à 2560) | rc | |---|---|---|---|---| | A1-M1 | `categoryService.ts` | vérification `parent_has_budget` retirée | 6 | 1 | | A1-M2 | `categoryService.ts` | vérification `parent_has_keywords` retirée | 5 | 1 | | A1-M3 | `categoryService.ts` | `AND amount <> 0` retiré (`budget_entries`) | 3 | 1 | | A1-M4 | `categoryService.ts` | `budget_template_entries` retiré de la requête | 1 | 1 | | A1-M5 | `categoryService.ts` | `AND is_active = 1` retiré (mots-clés) | 3 | 1 | | A1-M6 | `categoryService.ts` | mots-clés vérifiés avant le budget | 1 | 1 | | A1-M7 | `categoryService.ts` | `createCategory` sans vérification du contenu | 5 | 1 | | A1-M8 | `categoryService.ts` | vérification après l'`INSERT` | 3 | 1 | | A1-M9 | `categoryService.ts` | vérification par `getDb()` dans la transaction | 6 | 1 | | A1-M10 | `categoryService.ts` | contenu vérifié avant les règles de hiérarchie | 1 | 1 | | A1-M11 | `useCategories.ts` | `parent_has_keywords` rendu par son message | 2 | 1 | | A1-M12 | `fr.json` | message `parent_has_keywords` retiré | 3 | 1 | | B393-M1 | `useCategories.ts` | aucune relecture dans `checkHierarchy` | 3 | 1 | | B393-M2 | `useCategories.ts` | relecture par `loadCategories()` | 1 | 1 | | B393-M3 | `useCategories.ts` | garde de chargement inversée | 1 | 1 | | SR-M1 | `useCategories.ts` | le contrôle n'attend plus les écritures du glisser-déposer | 1 | 1 | | SR-M2 | `useCategories.ts` | promesse des écritures non gardée | 1 | 1 | | RM1 | `categoryHierarchy.ts` | tête de boucle cherchée sur tout le chemin | 1 | 1 | | RM2 | `useCategories.ts` | `START_REATTACH` garde l'erreur | 1 | 1 | | RM7 | `categoryService.ts` | règles sur toutes les lignes, inactives comprises | 1 | 1 | | RM11 | `CategoriesPage.tsx` | `state.tree` passé à la fenêtre | 1 | 1 | | RM15 | `categoryHierarchy.ts` | propre parent affiché en racine | 2 | 1 | | B429-M1 | `CategoryCombobox.tsx` | relecture après chaque rendu retirée | 1 | 1 | | B429-M2 | `CategoryCombobox.tsx` | `setPlacement(next)` sans `keepPlacement` | 1 | 1 | | B429-M3 | `placeListbox.ts` | `keepPlacement` rend toujours `next` | 2 | 1 | | R2 | `placeListbox.ts` | `spaceBelow < maxHeight` devient `<=` | 1 | 1 | | B384-M1 | `useImportWizard.ts` | `refreshFolder` n'efface plus l'erreur | 1 | 1 | | B384-M2 | `useImportWizard.ts` | `browseFolder` n'efface plus l'erreur | 1 | 1 | | B384-M3 | `useImportWizard.ts` | effacement avant le sélecteur | 1 | 1 | | R4 | `useImportWizard.ts` | scan, puis `SET_IMPORT_FOLDER` | 1 | 1 | | F22-fr / F22-en | `fr.json` / `en.json` | `folderExists` sans `{{name}}` | 1 / 1 | 1 / 1 | | RR7 | `fs_commands.rs` | `exists()` devient `is_dir()` | 1 (11 réussis) | 101 | | T4 | `locales.keys.test.ts` | `en` importé de `fr.json` | 1 | 1 | | T1 | `locales.keys.test.ts` | `missingKeys` liste aussi les conteneurs | 1 | 1 | | V1 | `ChangelogContent.tsx` | `.catch` sans `cancelled` | 1 | 1 | | X4 | `TransactionTable.tsx` | infobulle sans `t` | 1 | 1 | | X11 | `LicenseKeyForm.tsx` | `common.cancel` sans `t` | 1 | 1 | | X8 | `TransactionTable.tsx` | le clic déplie la ligne 1 | 1 | 1 | | R13 | `reorderRows.ts` | sous-total d'un bloc vide en fin de groupe retiré | 1 | 1 | | A2-M1 / A2-M2 | `fr.json` / `en.json` | deux étapes inversées / « Browse the reports » retirée | 1 / 1 | 1 / 1 | | D-M1 | `categoryService.ts` | contenu vérifié même sur un parent non saisissable | 3 | 1 | | D-M2 | `categoryService.ts` | `becomesParent` toujours faux (plus aucun refus de contenu) | 21 | 1 | | D-M3 | `categoryService.ts` | `createCategory` passe des lignes vides au contrôle | 7 | 1 | | R1 | `categoryService.ts` | `budget_entries` : `amount <> 0` réduit à `amount > 0` | 3 | 1 | | R2 | `categoryService.ts` | `budget_template_entries` : `amount <> 0` réduit à `amount > 0` | 3 | 1 | | R8 | `useCategories.ts` | garde « un chargement plus récent l'emporte » retirée | 1 | 1 | | RA-M1 | `useCategories.ts` | rattachement relu par `loadCategories()` | 1 | 1 | | R9 | `useCategories.ts` | relecture sans attendre les écritures du glisser-déposer | 1 | 1 | | Q6 | `useCategories.ts` | un échec de `readCategoriesAgain` part au bandeau de la page (mutation de la revue 25974) | 1 | 1 | ## Comptes - vitest : 156 fichiers, 2560 tests, verts (base : 155 fichiers, 2503 tests ; 2548 à la fin du premier tour, 2559 à la fin du second) ; `npm run build` vert (tsc + vite). - Rust : `cargo test --locked --all-targets`, 269 réussis, 1 ignoré (base : 268 et 1). Le second tour ne touche pas au Rust. - Sonde : rc=1, aux trois tours. ## Vérifications manuelles dues (prochaine release) 1. **Refus A1, formulaire de catégorie, création** (FR puis EN). Sur une catégorie feuille saisissable qui a un mot-clé actif, « Ajouter une catégorie » avec elle pour parent : le bandeau affiche le message `parent_has_keywords` traduit, et rien n'est créé. Même chose avec un montant de budget non nul, puis un montant dans un modèle de budget seulement : message `parent_has_budget`. Avec des montants tous à zéro et des mots-clés tous supprimés, la sous-catégorie est créée et le parent n'accepte plus de saisie. 2. **Refus A1, formulaire de catégorie, édition.** Changer le parent d'une catégorie pour l'une des catégories du point 1 : même refus, le formulaire reste ouvert, rien n'est enregistré. 3. **Refus A1, fenêtre « Vérifier la hiérarchie ».** Rattacher une orpheline sous une catégorie qui porte des mots-clés : le message s'affiche dans la fenêtre. Puis la racine : le rattachement passe et le message disparaît. 4. **Glisser-déposer, puis « Vérifier la hiérarchie »**, sur un profil qui a une boucle de parenté. L'orpheline est nommée, pas « — ». Un formulaire d'édition ouvert avec des changements non enregistrés les garde derrière la fenêtre, sans affichage de « Chargement » sur la page. 5. **Liste de catégories qui suit son champ.** Ouvrir la liste dès l'ouverture de la fenêtre « Lier transferts » du Bilan (fenêtre centrée qui grandit à l'arrivée des lignes), et de même dans « Nouveau mot-clé » (aperçu qui arrive). La liste reste collée sous son champ, sans clignotement ; une fois la fenêtre stable, l'application ne consomme plus de CPU (pas de boucle de rendu). 6. **Bandeau d'erreur de l'Import.** « Créer un dossier d'exemple » refusé (dossier déjà présent) : le bandeau s'affiche. « Actualiser » le retire. « Parcourir » puis Annuler le garde. Le choix d'un autre dossier le retire. 7. **Aide, section Tableau de bord** (Paramètres → Guide, FR et EN) : l'étape 1 nomme les huit étapes, dans l'ordre de la liste de démarrage. 8. **Un parent existant avec du contenu accepte une nouvelle sous-catégorie.** Sur un parent déjà non saisissable qui garde des transactions, des montants de budget ou des mots-clés (une feuille scindée par « Ajouter une catégorie » dans une version publiée), « Ajouter une catégorie » avec lui pour parent crée la sous-catégorie sans message. Même chose pour un rattachement sous lui dans la fenêtre « Vérifier la hiérarchie ». 9. **Formulaire ouvert derrière la fenêtre, après un rattachement.** Modifier une catégorie sans enregistrer, ouvrir « Vérifier la hiérarchie » et rattacher une orpheline : en refermant la fenêtre, le formulaire garde les modifications, et la page n'a pas affiché « Chargement ». ## Hors portée (décidé par Max) Filtres de période et l'astuce « sur 12 mois » (#479), fuite de `withoutFolders` au Journal (#478), garde des variables d'interpolation (#480), test du CHANGELOG à la release (#481), tests du consentement et de la garde Premium de `PriceFetchControl` (cours expérimentaux, sans enjeu). ## À regarder pour la revue - Niveau calculé : **normal**. Aucun fichier ne remplit une ligne du tableau Haut : aucun appel réseau, aucun consentement touché, aucun fichier ajouté à la racine ni dans `src-tauri/`. `fs_commands.rs` ne change que dans son module de tests. `REVIEW.md` change hors « Niveaux ». - **Limite connue (antérieure)** : un formulaire ouvert sur l'orpheline elle-même garde son ancien parent après son rattachement ; l'enregistrer tenterait de la remettre sous cet ancien parent, sous les mêmes vérifications (une boucle serait refusée). - Le second tour est relu (commentaire 25974, APPROVE). Les deux commits du troisième tour (`13dca87`, un test ; `c50ed53`, une ligne de doc) ne le sont pas encore. - `refreshFolder` est aussi le `onChanged` de `ImportHistoryPanel` : supprimer un import efface désormais un bandeau d'erreur resté affiché. - Deux techniques de test nouvelles, chacune dans un seul fichier : `node:sqlite` (`categoryService.test.ts`) et `useLayoutEffect` passé pour le `useEffect` du harnais (`CategoryCombobox.placement.test.ts`). - Les tests de `categoryService` lisent `consolidated_schema.sql` : le nouveau piège de `REVIEW.md` les cite. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 18 commits 2026-10-20 01:23:07 +00:00
Decision of Max of 2026-10-19 (review 25437, point 3): a category that
holds direct transactions, a non-zero budget amount (budget_entries or
budget_template_entries) or active keywords cannot become a parent. The
check (`checkParentContent`) runs through the transaction's handle,
before any write, in `updateCategory` (changed parent), `reattachCategory`
and now `createCategory`, which writes its row and the parent's flag in
one transaction. Order: transactions, budget, keywords; the first found
is the refusal (`parent_has_budget`, `parent_has_keywords` added to
`CategoryHierarchyError`, translated in FR and EN).

The service tests run the content queries as written on node:sqlite,
tables created from consolidated_schema.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unreleased #393 entries now say the refusal covers budget amounts
and keywords and applies when a subcategory is created; guide tip and
architecture.md (validation, createCategory, out-of-scope drag-and-drop,
the node:sqlite test note) follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a drag-and-drop, SET_TREE leaves state.categories with what the
tree shows, without the members of a loop: the window then showed "—"
for the orphan and offered a parent the service refused (review 25437,
point 1). checkHierarchy now reads the categories again before the
diagnostic, without the page's loading state, so a form open behind
the window stays as it is; a load started meanwhile wins. Pinned by a
drag-and-drop-then-check hook test, plus the refusal-then-success case
of the window's message (RM2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25437, point 2: the loop named by its own smallest member, not by
a smaller id under it (RM1); the page hands the window every category,
not the tree (RM11); a category that is its own parent stays out of the
budget, and displaysAsRoot says so (RM15). RM7 (depth counted on active
categories at the service) is pinned in categoryService.test.ts by
1a36d4e, RM2 by be4b923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision of Max of 2026-10-19 (review 25487, point 4):
docs.dashboard.steps[0] (FR and EN) and step 1 of the guide's Dashboard
section listed six of the eight steps. They now name the eight, by the
labels of onboarding.checklist.items, in the list's order, and say the
two visit steps are checked with the card's "Got it" or, for the
reports, a click on the outlined Trends card. A test holds the step to
the checklist's labels and order; the #446 changelog entries follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25431: a window centered on screen grows when its content
arrives, and moves its fields with no scroll nor resize; the open list,
placed in a portal, stayed where it was. While open, the combobox now
reads its position again after every render (useLayoutEffect without
dependencies); `keepPlacement` hands back the same state object for an
unchanged position, so React renders nothing more. Pinned by
CategoryCombobox.placement.test.ts (harness, useLayoutEffect mapped on
its useEffect, fake input) and keepPlacement's own tests. The boundary
test of placeListbox now sits at exactly 240 px below with room above
(point 1), and AddKeywordDialog's comment no longer claims the list is
cut (point 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 2 (pre-existing): the banner of a refused sample
folder asks to refresh the list, yet survived a "Refresh" that went
through, and a folder change. refreshFolder, and browseFolder once a
folder is chosen, now clear the error before reading the folder, as
selectSource does; a failed rescan puts up its own error, and a
cancelled picker keeps the banner. The hook tests also pin browseFolder's
order (review 25487, point 1): the folder is set before its scan, so the
root files shown are the new folder's. Fixed entry in both changelogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 3: the page's tests take the marked t and the key
guard compares no value, so a translation that lost {{name}} passed the
suite. The real locale files, through a real i18next with the app's
options, now render folderExists with the folder's name in French and in
English, and the two other codes without a leftover placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 1: the pre-check reads exists(), not is_dir(); under
is_dir() a file named "Exemple Tangerine" let "Exemple Desjardins" be
created before create_dir failed with a raw "File exists". The
reviewer's test pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25409, points 1 and 2: the canary now fails if both imports name
one file (T4), and a fixture pins that a missing branch is reported by
its leaves, never by its container (T1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25424, point 1: the `if (!cancelled)` of ChangelogContent's
.catch had no test (V1). Modelled on the late-answer test: the French
file rejects after the English one was read, and the history still shows
the English notes, not the empty message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25443, point 2: the transfer icon's tooltip goes through t with
its account and direction (X4), LicenseKeyForm's cancel button through
t (X11), and the notes textarea opens under the row whose button was
clicked, read in the Fragment keyed by that row's id (X8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25487, point 2 (R13): [A, z, Vide] gives z, Vide, A; dropping
the subtotal of an empty block at the final flush turned no test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25424, point 3: the English interface never says "Journal" (the
card is "Logs"), the French one says "Journaux". Unreleased entry only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25592, point 5: the commands/ line said 3 modules and 17
commands; generate_handler! registers 45, in 10 modules (fs 7,
export_import 5, profile 8, license 11, auth 5, token_store 1,
entitlements 1, feedback 2, backup 3, balance 2), listed as in
docs/architecture.md. Review 25487, point 7: balance/ (12),
categories-migration/ (5) and onboarding/ (3) added, of the 138
components (non-test .tsx under src/components).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: review follow-ups in REVIEW.md and architecture.md (#384, #437, #446, #449)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m22s
PR Check — Rust / rust (pull_request) Successful in 4m11s
1a3fa1dcc9
REVIEW.md, outside « Niveaux »: a pitfall on the vitest tests that read
src-tauri/ (a PR touching only src-tauri/** skips check-frontend, review
25592 point 6), and, in the Release zone, the --locked relay to re-prove
when @tauri-apps/cli or cargo-xwin is bumped (review 25416 point 2).
architecture.md: the same relay, the six cargo metadata calls coming
from the Tauri CLI only (25416 point 3); the mount error of Transactions
wiped by the first debounced page read, and the banner cleared by
refreshFolder and browseFolder (25592 points 7 and 2); the catch-up
measurement asserts success, not duration, and PriceFetchControl among
the harness users (25443 points 1 and 5); the Ctrl+click note's reason
as read in tauri 2.10.2 and wry 0.54.1 (25487 point 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added 1 commit 2026-10-20 01:27:23 +00:00
fix(categories): the hierarchy check reads after the drag-and-drop's writes (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m20s
PR Check — Rust / rust (pull_request) Successful in 3m52s
273cf36eb7
Self-review of the reload: updateCategorySortOrders writes one UPDATE
per category, each through the lock on its own, so a check opened right
after a drag-and-drop could read halfway and show half the move in the
tree. moveCategory keeps its write's promise, settled either way, and
checkHierarchy waits for it before reading. Pinned by a hook test with
the write held open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Niveau : normal

Verdict : APPROVE

Auto-revue de l'auteur (worker /autopilot), pas indépendante : une /pr-review indépendante suit.

Niveau (Step 0bis, REVIEW.md de main, fichier par fichier) : aucun des 31 fichiers ne remplit une ligne du tableau Haut. La ligne « code livré » ne s'applique pas : aucun appel réseau ajouté ni modifié, aucune URL, aucun consentement, et aucune ligne de Journal changée (useImportWizard.ts ne touche que le bandeau). fs_commands.rs ne change que dans son module #[cfg(test)] et figure dans la liste « au moins normaux ». Aucun fichier n'est ajouté à la racine ni directement dans src-tauri/ ; le seul fichier ajouté est src/components/shared/CategoryCombobox.placement.test.ts. REVIEW.md change hors « Niveaux » (hunks l. 127 et 186 ; python3 scripts/review-levels.py --check : 37 motifs, identiques). CLAUDE.md et docs/architecture.md sont au moins normaux ; les CHANGELOG et le guide, bas.

Ce qui a tourné (worktree, npm ci propre) :

  • sonde rc=1 ;
  • npm run build vert ;
  • npm test : 156 fichiers et 2548 tests verts (base : 155 et 2503) ;
  • cargo test --locked --all-targets : 269 réussis, 1 ignoré (base : 268 et 1) ;
  • 42 mutations, une à la fois, suite complète à chaque fois, toutes rouges sur le test visé, arbre propre après chacune (tableau dans le corps).

Checklist

  • Sécurité : aucun secret ni jeton, aucune donnée d'un tiers (chemins de test fictifs). Le SQL ajouté est paramétré ($1), et les codes de refus sont des constantes.
  • Exactitude : A1 suit la règle de Max sur les trois points d'entrée, avant toute écriture, par le handle de la transaction ; drapeaux liés en 1/0 ou en littéral. A2 nomme les huit étapes dans l'ordre de computeChecklist. Les fixes B suivent chaque commentaire, avec deux écarts motivés, consignés au journal du run :
    • checkHierarchy ne passe pas par loadCategories, qui démonterait le formulaire ouvert ;
    • browseFolder n'efface l'erreur qu'une fois un dossier choisi.
  • Tests : une épingle par comportement ajouté ou modifié, ni .only ni .skip ni reste de débogage. Le défaut antérieur (#384, bandeau) a son test rouge sans le correctif (B384-M1, B384-M2).
  • i18n : clés dans les deux langues (garde #453 verte) et un test exige un message par code de CategoryHierarchyError ; docs.* et guide suivent ; deux entrées CHANGELOG mises à jour et une ajoutée, dans les deux fichiers.
  • Données : aucune migration, aucun changement de schéma. consolidated_schema.sql est seulement lu par un test.
  • Fermeture : la timeline des huit issues porte un pull_ref de #482 en none, jamais en closes ; ni corps ni commit ne porte de mot de fermeture.

Constat corrigé dans cette PR

  1. src/hooks/useCategories.ts:423. La relecture de checkHierarchy pouvait tomber au milieu des écritures d'un glisser-déposer : updateCategorySortOrders envoie un UPDATE par catégorie, chacun repassant par le verrou. L'arbre aurait alors montré le déplacement à moitié appliqué. moveCategory garde la promesse de ses écritures (l. 399), et le contrôle l'attend (273cf36, test « reads the categories once the drag-and-drop's writes are over » ; SR-M1 et SR-M2 rouges). Build et suite rejoués après.

Suggestions (non bloquantes)

  1. Édition Gratuite (src/shared/entitlements.ts:26). Budget y est verrouillé : un profil Gratuit qui garde des montants de budget sur une catégorie ne peut pas les remettre à zéro, et cette catégorie ne peut pas devenir parent. La décision ne prévoit pas d'exception par édition : à Max de confirmer, ou d'ouvrir une issue.
  2. Commentaire Rust (src-tauri/src/database/profile_db.rs:1706) : « Prints, never asserts » porte la même imprécision que la phrase d'architecture.md corrigée ici. Non touché, puisque src-tauri/src/database/** est de niveau haut « toujours ». À faire avec une prochaine PR de ce fichier.
  3. node:sqlite (src/services/categoryService.test.ts:106). Le module est expérimental sous Node 22, qui l'annonce par un avertissement à l'exécution ; la CI est en Node 24. Les bases en mémoire ne sont pas fermées, sans effet sur la suite.
  4. Test de placement (CategoryCombobox.placement.test.ts:17). Il simule le useLayoutEffect par le useEffect du harnais. Le saut de rendu réel de React, et donc l'absence de boucle, reste à voir dans l'application (vérification manuelle 5).
  5. src/pages/ImportPage.tsx:176 : refreshFolder est aussi le onChanged de l'historique des imports, si bien que supprimer un import efface désormais un bandeau d'erreur resté affiché. C'est voulu, mais ce n'est pas épinglé.

Actions faites : corps de #475 corrigé (onboarding.card.acknowledge, re-GET intact). Aucun label posé ni retiré.

Niveau : normal **Verdict : APPROVE** Auto-revue de l'auteur (worker `/autopilot`), pas indépendante : une `/pr-review` indépendante suit. **Niveau** (Step 0bis, `REVIEW.md` de `main`, fichier par fichier) : aucun des 31 fichiers ne remplit une ligne du tableau Haut. La ligne « code livré » ne s'applique pas : aucun appel réseau ajouté ni modifié, aucune URL, aucun consentement, et aucune ligne de Journal changée (`useImportWizard.ts` ne touche que le bandeau). `fs_commands.rs` ne change que dans son module `#[cfg(test)]` et figure dans la liste « au moins normaux ». Aucun fichier n'est ajouté à la racine ni directement dans `src-tauri/` ; le seul fichier ajouté est `src/components/shared/CategoryCombobox.placement.test.ts`. `REVIEW.md` change hors « Niveaux » (hunks l. 127 et 186 ; `python3 scripts/review-levels.py --check` : 37 motifs, identiques). `CLAUDE.md` et `docs/architecture.md` sont au moins normaux ; les CHANGELOG et le guide, bas. **Ce qui a tourné** (worktree, `npm ci` propre) : - sonde rc=1 ; - `npm run build` vert ; - `npm test` : 156 fichiers et 2548 tests verts (base : 155 et 2503) ; - `cargo test --locked --all-targets` : 269 réussis, 1 ignoré (base : 268 et 1) ; - 42 mutations, une à la fois, suite complète à chaque fois, toutes rouges sur le test visé, arbre propre après chacune (tableau dans le corps). **Checklist** - Sécurité : aucun secret ni jeton, aucune donnée d'un tiers (chemins de test fictifs). Le SQL ajouté est paramétré (`$1`), et les codes de refus sont des constantes. - Exactitude : A1 suit la règle de Max sur les trois points d'entrée, avant toute écriture, par le handle de la transaction ; drapeaux liés en `1`/`0` ou en littéral. A2 nomme les huit étapes dans l'ordre de `computeChecklist`. Les fixes B suivent chaque commentaire, avec deux écarts motivés, consignés au journal du run : - `checkHierarchy` ne passe pas par `loadCategories`, qui démonterait le formulaire ouvert ; - `browseFolder` n'efface l'erreur qu'une fois un dossier choisi. - Tests : une épingle par comportement ajouté ou modifié, ni `.only` ni `.skip` ni reste de débogage. Le défaut antérieur (#384, bandeau) a son test rouge sans le correctif (B384-M1, B384-M2). - i18n : clés dans les deux langues (garde #453 verte) et un test exige un message par code de `CategoryHierarchyError` ; `docs.*` et guide suivent ; deux entrées CHANGELOG mises à jour et une ajoutée, dans les deux fichiers. - Données : aucune migration, aucun changement de schéma. `consolidated_schema.sql` est seulement lu par un test. - Fermeture : la timeline des huit issues porte un `pull_ref` de #482 en `none`, jamais en `closes` ; ni corps ni commit ne porte de mot de fermeture. **Constat corrigé dans cette PR** 1. `src/hooks/useCategories.ts:423`. La relecture de `checkHierarchy` pouvait tomber au milieu des écritures d'un glisser-déposer : `updateCategorySortOrders` envoie un `UPDATE` par catégorie, chacun repassant par le verrou. L'arbre aurait alors montré le déplacement à moitié appliqué. `moveCategory` garde la promesse de ses écritures (l. 399), et le contrôle l'attend (`273cf36`, test « reads the categories once the drag-and-drop's writes are over » ; SR-M1 et SR-M2 rouges). Build et suite rejoués après. **Suggestions (non bloquantes)** 1. **Édition Gratuite** (`src/shared/entitlements.ts:26`). Budget y est verrouillé : un profil Gratuit qui garde des montants de budget sur une catégorie ne peut pas les remettre à zéro, et cette catégorie ne peut pas devenir parent. La décision ne prévoit pas d'exception par édition : à Max de confirmer, ou d'ouvrir une issue. 2. **Commentaire Rust** (`src-tauri/src/database/profile_db.rs:1706`) : « Prints, never asserts » porte la même imprécision que la phrase d'`architecture.md` corrigée ici. Non touché, puisque `src-tauri/src/database/**` est de niveau haut « toujours ». À faire avec une prochaine PR de ce fichier. 3. **`node:sqlite`** (`src/services/categoryService.test.ts:106`). Le module est expérimental sous Node 22, qui l'annonce par un avertissement à l'exécution ; la CI est en Node 24. Les bases en mémoire ne sont pas fermées, sans effet sur la suite. 4. **Test de placement** (`CategoryCombobox.placement.test.ts:17`). Il simule le `useLayoutEffect` par le `useEffect` du harnais. Le saut de rendu réel de React, et donc l'absence de boucle, reste à voir dans l'application (vérification manuelle 5). 5. `src/pages/ImportPage.tsx:176` : `refreshFolder` est aussi le `onChanged` de l'historique des imports, si bien que supprimer un import efface désormais un bandeau d'erreur resté affiché. C'est voulu, mais ce n'est pas épinglé. **Actions faites** : corps de #475 corrigé (`onboarding.card.acknowledge`, re-GET intact). Aucun label posé ni retiré.
Author
Owner

CI lue par l'orchestrateur sur la tête 273cf36 : les deux contrôles sont verts.

  • check-frontend (run 597) : 156 fichiers et 2548 tests verts (155 et 2503 sur la base #475). categoryService.test.ts passe sous Node 24 avec node:sqlite.
  • check-rust (run 598) :
    • Cache restored successfully ;
    • garde des advisories : canari webkit2gtk présent ;
    • garde windows-sys : 9 checks, exit 0 ;
    • cargo test : 269 passés, 1 ignoré ;
    • cargo audit informatif : 663 crates, aucune vulnérabilité, 18 avertissements autorisés.

Décision de Max (2026-10-19), sur la question de l'auto-revue (commentaire 25877) : la règle « pas de montant de budget non nul sur un futur parent » s'applique aussi en édition Gratuite. Le cas est rare ; la page Budget verrouillée invite à passer à Base pour retirer les montants. Rien à changer dans cette PR.

CI lue par l'orchestrateur sur la tête `273cf36` : les deux contrôles sont **verts**. - `check-frontend` (run 597) : 156 fichiers et 2548 tests verts (155 et 2503 sur la base #475). `categoryService.test.ts` passe sous Node 24 avec `node:sqlite`. - `check-rust` (run 598) : - `Cache restored successfully` ; - garde des advisories : canari `webkit2gtk` présent ; - garde `windows-sys` : `9 checks, exit 0` ; - `cargo test` : 269 passés, 1 ignoré ; - `cargo audit` informatif : 663 crates, aucune vulnérabilité, 18 avertissements autorisés. Décision de Max (2026-10-19), sur la question de l'auto-revue (commentaire 25877) : la règle « pas de montant de budget non nul sur un futur parent » **s'applique aussi en édition Gratuite**. Le cas est rare ; la page Budget verrouillée invite à passer à Base pour retirer les montants. Rien à changer dans cette PR.
Author
Owner

Niveau : normal

Verdict : APPROVE

Résumé. Le code fait ce que dit le corps de la PR. La règle A1 s'applique aux trois points d'entrée, avant toute écriture, par le handle d'un seul withTransaction. A2 et les suites des huit revues sont faites. Build, suite et CI sont verts, et 12 des 15 mutations du relecteur rougissent ; les trois survivantes sont des épingles sur du code correct. Reste une question d'intention pour Max avant le merge : le contenu se vérifie aussi sur une catégorie déjà parent, même non saisissable. Là, le refus ne protège rien et bloque l'ajout de sous-catégories, possible dans toutes les versions publiées.

Le niveau est calculé sur les 31 fichiers, et aucun ne remplit une ligne du tableau Haut :

  • le code livré n'ajoute ni appel réseau, ni URL, ni consentement ;
  • fs_commands.rs ne change que dans son module de tests ;
  • REVIEW.md change aux lignes 127 et 186, hors de la section « Niveaux » ;
  • aucun fichier n'est ajouté à la racine ni dans src-tauri/ ;
  • node:sqlite est un module intégré à Node, sans paquet.

Ce que la revue a exécuté

Sur un clone détaché hors du dépôt, à la tête 273cf36, après npm ci :

  • sonde volontairement rouge : rc=1 ;
  • npm run build : rc=0 ;
  • npm test : 156 fichiers, 2548 tests, rc=0 (les chiffres du corps de la PR) ;
  • CI lue dans les journaux :
    • check-frontend, run 597 : checkout de 273cf36, Node v24.22.0, build, 156 fichiers et 2548 tests verts ;
    • check-rust, run 598 : checkout de 273cf36, refuses_a_file_named_like_a_sample_folder_and_writes_nothing ... ok, 269 tests réussis, 1 ignoré.
  • Rust non recompilé en local : le seul changement Rust est un test, que la CI exécute. La mutation RR7 de l'auteur a été vérifiée à la lecture : avec is_dir(), create_dir("Exemple Desjardins") passerait, puis create_dir("Exemple Tangerine") échouerait sur « File exists ». L'erreur et desjardins_created changeraient tous deux.

Mutations du relecteur

Les mutations sont appliquées une à la fois, avec la suite complète. Après chacune, le fichier est restauré par git checkout -- et l'arbre redevient propre.

# Fichier Mutation Tests rouges
R1 categoryService.ts budget_entries : amount <> 0 réduit à amount > 0 0
R2 categoryService.ts budget_template_entries : amount <> 0 réduit à amount > 0 0
R3 categoryService.ts refus seulement à partir de deux lignes (> 1) 9
R4 categoryService.ts checkNewParent vérifie le contenu de la catégorie déplacée, pas celui du parent 11
R5 categoryService.ts createCategory sans vérification du contenu (A1-M7 rejouée) 5
R6 categoryService.ts seule la vérification des transactions tourne 10
R7 categoryService.ts createCategory vérifie le contenu par getDb(), avant la transaction 4
R8 useCategories.ts relecture de checkHierarchy toujours appliquée (garde fetchId retirée) 0
R9 useCategories.ts le contrôle n'attend plus les écritures du glisser-déposer 1
R10 useCategories.ts un échec de la relecture ou du diagnostic est avalé 1
R11 CategoryCombobox.tsx relecture limitée à l'ouverture (dépendances [open]) 1
R12 placeListbox.ts keepPlacement rend une copie égale, pas le même objet 2
R13 placeListbox.ts samePlacement ignore top 3
R14 useImportWizard.ts refreshFolder efface le bandeau après le scan, pas avant 1
R15 useImportWizard.ts browseFolder garde le bandeau (B384-M2 rejouée) 1

Douze rouges sur quinze. Les trois survivantes portent sur du code correct (suggestions 1 et 2).

Question d'intention, à trancher par Max avant le merge

Un parent déjà parent, même non saisissable, est vérifié aussi. checkParentContent (src/services/categoryService.ts:101) ne lit pas is_inputable. C'est la décision MEDIUM du run, « contenu vérifié quel que soit l'état du parent », épinglée par categoryService.test.ts:457. La décision de Max (commentaire 25898 sur #472) dit « ne peut pas devenir parent », et sa raison est que le parent cesse d'être saisissable. Or markAsParent (:64, WHERE … AND is_inputable = 1) ne change rien à un parent déjà non saisissable. Pour un tel parent, le refus ne protège donc rien.

  • Cas atteignable. Jusqu'à la v0.17.2, createCategory acceptait tout parent de profondeur permise, sans regarder ce qu'il portait. Une feuille qui avait des mots-clés, des transactions ou des montants, scindée une fois par « Ajouter une catégorie », est aujourd'hui un parent non saisissable qui garde tout ce contenu. Avec cette PR, aucune sous-catégorie ne peut plus y être créée ni rattachée, que ce soit au formulaire ou dans la fenêtre, tant que ce contenu n'est pas déplacé ou effacé.

  • Sonde du service (avec le faux de categoryService.test.ts). P est non saisissable, avec l'enfant 10 dessous. createCategory sous P est refusé en parent_has_budget, parent_has_keywords ou parent_has_transactions selon ce que P porte, avec pour seules écritures ["BEGIN","ROLLBACK"]. Sans contenu, la création passe et P reste à is_inputable = 0.

  • Remède introuvable pour les montants. Sonde de buildBudgetYearRows : P porte 100 à son nom, et son enfant 40.

    • Si P est non saisissable, aucune ligne de P n'est éditable et son sous-total vaut 40 : les 100 n'apparaissent nulle part (useBudget.ts:282 et :337).
    • Si P est saisissable, la ligne « (direct) » montre les 100.

    Le message « remettez d'abord ces montants à zéro » ne peut donc se suivre qu'en cochant d'abord « Saisissable » dans le formulaire, ce que ni le message ni le guide ne disent.

Deux options :

  • (a) Ne vérifier le contenu que si le parent est saisissable, c'est-à-dire quand markAsParent le rendrait non saisissable. Il faut une lecture de plus par le handle, et un test de plus pour un parent non saisissable. Le test de la ligne 457, dont le parent est saisissable (valeur par défaut du faux), resterait vrai.
  • (b) Garder la règle, et dire dans le message de parent_has_budget comment retrouver ces montants.

La même question vaut pour parent_has_transactions, au formulaire et dans la fenêtre, vérifié de cette façon depuis #472.

Suggestions (non bloquantes)

  1. Montants négatifs non épinglés (R1, R2). Tous les refus de parent_has_budget sont testés avec des montants positifs (25, 120, 40, 10, 50). La grille enregistre le nombre tapé tel quel (BudgetTable.tsx:127), donc un montant négatif est possible, et amount > 0 le laisserait passer. Un refus à -25 dans chaque table suffit.
  2. « A load started meanwhile wins » non épinglé (R8). La phrase de la JSDoc (useCategories.ts:418, garde à la ligne 426) n'est tenue par aucun test. Le cas est peu atteignable aujourd'hui : la fenêtre couvre la page, et le hook ne s'abonne à aucun événement de données.
  3. Textes d'aide.
    • L'astuce du guide (docs/guide-utilisateur.md:298) et docs.categories.tips (FR et EN, ligne 1206) disent « remettez ses montants à zéro ». Or un montant de modèle ne se modifie pas : seul deleteTemplate existe, ce que le message d'erreur dit bien (« supprimez … les modèles qui en contiennent »).
    • Le message ne dit pas non plus que les montants comptent pour toutes les années, alors que la grille n'en montre qu'une. Remettre à zéro une année passée efface aussi son Budget vs réel.
  4. Commentaire périmé (placeListbox.test.ts:4-8). Il range parmi ce qui demande un DOM « la relecture après un rendu qui a déplacé le champ ». Or CategoryCombobox.placement.test.ts, ajouté dans le même commit, l'épingle sous le harnais (R11 rouge). Seule la fin de la boucle, le saut de rendu de React, reste à voir dans l'application.
  5. Formulaire ouvert derrière la fenêtre, après un rattachement (défaut antérieur, #472). checkHierarchy évite désormais loadCategories pour ne pas démonter un formulaire ouvert, mais reattachOrphan l'appelle encore (useCategories.ts:453). Un rattachement réussi passe donc la page en chargement et démonte CategoryDetailPanel : les changements non enregistrés du formulaire sont perdus. La vérification manuelle 4 ne couvre que l'ouverture de la fenêtre.
  6. Comptes de la documentation (détails).
    • CLAUDE.md:68 annonce « 10 modules » et en liste douze, dont deux sans commande.
    • La liste « Utilisateurs » du harnais (architecture.md:664) nomme neuf des 21 fichiers qui l'importent. C'est une dérive antérieure à cette PR.

Les points 1, 2, 4 et 6 portent sur du code correct et viennent après les épingles de la revue 25437. Mieux vaut les regrouper dans une issue de suivi que lancer un tour de revue de plus.

Vérifié sans constat

  • Transactions. Dans createCategory, la vérification du contenu, l'INSERT et markAsParent passent par le handle d'un seul withTransaction (R7 rouge). La vérification de profondeur, inchangée, se lit avant. Un refus n'écrit que BEGIN et ROLLBACK (sonde).
  • Codes et i18n. CATEGORY_HIERARCHY_ERROR_CODES et les clés FR/EN se correspondent, et la garde de parité de #453 est verte. Les boutons « Actualiser » et « Parcourir » sont nommés comme dans l'interface dans l'entrée #384.
  • Seeds. consolidated_schema.sql compte 150 catégories, 38 parents et 180 mots-clés, dont aucun sur un parent. C'est vrai aussi pour reinitializeCategories.
  • Faits cités dans la documentation.
    • generate_handler! enregistre 45 commandes : fs 7, export/import 5, profils 8, licence 11, compte 5, jetons 1, entitlements 1, feedback 2, sauvegarde 3, Bilan 2. handle_auth_callback porte l'annotation de commande, mais il est appelé par le lien profond, sans être enregistré.
    • Composants : balance/ 12, categories-migration/ 5, onboarding/ 3, 138 au total.
    • Versions des locks : tauri 2.10.2, wry 0.54.1, @tauri-apps/cli 2.10.0.
    • wry 0.54.1 sans gestionnaire : SetHandled(true) sous WebView2 (webview2/mod.rs:778), et connect_create seulement avec un gestionnaire sous WebKitGTK (webkitgtk/mod.rs:484). tauri-runtime-wry n'en pose un qu'à partir de on_new_window.
    • release.yml, lignes 53, 70 et 78.
    • Le nouveau piège de REVIEW.md liste bien les quatre seuls fichiers vitest qui lisent sous src-tauri/.
  • Fermeture. Dans la timeline des huit issues, le pull_ref de #482 est en none. Ni le corps de la PR ni ses 19 commits ne portent de mot de fermeture, et les 19 commits suivent le format conventionnel.
  • Sécurité. Le SQL est paramétré ($1), sans secret ni jeton, et les chemins de test sont fictifs. Aucune migration ni changement de schéma.

Actions faites

  • La copie hors dépôt (/tmp/pr482-review) et les fichiers /tmp/pr482-* sont supprimés en fin de revue.
  • Aucun label changé : les huit issues sont en status:approved, aucune n'est en status:review, et la PR n'en ferme aucune.
Niveau : normal **Verdict : APPROVE** **Résumé.** Le code fait ce que dit le corps de la PR. La règle A1 s'applique aux trois points d'entrée, avant toute écriture, par le handle d'un seul `withTransaction`. A2 et les suites des huit revues sont faites. Build, suite et CI sont verts, et 12 des 15 mutations du relecteur rougissent ; les trois survivantes sont des épingles sur du code correct. Reste une question d'intention pour Max avant le merge : le contenu se vérifie aussi sur une catégorie déjà parent, même non saisissable. Là, le refus ne protège rien et bloque l'ajout de sous-catégories, possible dans toutes les versions publiées. Le niveau est calculé sur les 31 fichiers, et aucun ne remplit une ligne du tableau Haut : - le code livré n'ajoute ni appel réseau, ni URL, ni consentement ; - `fs_commands.rs` ne change que dans son module de tests ; - `REVIEW.md` change aux lignes 127 et 186, hors de la section « Niveaux » ; - aucun fichier n'est ajouté à la racine ni dans `src-tauri/` ; - `node:sqlite` est un module intégré à Node, sans paquet. ### Ce que la revue a exécuté Sur un clone détaché hors du dépôt, à la tête `273cf36`, après `npm ci` : - sonde volontairement rouge : rc=1 ; - `npm run build` : rc=0 ; - `npm test` : 156 fichiers, 2548 tests, rc=0 (les chiffres du corps de la PR) ; - CI lue dans les journaux : - `check-frontend`, run 597 : checkout de `273cf36`, Node `v24.22.0`, build, 156 fichiers et 2548 tests verts ; - `check-rust`, run 598 : checkout de `273cf36`, `refuses_a_file_named_like_a_sample_folder_and_writes_nothing ... ok`, 269 tests réussis, 1 ignoré. - Rust non recompilé en local : le seul changement Rust est un test, que la CI exécute. La mutation RR7 de l'auteur a été vérifiée à la lecture : avec `is_dir()`, `create_dir("Exemple Desjardins")` passerait, puis `create_dir("Exemple Tangerine")` échouerait sur « File exists ». L'erreur et `desjardins_created` changeraient tous deux. ### Mutations du relecteur Les mutations sont appliquées une à la fois, avec la suite complète. Après chacune, le fichier est restauré par `git checkout --` et l'arbre redevient propre. | # | Fichier | Mutation | Tests rouges | |---|---|---|---| | R1 | `categoryService.ts` | `budget_entries` : `amount <> 0` réduit à `amount > 0` | **0** | | R2 | `categoryService.ts` | `budget_template_entries` : `amount <> 0` réduit à `amount > 0` | **0** | | R3 | `categoryService.ts` | refus seulement à partir de deux lignes (`> 1`) | 9 | | R4 | `categoryService.ts` | `checkNewParent` vérifie le contenu de la catégorie déplacée, pas celui du parent | 11 | | R5 | `categoryService.ts` | `createCategory` sans vérification du contenu (A1-M7 rejouée) | 5 | | R6 | `categoryService.ts` | seule la vérification des transactions tourne | 10 | | R7 | `categoryService.ts` | `createCategory` vérifie le contenu par `getDb()`, avant la transaction | 4 | | R8 | `useCategories.ts` | relecture de `checkHierarchy` toujours appliquée (garde `fetchId` retirée) | **0** | | R9 | `useCategories.ts` | le contrôle n'attend plus les écritures du glisser-déposer | 1 | | R10 | `useCategories.ts` | un échec de la relecture ou du diagnostic est avalé | 1 | | R11 | `CategoryCombobox.tsx` | relecture limitée à l'ouverture (dépendances `[open]`) | 1 | | R12 | `placeListbox.ts` | `keepPlacement` rend une copie égale, pas le même objet | 2 | | R13 | `placeListbox.ts` | `samePlacement` ignore `top` | 3 | | R14 | `useImportWizard.ts` | `refreshFolder` efface le bandeau après le scan, pas avant | 1 | | R15 | `useImportWizard.ts` | `browseFolder` garde le bandeau (B384-M2 rejouée) | 1 | Douze rouges sur quinze. Les trois survivantes portent sur du code correct (suggestions 1 et 2). ### Question d'intention, à trancher par Max avant le merge **Un parent déjà parent, même non saisissable, est vérifié aussi.** `checkParentContent` (`src/services/categoryService.ts:101`) ne lit pas `is_inputable`. C'est la décision MEDIUM du run, « contenu vérifié quel que soit l'état du parent », épinglée par `categoryService.test.ts:457`. La décision de Max (commentaire 25898 sur #472) dit « ne peut pas *devenir* parent », et sa raison est que le parent cesse d'être saisissable. Or `markAsParent` (`:64`, `WHERE … AND is_inputable = 1`) ne change rien à un parent déjà non saisissable. Pour un tel parent, le refus ne protège donc rien. - **Cas atteignable.** Jusqu'à la v0.17.2, `createCategory` acceptait tout parent de profondeur permise, sans regarder ce qu'il portait. Une feuille qui avait des mots-clés, des transactions ou des montants, scindée une fois par « Ajouter une catégorie », est aujourd'hui un parent non saisissable qui garde tout ce contenu. Avec cette PR, aucune sous-catégorie ne peut plus y être créée ni rattachée, que ce soit au formulaire ou dans la fenêtre, tant que ce contenu n'est pas déplacé ou effacé. - **Sonde du service** (avec le faux de `categoryService.test.ts`). P est non saisissable, avec l'enfant 10 dessous. `createCategory` sous P est refusé en `parent_has_budget`, `parent_has_keywords` ou `parent_has_transactions` selon ce que P porte, avec pour seules écritures `["BEGIN","ROLLBACK"]`. Sans contenu, la création passe et P reste à `is_inputable = 0`. - **Remède introuvable pour les montants.** Sonde de `buildBudgetYearRows` : P porte 100 à son nom, et son enfant 40. - Si P est non saisissable, aucune ligne de P n'est éditable et son sous-total vaut 40 : les 100 n'apparaissent nulle part (`useBudget.ts:282` et `:337`). - Si P est saisissable, la ligne « (direct) » montre les 100. Le message « remettez d'abord ces montants à zéro » ne peut donc se suivre qu'en cochant d'abord « Saisissable » dans le formulaire, ce que ni le message ni le guide ne disent. Deux options : - **(a)** Ne vérifier le contenu que si le parent est saisissable, c'est-à-dire quand `markAsParent` le rendrait non saisissable. Il faut une lecture de plus par le handle, et un test de plus pour un parent non saisissable. Le test de la ligne 457, dont le parent est saisissable (valeur par défaut du faux), resterait vrai. - **(b)** Garder la règle, et dire dans le message de `parent_has_budget` comment retrouver ces montants. La même question vaut pour `parent_has_transactions`, au formulaire et dans la fenêtre, vérifié de cette façon depuis #472. ### Suggestions (non bloquantes) 1. **Montants négatifs non épinglés (R1, R2).** Tous les refus de `parent_has_budget` sont testés avec des montants positifs (25, 120, 40, 10, 50). La grille enregistre le nombre tapé tel quel (`BudgetTable.tsx:127`), donc un montant négatif est possible, et `amount > 0` le laisserait passer. Un refus à `-25` dans chaque table suffit. 2. **« A load started meanwhile wins » non épinglé (R8).** La phrase de la JSDoc (`useCategories.ts:418`, garde à la ligne 426) n'est tenue par aucun test. Le cas est peu atteignable aujourd'hui : la fenêtre couvre la page, et le hook ne s'abonne à aucun événement de données. 3. **Textes d'aide.** - L'astuce du guide (`docs/guide-utilisateur.md:298`) et `docs.categories.tips` (FR et EN, ligne 1206) disent « remettez ses montants à zéro ». Or un montant de modèle ne se modifie pas : seul `deleteTemplate` existe, ce que le message d'erreur dit bien (« supprimez … les modèles qui en contiennent »). - Le message ne dit pas non plus que les montants comptent pour toutes les années, alors que la grille n'en montre qu'une. Remettre à zéro une année passée efface aussi son Budget vs réel. 4. **Commentaire périmé** (`placeListbox.test.ts:4-8`). Il range parmi ce qui demande un DOM « la relecture après un rendu qui a déplacé le champ ». Or `CategoryCombobox.placement.test.ts`, ajouté dans le même commit, l'épingle sous le harnais (R11 rouge). Seule la fin de la boucle, le saut de rendu de React, reste à voir dans l'application. 5. **Formulaire ouvert derrière la fenêtre, après un rattachement** (défaut antérieur, #472). `checkHierarchy` évite désormais `loadCategories` pour ne pas démonter un formulaire ouvert, mais `reattachOrphan` l'appelle encore (`useCategories.ts:453`). Un rattachement réussi passe donc la page en chargement et démonte `CategoryDetailPanel` : les changements non enregistrés du formulaire sont perdus. La vérification manuelle 4 ne couvre que l'ouverture de la fenêtre. 6. **Comptes de la documentation** (détails). - `CLAUDE.md:68` annonce « 10 modules » et en liste douze, dont deux sans commande. - La liste « Utilisateurs » du harnais (`architecture.md:664`) nomme neuf des 21 fichiers qui l'importent. C'est une dérive antérieure à cette PR. Les points 1, 2, 4 et 6 portent sur du code correct et viennent après les épingles de la revue 25437. Mieux vaut les regrouper dans une issue de suivi que lancer un tour de revue de plus. ### Vérifié sans constat - **Transactions.** Dans `createCategory`, la vérification du contenu, l'`INSERT` et `markAsParent` passent par le handle d'un seul `withTransaction` (R7 rouge). La vérification de profondeur, inchangée, se lit avant. Un refus n'écrit que `BEGIN` et `ROLLBACK` (sonde). - **Codes et i18n.** `CATEGORY_HIERARCHY_ERROR_CODES` et les clés FR/EN se correspondent, et la garde de parité de #453 est verte. Les boutons « Actualiser » et « Parcourir » sont nommés comme dans l'interface dans l'entrée #384. - **Seeds.** `consolidated_schema.sql` compte 150 catégories, 38 parents et 180 mots-clés, dont aucun sur un parent. C'est vrai aussi pour `reinitializeCategories`. - **Faits cités dans la documentation.** - `generate_handler!` enregistre 45 commandes : fs 7, export/import 5, profils 8, licence 11, compte 5, jetons 1, entitlements 1, feedback 2, sauvegarde 3, Bilan 2. `handle_auth_callback` porte l'annotation de commande, mais il est appelé par le lien profond, sans être enregistré. - Composants : `balance/` 12, `categories-migration/` 5, `onboarding/` 3, 138 au total. - Versions des locks : tauri 2.10.2, wry 0.54.1, `@tauri-apps/cli` 2.10.0. - wry 0.54.1 sans gestionnaire : `SetHandled(true)` sous WebView2 (`webview2/mod.rs:778`), et `connect_create` seulement avec un gestionnaire sous WebKitGTK (`webkitgtk/mod.rs:484`). tauri-runtime-wry n'en pose un qu'à partir de `on_new_window`. - `release.yml`, lignes 53, 70 et 78. - Le nouveau piège de `REVIEW.md` liste bien les quatre seuls fichiers vitest qui lisent sous `src-tauri/`. - **Fermeture.** Dans la timeline des huit issues, le `pull_ref` de #482 est en `none`. Ni le corps de la PR ni ses 19 commits ne portent de mot de fermeture, et les 19 commits suivent le format conventionnel. - **Sécurité.** Le SQL est paramétré (`$1`), sans secret ni jeton, et les chemins de test sont fictifs. Aucune migration ni changement de schéma. ### Actions faites - La copie hors dépôt (`/tmp/pr482-review`) et les fichiers `/tmp/pr482-*` sont supprimés en fin de revue. - Aucun label changé : les huit issues sont en `status:approved`, aucune n'est en `status:review`, et la PR n'en ferme aucune.
maximus added 4 commits 2026-10-20 23:47:08 +00:00
Decision of Max of 2026-10-20 (review 25921): transactions, non-zero
budget amounts and active keywords are checked only while the target
still takes input (is_inputable = 1, the condition of markAsParent),
that is when it really becomes a parent and stops taking input. An
existing parent, no longer inputable, accepts a new or reattached
subcategory whatever it holds, as in every released version: the
refusal protected nothing there. All three codes and all three entry
points follow the rule; the hierarchy rows now carry is_inputable,
and createCategory reads them through its transaction's handle.

Also pins a negative amount, in the grid and in a template, as a
refusal (review 25921, R1/R2): the grid stores what is typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 25921, point 5 (pre-existing since #472): after a reattachment,
reattachOrphan reloaded through loadCategories, whose loading state
unmounted the detail panel, so a category form open behind the window
lost its unsaved changes. The silent re-read of checkHierarchy becomes
readCategoriesAgain (waits for a drag-and-drop's writes, a newer load
wins, no loading state), used by both; a failed re-read after a
reattachment now shows in the window, where the user is.

Also pins the guard « a newer load wins » (review 25921, R8): a load
started during the check's read lands, the check's older answer is
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision of Max of 2026-10-20 and review 25921, point 3. The #393
changelog entries, the guide tip and docs.categories.tips (FR and EN)
now say a category that still takes input cannot become a parent, and
that an existing parent, no longer inputable, accepts new subcategories
as before. The parent_has_budget message and the tip say amounts of any
year count, to be set to zero in the grid year by year, and that a
template cannot be edited, only deleted; the tip adds that zeroing a
past year also takes its planned amounts out of Budget vs Actual.

architecture.md: the rule and its reason, the hierarchy rows now
carrying is_inputable, readCategoriesAgain after a reattachment (a
form open on the orphan itself keeps its old parent: known limit), and
the hook harness users presented as a count with the command that lists
them (21 on 2026-10-20), not as an exhaustive list (25921, point 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: placeListbox test header and CLAUDE.md module count (#429, #384)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m2s
PR Check — Rust / rust (pull_request) Successful in 3m52s
a17c1b7604
Review 25921, points 4 and 6: the header of placeListbox.test.ts still
listed the re-read after a render among what needs a DOM, though
CategoryCombobox.placement.test.ts pins it; only the paint and React's
skipped render remain. CLAUDE.md announced 10 command modules and listed
twelve: 12 modules, 10 of them with commands, 45 commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Second tour sur #482 : quatre commits en tête de la branche, pas encore relus (ni par une /pr-review indépendante ni par une auto-revue). Ils appliquent la décision de Max du 2026-10-20 et les suggestions de la revue 25921. Tête : a17c1b7.

  • 1386abd fix(categories) : le contenu d'un parent (transactions, montants de budget, mots-clés) n'est vérifié que si ce parent est encore saisissable, c'est-à-dire quand il deviendrait vraiment parent (décision de Max du 2026-10-20). Un parent existant accepte une sous-catégorie quoi qu'il porte, pour les trois codes et les trois points d'entrée. Le commit épingle aussi un montant négatif, dans la grille et dans un modèle (R1, R2).
  • bc66101 fix(categories) : après un rattachement, un formulaire ouvert derrière la fenêtre garde ses changements (défaut antérieur, #472, point 5). La relecture silencieuse readCategoriesAgain sert à l'ouverture comme au rattachement. Le commit épingle aussi la garde « un chargement plus récent l'emporte » (R8).
  • b626bcb fix(i18n) : CHANGELOG #393, guide, docs.categories.tips et architecture.md disent « devenir parent » au sens précis (point 3). Le message parent_has_budget et l'astuce disent que les montants comptent pour toute année et qu'un modèle se supprime, faute de pouvoir se modifier. La liste des utilisateurs du harnais n'est plus présentée comme complète (point 6).
  • a17c1b7 docs : en-tête de placeListbox.test.ts (point 4), compte des modules de CLAUDE.md (point 6).

Preuves : 8 mutations, toutes rouges ; sonde rc=1, build vert, 156 fichiers et 2559 tests verts. Aucun changement Rust. Le corps de la PR est à jour : décisions, tableau des preuves, comptes, vérifications manuelles 8 et 9.

Second tour sur #482 : quatre commits en tête de la branche, **pas encore relus** (ni par une `/pr-review` indépendante ni par une auto-revue). Ils appliquent la décision de Max du 2026-10-20 et les suggestions de la revue 25921. Tête : `a17c1b7`. - `1386abd` fix(categories) : le contenu d'un parent (transactions, montants de budget, mots-clés) n'est vérifié que si ce parent est encore saisissable, c'est-à-dire quand il deviendrait vraiment parent (décision de Max du 2026-10-20). Un parent existant accepte une sous-catégorie quoi qu'il porte, pour les trois codes et les trois points d'entrée. Le commit épingle aussi un montant négatif, dans la grille et dans un modèle (R1, R2). - `bc66101` fix(categories) : après un rattachement, un formulaire ouvert derrière la fenêtre garde ses changements (défaut antérieur, #472, point 5). La relecture silencieuse `readCategoriesAgain` sert à l'ouverture comme au rattachement. Le commit épingle aussi la garde « un chargement plus récent l'emporte » (R8). - `b626bcb` fix(i18n) : CHANGELOG #393, guide, `docs.categories.tips` et `architecture.md` disent « devenir parent » au sens précis (point 3). Le message `parent_has_budget` et l'astuce disent que les montants comptent pour toute année et qu'un modèle se supprime, faute de pouvoir se modifier. La liste des utilisateurs du harnais n'est plus présentée comme complète (point 6). - `a17c1b7` docs : en-tête de `placeListbox.test.ts` (point 4), compte des modules de `CLAUDE.md` (point 6). Preuves : 8 mutations, toutes rouges ; sonde rc=1, build vert, 156 fichiers et 2559 tests verts. Aucun changement Rust. Le corps de la PR est à jour : décisions, tableau des preuves, comptes, vérifications manuelles 8 et 9.
Author
Owner

Niveau : normal

Verdict : APPROVE

Résumé. Revue du second tour (1386abd, bc66101, b626bcb, a17c1b7), sur le diff complet de la PR. La décision de Max du 2026-10-20 est appliquée telle qu'écrite : le contenu d'un parent ne se vérifie que s'il est encore saisissable, pour les trois codes et sur les trois points d'entrée, à partir de lignes lues par le handle de la transaction. Après un rattachement, un formulaire ouvert derrière la fenêtre garde ses changements. Build, suite et CI sont verts sur a17c1b7, et 7 des 9 mutations du relecteur rougissent. Des deux survivantes, l'une est équivalente ; l'autre montre qu'aucun test ne tient l'affichage d'un échec de la relecture (suggestion 1).

Niveau calculé sur les 32 fichiers. Aucun ne remplit une ligne du tableau Haut :

  • le code livré n'ajoute ni appel réseau, ni URL, ni consentement (invoke("pick_folder") est un appel IPC déjà présent) ;
  • fs_commands.rs ne change que dans son module de tests ;
  • REVIEW.md change aux lignes 130 et 190 (« Release et mise à jour », « Pièges de revue »), hors « Niveaux » ;
  • le seul fichier ajouté, src/components/shared/CategoryCombobox.placement.test.ts, n'est ni à la racine ni dans src-tauri/ ;
  • le second tour ne touche que du TypeScript, des locales et de la documentation.

Ce que la revue a exécuté

Sur un clone détaché hors du dépôt (/tmp/pr482-review), à la tête a17c1b7, après npm ci :

  • sonde volontairement rouge : rc=1 ;
  • npm run build : rc=0 ;
  • npx vitest run : 156 fichiers, 2559 tests, rc=0 (les chiffres du corps). Sous Node 22.22 en local, node:sqlite affiche son avertissement expérimental, sans effet sur les tests ;
  • CI lue dans les journaux :
    • check-frontend, run 600 : checkout de a17c1b7, Node v24.22.0, build, 156 fichiers et 2559 tests verts ;
    • check-rust, run 601 : checkout de a17c1b7, cache restauré, gardes 2/2 et 9/9, refuses_a_file_named_like_a_sample_folder_and_writes_nothing ... ok, 269 réussis, 1 ignoré.
  • Rust non recompilé en local : le second tour n'y touche pas, et le seul changement Rust de la PR (un test, premier tour) tourne en CI.

Mutations du relecteur (second tour)

Les mutations sont appliquées une à la fois, avec la suite complète. Après chacune, le fichier est restauré par git checkout -- et l'arbre redevient propre.

# Fichier Mutation Tests rouges (sur 2559)
Q1 categoryService.ts createCategory lit les lignes par getDb() dans la transaction 9
Q2 categoryService.ts createCategory lit les lignes avant BEGIN, hors transaction 1
Q3 categoryService.ts markAsParent avant la vérification du contenu, dans createCategory 7
Q4 useCategories.ts readCategoriesAgain envoie SET_TREE au lieu de SET_CATEGORIES 2
Q5 useCategories.ts reattachOrphan ne relit plus quand le diagnostic suivant échoue (finally retiré) 1
Q6 useCategories.ts un échec de readCategoriesAgain part au bandeau de la page, comme dans loadCategories 0
Q7 useCategories.ts relecture après un rattachement lancée sans être attendue 1
Q8 categoryService.ts becomesParent lit is_inputable !== 0 0 (équivalente)
Q9 categoryService.ts les deux comptes de montants multipliés au lieu d'additionnés 11

Q8 est équivalente : la colonne est INTEGER NOT NULL DEFAULT 1, et toutes ses écritures lient 1/0 depuis la v0.2.5 (? 1 : 0, puis toSqlFlag dans updateCategory). La valeur lue vaut donc 0 ou 1.

Suggestions (non bloquantes)

  1. Échec de la relecture non épinglé (Q6). Trois sources disent qu'un échec de readCategoriesAgain s'affiche dans la fenêtre :

    • les JSDoc de useCategories.ts (lignes 417 et 452) ;
    • docs/architecture.md:286 ;
    • le corps de la PR.

    Aucun test ne le tient. Avec Q6, un échec à l'ouverture part au bandeau de la page, caché derrière la fenêtre. Le diagnostic tourne ensuite sur les anciennes catégories : c'est le défaut du point 1 de la revue 25437. Après un rattachement, la fenêtre ne dit rien.

    Un test suffit. getAllCategoriesWithCounts est rejeté une fois à l'ouverture, puis une fois après un rattachement réussi. Il vérifie que hierarchyCheck.error vaut { message }, que state.error reste null et que isSaving repasse à false. C'est une épingle sur du code correct : elle relève d'une issue de suivi, pas d'un tour de plus.

  2. Corps de la PR à mettre à jour.

    • « À regarder pour la revue » demande encore à Max de confirmer la règle en édition Gratuite. Il l'a tranchée le 2026-10-19 (commentaire 25885 : la règle s'applique aussi en Gratuite), et /pr-brief relirait la question comme ouverte.
    • La ligne « commits du second tour pas encore relus » tombe avec cette revue.
  3. Lignes d'ajustement hors de la règle, pour information : la décision du 2026-10-19 ne les nomme pas. adjustment_entries.category_id n'est pas vérifié, mais l'effet reste limité. Seule la page Ajustements lit ces lignes (adjustmentService.ts, aucun rapport ni budget). Sur cette page, une ligne dont la catégorie n'est plus saisissable garde sa catégorie, avec un champ vide (#429). Rien à changer, sauf avis contraire de Max.

  4. Détail de doc (architecture.md:664). grep -rl "test/hookHarness" src rend 22 lignes : les 21 fichiers de test, plus le harnais lui-même (src/test/hookHarness.ts). Avec --include='*.test.*', la commande rend 21.

Hors PR : deux avis RustSec sur le chemin de l'updater

L'étape cargo audit du run 601, informative, écrit error: 2 vulnerabilities found! :

  • RUSTSEC-2026-0382, rustls-webpki 0.103.15, corrigé en 0.103.16, daté du 2026-10-20 ;
  • RUSTSEC-2026-0383, webpki-root-certs 1.0.6, corrigé en 1.0.10, daté du 2026-10-19.

Cette PR ne touche pas au lock. rustls-webpki est atteint par tauri-plugin-updater → reqwest 0.13 → rustls 0.23.46 (cargo tree -i, cible Linux) : c'est le chemin de #447 et #455. Le run quotidien d'audit.yml du 2026-10-20 (599, 06:00 UTC) est passé avant la parution des deux avis. Celui du 2026-10-21 lira la même base que le run 601 et rougira. Aucune issue n'est ouverte à ce jour.

Vérifié sans constat

  • Règle du 2026-10-20.
    • becomesParent lit is_inputable dans les lignes de la hiérarchie, lues par le handle (HIERARCHY_ROWS_SQL), avec la condition de markAsParent (is_inputable = 1).
    • Les trois points d'entrée l'appliquent, et un parent existant passe sans aucune lecture de contenu (tests EXISTING_PARENT, mutation D-M1 de l'auteur).
    • Une feuille rendue non saisissable par la case « Saisissable » du formulaire passe aussi. C'est conforme à la définition : elle est déjà hors de la saisie, et le refus n'y protégerait rien.
    • À l'inverse, un parent dont deactivateCategory a retiré le dernier enfant redevient saisissable et se vérifie de nouveau.
  • Transactions.
    • Dans createCategory, la profondeur se lit avant la transaction, comme avant. Les lignes, le contenu, l'INSERT et markAsParent passent ensuite par le handle d'un seul withTransaction (Q1, Q2 et Q3 rouges).
    • Les répartitions sont des lignes de transactions (parent_transaction_id) : la vérification des transactions les compte.
  • Relecture sans état de chargement.
    • CategoryForm garde son état local tant que initialData ne change pas (useEffect sur [initialData]), et SET_CATEGORIES ne touche pas editingCategory. Sans SET_LOADING, le formulaire reste donc monté avec ses changements.
    • Un rattachement n'efface plus le bandeau de la page. Il reste cohérent avec le formulaire gardé, dont il explique le refus.
  • Textes.
    • Un modèle de budget ne se modifie pas : il n'existe que saveAsTemplate, applyTemplate et deleteTemplate, avec un bouton de suppression dans TemplateActions.
    • Une remise à zéro dans la grille écrit 0 (une cellule) ou supprime la ligne (toute l'année) : amount <> 0 couvre les deux cas.
    • « Budget vs Réel » est le libellé de l'interface.
    • FR et EN disent la même chose dans le CHANGELOG, le guide, docs.categories.tips et les deux messages d'erreur.
  • Comptes.
    • CLAUDE.md est exact : 12 modules hors mod.rs, 10 avec des commandes, 45 entrées dans generate_handler!.
    • auth_commands.rs porte 6 annotations pour 5 commandes enregistrées : handle_auth_callback passe par le lien profond.
  • Fermeture.
    • Les huit issues sont ouvertes, en status:approved, et leur timeline porte un pull_ref de #482 en none.
    • Aucun des 23 commits ne porte de mot de fermeture, et tous suivent le format conventionnel.
  • Sécurité.
    • Le SQL est paramétré ($1).
    • Les lignes ajoutées ne portent ni secret, ni jeton, ni donnée d'un tiers.
    • La PR n'ajoute ni migration ni dépendance : node:sqlite est intégré à Node.

Actions faites

  • Aucun label changé : les huit issues sont en status:approved, aucune n'est en status:review, et la PR n'en ferme aucune.
  • La copie hors dépôt (/tmp/pr482-review) et les fichiers /tmp/pr482-* sont supprimés en fin de revue.
Niveau : normal **Verdict : APPROVE** **Résumé.** Revue du second tour (`1386abd`, `bc66101`, `b626bcb`, `a17c1b7`), sur le diff complet de la PR. La décision de Max du 2026-10-20 est appliquée telle qu'écrite : le contenu d'un parent ne se vérifie que s'il est encore saisissable, pour les trois codes et sur les trois points d'entrée, à partir de lignes lues par le handle de la transaction. Après un rattachement, un formulaire ouvert derrière la fenêtre garde ses changements. Build, suite et CI sont verts sur `a17c1b7`, et 7 des 9 mutations du relecteur rougissent. Des deux survivantes, l'une est équivalente ; l'autre montre qu'aucun test ne tient l'affichage d'un échec de la relecture (suggestion 1). Niveau calculé sur les 32 fichiers. Aucun ne remplit une ligne du tableau Haut : - le code livré n'ajoute ni appel réseau, ni URL, ni consentement (`invoke("pick_folder")` est un appel IPC déjà présent) ; - `fs_commands.rs` ne change que dans son module de tests ; - `REVIEW.md` change aux lignes 130 et 190 (« Release et mise à jour », « Pièges de revue »), hors « Niveaux » ; - le seul fichier ajouté, `src/components/shared/CategoryCombobox.placement.test.ts`, n'est ni à la racine ni dans `src-tauri/` ; - le second tour ne touche que du TypeScript, des locales et de la documentation. ### Ce que la revue a exécuté Sur un clone détaché hors du dépôt (`/tmp/pr482-review`), à la tête `a17c1b7`, après `npm ci` : - sonde volontairement rouge : rc=1 ; - `npm run build` : rc=0 ; - `npx vitest run` : 156 fichiers, 2559 tests, rc=0 (les chiffres du corps). Sous Node 22.22 en local, `node:sqlite` affiche son avertissement expérimental, sans effet sur les tests ; - CI lue dans les journaux : - `check-frontend`, run 600 : checkout de `a17c1b7`, Node `v24.22.0`, build, 156 fichiers et 2559 tests verts ; - `check-rust`, run 601 : checkout de `a17c1b7`, cache restauré, gardes 2/2 et 9/9, `refuses_a_file_named_like_a_sample_folder_and_writes_nothing ... ok`, 269 réussis, 1 ignoré. - Rust non recompilé en local : le second tour n'y touche pas, et le seul changement Rust de la PR (un test, premier tour) tourne en CI. ### Mutations du relecteur (second tour) Les mutations sont appliquées une à la fois, avec la suite complète. Après chacune, le fichier est restauré par `git checkout --` et l'arbre redevient propre. | # | Fichier | Mutation | Tests rouges (sur 2559) | |---|---|---|---| | Q1 | `categoryService.ts` | `createCategory` lit les lignes par `getDb()` dans la transaction | 9 | | Q2 | `categoryService.ts` | `createCategory` lit les lignes avant `BEGIN`, hors transaction | 1 | | Q3 | `categoryService.ts` | `markAsParent` avant la vérification du contenu, dans `createCategory` | 7 | | Q4 | `useCategories.ts` | `readCategoriesAgain` envoie `SET_TREE` au lieu de `SET_CATEGORIES` | 2 | | Q5 | `useCategories.ts` | `reattachOrphan` ne relit plus quand le diagnostic suivant échoue (`finally` retiré) | 1 | | Q6 | `useCategories.ts` | un échec de `readCategoriesAgain` part au bandeau de la page, comme dans `loadCategories` | **0** | | Q7 | `useCategories.ts` | relecture après un rattachement lancée sans être attendue | 1 | | Q8 | `categoryService.ts` | `becomesParent` lit `is_inputable !== 0` | **0** (équivalente) | | Q9 | `categoryService.ts` | les deux comptes de montants multipliés au lieu d'additionnés | 11 | Q8 est équivalente : la colonne est `INTEGER NOT NULL DEFAULT 1`, et toutes ses écritures lient `1`/`0` depuis la v0.2.5 (`? 1 : 0`, puis `toSqlFlag` dans `updateCategory`). La valeur lue vaut donc `0` ou `1`. ### Suggestions (non bloquantes) 1. **Échec de la relecture non épinglé (Q6).** Trois sources disent qu'un échec de `readCategoriesAgain` s'affiche dans la fenêtre : - les JSDoc de `useCategories.ts` (lignes 417 et 452) ; - `docs/architecture.md:286` ; - le corps de la PR. Aucun test ne le tient. Avec Q6, un échec à l'ouverture part au bandeau de la page, caché derrière la fenêtre. Le diagnostic tourne ensuite sur les anciennes catégories : c'est le défaut du point 1 de la revue 25437. Après un rattachement, la fenêtre ne dit rien. Un test suffit. `getAllCategoriesWithCounts` est rejeté une fois à l'ouverture, puis une fois après un rattachement réussi. Il vérifie que `hierarchyCheck.error` vaut `{ message }`, que `state.error` reste `null` et que `isSaving` repasse à `false`. C'est une épingle sur du code correct : elle relève d'une issue de suivi, pas d'un tour de plus. 2. **Corps de la PR à mettre à jour.** - « À regarder pour la revue » demande encore à Max de confirmer la règle en édition Gratuite. Il l'a tranchée le 2026-10-19 (commentaire 25885 : la règle s'applique aussi en Gratuite), et `/pr-brief` relirait la question comme ouverte. - La ligne « commits du second tour pas encore relus » tombe avec cette revue. 3. **Lignes d'ajustement hors de la règle**, pour information : la décision du 2026-10-19 ne les nomme pas. `adjustment_entries.category_id` n'est pas vérifié, mais l'effet reste limité. Seule la page Ajustements lit ces lignes (`adjustmentService.ts`, aucun rapport ni budget). Sur cette page, une ligne dont la catégorie n'est plus saisissable garde sa catégorie, avec un champ vide (#429). Rien à changer, sauf avis contraire de Max. 4. **Détail de doc** (`architecture.md:664`). `grep -rl "test/hookHarness" src` rend 22 lignes : les 21 fichiers de test, plus le harnais lui-même (`src/test/hookHarness.ts`). Avec `--include='*.test.*'`, la commande rend 21. ### Hors PR : deux avis RustSec sur le chemin de l'updater L'étape `cargo audit` du run 601, informative, écrit `error: 2 vulnerabilities found!` : - RUSTSEC-2026-0382, `rustls-webpki` 0.103.15, corrigé en 0.103.16, daté du 2026-10-20 ; - RUSTSEC-2026-0383, `webpki-root-certs` 1.0.6, corrigé en 1.0.10, daté du 2026-10-19. Cette PR ne touche pas au lock. `rustls-webpki` est atteint par `tauri-plugin-updater` → `reqwest` 0.13 → `rustls` 0.23.46 (`cargo tree -i`, cible Linux) : c'est le chemin de #447 et #455. Le run quotidien d'`audit.yml` du 2026-10-20 (599, 06:00 UTC) est passé avant la parution des deux avis. Celui du 2026-10-21 lira la même base que le run 601 et rougira. Aucune issue n'est ouverte à ce jour. ### Vérifié sans constat - **Règle du 2026-10-20.** - `becomesParent` lit `is_inputable` dans les lignes de la hiérarchie, lues par le handle (`HIERARCHY_ROWS_SQL`), avec la condition de `markAsParent` (`is_inputable = 1`). - Les trois points d'entrée l'appliquent, et un parent existant passe sans aucune lecture de contenu (tests `EXISTING_PARENT`, mutation D-M1 de l'auteur). - Une feuille rendue non saisissable par la case « Saisissable » du formulaire passe aussi. C'est conforme à la définition : elle est déjà hors de la saisie, et le refus n'y protégerait rien. - À l'inverse, un parent dont `deactivateCategory` a retiré le dernier enfant redevient saisissable et se vérifie de nouveau. - **Transactions.** - Dans `createCategory`, la profondeur se lit avant la transaction, comme avant. Les lignes, le contenu, l'`INSERT` et `markAsParent` passent ensuite par le handle d'un seul `withTransaction` (Q1, Q2 et Q3 rouges). - Les répartitions sont des lignes de `transactions` (`parent_transaction_id`) : la vérification des transactions les compte. - **Relecture sans état de chargement.** - `CategoryForm` garde son état local tant que `initialData` ne change pas (`useEffect` sur `[initialData]`), et `SET_CATEGORIES` ne touche pas `editingCategory`. Sans `SET_LOADING`, le formulaire reste donc monté avec ses changements. - Un rattachement n'efface plus le bandeau de la page. Il reste cohérent avec le formulaire gardé, dont il explique le refus. - **Textes.** - Un modèle de budget ne se modifie pas : il n'existe que `saveAsTemplate`, `applyTemplate` et `deleteTemplate`, avec un bouton de suppression dans `TemplateActions`. - Une remise à zéro dans la grille écrit `0` (une cellule) ou supprime la ligne (toute l'année) : `amount <> 0` couvre les deux cas. - « Budget vs Réel » est le libellé de l'interface. - FR et EN disent la même chose dans le CHANGELOG, le guide, `docs.categories.tips` et les deux messages d'erreur. - **Comptes.** - `CLAUDE.md` est exact : 12 modules hors `mod.rs`, 10 avec des commandes, 45 entrées dans `generate_handler!`. - `auth_commands.rs` porte 6 annotations pour 5 commandes enregistrées : `handle_auth_callback` passe par le lien profond. - **Fermeture.** - Les huit issues sont ouvertes, en `status:approved`, et leur timeline porte un `pull_ref` de #482 en `none`. - Aucun des 23 commits ne porte de mot de fermeture, et tous suivent le format conventionnel. - **Sécurité.** - Le SQL est paramétré (`$1`). - Les lignes ajoutées ne portent ni secret, ni jeton, ni donnée d'un tiers. - La PR n'ajoute ni migration ni dépendance : `node:sqlite` est intégré à Node. ### Actions faites - Aucun label changé : les huit issues sont en `status:approved`, aucune n'est en `status:review`, et la PR n'en ferme aucune. - La copie hors dépôt (`/tmp/pr482-review`) et les fichiers `/tmp/pr482-*` sont supprimés en fin de revue.
maximus added 2 commits 2026-10-21 00:16:59 +00:00
Review 25974, suggestion 1 (Q6): the JSDoc, architecture.md and the PR
body say a failure of readCategoriesAgain shows in the hierarchy window,
and no test held it. The read is rejected once at the opening (the
diagnostic does not run on the old categories) and once after a
committed reattachment: each time the window shows the message, the
page's banner behind it stays empty, and saving is over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: the command that lists the hook harness users counts test files only (#437)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m8s
PR Check — Rust / rust (pull_request) Successful in 3m43s
c50ed53ba7
Review 25974, suggestion 4: `grep -rl "test/hookHarness" src` printed
22 lines, the harness itself included; with --include='*.test.*' it
prints the 21 test files the sentence counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Troisième tour sur #482 : deux commits en tête de la branche (a17c1b7..c50ed53), un test et une ligne de doc, pas encore relus. Ils ne touchent ni au code livré, ni au lock, ni au Rust, ni à un workflow.

  • 13dca87 test(categories) : un échec de la relecture des catégories s'affiche dans la fenêtre « Vérifier la hiérarchie », à l'ouverture comme après un rattachement réussi. Le bandeau de la page reste vide et l'enregistrement se termine (revue 25974, suggestion 1 ; Q6 rougit).
  • c50ed53 docs : dans architecture.md, la commande qui liste les utilisateurs du harnais filtre les fichiers de test et rend les 21 annoncés (suggestion 4).

Corps de la PR à jour. La question sur l'édition Gratuite est retirée, tranchée le 2026-10-19 (commentaire 25885). Les deux décisions de Max du 2026-10-20 y sont consignées : les lignes d'ajustement ne bloquent pas, et les avis RustSec sont suivis par #484, hors pile. Le tableau des preuves et les comptes sont mis à jour.

Preuves : sonde rc=1, build vert, 156 fichiers et 2560 tests verts.

Troisième tour sur #482 : deux commits en tête de la branche (`a17c1b7..c50ed53`), un test et une ligne de doc, **pas encore relus**. Ils ne touchent ni au code livré, ni au lock, ni au Rust, ni à un workflow. - `13dca87` test(categories) : un échec de la relecture des catégories s'affiche dans la fenêtre « Vérifier la hiérarchie », à l'ouverture comme après un rattachement réussi. Le bandeau de la page reste vide et l'enregistrement se termine (revue 25974, suggestion 1 ; Q6 rougit). - `c50ed53` docs : dans `architecture.md`, la commande qui liste les utilisateurs du harnais filtre les fichiers de test et rend les 21 annoncés (suggestion 4). Corps de la PR à jour. La question sur l'édition Gratuite est retirée, tranchée le 2026-10-19 (commentaire 25885). Les deux décisions de Max du 2026-10-20 y sont consignées : les lignes d'ajustement ne bloquent pas, et les avis RustSec sont suivis par #484, hors pile. Le tableau des preuves et les comptes sont mis à jour. Preuves : sonde rc=1, build vert, 156 fichiers et 2560 tests verts.
Author
Owner

CI lue par l'orchestrateur sur la tête c50ed53 : les deux contrôles sont verts.

  • check-frontend (run 602) : 156 fichiers et 2560 tests verts.
  • check-rust (run 603) :
    • garde windows-sys : 9 checks, exit 0 ;
    • cargo test : 269 passés, 1 ignoré.
  • L'étape cargo audit de check-rust, informative, écrit error: 2 vulnerabilities found!. Ce sont RUSTSEC-2026-0382 et RUSTSEC-2026-0383, présentes sur main et suivies par #484 (décision de Max du 2026-10-20 : issue seulement, hors de cette pile).

Les deux commits du troisième tour (13dca87 test, c50ed53 doc) n'ont pas eu de nouvelle revue, comme le proposait le commentaire 25974 pour ces épingles. Le reste de la PR est relu (commentaires 25921 et 25974, APPROVE).

CI lue par l'orchestrateur sur la tête `c50ed53` : les deux contrôles sont **verts**. - `check-frontend` (run 602) : 156 fichiers et 2560 tests verts. - `check-rust` (run 603) : - garde `windows-sys` : `9 checks, exit 0` ; - `cargo test` : 269 passés, 1 ignoré. - L'étape `cargo audit` de `check-rust`, informative, écrit `error: 2 vulnerabilities found!`. Ce sont RUSTSEC-2026-0382 et RUSTSEC-2026-0383, présentes sur `main` et suivies par #484 (décision de Max du 2026-10-20 : issue seulement, hors de cette pile). Les deux commits du troisième tour (`13dca87` test, `c50ed53` doc) n'ont pas eu de nouvelle revue, comme le proposait le commentaire 25974 pour ces épingles. Le reste de la PR est relu (commentaires 25921 et 25974, APPROVE).
maximus changed target branch from issue-446-review-followups to main 2026-10-21 00:31:41 +00:00
maximus merged commit c50ed53ba7 into main 2026-10-21 00:31:46 +00:00
Author
Owner

Mergée en avance rapide par /merge-stack le 2026-10-20, avec les huit maillons de la pile (#468 → #475), sur la tête relue et validée c50ed53. main est maintenant à c50ed53.

  • La tête cumulée a été éprouvée en local avant le merge, dans un worktree jetable avec un npm ci propre :
    • la sonde sort en code 1 ;
    • le build passe, et npm test donne 156 fichiers et 2560 tests verts ;
    • cargo check --locked passe, et cargo test --locked donne 269 tests réussis et 1 ignoré ;
    • les gardes de check-rust.yml passent : 2 vérifications sur les avis acceptés, 9 sur l'épinglage windows-sys.
  • Ajoutés après la dernière revue (25974), et non relus : 13dca87 (test Q6) et c50ed53 (une ligne de doc). Ce sont des tests et de la doc seulement, classés triviaux.
  • Aucune issue fermée par cette PR (références simples) ; les huit issues de la pile sont fermées par leurs propres PR, milestone planned-2026-10-16-lot-v018 fermée.
Mergée en avance rapide par `/merge-stack` le 2026-10-20, avec les huit maillons de la pile (#468 → #475), sur la tête relue et validée `c50ed53`. `main` est maintenant à `c50ed53`. - La tête cumulée a été éprouvée en local avant le merge, dans un worktree jetable avec un `npm ci` propre : - la sonde sort en code 1 ; - le build passe, et `npm test` donne 156 fichiers et 2560 tests verts ; - `cargo check --locked` passe, et `cargo test --locked` donne 269 tests réussis et 1 ignoré ; - les gardes de `check-rust.yml` passent : 2 vérifications sur les avis acceptés, 9 sur l'épinglage `windows-sys`. - Ajoutés après la dernière revue (25974), et non relus : `13dca87` (test Q6) et `c50ed53` (une ligne de doc). Ce sont des tests et de la doc seulement, classés triviaux. - Aucune issue fermée par cette PR (références simples) ; les huit issues de la pile sont fermées par leurs propres PR, milestone `planned-2026-10-16-lot-v018` fermée.
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#482
No description provided.