fix: checklist step 5, secondary-profile migrations and Linux database paths (#390) #394

Merged
maximus merged 5 commits from issue-390-manual-flag-profile-migrations into main 2026-10-07 02:00:03 +00:00
Owner

Refs #390

Ce qui change pour l'utilisateur

  • Liste de démarrage : classer une transaction à la main sur la page Transactions coche l'étape 5. Les transactions classées à la main avant cette version sont comptées dès l'ouverture du profil, et une sauvegarde JSON ou CSV exportée entre-temps (elle porte "true") se restaure correctement.
  • Profils secondaires : ils reçoivent enfin les mises à jour de leur base. Jusqu'ici, un profil autre que le profil par défaut gardait le schéma de la version qui l'avait créé : créé avant la 0.15.0, il n'avait pas le format d'import des sources ; créé avant la 0.10.0, il n'avait pas le détail par titre du Bilan. À l'ouverture d'un tel profil, l'application garde d'abord une copie de sa base dans migration-backups/, à côté des bases, puis applique les versions manquantes. La copie est supprimée avec le profil.
  • Linux : supprimer un profil supprime enfin sa base, qui restait dans ~/.config/com.simpl.resultat/. Les colonnes de rendement du Bilan affichent les rendements au lieu d'un tiret par cellule, et repair_migrations agit enfin.
  • Journal (Paramètres → Système) : chaque mise à jour, copie, réparation ou arrêt y est écrit, avec des noms de fichiers seulement.

Écarts au corps de l'issue

Décisions de Max du 2026-10-06, prises en cours de /fix-issue :

  • Pas de migration v18. tauri-plugin-sql n'applique les migrations qu'à l'URL enregistrée par add_migrations (sqlite:simpl_resultat.db) et retire la liste au premier chargement. Une v18 n'aurait réparé que le profil par défaut. La réparation des drapeaux texte se fait donc à l'ouverture de chaque profil.
  • Le rattrapage des migrations des profils secondaires est inclus dans #390.
  • Trouvé par le plan checker : le plugin range les bases sous app_config_dir(), alors que trois commandes les cherchaient sous app_data_dir(). C'est le même dossier sous Windows et macOS, pas sous Linux. Les trois commandes sont corrigées par le même helper.

Technique

TypeScript

  • utils/sqlFlag.ts : toSqlFlag transforme un booléen, un nombre ou la chaîne d'une sauvegarde en 0/1. updateTransactionCategory, parseImportedCsv et l'INSERT de restauration l'utilisent.
  • db.ts : connectToProfile invoque prepare_profile_db après repair_migrations et avant Database.load. describePreparation choisit les lignes de Journal, et un échec n'empêche pas l'ouverture.

Rust

  • lib.rs : la liste sort de run() dans migrations() sans qu'un octet bouge. git diff -U0 main -- src-tauri/src/lib.rs ne montre que les lignes d'enveloppe, l'URL passée par DEFAULT_PROFILE_DB, la commande enregistrée et un commentaire de test hors du bloc. Les SHA-384 de v1 à v17, calculés sur main avant le déplacement par un crate jetable qui recopiait le bloc tel quel, sont épinglés par migration_checksums_match_the_released_ones.
  • database/profile_db.rs :
    • infer_floor déduit du schéma le plancher d'une base sans historique ;
    • catch_up écrit l'historique en une transaction, puis applique chaque version dans la sienne, avec les clés étrangères actives ;
    • backup_to copie la base par VACUUM INTO vers un chemin lié, synchronise un .tmp, puis le renomme ;
    • history_sql, repair_manual_flags, prepare_at, et scrub qui retire le dossier des messages.
  • profile_commands.rs :
    • profile_db_path résout app_config_dir() et valide le nom (simpl_resultat.db ou profile_<hex>.db) ;
    • prepare_profile_db est synchrone, donc sérialisée sur le fil principal ;
    • delete_profile_db emporte aussi -wal, -shm, -journal et les copies ;
    • get_new_profile_init_sql ajoute l'historique complet, si bien qu'un nouveau profil ne passe jamais par l'inférence ;
    • repair_migrations cherche dans le bon dossier.
  • balance_commands.rs : compute_account_return passe par profile_db_path, et son message ne contient plus de chemin.
  • Cargo.toml : tauri avec features = ["test"] en dev-dependency, pour mock_app(). La feature n'ajoute aucune crate et Cargo.lock est inchangé.
  • database/fixtures/ : les sept états de création publiés, copiés à l'identique de git (v0.3.0 consolidé + seed, seed v0.4.0, consolidés v0.8.4, v0.9.0, v0.9.1, v0.10.0). L'état courant est CONSOLIDATED_SCHEMA.

Documentation

  • Nouvel ADR 0021 et amendements des ADR 0003 et 0005, dont deux affirmations étaient fausses.
  • Mises à jour : architecture.md, CLAUDE.md, règle .claude/rules/sql-migrations.md (une correction de seed se reporte aussi dans le schéma consolidé), guide FR et EN avec les clés docs.*, CHANGELOG FR et EN (trois entrées sous Corrigé).

Preuves

  • Rouge d'abord :
    • updateTransactionCategory liait [12, true, 5] au lieu de [12, 1, 5] ;
    • la restauration JSON stockait 'true' ;
    • le CSV lisait "true" comme 0.
  • Suites :
    • vitest 1498 → 1519 ;
    • Rust 224 → 254 ;
    • npm run build vert, cargo check sans avertissement.
  • Données réelles : chaque état publié, rattrapé, a les mêmes colonnes, clés étrangères et index qu'un profil neuf. Les données de test passent par les vraies migrations :
    • compte CELI déplacé par v12 et v13 ;
    • compte coté converti en titre par v15 et v16 ;
    • snapshot orphelin retiré par v11 ;
    • source débit/crédit complétée par v17.
  • Mutations, une à la fois sur l'arbre committé, en vérifiant que chaque mutation a bien atterri, puis restauration par git (22/22 rouges) :
Mutation Rouges
M1 booléen lié de nouveau 2
M2 toSqlFlag oublie "true" 4
M3 CSV relu par parseInt 1
M4 restauration de la valeur brute 2
M5 connectToProfile saute la préparation 3
R1 plancher 17 → 16 2
R2 plancher 16 → 15 (rejoue v16 sur E) 6
R3 plancher 10 → 11 (saute v11 sur C/D) 3
R4 versions à appliquer sans le plancher 12
R5 pas de reprise sur une clé étrangère 2
R6 reprise sans réparation 2
R7 copie après l'écriture de l'historique 2
R8 réparation sans son WHERE 2
R9 bases cherchées sous app_data_dir 1
R10 n'importe quel nom de fichier accepté 1
R11 scrub garde le dossier 1
R12 history_sql n'échappe pas ' 1
R13 le profil par défaut rattrapé aussi 1
R14 clés étrangères coupées pour tout le rattrapage 3
R15 la suppression garde les copies 1
R16 nouveau profil sans historique 1
R17 une copie existante écrasée 1

Non gardé par les tests :

  • L'atomicité de l'écriture de l'historique n'a pas de test de plantage, mais la reprise après une écriture partielle est testée : historique vide ou limité à 1-3, sans rejouer le seed v2.
  • Le garde explicite contre v1/v2 est inatteignable tant que le plancher tient ; c'est une seconde ligne de défense.
  • La distinction entre les deux dossiers n'est visible que sous Linux, la plateforme de la CI.
  • Le dossier réel où le plugin range les bases demande une vérification à la main (liste ci-dessous).

Plan checker : trois passes, chacune refusée.

  • Passe 1 : bloquant sur le dossier Linux.
  • Passe 2 : majeur sur les clés étrangères coupées.
  • Passe 3 : majeur sur sync_all sous Windows, déjà réglé dans le code par OpenOptions::new().write(true).

Max a validé la suite après la troisième passe.

Post-merge checklist (hence Refs, not a closing keyword)

  1. Build local ou release : classer une transaction sur la page Transactions, puis revenir au tableau de bord. La case 5 doit être cochée.
  2. Sous Linux (Pop!_OS), avec un vrai profil secondaire ancien : à l'ouverture, le Journal doit afficher « database updated with versions … », la copie doit apparaître dans ~/.config/com.simpl.resultat/migration-backups/, et l'import doit fonctionner dans ce profil.
  3. Sous Linux, supprimer un profil de test : son .db et ses fichiers annexes doivent disparaître de ~/.config/com.simpl.resultat/.
  4. Sous Linux, les colonnes de rendement du Bilan doivent être remplies.
  5. Un profil qui avait des classements manuels avant la mise à jour doit avoir sa case 5 cochée.
  6. #390 sera fermée à la main, avec les preuves.

🤖 Generated with Claude Code

Refs #390 ## Ce qui change pour l'utilisateur - **Liste de démarrage** : classer une transaction à la main sur la page Transactions coche l'étape 5. Les transactions classées à la main avant cette version sont comptées dès l'ouverture du profil, et une sauvegarde JSON ou CSV exportée entre-temps (elle porte `"true"`) se restaure correctement. - **Profils secondaires** : ils reçoivent enfin les mises à jour de leur base. Jusqu'ici, un profil autre que le profil par défaut gardait le schéma de la version qui l'avait créé : créé avant la 0.15.0, il n'avait pas le format d'import des sources ; créé avant la 0.10.0, il n'avait pas le détail par titre du Bilan. À l'ouverture d'un tel profil, l'application garde d'abord une copie de sa base dans `migration-backups/`, à côté des bases, puis applique les versions manquantes. La copie est supprimée avec le profil. - **Linux** : supprimer un profil supprime enfin sa base, qui restait dans `~/.config/com.simpl.resultat/`. Les colonnes de rendement du Bilan affichent les rendements au lieu d'un tiret par cellule, et `repair_migrations` agit enfin. - **Journal** (Paramètres → Système) : chaque mise à jour, copie, réparation ou arrêt y est écrit, avec des noms de fichiers seulement. ## Écarts au corps de l'issue Décisions de Max du 2026-10-06, prises en cours de `/fix-issue` : - **Pas de migration v18.** tauri-plugin-sql n'applique les migrations qu'à l'URL enregistrée par `add_migrations` (`sqlite:simpl_resultat.db`) et retire la liste au premier chargement. Une v18 n'aurait réparé que le profil par défaut. La réparation des drapeaux texte se fait donc à l'ouverture de chaque profil. - **Le rattrapage des migrations des profils secondaires est inclus dans #390.** - **Trouvé par le plan checker** : le plugin range les bases sous `app_config_dir()`, alors que trois commandes les cherchaient sous `app_data_dir()`. C'est le même dossier sous Windows et macOS, pas sous Linux. Les trois commandes sont corrigées par le même helper. ## Technique **TypeScript** - `utils/sqlFlag.ts` : `toSqlFlag` transforme un booléen, un nombre ou la chaîne d'une sauvegarde en 0/1. `updateTransactionCategory`, `parseImportedCsv` et l'INSERT de restauration l'utilisent. - `db.ts` : `connectToProfile` invoque `prepare_profile_db` après `repair_migrations` et avant `Database.load`. `describePreparation` choisit les lignes de Journal, et un échec n'empêche pas l'ouverture. **Rust** - `lib.rs` : la liste sort de `run()` dans `migrations()` sans qu'un octet bouge. `git diff -U0 main -- src-tauri/src/lib.rs` ne montre que les lignes d'enveloppe, l'URL passée par `DEFAULT_PROFILE_DB`, la commande enregistrée et un commentaire de test hors du bloc. Les SHA-384 de v1 à v17, calculés sur `main` avant le déplacement par un crate jetable qui recopiait le bloc tel quel, sont épinglés par `migration_checksums_match_the_released_ones`. - `database/profile_db.rs` : - `infer_floor` déduit du schéma le plancher d'une base sans historique ; - `catch_up` écrit l'historique en une transaction, puis applique chaque version dans la sienne, avec les clés étrangères actives ; - `backup_to` copie la base par `VACUUM INTO` vers un chemin lié, synchronise un `.tmp`, puis le renomme ; - `history_sql`, `repair_manual_flags`, `prepare_at`, et `scrub` qui retire le dossier des messages. - `profile_commands.rs` : - `profile_db_path` résout `app_config_dir()` et valide le nom (`simpl_resultat.db` ou `profile_<hex>.db`) ; - `prepare_profile_db` est synchrone, donc sérialisée sur le fil principal ; - `delete_profile_db` emporte aussi `-wal`, `-shm`, `-journal` et les copies ; - `get_new_profile_init_sql` ajoute l'historique complet, si bien qu'un nouveau profil ne passe jamais par l'inférence ; - `repair_migrations` cherche dans le bon dossier. - `balance_commands.rs` : `compute_account_return` passe par `profile_db_path`, et son message ne contient plus de chemin. - `Cargo.toml` : tauri avec `features = ["test"]` en dev-dependency, pour `mock_app()`. La feature n'ajoute aucune crate et `Cargo.lock` est inchangé. - `database/fixtures/` : les sept états de création publiés, copiés à l'identique de git (v0.3.0 consolidé + seed, seed v0.4.0, consolidés v0.8.4, v0.9.0, v0.9.1, v0.10.0). L'état courant est `CONSOLIDATED_SCHEMA`. **Documentation** - Nouvel ADR 0021 et amendements des ADR 0003 et 0005, dont deux affirmations étaient fausses. - Mises à jour : `architecture.md`, `CLAUDE.md`, règle `.claude/rules/sql-migrations.md` (une correction de seed se reporte aussi dans le schéma consolidé), guide FR et EN avec les clés `docs.*`, CHANGELOG FR et EN (trois entrées sous Corrigé). ## Preuves - **Rouge d'abord** : - `updateTransactionCategory` liait `[12, true, 5]` au lieu de `[12, 1, 5]` ; - la restauration JSON stockait `'true'` ; - le CSV lisait `"true"` comme 0. - **Suites** : - vitest 1498 → **1519** ; - Rust 224 → **254** ; - `npm run build` vert, `cargo check` sans avertissement. - **Données réelles** : chaque état publié, rattrapé, a les mêmes colonnes, clés étrangères et index qu'un profil neuf. Les données de test passent par les vraies migrations : - compte CELI déplacé par v12 et v13 ; - compte coté converti en titre par v15 et v16 ; - snapshot orphelin retiré par v11 ; - source débit/crédit complétée par v17. - **Mutations**, une à la fois sur l'arbre committé, en vérifiant que chaque mutation a bien atterri, puis restauration par git (22/22 rouges) : | Mutation | Rouges | |---|---| | M1 booléen lié de nouveau | 2 | | M2 `toSqlFlag` oublie `"true"` | 4 | | M3 CSV relu par `parseInt` | 1 | | M4 restauration de la valeur brute | 2 | | M5 `connectToProfile` saute la préparation | 3 | | R1 plancher 17 → 16 | 2 | | R2 plancher 16 → 15 (rejoue v16 sur E) | 6 | | R3 plancher 10 → 11 (saute v11 sur C/D) | 3 | | R4 versions à appliquer sans le plancher | 12 | | R5 pas de reprise sur une clé étrangère | 2 | | R6 reprise sans réparation | 2 | | R7 copie après l'écriture de l'historique | 2 | | R8 réparation sans son `WHERE` | 2 | | R9 bases cherchées sous `app_data_dir` | 1 | | R10 n'importe quel nom de fichier accepté | 1 | | R11 `scrub` garde le dossier | 1 | | R12 `history_sql` n'échappe pas `'` | 1 | | R13 le profil par défaut rattrapé aussi | 1 | | R14 clés étrangères coupées pour tout le rattrapage | 3 | | R15 la suppression garde les copies | 1 | | R16 nouveau profil sans historique | 1 | | R17 une copie existante écrasée | 1 | **Non gardé par les tests** : - L'atomicité de l'écriture de l'historique n'a pas de test de plantage, mais la reprise après une écriture partielle est testée : historique vide ou limité à 1-3, sans rejouer le seed v2. - Le garde explicite contre v1/v2 est inatteignable tant que le plancher tient ; c'est une seconde ligne de défense. - La distinction entre les deux dossiers n'est visible que sous Linux, la plateforme de la CI. - Le dossier réel où le plugin range les bases demande une vérification à la main (liste ci-dessous). **Plan checker** : trois passes, chacune refusée. - Passe 1 : bloquant sur le dossier Linux. - Passe 2 : majeur sur les clés étrangères coupées. - Passe 3 : majeur sur `sync_all` sous Windows, déjà réglé dans le code par `OpenOptions::new().write(true)`. Max a validé la suite après la troisième passe. ## Post-merge checklist (hence `Refs`, not a closing keyword) 1. Build local ou release : classer une transaction sur la page Transactions, puis revenir au tableau de bord. La case 5 doit être cochée. 2. Sous Linux (Pop!_OS), avec un vrai profil secondaire ancien : à l'ouverture, le Journal doit afficher « database updated with versions … », la copie doit apparaître dans `~/.config/com.simpl.resultat/migration-backups/`, et l'import doit fonctionner dans ce profil. 3. Sous Linux, supprimer un profil de test : son `.db` et ses fichiers annexes doivent disparaître de `~/.config/com.simpl.resultat/`. 4. Sous Linux, les colonnes de rendement du Bilan doivent être remplies. 5. Un profil qui avait des classements manuels avant la mise à jour doit avoir sa case 5 cochée. 6. #390 sera fermée à la main, avec les preuves. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 4 commits 2026-10-07 00:56:34 +00:00
updateTransactionCategory bound the JS boolean `true`. tauri-plugin-sql
passes a bool to sqlx as a JSON value, so the column received the TEXT
'true', which the getting-started checklist's `is_manually_categorized = 1`
never matches: step 5 never ticked.

toSqlFlag turns a boolean, a number or a backup's string into 0/1. It is
used when a category is set by hand and when a JSON or CSV backup is
restored, since a backup exported while the bug stood carries "true"
(the CSV path read it as 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tauri-plugin-sql migrates only the database registered with add_migrations,
the default profile's. A secondary profile kept the consolidated schema of
the version that created it and never received a migration afterwards.

prepare_profile_db runs before every Database.load:
- secondary profiles catch up with the same migrations() list (pinned
  SHA-384), history kept in _sqlx_migrations; a database without history
  gets a floor inferred from its schema (the seven released creation
  states are test fixtures), so nothing at or below it is ever replayed;
- a VACUUM INTO copy is taken before the first write, synced, then renamed;
- each version runs in its own transaction with foreign keys on; one that
  fails on a foreign key is retried once without them and its orphans are
  set to NULL where the key declares ON DELETE SET NULL;
- every profile gets its TEXT manual-categorization flags repaired;
- failures are logged with file names only and the profile still opens.

New profiles are born with their full history (get_new_profile_init_sql).

The plugin keeps databases under app_config_dir(), not app_data_dir():
the same folder on Windows and macOS, not on Linux. repair_migrations,
delete_profile_db and compute_account_return looked in the wrong folder
there. profile_db_path resolves the plugin's folder and validates the
file name; deleting a profile now also removes its SQLite side files and
catch-up copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0021 records the catch-up rules and amends ADR 0003 and 0005, whose
claims about secondary profiles were false. architecture.md, CLAUDE.md
and the migration rule describe the two migration paths; the guide tells
where the pre-update copy lives and how to remove a database a deletion
on Linux left behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(profiles): cover a v0.15-v0.17 secondary profile without history (#390)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
PR Check — Rust / rust (pull_request) Successful in 5m9s
71147c5083
The released-states loop stops at v0.10-v0.14 (state E). State F, the
consolidated schema of v0.15.0 to v0.17.0 without a history, only had
its floor checked; this test runs the whole preparation on it: history
recorded up to v17, nothing applied, no copy, flag repaired.

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

Revue adversariale — PR #394 (#390)

Verdict : APPROVE

Résumé. Le correctif est correct et solidement gardé : le drapeau est lié en entier et réparé dans tous les profils, les profils secondaires rattrapent leurs migrations à partir d'un plancher déduit d'un ensemble d'états publiés que j'ai vérifié complet, et les trois commandes visent enfin le dossier du plugin. Aucun bloquant. Les suggestions portent sur des épingles manquantes autour d'un code correct, sur le Journal d'un rattrapage arrêté et sur la rétention des copies.

Ce que j'ai rejoué

  • Suites sur la tête 71147c5, dans une copie hors dépôt (git archive, npm ci, dossier cible cargo séparé) : vitest 1519/1519, cargo test 254/254. CI de la tête lue dans les logs : run 493 (frontend) 1519 verts et build vert, run 494 (Rust) 254 verts, cargo check sans avertissement.
  • Checksums : recalculés hors du code de la PR, par un parseur Python des littéraux Rust de lib.rs (continuations \ comprises) et des fichiers include_str!, sur main (8907eef) et sur le tag v0.17.0. Les 17 SHA-384 sont identiques aux valeurs épinglées par migration_checksums_match_the_released_ones. git diff -U0 main -- src-tauri/src/lib.rs ne touche que l'enveloppe.
  • Fixtures : leurs blobs git sont ceux des fichiers publiés (227d3b7, aeba40c, 0582449, cb593ec, 6341d9d, 83a08a9, 17f5503). Le relevé des 51 tags qui ont des profils (67 en tout) confirme que l'ensemble est fermé : le seed 0582449 n'a existé qu'en v0.4.0 (aeba40c revient en v0.4.1), le consolidé 227d3b7 couvre v0.3.0 à v0.8.3, puis un état par changement (v0.8.4, v0.9.0, v0.9.1, v0.10.0 à v0.14.0, v0.15.0 à v0.17.0). Le nom profile_${id.split("-")[0]}.db n'a pas changé depuis v0.3.0, et aucun code TypeScript n'a jamais modifié un schéma à l'exécution (git log -S "ALTER TABLE" -- src/ est vide) : pas d'état hors fixtures.
  • Prémisses du plugin, lues dans tauri-plugin-sql 2.3.2 : app_config_dir() dans wrapper.rs, liste retirée au premier load de la seule URL enregistrée, booléen lié par query.bind(value) (valeur JSON, donc texte). sqlx-sqlite 0.8.6 active les clés étrangères par défaut et ne fixe pas journal_mode : les bases sont en journal de retour arrière.
  • Fermeture : la timeline de #390 porte le pull_ref de #394 avec ref_action: none, et aucun mot-clé de fermeture n'apparaît dans le corps ni dans les commits. Cohérent avec Refs et une fermeture à la main. Commits conventionnels, aucune donnée personnelle dans les lignes ajoutées.

Mutations du relecteur

Une à la fois sur la copie, atterrissage vérifié (occurrence unique avant, texte muté présent après), restauration depuis une copie vierge et comparaison octet par octet.

Mutation Rouges
RV1 copie en échec ignorée, le rattrapage continue 1
RV2 l'historique du rattrapage stocke un mauvais checksum 5
RV3 history_sql (nouveau profil) stocke un mauvais checksum 1
RV4 rapport final des clés étrangères toujours vide 0
RV5 la suppression garde le -journal 0
RV6 catch_up_error rendu sans nettoyer le dossier 0
RV7 la reprise sans clés oublie les violations préexistantes 0
RV8 compute_account_return revenu à app_data_dir 0
RT1 Database.load lancé avant la fin de prepare_profile_db 0 (13/13 verts)
RT2 le Journal perd la ligne des violations préexistantes 0

RV8 : profile_db_path est épinglé (R9 de l'auteur), son appel dans compute_account_return ne l'est pas ; la vérification n° 4 de la checklist post-merge le couvre.

Sonde pour RV6 (test ajouté à la copie seulement) : un vrai échec de VACUUM INTO, provoqué par un lien symbolique pendant à la place du .tmp, donne backup_failed: unable to open database: <dossier>/migration-backups/profile_0a1b2c3d-before-v17.db.tmp. Le code de la PR rend <profile folder>/migration-backups/… (vert) ; sous RV6, le chemin complet passe (rouge). Le message de SQLite nomme donc bien le dossier, et seul scrub l'en retire.

Suggestions (non bloquantes)

  1. L'ordre « préparer, puis ouvrir » n'est épinglé que par l'ordre d'appel. Le test prepares after the checksum repair and before Database.load compare invocationCallOrder : RT1, qui lance Database.load dès l'appel de prepare_profile_db puis attend les deux, le laisse vert, comme toute la suite db.test.ts. C'est pourtant la propriété qui protège les données : en production la commande est synchrone sur le fil principal et load asynchrone, ils se chevaucheraient. Un h.prepare différé (promesse résolue à la main) et l'assertion que Database.load n'est pas appelé avant sa résolution la fermeraient.
  2. Le Journal ne nomme pas la copie quand le rattrapage s'arrête. Sur une erreur de l'écriture de l'historique ou de apply_pending, catch_up rend Err(String) et report.backup_file est perdu : la ligne d'erreur dit applied before stopping: [...] sans le nom de la copie, alors que c'est le cas où on en a besoin, et le corps de la PR annonce que chaque copie est écrite au Journal. Rendre le nom de la copie avec l'erreur suffirait.
  3. Le nettoyage du dossier n'est épinglé qu'au niveau de scrub (RV6). a_failed_backup_writes_nothing provoque une erreur de create_dir_all, dont le message ne contient aucun chemin : son !error.contains(dir) ne peut pas échouer. Or un vrai échec de VACUUM INTO nomme le fichier avec son dossier (sonde ci-dessus). La sonde peut entrer telle quelle dans la suite : le lien pendant fait échouer VACUUM INTO même en root, alors qu'un dossier en lecture seule n'échouerait pas dans le conteneur de la CI, qui tourne en root.
  4. -journal n'est pas épinglé (RV5). Comme sqlx 0.8.6 laisse le journal par défaut, -journal est le seul fichier annexe que cette application produit (journal chaud après un plantage), alors que le test crée -wal et -shm. Ajouter profile_0a1b2c3d.db-journal aux fichiers du test suffit.
  5. Deux comportements de la reprise sans clés ne sont épinglés nulle part : le rapport des violations préexistantes (RV4 côté Rust, RT2 côté Journal) et le fait que la reprise ne touche pas aux violations antérieures à la version (RV7 : avec un ensemble before vide, elle remettrait à NULL des lignes sans rapport avec la migration). Une violation préexistante dans le profil sans catégorie 31 couvrirait RV4 et RV7 ; une assertion sur la ligne point to a missing row couvrirait RT2.
  6. Délai de démarrage. App.tsx arme un délai de 10 s (STARTUP_TIMEOUT_MS) autour de connectActiveProfile, qui couvre désormais le rattrapage du profil actif. Au-delà, startupError reste posé et la page d'erreur s'affiche alors que le rattrapage aboutit. Peu probable avec des bases de taille courante, mais à citer dans les points de vigilance de l'ADR 0021.
  7. Les copies s'accumulent. Chaque lot de versions futures ajoute un -before-vN.db en clair par profil secondaire, retiré seulement avec le profil. Et une copie existante est gardée telle quelle : après un rattrapage qui a échoué puis abouti plus tard avec la même première version, elle précède les données saisies entre-temps. Une rétention (garder la dernière) et une phrase au guide relèvent d'une issue de suivi.
  8. Hygiène des tests : unique_dir ne nettoie pas ses dossiers. Chaque passage de la suite Rust laisse 42 dossiers, soit 16 Mo de bases SQLite, dans /tmp.

Recommandation : merger. Les points 1, 3, 4, 5 et 8 sont des épingles sur du code correct, et 2, 6 et 7 des améliorations : les regrouper dans une issue de suivi, ou en tête de branche, plutôt qu'un tour de revue de plus.

Actions faites

  • Copie hors dépôt /tmp/pr394-review, dossier cible /tmp/pr394-target, copies vierges, scripts et logs /tmp/pr394-*, et les dossiers de test laissés dans /tmp par mes passages : supprimés en fin de revue. Aucun fichier du dépôt touché.
  • Label de #390 : status:review → status:approved.
## Revue adversariale — PR #394 (#390) **Verdict : APPROVE** **Résumé.** Le correctif est correct et solidement gardé : le drapeau est lié en entier et réparé dans tous les profils, les profils secondaires rattrapent leurs migrations à partir d'un plancher déduit d'un ensemble d'états publiés que j'ai vérifié complet, et les trois commandes visent enfin le dossier du plugin. Aucun bloquant. Les suggestions portent sur des épingles manquantes autour d'un code correct, sur le Journal d'un rattrapage arrêté et sur la rétention des copies. ### Ce que j'ai rejoué - **Suites** sur la tête `71147c5`, dans une copie hors dépôt (`git archive`, `npm ci`, dossier cible cargo séparé) : vitest **1519/1519**, `cargo test` **254/254**. CI de la tête lue dans les logs : run 493 (frontend) 1519 verts et build vert, run 494 (Rust) 254 verts, `cargo check` sans avertissement. - **Checksums** : recalculés hors du code de la PR, par un parseur Python des littéraux Rust de `lib.rs` (continuations `\` comprises) et des fichiers `include_str!`, sur `main` (`8907eef`) et sur le tag `v0.17.0`. Les 17 SHA-384 sont identiques aux valeurs épinglées par `migration_checksums_match_the_released_ones`. `git diff -U0 main -- src-tauri/src/lib.rs` ne touche que l'enveloppe. - **Fixtures** : leurs blobs git sont ceux des fichiers publiés (`227d3b7`, `aeba40c`, `0582449`, `cb593ec`, `6341d9d`, `83a08a9`, `17f5503`). Le relevé des 51 tags qui ont des profils (67 en tout) confirme que l'ensemble est fermé : le seed `0582449` n'a existé qu'en v0.4.0 (`aeba40c` revient en v0.4.1), le consolidé `227d3b7` couvre v0.3.0 à v0.8.3, puis un état par changement (v0.8.4, v0.9.0, v0.9.1, v0.10.0 à v0.14.0, v0.15.0 à v0.17.0). Le nom `profile_${id.split("-")[0]}.db` n'a pas changé depuis v0.3.0, et aucun code TypeScript n'a jamais modifié un schéma à l'exécution (`git log -S "ALTER TABLE" -- src/` est vide) : pas d'état hors fixtures. - **Prémisses du plugin**, lues dans tauri-plugin-sql 2.3.2 : `app_config_dir()` dans `wrapper.rs`, liste retirée au premier `load` de la seule URL enregistrée, booléen lié par `query.bind(value)` (valeur JSON, donc texte). sqlx-sqlite 0.8.6 active les clés étrangères par défaut et ne fixe pas `journal_mode` : les bases sont en journal de retour arrière. - **Fermeture** : la timeline de #390 porte le `pull_ref` de #394 avec `ref_action: none`, et aucun mot-clé de fermeture n'apparaît dans le corps ni dans les commits. Cohérent avec `Refs` et une fermeture à la main. Commits conventionnels, aucune donnée personnelle dans les lignes ajoutées. ### Mutations du relecteur Une à la fois sur la copie, atterrissage vérifié (occurrence unique avant, texte muté présent après), restauration depuis une copie vierge et comparaison octet par octet. | Mutation | Rouges | |---|---| | RV1 copie en échec ignorée, le rattrapage continue | 1 | | RV2 l'historique du rattrapage stocke un mauvais checksum | 5 | | RV3 `history_sql` (nouveau profil) stocke un mauvais checksum | 1 | | RV4 rapport final des clés étrangères toujours vide | **0** | | RV5 la suppression garde le `-journal` | **0** | | RV6 `catch_up_error` rendu sans nettoyer le dossier | **0** | | RV7 la reprise sans clés oublie les violations préexistantes | **0** | | RV8 `compute_account_return` revenu à `app_data_dir` | **0** | | RT1 `Database.load` lancé avant la fin de `prepare_profile_db` | **0** (13/13 verts) | | RT2 le Journal perd la ligne des violations préexistantes | **0** | RV8 : `profile_db_path` est épinglé (R9 de l'auteur), son appel dans `compute_account_return` ne l'est pas ; la vérification n° 4 de la checklist post-merge le couvre. **Sonde pour RV6** (test ajouté à la copie seulement) : un vrai échec de `VACUUM INTO`, provoqué par un lien symbolique pendant à la place du `.tmp`, donne `backup_failed: unable to open database: <dossier>/migration-backups/profile_0a1b2c3d-before-v17.db.tmp`. Le code de la PR rend `<profile folder>/migration-backups/…` (vert) ; sous RV6, le chemin complet passe (rouge). Le message de SQLite nomme donc bien le dossier, et seul `scrub` l'en retire. ### Suggestions (non bloquantes) 1. **L'ordre « préparer, puis ouvrir » n'est épinglé que par l'ordre d'appel.** Le test `prepares after the checksum repair and before Database.load` compare `invocationCallOrder` : RT1, qui lance `Database.load` dès l'appel de `prepare_profile_db` puis attend les deux, le laisse vert, comme toute la suite `db.test.ts`. C'est pourtant la propriété qui protège les données : en production la commande est synchrone sur le fil principal et `load` asynchrone, ils se chevaucheraient. Un `h.prepare` différé (promesse résolue à la main) et l'assertion que `Database.load` n'est pas appelé avant sa résolution la fermeraient. 2. **Le Journal ne nomme pas la copie quand le rattrapage s'arrête.** Sur une erreur de l'écriture de l'historique ou de `apply_pending`, `catch_up` rend `Err(String)` et `report.backup_file` est perdu : la ligne d'erreur dit `applied before stopping: [...]` sans le nom de la copie, alors que c'est le cas où on en a besoin, et le corps de la PR annonce que chaque copie est écrite au Journal. Rendre le nom de la copie avec l'erreur suffirait. 3. **Le nettoyage du dossier n'est épinglé qu'au niveau de `scrub`** (RV6). `a_failed_backup_writes_nothing` provoque une erreur de `create_dir_all`, dont le message ne contient aucun chemin : son `!error.contains(dir)` ne peut pas échouer. Or un vrai échec de `VACUUM INTO` nomme le fichier avec son dossier (sonde ci-dessus). La sonde peut entrer telle quelle dans la suite : le lien pendant fait échouer `VACUUM INTO` même en root, alors qu'un dossier en lecture seule n'échouerait pas dans le conteneur de la CI, qui tourne en root. 4. **`-journal` n'est pas épinglé** (RV5). Comme sqlx 0.8.6 laisse le journal par défaut, `-journal` est le seul fichier annexe que cette application produit (journal chaud après un plantage), alors que le test crée `-wal` et `-shm`. Ajouter `profile_0a1b2c3d.db-journal` aux fichiers du test suffit. 5. **Deux comportements de la reprise sans clés ne sont épinglés nulle part** : le rapport des violations préexistantes (RV4 côté Rust, RT2 côté Journal) et le fait que la reprise ne touche pas aux violations antérieures à la version (RV7 : avec un ensemble `before` vide, elle remettrait à NULL des lignes sans rapport avec la migration). Une violation préexistante dans le profil sans catégorie 31 couvrirait RV4 et RV7 ; une assertion sur la ligne `point to a missing row` couvrirait RT2. 6. **Délai de démarrage.** `App.tsx` arme un délai de 10 s (`STARTUP_TIMEOUT_MS`) autour de `connectActiveProfile`, qui couvre désormais le rattrapage du profil actif. Au-delà, `startupError` reste posé et la page d'erreur s'affiche alors que le rattrapage aboutit. Peu probable avec des bases de taille courante, mais à citer dans les points de vigilance de l'ADR 0021. 7. **Les copies s'accumulent.** Chaque lot de versions futures ajoute un `-before-vN.db` en clair par profil secondaire, retiré seulement avec le profil. Et une copie existante est gardée telle quelle : après un rattrapage qui a échoué puis abouti plus tard avec la même première version, elle précède les données saisies entre-temps. Une rétention (garder la dernière) et une phrase au guide relèvent d'une issue de suivi. 8. **Hygiène des tests** : `unique_dir` ne nettoie pas ses dossiers. Chaque passage de la suite Rust laisse 42 dossiers, soit 16 Mo de bases SQLite, dans `/tmp`. **Recommandation** : merger. Les points 1, 3, 4, 5 et 8 sont des épingles sur du code correct, et 2, 6 et 7 des améliorations : les regrouper dans une issue de suivi, ou en tête de branche, plutôt qu'un tour de revue de plus. ### Actions faites - Copie hors dépôt `/tmp/pr394-review`, dossier cible `/tmp/pr394-target`, copies vierges, scripts et logs `/tmp/pr394-*`, et les dossiers de test laissés dans `/tmp` par mes passages : supprimés en fin de revue. Aucun fichier du dépôt touché. - Label de #390 : `status:review` → `status:approved`.
maximus added 1 commit 2026-10-07 01:51:22 +00:00
test(profiles): pin the review's quick suggestions on the catch-up (#390)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 3m39s
5910387eb7
From the /pr-review of PR #394:
- the profile database opens only once prepare_profile_db has resolved
  (deferred preparation; a load started early used to pass, RT1);
- a real VACUUM INTO failure, through a dangling symlink, names the full
  path and comes out scrubbed (RV6); the test first checks the raw message
  does name the folder;
- deleting a profile removes the -journal side file, the only one this
  app produces (RV5);
- test folders are removed when each test ends, a failing one included
  (TestDir guard), instead of 42 folders left in /tmp per run.

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

Suggestions rapides de la revue appliquées (commit 5910387, tête non relue)

À la demande de Max (« appliquer les rapides, puis merger »), les suggestions 1, 3, 4 et 8 de la revue sont appliquées en tête de branche. La nouvelle tête 5910387 n'a pas été relue. #390 repasse en status:review.

Ce qui change (tests seulement, aucun code de production)

  1. Ordre « préparer, puis ouvrir » (suggestion 1, RT1). Nouveau test opens the database only once the preparation has finished dans db.test.ts : la préparation est une promesse résolue à la main, et Database.load ne doit pas être appelé avant sa résolution.
  2. Fuite du chemin dans une vraie erreur de copie (suggestion 3, RV6). Nouveau test a_real_backup_failure_names_no_folder (Unix seulement) : un lien symbolique pendant remplace le .tmp, et VACUUM INTO échoue, même en root. Le test vérifie d'abord que le message brut de backup_to nomme bien le dossier, puis que catch_up_error de prepare_at en sort sans le dossier.
  3. -journal (suggestion 4, RV5). Le test de suppression crée aussi profile_0a1b2c3d.db-journal, le seul fichier annexe que l'application produit, et vérifie qu'il disparaît.
  4. Dossiers temporaires des tests (suggestion 8). unique_dir rend un garde TestDir, qui supprime son dossier à la fin du test, échec compris. Le test de suppression de profile_commands.rs l'utilise aussi. Après une suite Rust complète, ls -d /tmp/simpl-resultat-* ne trouve plus rien.

Mutations (arbre committé, restauration par git)

Mutation Avant Mutation Après
RT1 Database.load lancé avant la fin de prepare_profile_db 14 verts 2 rouges 14 verts
RV6 scrub rend le message tel quel 27 verts 2 rouges 27 verts
RV5 la suppression oublie -journal 12 verts 1 rouge 12 verts
Garde TestDir — — /tmp/simpl-resultat-* vide après la suite Rust entière, y compris après des passes où des tests ont échoué

Suites sur 5910387

  • vitest 1520 (1519 + 1) et build verts ;
  • cargo test 255 (254 + 1) ;
  • cargo check sans avertissement.

Le reste

Les suggestions 2, 5, 6 et 7 sont regroupées dans #395 : copie nommée quand le rattrapage s'arrête, violations préexistantes épinglées (RV4, RV7, RT2), délai de démarrage, rétention des copies.

🤖 Generated with Claude Code

## Suggestions rapides de la revue appliquées (commit `5910387`, tête non relue) À la demande de Max (« appliquer les rapides, puis merger »), les suggestions 1, 3, 4 et 8 de la revue sont appliquées en tête de branche. **La nouvelle tête `5910387` n'a pas été relue.** #390 repasse en `status:review`. ### Ce qui change (tests seulement, aucun code de production) 1. **Ordre « préparer, puis ouvrir »** (suggestion 1, RT1). Nouveau test `opens the database only once the preparation has finished` dans `db.test.ts` : la préparation est une promesse résolue à la main, et `Database.load` ne doit pas être appelé avant sa résolution. 2. **Fuite du chemin dans une vraie erreur de copie** (suggestion 3, RV6). Nouveau test `a_real_backup_failure_names_no_folder` (Unix seulement) : un lien symbolique pendant remplace le `.tmp`, et `VACUUM INTO` échoue, même en root. Le test vérifie d'abord que le message brut de `backup_to` nomme bien le dossier, puis que `catch_up_error` de `prepare_at` en sort sans le dossier. 3. **`-journal`** (suggestion 4, RV5). Le test de suppression crée aussi `profile_0a1b2c3d.db-journal`, le seul fichier annexe que l'application produit, et vérifie qu'il disparaît. 4. **Dossiers temporaires des tests** (suggestion 8). `unique_dir` rend un garde `TestDir`, qui supprime son dossier à la fin du test, échec compris. Le test de suppression de `profile_commands.rs` l'utilise aussi. Après une suite Rust complète, `ls -d /tmp/simpl-resultat-*` ne trouve plus rien. ### Mutations (arbre committé, restauration par git) | Mutation | Avant | Mutation | Après | |---|---|---|---| | RT1 `Database.load` lancé avant la fin de `prepare_profile_db` | 14 verts | **2 rouges** | 14 verts | | RV6 `scrub` rend le message tel quel | 27 verts | **2 rouges** | 27 verts | | RV5 la suppression oublie `-journal` | 12 verts | **1 rouge** | 12 verts | | Garde `TestDir` | — | — | `/tmp/simpl-resultat-*` vide après la suite Rust entière, y compris après des passes où des tests ont échoué | ### Suites sur `5910387` - vitest **1520** (1519 + 1) et build verts ; - `cargo test` **255** (254 + 1) ; - `cargo check` sans avertissement. ### Le reste Les suggestions 2, 5, 6 et 7 sont regroupées dans **#395** : copie nommée quand le rattrapage s'arrête, violations préexistantes épinglées (RV4, RV7, RT2), délai de démarrage, rétention des copies. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus merged commit 5910387eb7 into main 2026-10-07 02:00:03 +00:00
maximus deleted branch issue-390-manual-flag-profile-migrations 2026-10-07 02:00:03 +00:00
Author
Owner

La PR est mergée en avance rapide sur 5910387, par l'API, avec la tête épinglée.

Validation de la tête dans un worktree propre :

  • npm ci ;
  • sonde volontairement rouge : code de sortie 1, donc le rejeu sait échouer ;
  • build vert ;
  • vitest 1520/1520 ;
  • cargo check sans avertissement ;
  • cargo test 255/255 ;
  • aucun dossier laissé dans /tmp.

Revue : l'APPROVE portait sur 71147c5. Le commit 5910387, qui ne contient que des tests, a été ajouté après la revue et n'a pas été relu, à la demande de Max : appliquer les suggestions rapides, puis merger. Sa CI est verte (runs 495 et 496).

Suites :

  • #390 reste ouverte pour la liste post-merge (status:blocked) ;
  • #395 regroupe les suggestions 2, 5, 6 et 7 de la revue.
La PR est mergée en avance rapide sur `5910387`, par l'API, avec la tête épinglée. **Validation de la tête dans un worktree propre** : - `npm ci` ; - sonde volontairement rouge : code de sortie 1, donc le rejeu sait échouer ; - build vert ; - vitest 1520/1520 ; - `cargo check` sans avertissement ; - `cargo test` 255/255 ; - aucun dossier laissé dans `/tmp`. **Revue** : l'APPROVE portait sur `71147c5`. Le commit `5910387`, qui ne contient que des tests, a été ajouté après la revue et n'a pas été relu, à la demande de Max : appliquer les suggestions rapides, puis merger. Sa CI est verte (runs 495 et 496). **Suites** : - #390 reste ouverte pour la liste post-merge (`status:blocked`) ; - #395 regroupe les suggestions 2, 5, 6 et 7 de la revue.
maximus referenced this pull request from a commit 2026-10-07 02:01:26 +00:00
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#394
No description provided.