diff --git a/STATE.md b/STATE.md index 8a62f2d..b672c00 100644 --- a/STATE.md +++ b/STATE.md @@ -1,8 +1,8 @@ # STATE — Simpl'Résultat -> Derniere MAJ : 2026-09-27 (**#346 mergée — PR #362, `main` `170b22d`, merge API en `fast-forward-only`, SHA inchangés.** « Obtenir Base » actif sur les écrans Base (page produit localisée), Premium désactivé avec une note honnête, parcours d'achat et « Télécharger la dernière version » en Gratuite, texte de la carte Compte. `/pr-review` APPROVE, ses suggestions appliquées dans `170b22d` avec preuve du rouge : 12 mutations rejouées sur la tête finale, 11 rouges, la 12e demande un clic et reste à vérifier à la main ; 1257 → **1266** vitest, CI vérifiée dans le log (run 430). **Contrainte de release** : aucun tag tant que la-compagnie-maximus#141 et maximus-api#62 sont ouvertes, le checkout du site étant en mode test. Vague desktop à 2/3, reste #347.) +> Derniere MAJ : 2026-10-01 (**#347 mergée — PR #364, `main` `cdf4df8`, merge API en `fast-forward-only` ; ferme aussi #360, et la vague `planned-2026-09-24-paiements` est close 3/3.** L'édition ne dépend plus que de la clé (D12), les erreurs de saisie sont nommées, une clé révoquée reste signalée pour la session (D13), plus le compteur de machines, les dates `fr-CA`/`en-CA` et « Sans échéance ». Deux revues APPROVE, leurs suggestions appliquées avec preuve du rouge, dont deux défauts réels : le jeton d'une clé précédente qui survivait à l'enregistrement d'une autre clé, et la course entre activation et nouvelle clé. Rust **151**, vitest **1311**. **#365 ouverte en parallèle, pendant le go-live** : une licence remboursée et révoquée reste pleinement fonctionnelle hors ligne.) > -> Précédente MAJ : 2026-09-27 (**#270 mergée — PR #359, `main` `0c9b8f8`, merge API en rebase ff, SHA inchangés.** Chemins `/v1` + `product`, contrat d'erreur `LicenseError` (JSON `{code}` comme `FetchPriceError`) et 14 messages FR/EN, hook `useLicenseMachines` à reducer testé, URL d'achat localisée. Trois passes `/pr-review` (APPROVE ×3), leurs 12 suggestions appliquées avec preuve du rouge ; 111 → **128** Rust, 1181 → **1246** vitest, CI vérifiée dans les logs à chaque tête. **La 3e revue a trouvé un défaut serveur** : réactiver une machine déjà désactivée répond 500 (contrainte `uq_license_machine` sans condition sur `is_active`) → maximus-api#70, prérequis du go-live. Et **#360** : un jeton d'une autre machine (réinstallation, migration de poste) bloque en Gratuite sans issue dans l'interface. Vague desktop à 1/3 (#346 → #347) ; volet site clos (la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod).) +> Précédente MAJ : 2026-09-27 (**#346 mergée — PR #362, `main` `170b22d`, merge API en `fast-forward-only`, SHA inchangés.** « Obtenir Base » actif sur les écrans Base (page produit localisée), Premium désactivé avec une note honnête, parcours d'achat et « Télécharger la dernière version » en Gratuite, texte de la carte Compte. `/pr-review` APPROVE, ses suggestions appliquées dans `170b22d` avec preuve du rouge : 12 mutations rejouées sur la tête finale, 11 rouges, la 12e demande un clic et reste à vérifier à la main ; 1257 → **1266** vitest, CI vérifiée dans le log (run 430). **Contrainte de release** : aucun tag tant que la-compagnie-maximus#141 et maximus-api#62 sont ouvertes, le checkout du site étant en mode test. Vague desktop à 2/3, reste #347.) ## Position actuelle @@ -20,6 +20,7 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA ## Decisions recentes +- 2026-10-01 : **#347 mergée (PR #364, ferme aussi #360) — l'édition ne dépend plus que de la clé, et deux revues ont trouvé ce que les tests ne gardaient pas.** `/analyze` (09-29) a revérifié le corps du 09-24 contre `main` et `maximus-api`, puis fait trancher deux décisions par Max. **D12** : le jeton d'activation ne fait plus jamais retomber en Gratuite. Une clé copiée sans son jeton gardait déjà son édition, donc la règle ne protégeait rien ; elle piégeait la restauration légitime de données (#360) et toute machine activée au bout des 2 ans du jeton. **D13** : clé révoquée = drapeau de session. Livré : `LicenseError` étendu aux erreurs de saisie (`invalid_prefix`, `invalid_signature`, `malformed`, `unknown_edition`), nommées à la source par `store_license_in` et `read_license_in` ; édition résolue depuis la seule `license.key` (`entitlements_from_dir`) ; `LicenseKeyForm` extrait ; compteur n/limite, dates `fr-CA`/`en-CA`, « Sans échéance » au-delà de 50 ans ; tests de #363 absorbés. **Écart au corps de l'issue** : le drapeau révoqué vit dans `LicenseContext`, parce que le plan checker a vu que le reducer du hook meurt avec la carte. **Revue 1 (APPROVE)** : 3 mutations vertes (état « Activée » non testé à son point de décision, filtre des erreurs sous Machines, coche verte) et un défaut préexistant, le jeton d'une clé précédente qui survivait à l'enregistrement d'une autre clé (« Activée » sans « Activer », cours refusés au nom de l'ancienne licence). Corrigés dans `3f90635` : `activation_status_in`, jeton supprimé au changement de clé, carte remontée par `licenseIdentity`, message `invalidSignature` qui renvoie d'abord à la copie (`jsonwebtoken` vérifie la signature avant de décoder la charge, donc une copie abîmée finit souvent en `InvalidSignature`). **Revue 2 (APPROVE)** : course activation / nouvelle clé, badge « Activée » d'une clé révoquée, renvoi de l'ADR ; corrigés dans `cdf4df8`. Preuves : reproduction rouge sur `5d549d6`, puis 27 + 8 + 4 mutations rouges ; Rust 128 → 151, vitest 1266 → 1311, CI vérifiée dans les logs à chaque tête. Trouvé en route : le 403 `license_revoked` des cours s'affiche comme « abonnement premium requis » (consigné sur #365). Merge par l'API en `fast-forward-only`, milestone `planned-2026-09-24-paiements` fermée 3/3. Aucune migration DB (v1→v17). (ref #347, #360, #363, PR #364) - 2026-09-27 : **#346 mergée (PR #362) — « Obtenir Base » ouvre enfin la page produit, et la revue a trouvé que le câblage des boutons n'était gardé par aucun test.** Livré par la PR : `upsellCtaFor` (règle pure : Base achetable, Premium désactivé avec la note « pas encore offert en ligne ») ; `PurchaseButton`, `HaveKeyButton` (rendu seulement sous un routeur, via `useInRouterContext`) et `DownloadLatestButton` partagés ; `UpsellGate` suit la règle et `ProfileFormModal` le réutilise (fin du mode upsell compact) ; carte Mises à jour et page d'erreur en Gratuite ; texte de la carte Compte tranché le 09-24 ; amendement de l'ADR 0017. **`/pr-review` APPROVE**, avec un constat principal : la PR supposait que sans jsdom seules les fonctions pures se testent, or `renderToStaticMarkup` tourne dans l'environnement node de vitest et `FilterPanel.test.tsx` appelait déjà un composant comme une fonction ; six mutations des composants passaient la suite sans un rouge. **Appliqué dans `170b22d`** : 9 tests de rendu (`UpsellCtas.render.test.tsx`) ; un `logError` sur l'échec d'`openUrl` dans les trois sites qui ouvrent la page produit (`initLogCapture` ne capte pas `unhandledrejection`, donc rien n'arrivait dans Journaux) ; un commentaire dans `upsellCtaFor` (le bouton ne vend que Base, donc rendre Premium achetable sans lui donner le palier ferait vendre Base sur le Bilan) ; la réserve sur la carte Compte dans l'ADR (`check_account_edition` accorde encore Premium à un abonnement actif, chemin dormant tant qu'aucun abonnement n'est vendu) ; l'étape d'achat placée avant la saisie de la clé dans le guide et les docs intégrées ; trois corrections de CHANGELOG. **Preuve** : 12 mutations rejouées sur la tête finale dans un worktree jetable (`npm ci`), 11 rouges avec code de sortie 1. La 12e (`ProfileFormModal` sans `onNavigate={onClose}`) demande un clic et reste à vérifier à la main ; `LicenseCard` n'a pas de test, ses hooks réels empêchent de l'appeler comme une fonction. Merge par l'API en `fast-forward-only` épinglé sur `head_commit_id`, #346 fermée par `Fixes`. Aucune migration DB (v1→v17). (ref #346, PR #362) - 2026-09-27 : **#270 mergée (PR #359) — le client de licence parle `/v1`, nomme chaque refus, et trois revues ont trouvé ce que les tests ne gardaient pas.** `/fix-issue 270` sur le corps réécrit par le `/plan-run` du 09-24. Plan vérifié par le plan checker en 2 itérations (la 1re : un refus serveur sur cinq sans test). **Livré** : `POST /v1/licenses/{activate,deactivate,verify}` avec `product` et clé rognée dans `*_inner` ; `LicenseError` sérialisé `{code}` comme `FetchPriceError`, lu dans `error.code` de l'enveloppe (un 404 n'est jamais « route absente » ; un 404 sans code connu est `server`) ; verify répond **toujours 200**, donc `{valid:false}` devient une erreur codée au lieu de la liste vide qui affichait « Aucune machine activée » pour une clé révoquée ; corps d'un non-2xx lu sans exiger de JSON (page HTML 502 → `server`), corps 2xx illisible → `invalid_response`. TS : `parseLicenseError` + table vers 14 clés `license.errors.*` (pluriels i18next pour le délai), wrappers typés `{ok, value|error}`, hook `useLicenseMachines` à reducer exporté (erreur d'activation distincte de l'erreur de liste, parcours limite de machines en 4 requêtes sous le 5/min/IP du serveur, boutons inactifs pendant une désactivation ou un rechargement), `purchaseUrl` dans `src/shared/`. Désactivation idempotente (`no_active_activation` = fait), écart assumé et documenté. **Méthode** : chaque défaut d'origine réintroduit sur l'arbre final et compté en rouge (11 mutations) ; puis **3 passes `/pr-review`, toutes APPROVE**, dont les mutations ont trouvé 7 comportements corrects mais non gardés (un 404 lu comme clé introuvable, une table de messages comparée à elle-même, l'idempotence qui avale `license_not_found`, la relecture du statut après désactivation, `machine_name` à `null`, un 2xx illisible, deux courses de boutons) — chacun épinglé puis prouvé rouge. Une puce du CHANGELOG retirée (l'édition ne change pas visiblement après une activation) et une reformulée (l'ancienne URL est redirigée par le site depuis la-compagnie-maximus#153). **Hors PR, trouvés en route** : maximus-api#70 (réactivation d'une machine déjà désactivée → 500 ; `UNIQUE(license_id, machine_id)` sans condition sur `is_active` + `INSERT` sans `onConflict` ; reproduit sur Postgres embarqué ; upsert recommandé, sans migration ; ajouté comme prérequis sur maximus-api#62) ; #360 (jeton d'une autre machine → Gratuite, section Machines masquée, ressaisie de clé inopérante ; trois pistes, `status:triage`). Merge par l'API (rebase ff, `main` `0c9b8f8`). Aucune migration DB (v1→v17). (ref #270, PR #359, #360, maximus-api#70) - 2026-09-24 : **Vague paiements planifiée via `/plan-run spec-paiements` — deux milestones `planned-2026-09-24-paiements`, un défaut de licence trouvé en revue, et des clés de 100 ans.** Préambule : `/warmup` (audit quotidien vert, runs 415/416), puis commit des skills en attente depuis le 09-13 (`11e12ab` : `/issue-license`, `verify-release.sh`, garde « clés testeurs avant le tag » de `/release`) — **le courriel personnel de Michel retiré avant le premier commit** : le dépôt `Simpl-Resultat` est public et l'adresse n'était dans aucun fichier suivi ; celui de Max conservé, déjà présent dans 3 fichiers suivis. Le classifier a refusé de charger `purge-secret-forgejo` (« Git Destructive ») : scan des lignes ajoutées fait à la main. **Étape 0** : spec d'origine dans la-compagnie-maximus (07-11), corps issus de `/analyze 270` (09-13) jamais passés en `/review-spec`, donc **toutes les affirmations revérifiées** contre le code et la prod. Résultats : `verify` répond **toujours 200** (`{valid:false}` → le client affiche « Aucune machine activée » pour une clé révoquée) ; un 404 d'activate est un `license_not_found` métier, pas une route absente ; `ErrorPage` est hors routeur ; `machine_limit` est déjà dans le JWT local ; le 429 `rate_limit_exceeded` n'était traité nulle part ; `FetchPriceError` est le modèle d'erreur à reproduire. Décisions de Max D1-D8 : #153 dans la vague (seule réparation du bouton « Acheter » des v0.15.0 gratuites, qui ne recevront jamais #270) ; 409 → message + liste dépliée via verify ; note Premium « pas encore offert en ligne » ; texte exact de la carte Compte ; `removeLicense` retirée ; Windows d'abord sans détection ; #270 établit le contrat des codes d'erreur ; périmètre inchangé. **`/review-spec` (3 experts) → 🔴** : la tâche D2 effaçait le message 409 (`loadMachines()` commence par `setMachineError(null)`) et « Aucune machine » restait affichée en erreur ; le repli `activateError` (« Activation failed » en EN) contredisait #270. Constats chiffrés revérifiés à la source, aucun réfuté. **Le défaut de fond** : `signActivationJwt` signe le jeton d'activation pour **2 ans**, la clé pour 10, et le client valide le jeton avec `strict_validation()` → une machine qui a cliqué « Activer » retombe en Gratuite au bout de 2 ans, alors qu'une clé jamais activée garde son édition ; « Activer » ne rappelait pas `refresh()`. D10 : un jeton expiré mais signé pour cette machine compte comme absent (#347), ce qui corrige aussi les jetons déjà émis. **Durée des clés** : retirer l'expiration est impossible (tous les binaires installés exigent `exp`, CWE-613) ; 100 ans passe partout, et les 10 ans de la décision #15 n'étaient que le maximum du schéma de `/generate` → D9 : 100 ans hors vague (maximus-api#63, et la-compagnie-maximus#157 pour les CGV, dont « mises à jour pendant sa durée » est à reformuler) ; l'état « clé expirée le {date} » est abandonné, « Sans échéance » au-delà de 50 ans. D11 : lien « Télécharger la dernière version » en Gratuite, sans appel réseau. Lot de 9 corrections intégré : hook `useLicenseMachines` à reducer testé, `PurchaseButton` partagé, délai de 5 s et repli complet côté site, routes `/telecharger/*` et repli `latest.json` retirés, redirection sans langue, etc. **Mécanique** : milestones créées (simpl-resultat id 87, site id 88) plutôt que renommage de `spec-paiements`, qui reste le parapluie des trois dépôts ; #350 ouverte (hachage HMAC de l'identifiant machine, avant go-live). Step 9 vérifié sur l'état réel de Forgejo. **Gotcha skills** : `plan-run` Step 6bis laisse entendre qu'un seul `/autopilot ` couvre deux milestones homonymes, mais `autopilot` refuse le multi-dépôts (`SKILL.md:53`) → un run par dépôt. Aucune migration DB (v1→v17). (ref #270, #346, #347, #350, la-compagnie-maximus#153/#157, maximus-api#63) @@ -29,13 +30,12 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA - 2026-09-04 : **#343 ouverte, #344 mergée — l'audit quotidien rouge depuis 17 jours, corrigé (`main` `5154c52`).** Découvert au `/warmup` : `audit.yml` échouait tous les matins depuis le **2026-08-19** (runs 375→391), dernier vert le 08-18, **aucun push sur `main` dans l'intervalle**. Une seule vulnérabilité : **RUSTSEC-2026-0258** / GHSA-q83h-524g-xf6h, `h2` 0.4.13, datée du 08-17 — DoS de sévérité faible, les frames HTTP/2 `DATA` vides sont acceptées et mises en file **sans aucune limite**, donc sur un flux non drainé la mémoire croît sans borne et la longueur peut déborder en panique. Le délai 08-17→08-19 est la propagation dans l'advisory-db. Ces 17 jours **sont** le mode d'échec que l'ADR 0018 visait à empêcher — « alarme permanente = alarme ignorée » — cette fois par le haut : le gate criait, personne ne regardait. **Atteignabilité par triple explicite**, jamais `--target all` : `h2` est sur les **deux** cibles livrées par deux chemins, `hyper`←`hyper-rustls`←`reqwest 0.13.2`←`tauri-plugin-updater` (le chemin de la mise à jour automatique) et `hyper-tls`←`reqwest 0.12.28` déclaré directement en `Cargo.toml:43` (licence, auth, cours, rétroaction — 4 modules). Correctif publié + crate atteignable = **règle 1** de `.cargo/audit.toml`, donc corrigé et non suppressé ; ni le fichier de suppressions ni le garde-fou de `check-rust.yml` ne bougent. **Le piège, et la raison d'un diff de 2 lignes** : `cargo update -p h2` donne bien 0.4.19 mais réaiguille au passage 7 pointeurs `windows-sys` (`colored`, `dirs-sys`, `errno`, `rustix`, `rustls-platform-verifier`, `tempfile`, `winapi-util`) de `0.61.2` vers 0.48/0.52/0.59 — **exactement ce que #321 avait défait délibérément le 08-15**. Laisser passer aurait régressé cette décision en silence, 3 semaines après. Le lock a donc été restauré et **seule la strophe `h2`** appliquée (version + checksum), après avoir **retracé depuis la source** — `git show v0.14.0:src-tauri/Cargo.lock`, les 7 crates comparés un à un — que `0.61.2` est bien l'état du tag, donc la seule configuration `windows-sys` dont on ait la preuve qu'elle compile sous Windows ; relayer la note de #321 n'aurait pas suffi. `cargo metadata --locked` accepte le résultat **sans le réécrire**. **`/pr-review` APPROVE et a trouvé ce que j'avais manqué** : une strophe écrite à la main ne garantit plus ni checksum ni liste de dépendances, et `h2` 0.4.19 **resserre sa borne `http` de `^1` à `^1.1`** — le genre de changement qui invalide un lock épinglé ; satisfait ici (`http 1.4.0`), checksum authentifié par `sha256sum` du `.crate`, et nuance utile qu'il pose : `cargo metadata --locked` prouve le graphe, **pas** le checksum — c'est le `Downloaded`→`Compiling` du run 392 qui le prouve. **Preuves plutôt que déductions** : contrôle sur l'ancien lock (exit 1 sur RUSTSEC-2026-0258) montrant que l'outil local reproduit le rouge de la CI ; log de la CI lu ligne à ligne parce que l'étape `cargo audit` de `check-rust` est `continue-on-error: true` et qu'un job vert n'y prouve rien ; garde-fou tracé (`Suppression guard: 2 checks, exit 0`) ; **111 Rust** ; et `workflow_dispatch` d'`audit.yml` **sur `main` après merge** (run 393, `Job succeeded`) plutôt que d'inférer le vert du run de PR. **Deux acquis au passage** : (1) le **glob `src-tauri/**` est enfin prouvé** — cette PR est le cas test isolé attendu depuis #232 (lock + 2 `.md` seulement) : `check-rust` a tourné, `check-frontend` correctement sauté par sa denylist ; le suivi peut être fermé ; (2) **#345 ouverte** — la contrainte `windows-sys 0.61.2` en est à sa 3e rencontre (#310 l'introduit, #321 la défait, #344 l'évite à la main) et ne vit dans **aucun** ADR, aucun commentaire de `Cargo.toml`, aucun garde CI : seulement 3 lignes de prose de STATE.md et un message de commit, donc le prochain `cargo update` la régressera en silence. Merge via l'API Forgejo (rebase ff, SHA inchangé) plutôt qu'en local, ce qui évite la réconciliation manuelle des PRs. Aucune migration DB (v1→v17). (ref #343, #345, PR #344) - 2026-08-15 : **#321 + #322 mergées ff-only (`main` `97d376b`) — deux PRs de 18 jours, deux revues du 08-14 jamais traitées, et une revue qui se corrige elle-même.** Passes de re-vérification lancées depuis le main loop (jamais en sous-agent, [[feedback-pr-review-subagent-forks]]) et séquencées plutôt que parallélisées — deux revues concurrentes partagent le même chemin de scratch et s'écrasent entre le Write et le POST (gotcha du chantier import). **Les deux passes ont retracé les claims depuis la source au lieu de relayer, et les deux ont trouvé une erreur dans la revue qu'elles re-vérifiaient.** (1) Sur #321, la revue du 08-14 lisait les 7 réaiguillages `windows-sys` comme une dérive de re-résolution et demandait un build Windows avant de tagger : **le sens est l'inverse** — #310 les avait fait *descendre* de `0.61.2` vers 0.48/0.52/0.59, cette PR les *remonte* à `0.61.2`, l'état exact du tag `v0.14.0` (`git show v0.14.0:src-tauri/Cargo.lock`, crate par crate). v0.14.0 taggée le 07-18, #310 mergée le 07-27 → le build NSIS de v0.14.0 a réellement compilé contre `0.61.2`. **La configuration non prouvée sous Windows était celle de `main`** ; la PR réduit le risque. (2) Sur #322, la revue du 08-14 annonçait « trois » déclencheurs et mappait `UpdateCard.tsx:201` à la page d'erreur — faux dans les deux sens : `:201` est le retry de l'état `error` de la carte, et `ErrorPage.tsx:82` est un **4e** site distinct qui **détecte seulement** (`handleCheckUpdate` s'arrête à `setUpdateStatus("available")`). Appliquer « écrire trois » aurait échangé une inexactitude contre une autre. **Le blocage de #322 était réel et non traité** : la checklist demandait de cocher `readyToInstall` **avant** l'invite d'authentification, alors que `download_and_install` fait `download(...).await?` puis `install(bytes)` dans le même appel (`updater.rs:723-729`), que `Finished` est un no-op côté UI (`useUpdater.ts:119-121`) et que `READY_TO_INSTALL` n'est dispatché qu'après le retour de `dpkg -i` — l'invite pkexec s'ouvre donc **par-dessus « Téléchargement en cours… »**. Conséquence exacte de l'ordre des cases : sans agent polkit l'app se fige en `downloading`, le testeur guette un blocage après `readyToInstall`, et rapporte « le téléchargement bloque » au lieu de « pas d'agent polkit » — **la page produisait la fausse trace qu'elle existe pour éliminer**. Corrigé en `97d376b` avec 3 mineurs (les 4 déclencheurs, les préfixes `/api/v1/packages/` (DELETE) vs `/api/packages/` (PUT) que `release.yml:217-219` commente déjà, ordre du changelog `SKILL.md`) + titre de PR (« step 9 » → « as step 10 »). Merge : rebase des 2 branches (15 commits de retard) en pile linéaire, **preuve que le rebase préserve `.cargo/`/`.forgejo/`/`Cargo.lock` au byte près** vs le commit validé par la CI run 338, puis CI re-jouée quand même (runs 370/371 verts, garde-fou **exécuté et tracé**, `cargo audit` 0 vulnérabilité / 22 warnings autorisés) et `cargo test` **111 passed** — le « 106 » de la description de #321 était périmé, le chantier import ayant ajouté 5 tests. Issues #312/#313/#315 auto-fermées par `Resolves`, PRs fermées à la main (Forgejo ne détecte pas un merge local comme *merged*), branches supprimées. **#314 trouvée caduque au passage** : `audit.yml` tire bien tous les jours à 06:00 UTC — 19 runs `schedule` verts du 07-28 au 08-15 — et son `workflow_dispatch` de référence datait du 07-27 à 23:56Z, soit ~6 h avant le premier tir programmé possible. L'issue a été écrite avant que le scheduler ait pu s'exprimer, pas après l'avoir vu échouer. (ref #312, #313, #314, #315, PRs #321/#322) - 2026-08-14 : **Import CSV — chantier complet livré (10 issues, migration v17)**. Cycle intégral en une session : revue d'implémentation → `/spec` → `/review-spec` → `/plan-run` → `/autopilot` → `/pr-review` ×10 → merge ff-only. **Le diagnostic a tenu, mais trois choses ont été trouvées en cours de route que personne n'avait vues.** (1) Au re-ancrage Phase 3b : `dataExportService` ne sérialise ni `import_sources` ni `import_config_templates` et fait `DELETE FROM import_sources` à la restauration — **restaurer une sauvegarde détruisait toutes les configurations d'import**, 3e voie de perte du format, indépendante du bug racine (→ #331, + `withTransaction` que le service n'avait nulle part). (2) Un worker a **mesuré** que l'exemple « Solde 2024 » de l'issue n'est pas défectueux (`parseFloat` s'arrête sur le `S`) et a ajouté la fixture qui l'est vraiment. (3) Un autre a mesuré que chez RBC `CAD$` et un `Cheque Number` presque vide **sont** sparse-complementary : le générique les apparie et une ligne de chèque s'importe à −234,05 au lieu de −6,95 — d'où l'override par signature. **Correction de la revue initiale** : `parseFrenchAmount("50,00-")` ne rend pas 50 mais **5000** — `"100,00 CAD"` → 10000, `"1 234,56 CR"` → 123456 : erreur de **facteur 100** qui passait `isNaN` et comptait donc la ligne comme VALIDE. Le ×100 atteignait aussi l'import de titres #245. Durcissement global assumé : un relevé à suffixe de devise produit désormais des **lignes en erreur** plutôt que des valeurs fausses. **`/pr-review` a bloqué 3 des 10 PRs, tous fondés** : (a) #336 — la règle débit/crédit testait `isNaN(debit) && isNaN(credit)`, donc une cellule illisible à côté du remplissage `0,00` calculait 0 − 0 = 0 et importait en silence, **le bug même que la PR fermait, une cellule plus loin** (rejoué sur sa propre fixture : 6 transactions à 0,00 $, 0 erreur) ; (b) #337 — `detectDescriptionColumn` retournait la colonne préférée **sans veto par les données**, et le mot-clé `transaction` capturait la colonne DEBIT/CREDIT de Tangerine, tuant la catégorisation ; (c) #340 — **régression réelle** : la signature Desjardins est 4 libellés génériques appariés par sous-ensemble, et une signature court-circuitait le scan sparse-complementary → un fichier `Date;Description;Débit;Crédit;Montant;Solde` lu correctement AVANT devenait `positive_expense` APRÈS, chaque dépôt en dépense. Corrigés en 3 commits sur la tête de pile (`484c4be`, `ef7de3c`, `37b832e`), **chacun vérifié par mutation** — et cette vérification a révélé qu'un de mes propres tests **passait la mutation** (le fix sur les signatures génériques empêchait la branche gardée d'être atteinte) : il a fallu un fichier reconnu par une signature *légitime* pour l'exercer. Leçon : un garde qui ne peut pas échouer ne garde rien, y compris quand c'est soi qui l'écrit. Incident run : le worker #331 tué par une limite de session pendant sa validation — travail récupéré depuis son worktree et revalidé de zéro (le rationale de sa PR est une reconstruction, signalé comme tel). Gotcha parallélisation : deux agents de revue partageaient `scratchpad/review.md`, l'un a écrasé l'autre entre Write et POST → **donner un chemin de scratch unique par agent**. Suivis ouverts : refus du format à indicateur borné à `amountCol ± 1` ; dérives de comptes préexistantes dans `CLAUDE.md`/`architecture.md` (mesurées et documentées, non corrigées). (ref #323-#332, PRs #333-#342) -- 2026-07-27 : **#310 + #311 mergées — `cargo audit` 9→0, `npm audit` 3→2 acceptées** (`main` `a14258b`, ff-only ; PRs #316 → #318 stackées, CI verte des deux côtés). Parties d'un `/analyse-vulnerabilite` : rapport Défenseur **écarté comme source** (VPS injoignable — SSH Tailscale en attente d'auth navigateur, aucun lien imprimé ; rapport local du 2026-05-06, 82 jours) → vérité live = `cargo audit 0.22.2` + `npm audit` sur `main`, advisory-db `0bfde9d6`. **Le triage d'origine de #310 révisé sur un point** : `quick-xml` (2× 7.5 high) n'est **compilé sur aucune des deux cibles livrées** — `cargo tree -i` vide sur `x86_64-pc-windows-msvc` **et** `x86_64-unknown-linux-gnu`, présent seulement sur `x86_64-apple-darwin` via `plist`←`tauri` (dép. conditionnelle Apple). Il n'y avait donc **rien à attendre de Tauri**, contrairement au « probablement bloqué en amont » du corps. Méthode retenue : tracer **par triple explicite**, jamais `--target all` (qui répond « présent » pour des deps Apple jamais compilées ici) — mémoire [[reference-cargo-audit-reachability-par-cible]]. 6 advisories atteignables corrigées par `cargo update -p rustls-webpki -p tar` (0.103.9→**0.103.13** couvrant les 4, 0.4.44→**0.4.46**) sans toucher `Cargo.toml` ; dérive de lock expliquée et bornée (7 pointeurs `windows-sys` repointés vers des versions **déjà présentes**, 666 paquets avant/après, deps Windows-only → no-op sur Linux ; `--precise` donne le même résultat, c'est la re-résolution de cargo 1.94.1). Les 3 restantes → `.cargo/audit.toml` versionné, lu par les deux workflows sans qu'aucun ne le sache. **Le plan checker a levé un MAJOR fondé** : une liste de suppressions sans déclencheur de retrait **inverse** le problème de l'alarme (l'audit resterait vert si `quick-xml` redevenait atteignable) — d'où 3 règles en **ADR 0018** : admission sur preuve par cible livrée ou absence de correctif ; clé **par ID d'advisory jamais par crate** (une nouvelle advisory sur le même crate repasse au rouge) ; **garde-fou bloquant** dans `check-rust.yml`, placé après `cargo check` (index chaud) avec `--locked`, testant le **code de sortie séparément de la sortie** (un crate absent et un `cargo tree` en panne impriment tous deux du vide) + **canari** `tar`. Le pendant — suppression devenue *inutile* — est #312. **Leçon du 1er run CI** : le garde était **muet en cas de succès**, donc indiscernable dans les logs d'une étape non exécutée — exactement le mode d'échec silencieux que la PR combat (même famille que le « glob `src-tauri/**` non prouvé » de #232) → il logge maintenant chaque vérification (`Suppression guard: 4 checks, exit 0`, visible run 336). Preuve que `.cargo/audit.toml` est bien lu en conteneur : le `workflow_dispatch` de `audit.yml` sur la branche est **vert**, alors que le bump seul n'aurait éliminé que 6 des 9. **npm** (#311) : `postcss` 8.5.13→**8.5.23 sans override** — `vite` déclare `^8.5.3`, seul le lock était périmé (≠ cas #241 où le parent pinnait) ; `nanoid` suit dans sa borne ; **CSS émis identique octet pour octet** (postcss *est* le pipeline CSS, un build vert n'aurait prouvé que la compilation) ; `react-router` **accepté** — advisory mode **RSC**, or `App.tsx:109` monte un `BrowserRouter` client-only, et `react-router-dom` est **figé à 7.18.1** (v8 a fusionné le paquet dans `react-router`) donc le « fix » npm est un **downgrade** en 7.11.0 → sortir de la plage = migration, pas bump (#317, titre portant `react-router` pour la dédup par sous-chaîne de `/analyse-vulnerabilite`). Asymétrie à connaître : **aucun `npm audit` en CI**, donc rien ne rougit côté front — écrit dans `architecture.md` + `CLAUDE.md` pour que les 2 high permanents ne se lisent pas comme une régression. Gotchas Forgejo relevés : logs de job **uniquement via l'URL web** (l'API v1 répond 404 partout) et `head_branch` = `#` sur les runs `pull_request` → [[reference-forgejo-ci-logs-et-head-branch]]. 106 Rust + 871 vitest + builds verts. Aucune migration DB (v1→v16). (ref #310, #311, PRs #316/#318, #312-#315, #317) ## Blockers actifs - Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers. - **Aucun tag avant le go-live du paiement.** Depuis #346 (`main` `170b22d`), « Obtenir Base » est actif et ouvre la page produit, dont le checkout reste en mode test tant que la-compagnie-maximus#141 et maximus-api#62 sont ouvertes (#141 porte déjà « release desktop re-gated ») : une release avant ce go-live livrerait un bouton d'achat dont le paiement ne peut pas aboutir. Vérification manuelle en attente : en Gratuite, le modal de création de profil ouvert depuis `ProfileSwitcher`, puis « J'ai déjà une clé », doit se fermer sur la carte Licence (seule mutation que les tests ne gardent pas). -- Backlog ouvert (`status:ready`, non bloqué) : **vague `planned-2026-09-24-paiements`** (planifiée le 2026-09-24) — desktop **2/3** : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, reste #347 (à partir de `main`) ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). Hors vague, avant go-live : **maximus-api#70** (réactivation d'une machine déjà désactivée → 500, prérequis de maximus-api#62), **#360** (jeton d'une autre machine → Gratuite sans issue, à trancher), #350 (hachage de l'identifiant machine), maximus-api#63 + la-compagnie-maximus#157 (clés de 100 ans + CGV, à déployer ensemble), #363 (tester un `exp` au-delà de 2^32 côté client, suivi de maximus-api#63, priorité basse) ; go-live = la-compagnie-maximus#141 + maximus-api#62 ; `spec-paiements` reste le parapluie des trois dépôts ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **3/4** (#232 livrée le 2026-07-27 ; reste **#234** connectivité du serveur de cache runner, qui débloquerait `Swatinem/rust-cache` — accès hôte VPS requis). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist. +- Backlog ouvert (`status:ready`, non bloqué) : **vague `planned-2026-09-24-paiements` close 3/3** (milestone fermée le 2026-10-01) : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, #347 (PR #364, ferme aussi #360) mergée le 2026-10-01 ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). Hors vague, avant go-live : **#365** (vérification en ligne de la révocation : une licence remboursée et révoquée reste fonctionnelle hors ligne, constat de Max pendant le go-live ; y est aussi consigné le 403 `license_revoked` des cours affiché comme « abonnement premium requis »), maximus-api#70 (réactivation d'une machine désactivée → 500, mergée sur `main` de maximus-api le 09-29, déploiement non vérifié), #350 (hachage de l'identifiant machine), maximus-api#63 + la-compagnie-maximus#157 (clés de 100 ans mergées côté API, CGV à déployer ensemble), reste de #363 (plafond de `/issue-license` après déploiement de #63, essai d'une vraie clé de 100 ans) ; côté serveur, `/v1/prices` ne vérifie pas qu'une machine est encore active (une machine désactivée garde les cours jusqu'à l'expiration de son jeton), issue maximus-api à ouvrir ; go-live = la-compagnie-maximus#141 + maximus-api#62 ; `spec-paiements` reste le parapluie des trois dépôts ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **3/4** (#232 livrée le 2026-07-27 ; reste **#234** connectivité du serveur de cache runner, qui débloquerait `Swatinem/rust-cache` — accès hôte VPS requis). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist. - Suivis des advisories : **#312, #313 et #315 fermées le 2026-08-15** (PRs #321/#322 mergées). **#314 fermée le 2026-08-15, prémisse infirmée** — elle affirmait « zéro run `schedule` », or `audit.yml` tire tous les jours à 06:00 UTC (**19 runs `schedule` verts** du 07-28 au 08-15, sans trou) ; elle a été écrite le 07-27 vers 23:56Z, ~6 h avant le premier tir programmé possible, et décrivait donc une fenêtre trop courte, pas un scheduler en panne. Ces 19 runs confirment au passage que `.cargo/audit.toml` est bien lu en conteneur **sur `main`** — l'ADR 0018 ne l'avait vérifié que par un `workflow_dispatch` sur une branche. **#343 fermée le 2026-09-04** (PR #344) — RUSTSEC-2026-0258 sur `h2`, la seule vulnérabilité qui rougissait l'audit depuis le 08-19 ; l'audit est de nouveau vert sur `main`, prouvé par `workflow_dispatch` (run 393) et non déduit du run de PR. **#348 fermée le 2026-09-21** (PR #349) — RUSTSEC-2026-0285 sur `rustls`, seule vulnérabilité rougissant l'audit depuis le 09-15 (7 runs, 405→411) ; vert reprouvé par `workflow_dispatch` sur `main` (run 414 : 666 paquets, zéro vulnérabilité, 12 warnings autorisés). Deux advisories en cinq semaines sur le même chemin, celui de la mise à jour automatique. Restent **#317** (re-évaluer `react-router` à une migration v8), **#319** (déclencheur de retrait pour `rsa`, désormais **seule** entrée de `.cargo/audit.toml` — le garde-fou est passé à 2 vérifications) et **#345** (domicilier la contrainte `windows-sys 0.61.2` — analysée le 09-04 : le corps d'origine, écrit de mémoire en fin de revue, se trompait sur trois points. Le déclencheur n'est pas « n'importe quel `cargo update` » mais tout bump atteignant le sous-arbre réseau/TLS — mesuré : `-p anyhow` ne bouge rien, `-p hyper` fait descendre les 7 — soit précisément là où tombent les advisories. **Prémisse restreinte le 2026-09-21 par #348** : `-p rustls`, au cœur de ce même sous-arbre, n'en déplace **aucun** — le déclencheur tient à `hyper` et à ce qu'il tire, pas à la couche TLS en général, donc « sous-arbre réseau/TLS » prédit une dérive là où il n'y en a pas et ferait écrire une strophe à la main sans nécessité. D'où la suggestion portée en commentaire sur l'issue : formuler le garde-fou sur la **version résolue de `windows-sys`** plutôt que sur une liste de crates déclencheurs — la liste est ce qui vient d'être trouvé faux, la version est ce qui est protégé. #310 avait **bien** vu la dérive et l'avait jugée inoffensive *sur Linux*, où elle l'est ; son coût tombe sur le build Windows que personne ne mesurait : **3 versions de `windows-sys` compilées contre 5**. Aucun risque de correction — les 7 crates déclarent des bornes larges couvrant les deux états. Et cargo annonce « Locking 1 package » en écrivant 8 changements, donc le silence est le sien. Décision : garde-fou CI sur la version + ADR portant la condition de sortie. Le check croisé Windows depuis Linux a été **testé et écarté** — `cc-rs` s'arrête sur `lib.exe`, il faudrait `cargo-xwin`). Côté npm, **#358** ouverte le 2026-09-26 par `/analyse-vulnerabilite` : `npm audit` remonte 7 findings (4 high, 3 moderate) sur 4 dépendances racines, contre les 2 acceptés au 07-27, et chaque correctif tient dans la borne déjà déclarée ; le lock se régénère avec `npx npm@11` (npm 10 plante sur un bump de vitest 4.1.x). - Nouveau bug ouvert : **#320** — les installations `.rpm` ne reçoivent aucune mise à jour (`latest.json` ne porte qu'une entrée `linux-x86_64` construite depuis le `.deb`, `release.yml:104-122`), alors que les `.rpm` continuent d'être publiés en assets. Documenté en §3 de `docs/qa-update-cycle.md` (« ne pas tester : cassé, pas seulement non vérifié »). - Suivis ouverts du chantier import (milestone fermée) : le refus du format « montant absolu + indicateur D/C » ne scanne que `amountCol ± 1`, donc une colonne D/C placée ailleurs passe encore ; dérives de comptes préexistantes dans `CLAUDE.md` et `docs/architecture.md` (composants, pages, services, `Version actuelle : 0.6.3` vs tag v0.14.0) mesurées et documentées en revue, non corrigées. -- **Étape 10 de la release v0.15.0 consignée le 2026-09-13** — plus un blocker : cycle réel v0.14.0 → v0.15.0 validé par les deux testeurs ; trace rédigée pour le corps de la release (`PATCH` à appliquer par Max) ; invite d'authentification Linux non consignée ; #320 reste ouverte (`.rpm`). Deux restes : (1) le texte de la carte Compte Maximus (« Le compte est requis uniquement pour les fonctionnalités Premium », hérité de v0.7) contredit v0.15.0 où Premium vient de la clé, et a vraisemblablement conduit un testeur à se connecter pour rien (code de vérification Logto reçu par courriel) — reformulation livrée par #346 (PR #362), avec une réserve notée dans l'ADR 0017 (exacte tant que le chemin abonnement → Premium reste dormant) ; (2) le flux Logto exact (connexion par code, mot de passe oublié ou inscription) reste à lire dans le journal d'audit. Position produit : v0.15.0 ; `[Unreleased]` porte l'entrée `rustls` du 2026-09-21, puis #270 et #346. +- **Étape 10 de la release v0.15.0 consignée le 2026-09-13** — plus un blocker : cycle réel v0.14.0 → v0.15.0 validé par les deux testeurs ; trace rédigée pour le corps de la release (`PATCH` à appliquer par Max) ; invite d'authentification Linux non consignée ; #320 reste ouverte (`.rpm`). Deux restes : (1) le texte de la carte Compte Maximus (« Le compte est requis uniquement pour les fonctionnalités Premium », hérité de v0.7) contredit v0.15.0 où Premium vient de la clé, et a vraisemblablement conduit un testeur à se connecter pour rien (code de vérification Logto reçu par courriel) — reformulation livrée par #346 (PR #362), avec une réserve notée dans l'ADR 0017 (exacte tant que le chemin abonnement → Premium reste dormant) ; (2) le flux Logto exact (connexion par code, mot de passe oublié ou inscription) reste à lire dans le journal d'audit. Position produit : v0.15.0 ; `[Unreleased]` porte l'entrée `rustls` du 2026-09-21, puis #270, #346 et #347.