diff --git a/CHANGELOG.md b/CHANGELOG.md index 283e48349..ca07fcb01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ Le contenu est rédigé en français à destination des **fiduciaires, PME, ind ### Added +- **Consulter le journal d'audit à l'écran.** La trace de chaque opération qui modifie les livres se lisait depuis la v0.12.0 par l'interface de programmation seulement, ce qui supposait un outil technique. Elle se consulte désormais dans l'application — *Administration → Journal d'audit*, pour les comptables et les administrateurs : filtres par période, type d'entité, numéro et action, actions et types traduits dans la langue de l'installation, détail de chaque entrée, export CSV. ⚠️ Les dates filtrent sur des **jours UTC** ; le tableau affiche l'heure locale. ([#378](https://github.com/guycorbaz/kesh/issues/378)) + - **Annuler un règlement client.** Un règlement enregistré par erreur était jusqu'ici **définitivement incorrigible** : la contre-passation directe de son écriture était refusée, et la seule sortie — une écriture manuelle — laissait la facture se dire réglée quand le grand livre disait le contraire. La fiche facture liste désormais ses règlements, et chacun s'**annule par contre-passation** : une écriture inverse **datée du jour**, le règlement retiré de la facture, le montant redevenu dû. Les deux écritures restent visibles au grand livre. Le bouton est remplacé par la raison quand l'annulation n'est pas possible : facture **créditée** (le règlement est alors un paiement à lettrer), exercice du règlement **clôturé** (un administrateur doit le rouvrir), règlement **rapproché** d'une transaction bancaire (annulez d'abord le rapprochement — le bouton est à côté du motif), compte **archivé**, ou aucun exercice ouvert pour la date du jour. diff --git a/README.md b/README.md index 5fdf85034..13ebc1ec3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ ## Fonctionnalités -- **Comptabilité en partie double** — plan comptable suisse, écritures validées, audit log +- **Comptabilité en partie double** — plan comptable suisse, écritures validées, journal d'audit consultable à l'écran - **Carnet d'adresses & catalogue produits** — contacts, conditions de paiement, TVA - **Facturation QR Bill 2.2** — génération PDF conforme au standard suisse, **adresses structurées (type S)** conformes à l'exigence SIX en vigueur (rue / n° / NPA / localité séparés) ✓ - **Ventilation du chiffre d'affaires par ligne de facture** — chaque ligne peut porter son propre **compte de produit** (honoraires, marchandises, prestations…), sélectionnable dans le formulaire ; une ligne laissée vide suit le compte par défaut de la société, affiché explicitement. L'écriture comptable ventile le crédit sur les bons comptes dès la validation, avoirs compris — compte de résultat exploitable sans reclassement manuel ✓ @@ -217,7 +217,7 @@ Le projet suit une approche **BMAD** (Breakthrough Method of Agile AI-driven Dev | v0.11.0 | **E23 Dette i18n** (#316, #283) — les écrans réclamaient des libellés que **aucun** des quatre catalogues ne contenait : Kesh servait alors du **français correct** à un germanophone, sans erreur ni avertissement. **1600 messages écrits** — 358 libellés français neufs et 414 dans chacune des trois autres langues, les quatre catalogues portant désormais **les mêmes 1630 clés** —, et **six garde-fous** posés pour que le défaut ne puisse plus revenir en silence — le principal étant devenu **inconditionnel** : une clé sans traduction ne peut plus être tolérée, elle fait rougir le gate | ✅ Done | | v0.11.1 (hotfix) | **Trois défauts qui ne disaient rien** — l'**écran d'import bancaire se vidait** dès qu'on y déposait un fichier CAMT.053, rendant la fonction inutilisable ; un **compte de facturation pouvait s'effacer tout seul** après être devenu non imputable, faisant échouer la facturation suivante sur une erreur qui ne nommait pas la cause ; le **filtre Statut des factures** rendait une liste vide trompeuse depuis la page 2. Plus un en-tête de colonne sans nom pour les lecteurs d'écran | ✅ Done | | v0.12.0 | **E24 « Les livres justes »** — première vague de correction issue d'un **audit par trois experts** (comptable, fiscaliste, directeur financier) mené le 2026-08-26 sur l'ensemble du logiciel. ⚠️ **Le défaut fondateur** : l'encaissement d'une facture client ne produisait **aucune écriture comptable** — le compte débiteurs accumulait des débits jamais crédités, la banque était sous-évaluée, et **le rapprochement bancaire était impossible**. Le mode d'échec est silencieux : le bilan reste équilibré, aucun contrôle ne rougit. Cette vague livre l'**écriture d'encaissement**, le **grand livre** (l'extrait de compte, qui n'existait pas — « l'instrument qui rend les autres défauts visibles »), la **contre-passation d'une écriture** — corriger sans effacer, l'écriture inverse laissant l'origine intacte, comme l'exige l'art. 958f CO : *l'exigence n'est pas qu'on ne se trompe jamais, c'est que la correction soit apparente* —, le **gel** qui la rend obligatoire (une écriture enregistrée n'est plus ni modifiable ni supprimable, par personne), le **verrou de période** — arrêter les livres à une date, pour qu'un trimestre TVA déjà déclaré ne bouge plus dans le dos de personne —, et la correction des comptes de clôture. ⚠️ **L'epic est clos, la vague ne l'est pas** : le jalon [Vague 1](https://github.com/guycorbaz/kesh/milestone/3) compte encore une quinzaine d'issues ouvertes — le journal d'audit n'est consultable par aucun écran et la Balance ne concorde pas avec le Bilan. **Publiée le 2026-09-21**, cette version emporte aussi les premières stories de l'E25 — piste de contrôle inaltérable, quatorze routes tracées, consultation du journal d'audit par l'API, retypage d'un compte mouvementé, numérotation des écritures par compteur, verrou de période à la suppression. ⚠️ **Elle n'est pas encore destinée à tenir une comptabilité réelle** : plusieurs fonctions d'un exercice complet manquent, et l'export de sauvegarde est incomplet ([#386], corrigé depuis — à paraître en v0.12.1). | ✅ Publiée | -| v0.12.1 | **E25 « Vague 1, la suite »** — l'Epic 24 s'appelait « vague 1 » et **n'en avait livré que le quart** ; cet epic-ci ne se clôt qu'avec le jalon [Vague 1](https://github.com/guycorbaz/kesh/milestone/3) **à zéro issue ouverte**. Livré à ce jour : la **piste de contrôle survit désormais à l'import d'une sauvegarde** ([#376]) — les entrées de l'archive sont *fusionnées* avec les vôtres au lieu de les remplacer, et chacune porte le nom de son auteur **au moment de l'écriture**, un instantané qui survit au remplacement des comptes utilisateurs. ⚠️ **Le motif n'est pas théorique** : effacer ses traces en important un backup était un scénario de blanchiment, relevé au III.3 de l'audit du 2026-08-26. La **réinitialisation des données de démonstration** exige désormais le rôle d'administrateur ([#377]) — *sa seule protection était un état, pas un droit*. S'y ajoute le **comblement des trous d'alimentation** ([#379]) : la gestion des utilisateurs, la modification de la société, les personnes de contact et les pièces importées laissent désormais une trace — **quatorze routes** qui n'en écrivaient aucune, dont le **changement de rôle**, qui porte son propre libellé pour qu'une promotion au droit d'écrire dans les livres ne se noie pas parmi les activations. ⚠️ **L'effacement d'une adresse par omission est tracé** lui aussi : sur ces écrans, un champ laissé vide efface, et détruire le canal de recouvrement d'un compte ne laissait jusqu'ici aucune trace. Enfin, **un numéro d'écriture n'est plus jamais réattribué** ([#381]) : supprimer la dernière écriture d'un exercice rendait son numéro à la suivante, au contenu différent, sans que rien ne le signale ; il vient désormais d'un compteur qui ne redescend pas. Le trou, lui, reste possible — mais il se voit, et le journal d'audit l'explique. Et le **verrou de période tient aussi contre la suppression** ([#443]) : faire disparaître une facture validée emportait son écriture même datée d'un trimestre déjà déclaré. Et **supprimer une facture validée n'est plus possible — il faut la « dévalider »** ([#440]) : jusqu'ici, le bouton *Supprimer* effaçait la facture **et son écriture comptable** d'un seul geste, si bien que la destruction d'une écriture était un **effet de bord du mot « supprimer »**. Le geste porte désormais son nom, il repasse la facture en brouillon en **conservant son numéro** — la revalidation reprend *le même* —, et **huit motifs** peuvent le refuser, chacun nommé. *Le cas le plus fréquent, l'erreur de saisie repérée après validation, se corrige donc sans émettre un avoir qui ne correspondrait à aucune réalité commerciale.* ⚠️ **Restent ouverts** : l'**écran** de consultation ([#378]) — la piste de contrôle se lit désormais par l'**API**, filtrable et exportable en CSV dans la langue de l'installation, mais aucune page ne la rend lisible depuis l'application —, la séquence d'**installation** ([#434]) et les **gestes de session** ([#435]) — ces deux dernières découvertes en comblant les premières, et **inscrites au registre des routes** que la story a posé, avec le numéro de l'issue qui les suit. | 🚧 En cours | +| v0.12.1 | **E25 « Vague 1, la suite »** — l'Epic 24 s'appelait « vague 1 » et **n'en avait livré que le quart** ; cet epic-ci ne se clôt qu'avec le jalon [Vague 1](https://github.com/guycorbaz/kesh/milestone/3) **à zéro issue ouverte**. Livré à ce jour : le **journal d'audit survit désormais à l'import d'une sauvegarde** ([#376]) — les entrées de l'archive sont *fusionnées* avec les vôtres au lieu de les remplacer, et chacune porte le nom de son auteur **au moment de l'écriture**, un instantané qui survit au remplacement des comptes utilisateurs. ⚠️ **Le motif n'est pas théorique** : effacer ses traces en important un backup était un scénario de blanchiment, relevé au III.3 de l'audit du 2026-08-26. La **réinitialisation des données de démonstration** exige désormais le rôle d'administrateur ([#377]) — *sa seule protection était un état, pas un droit*. S'y ajoute le **comblement des trous d'alimentation** ([#379]) : la gestion des utilisateurs, la modification de la société, les personnes de contact et les pièces importées laissent désormais une trace — **quatorze routes** qui n'en écrivaient aucune, dont le **changement de rôle**, qui porte son propre libellé pour qu'une promotion au droit d'écrire dans les livres ne se noie pas parmi les activations. ⚠️ **L'effacement d'une adresse par omission est tracé** lui aussi : sur ces écrans, un champ laissé vide efface, et détruire le canal de recouvrement d'un compte ne laissait jusqu'ici aucune trace. Enfin, **un numéro d'écriture n'est plus jamais réattribué** ([#381]) : supprimer la dernière écriture d'un exercice rendait son numéro à la suivante, au contenu différent, sans que rien ne le signale ; il vient désormais d'un compteur qui ne redescend pas. Le trou, lui, reste possible — mais il se voit, et le journal d'audit l'explique. Et le **verrou de période tient aussi contre la suppression** ([#443]) : faire disparaître une facture validée emportait son écriture même datée d'un trimestre déjà déclaré. Et **supprimer une facture validée n'est plus possible — il faut la « dévalider »** ([#440]) : jusqu'ici, le bouton *Supprimer* effaçait la facture **et son écriture comptable** d'un seul geste, si bien que la destruction d'une écriture était un **effet de bord du mot « supprimer »**. Le geste porte désormais son nom, il repasse la facture en brouillon en **conservant son numéro** — la revalidation reprend *le même* —, et **huit motifs** peuvent le refuser, chacun nommé. *Le cas le plus fréquent, l'erreur de saisie repérée après validation, se corrige donc sans émettre un avoir qui ne correspondrait à aucune réalité commerciale.* Le journal d'audit se **consulte enfin à l'écran** ([#378]) — *Administration → Journal d'audit*, pour les comptables et les administrateurs, filtrable et exportable en CSV dans la langue de l'installation. ⚠️ **Restent ouverts** : la séquence d'**installation** ([#434]) et les **gestes de session** ([#435]) — toutes deux découvertes en comblant les trous d'alimentation, et **inscrites au registre des routes** que la story a posé, avec le numéro de l'issue qui les suit. | 🚧 En cours | | v0.13.0 (prévu) | **E15 Lettrage, justificatifs & compléments** — ✅ **dégelé le 2026-08-28** : le lettrage rapproche deux lignes d'un même compte, et une facture cliente réglée en a désormais deux — l'écriture d'encaissement livrée par l'E24 (#371) crédite le compte débiteurs que la vente avait débité. La spécification est faite — onze passes de revue, jugée comptablement juste — mais elle a été écrite **avant** ce changement de modèle : elle sera relue contre la table `invoice_settlements`, qui en est le substrat. | 📋 Backlog | | v0.5+ (prévu) | **Tableau de bord & Comptabilité personnelle** — widgets configurables sur la page d'accueil (évolution du patrimoine fortune & dettes mois par mois [#164], donut de répartition des dépenses par compte/sous-compte [#165], comparatif recettes/dépenses mensuel [#166]) ; **Budgets** (E13 [#196]) + comparatif budget validé vs réalité [#197] | 📋 Backlog | diff --git a/_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md b/_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md new file mode 100644 index 000000000..064a9a080 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md @@ -0,0 +1,414 @@ +# Story 25.1c-b : L'écran de consultation du journal d'audit + +Status: split + +⛔ **FICHE DÉCOUPÉE LE 2026-09-15 — NE PAS IMPLÉMENTER CE DOCUMENT.** Arbitrage du Project Lead après la +passe 2 de validation (sévérité MEDIUM → MEDIUM sur des défauts d'origine répartis dans cinq zones) : + +- **`25-1c-b1-journal-audit-ecran.md`** — l'écran ; +- **`25-1c-b2-journal-audit-textes.md`** — les manuels, le README, le vocabulaire. + +L'extraction du téléchargement (volet C) **sort** de la story, vers une issue de dette. Cette fiche reste +la **référence** de ce que les passes 1 et 2 ont établi ; les treize findings de la passe 2 sont appliqués +dans les deux fiches filles, pas ici. + +⚠️ **Issue du SPLIT de la 25-1c** (arbitrage du Project Lead du 2026-09-15) : + +| | objet | état | +|---|---|---| +| 25-1c-zero | la colonne `audit_log.company_id` | done — PR #437 ouverte | +| 25-1c-a | la route de consultation et son export CSV — backend | ready-for-dev, validée | +| **25-1c-b** *(celle-ci)* | **l'écran, le menu, les libellés, les E2E, les manuels** | ready-for-dev | + +⛔ **Dépendance dure** : cet écran consomme `GET /api/v1/audit-log` et `/export.csv` **tels que la +25-1c-a les livre** (`25-1c-a-journal-audit-route.md`, AC 5-14). **Ne pas implémenter avant le merge de +la 25-1c-a** — la branche est empilée sur la sienne et se rebase sur `main` après. + +⛔ **C'est cette story qui FERME #378** : le titre de sa PR porte `closes #378` (le dépôt merge en +squash — § *Commits qui adressent une issue*). + +## Story + +**En tant que** comptable ou administrateur, +**je veux** consulter le journal d'audit de ma société dans l'application — le filtrer, déplier le +détail d'une entrée, l'exporter, +**afin de** **produire** la trace des corrections apportées aux livres sans accès direct à la base — +« *"apparent" et "archivé dans une table que personne ne peut lire" ne sont pas la même chose* » (#378). + +**Couvre** : [#378], volet écran. **Ferme** l'issue. + +## ✅ Arbitrages du Project Lead — 2026-09-15 (`epic-25-vague1-suite.md`) + +1. **Qui consulte** : **Comptable** et **Admin** ; ni Consultation, ni clé API. +2. **Filtre strict par société** (porté par la route). +3. **Affichage sobre** — cité de l'epic : *« le code d'action tel quel, les types d'entité traduits, les + détails en JSON indenté »*. +4. **Vocabulaire** : **« journal d'audit »** — `Audit-Protokoll`, `registro di audit`, `audit log`. + +⚠️ **Hérités de la 25-1c-a, NON arbitrés — à confirmer en revue** : une entrée **sans société** est +affichée (AC 5 : « société indéterminée ») ; l'**export écrit une entrée d'audit** ; les cellules du +**CSV** gardent les codes bruts. + +## Acceptance Criteria + +### Volet A — la feature `frontend/src/lib/features/audit-log/` + +**1. Les types** — `audit-log.types.ts` : `AuditLogEntry` en miroir **exact** du DTO de la 25-1c-a +(AC 8) — `id`, `createdAt` (chaîne ISO UTC finissant par `Z`), `actorLabel`, `actorType: 'user' | +'api_key'`, `actorApiKeyId: number | null`, `userId`, `action`, `entityType`, `entityId`, +`companyId: number | null`, `details: unknown` ; `AuditLogQuery` (`dateFrom`, `dateTo`, `entityType`, +`entityId`, `action`, `offset`, `limit`). Liste : l'enveloppe `{ items, total, offset, limit }` déjà +typée (`src/lib/shared/types/user.ts:22-27`). + +**2. L'API** — `audit-log.api.ts`, via `apiClient` : + +- `listAuditLog(query)` → `GET /api/v1/audit-log` ; **un paramètre vide ou absent n'est pas envoyé** ; +- `exportAuditLogCsv(query)` → `apiClient.getBlob(...)` (`api-client.ts:526-529`), nom de fichier lu + dans `Content-Disposition` par `parseContentDispositionFilename`, téléchargement par + `triggerDownload` — **tous deux importés du module partagé de l'AC 11**, jamais recopiés. + +**3. Les libellés des 28 types d'entité** — `entity-type-label.ts`, **sur le patron exact de +`imported-supplier-invoices/error-label.ts:28-39`** — **la CARTE seulement** : sa fonction +`importErrorLabel` (`:41-48`) a un repli **différent** de celui qu'exige cet AC (cf. ci-dessous). Une carte exportée +`ENTITY_TYPE_LABELS: Record` (code → [suffixe de clé, repli français]) et une +fonction `entityTypeLabel(code)`. Clés `audit-log-entity-`, suffixe = code avec `_` → `-`. + +| code | repli | code | repli | +|---|---|---|---| +| `account` | Compte | `export` | Export | +| `api_key` | Clé API | `fiscal_year` | Exercice comptable | +| `bank_account` | Compte bancaire | `imported_supplier_invoice` | Facture importée | +| `bank_imports` | Import bancaire | `installation` | Installation | +| `bank_profiles` | Profil bancaire | `invoice` | Facture | +| `bank_transaction` | Transaction bancaire | `journal_entry` | Écriture | +| `company` | Société | `payment_batch` | Lot de paiement | +| `company_dunning_settings` | Réglages de recouvrement | `product` | Produit | +| `company_invoice_settings` | Réglages de facturation | `project` | Projet | +| `contact` | Contact | `reconciliation_rules` | Règle d'affectation | +| `contact_person` | Personne de contact | `report` | Rapport | +| `credit_note` | Avoir | `supplier_invoice` | Facture fournisseur | +| `dunning_level` | Niveau de rappel | `user` | Utilisateur | +| `email_template` | Modèle d'e-mail | `vat_rate` | Taux de TVA | + +- ⚠️ **Les codes au pluriel** (`bank_imports`, `bank_profiles`, `reconciliation_rules`) sont les codes + **réels** : ne pas les « corriger » — la carte doit coïncider avec ce que la base contient. +- ⛔ **Un code INCONNU s'affiche tel quel**, sans erreur — **et c'est ici que le patron s'arrête** : + `importErrorLabel` rend pour un code inconnu un message **traduit et interpolé** + (`imported-supplier-invoices-error-unknown`, `error-label.ts:45-47`) ; `entityTypeLabel` rend le **code + brut**, sans appel à `i18nMsg`. Motif : le journal conserve les codes des versions + antérieures et des archives fusionnées, et un code futur ne doit rien casser. +- **La liste est recomptée depuis la source**, pas recopiée (cf. Dev Notes : 28 types littéraux, trois + sites non littéraux vérifiés à la main). + +**4. Les paramètres d'URL** — `query-helpers.ts` sur le patron de +`journal-entries/query-helpers.ts:19-121` : sérialisation qui **omet** les valeurs par défaut, +lecture qui **ignore** les valeurs invalides (date mal formée, `entityId` non numérique ou `<= 0`, +`entityType` hors carte **conservé** — un code historique doit rester filtrable). + +### Volet B — l'écran `frontend/src/routes/(app)/audit-log/` + +**5. La page** — `+page.svelte` : + +- **filtres** : période (`Input type="date"`, du / au), type d'entité (`Select` : « Tous » + les 28 + libellés), identifiant d'entité (numérique, **désactivé tant qu'aucun type n'est choisi** — la route + répond 400 sinon), action (texte, anti-rebond 300 ms) ; **synchronisés dans l'URL** et relus au + montage (patron `journal-entries/+page.svelte:138-183`) ; bouton « Réinitialiser » ; +- **tableau**, colonnes : **Date** (heure **locale** du navigateur, `Intl.DateTimeFormat` de la locale + courante, depuis `createdAt` UTC) · **Auteur** (`actorLabel`, avec la mention « clé API » si + `actorType === 'api_key'`) · **Action** (code **brut**, police à chasse fixe) · **Type d'entité** + (`entityTypeLabel`) · **N°** (`entityId`, « — » s'il vaut `0`) · **Société** (« société + indéterminée » si `companyId` est `null`, **rien d'autre sinon** — la société est celle de + l'utilisateur) · **Détails** (bouton qui déplie un `
` de `JSON.stringify(details, null, 2)` ;
+  absent si `details` est `null`) ;
+- **pagination** Précédent / Suivant, « X–Y sur N », `limit` 50, garde `if (loading) return` (patron
+  `journal-entries/+page.svelte:245-265`) ;
+- **trois états distincts** — chargement, **erreur** (message, jamais une liste vide silencieuse —
+  patron `supplier-invoices/+page.svelte:429-438`), vide ;
+- **export CSV** : bouton qui transmet **les filtres affichés**, désactivé pendant l'export ; un 400
+  `RESULT_TOO_LARGE` affiche un message dédié (« Trop de résultats : affinez les filtres »), toute
+  autre erreur passe par `notifyError`.
+
+**6. La garde** — `+page.ts`, patron **exact** de `supplier-invoices/import/+page.ts` : `ssr = false`,
+redirection 302 vers `/` si le rôle n'est ni `Admin` ni `Comptable`. ⚠️ Le masquage d'interface ne
+protège rien : c'est le 403 de la route qui fait foi.
+
+**7. Le menu** — entrée `{ i18nKey: 'nav-audit-log', fallback: "Journal d'audit", href: '/audit-log' }`
+dans la liste **`comptableOnly`** du groupe `administration` (`routes/(app)/+layout.svelte:130-136`).
+⚠️ **`'audit-log'` s'ajoute à `FAMILLES_RESOLUES['nav-']`** (`i18n-keys.test.ts:321-327`) : une clé de
+menu est une donnée lue par `getItemLabel`, et c'est ce trou qui avait laissé quatre entrées en
+français dans les quatre langues (commentaire `:303-307`).
+
+**8. Les sélecteurs** — `data-testid` sur tout ce que les tests visent : `audit-log-table`,
+`audit-log-row`, `audit-log-empty`, `audit-log-error`, `audit-log-loading`, `audit-log-export`,
+`audit-log-filter-date-from`, `-date-to`, `-entity-type`, `-entity-id`, `-action`, `-reset`,
+`audit-log-details-toggle`, `audit-log-details`, `audit-log-prev`, `audit-log-next`. ⛔ **Aucun
+sélecteur par libellé** — `e2e-selecteurs-traduits.test.ts` le refuserait, et `DETTE_CONNUE` ne
+s'allonge pas.
+
+### Volet C — le téléchargement partagé
+
+**9. `frontend/src/lib/shared/utils/download.ts`** reçoit `triggerDownload` et
+`parseContentDispositionFilename`, **déplacés** depuis `features/export/exports.api.ts:52-110`, avec
+leurs tests. **Comportement identique**.
+
+⛔ **`parseContentDispositionFilename` n'a PAS la même distribution que `triggerDownload`, et l'oublier
+casse la compilation** :
+
+| site | aujourd'hui | après |
+|---|---|---|
+| `features/export/exports.api.ts:52` | **définition d'origine**, exportée | supprimée — importée de `download.ts` |
+| `features/admin-backup/admin-backup.api.ts:55` | **seconde copie, exportée** — son commentaire `DC-B1` (`:10-14`) planifiait déjà l'extraction « vers `lib/shared/utils/download.ts` » | supprimée — importée de `download.ts` |
+| `features/imported-supplier-invoices/imported-supplier-invoices.api.ts:11` | **import croisé** depuis `$lib/features/export/exports.api` | **repointé** sur `$lib/shared/utils/download` — sans quoi il ne compile plus |
+
+Les tests des deux copies (`exports.api.test.ts`, `admin-backup.api.test.ts`) se **fusionnent** dans
+celui de `download.ts`, sans perdre un cas ; les deux copies étant identiques, un cas présent d'un seul
+côté est conservé.
+
+**10. Les quatre copies existantes de `triggerDownload`** l'importent désormais —
+`reports.api.ts:348`, `exports.api.ts:98`, `imported-supplier-invoices.api.ts:71`,
+`admin-backup.api.ts:92`. Motif : la règle DRY du `CLAUDE.md`, et le commentaire même de
+`exports.api.ts:89-92`, qui reportait l'extraction « si > 2 features dupliquent » — elles sont
+quatre, l'écran serait la cinquième.
+
+⚠️ **Ce volet est un rollout mécanique, séparable** : s'il résiste (un test d'une autre feature qui
+casse pour une raison sans rapport), **l'AC 9 reste obligatoire** et l'AC 10 peut sortir en issue de
+dette — à écrire, pas à taire.
+
+### Volet D — les libellés et le vocabulaire
+
+**11. Les clés i18n**, dans les quatre catalogues (`crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl`),
+en un bloc `# --- Journal d'audit — écran (Story 25-1c-b) ---` : `nav-audit-log`, titre et sous-titre de
+page, libellés des filtres et du bouton de réinitialisation, en-têtes de colonnes, mentions « clé API »
+et « société indéterminée », bouton de détail, états vide / erreur / trop de résultats, bouton d'export,
+et les **28** `audit-log-entity-*`. **Le nombre total se recompte depuis la source** à l'implémentation
+(`grep -c '^audit-log-\|^nav-audit-log' …`) et s'écrit avec sa ventilation ; les 12 clés
+`audit-log-csv-header-*` et `audit-log-export-error-too-large` de la 25-1c-a **existent déjà** et ne se
+recréent pas.
+
+**12. Les gardes i18n du frontend, mises à jour avec leur ventilation** (`src/lib/shared/i18n-keys.test.ts`) :
+
+- `MOTIFS_DYNAMIQUES['audit-log-entity-']` et **`CARDINALITES['audit-log-entity-'] =
+  Object.keys(ENTITY_TYPE_LABELS).length`** — **lu depuis la production**, sur le précédent
+  `imported-supplier-invoices-error-` (`:299`), et non recopié ;
+- `FAMILLES_RESOLUES['nav-']` (AC 7) ;
+- les compteurs d'`ATTENDU` (`:206-213`) — `sitesTotal`, et selon la forme des appels `sitesGabarit`,
+  `sitesNonResolus`, `relais` — **recomptés**, leur historique commenté prolongé ;
+- `CANDIDATES_ATTENDUES` d'`i18n-libelle-en-dur.test.ts:115` si une fonction `*Label` est ajoutée
+  (`entityTypeLabel` en est une).
+
+**13. Le vocabulaire, porté au glossaire** — `docs/i18n-glossaire.md`, **partie A** (terme arbitré) :
+« journal d'audit » | `Audit-Protokoll` | `registro di audit` | `audit log` | arbitrage du 2026-09-15, clé
+`nav-audit-log`. ⛔ **Règle 3 du glossaire : un terme de la partie A s'installe en mettant à jour TOUTES
+ses occurrences déjà livrées.** Deux clés disent aujourd'hui autre chose :
+
+| clé | fr | de | it | en |
+|---|---|---|---|---|
+| `fiscal-year-reopen-confirmation-body` | « piste d'audit » | « Audit-Protokoll » | « pista di audit » | « audit trail » |
+| `export-global-content-excludes` | « journal d'audit interne » | « internes Audit-Log » | « registro audit interno » | « internal audit log » |
+
+⇒ **alignées** sur les quatre termes. Le grep `grep -rniE "audit" crates/kesh-i18n/locales/*/messages.ftl`
+se rejoue pour trouver une troisième occurrence. ⚠️ **Greper le mot `audit` seul, jamais la locution** :
+le catalogue français écrit l'apostrophe **typographique** (`’`) — un motif `piste d'audit` rend zéro
+ligne sur `fr-CH:766`, vérifié à la spécification. Les **libellés des 28 types** suivent la partie A là où
+elle les atteste ; un terme récurrent qu'elle n'atteste pas va en **partie B**, avec sa valeur proposée.
+
+### Volet E — ce que l'écran rend faux
+
+**14. Le manuel utilisateur, `user-manual.tex:497-503`** — l'encadré « Ce que le logiciel ne fait pas
+encore à votre place » affirme : *« Ce qui manque encore, c'est la consultation du journal d'audit depuis
+l'application »*. **Devient faux.** ⚠️ **Le même encadré porte un second énoncé DÉJÀ faux**, constaté à
+la spécification : *« Le verrou reste par ailleurs annuel : aucun verrou de période plus fin n'existe
+encore »* — la Story **24-4c** a livré le verrou de période (PR #425). Les deux se corrigent **ensemble** ;
+si l'encadré n'a plus rien à dire, il disparaît.
+
+**15. Le manuel utilisateur, `user-manual.tex:1611-1616`** — *« n'est pas encore consultable via un écran
+dédié dans l'interface (page de consultation prévue pour une version ultérieure) »*. → décrire l'écran :
+menu **Administration → Journal d'audit**, réservé au Comptable et à l'Admin, filtres, détail, export CSV
+(dates en **UTC** dans le fichier, en heure locale à l'écran).
+
+**16. Le manuel d'administration, `admin-manual.tex:1786`** — après la 25-1c-a, il dira qu'une route
+existe et que l'écran reste à venir : **l'écran existe**.
+
+**17. Le README**, feuille de route : `README.md:218` (« le journal d'audit n'est consultable par aucun
+écran ») et `:219` (« la consultation depuis l'application ([#378]), qui n'existe toujours pas »). →
+mis à jour **dans le même commit** (§ *Synchroniser le planning du README*).
+
+**18. Régénération et contrôle** : `make fr`, PDF commités, **PDF aplati vérifié**
+(`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) — anciennes phrases absentes, nouvelles présentes. ⛔ En
+LaTeX le souligné s'écrit `\_`. `website/index.html:106` et `roadmap.html:105` (« an audit log that a
+backup import no longer replaces ») **restent vrais** et ne se touchent pas.
+
+### Volet F — les tests
+
+**19. Vitest** :
+
+- `entity-type-label` : les 28 codes rendent leur clé ; `_` → `-` ; **un code inconnu rend le code
+  brut** ;
+- `query-helpers` : aller-retour URL, valeurs invalides ignorées, `entityType` inconnu conservé ;
+- `audit-log.api` : un paramètre vide n'est pas envoyé ; l'export passe par `getBlob` et le nom de
+  `Content-Disposition` ;
+- `download.ts` : les tests déplacés passent **sans modification de leurs assertions** ;
+- la page : `load()` **redirige** un rôle Consultation (patron `routes/(app)/users/users-page.test.ts:26-53`) ;
+  rendu des trois états, de « société indéterminée », de « — » pour `entityId = 0`, et du dépliage du
+  détail, API mockée (patron `contacts/contacts-page.test.ts:17-40`).
+
+**20. E2E Playwright** — `frontend/tests/e2e/audit-log.spec.ts`, patron `journal-entries.spec.ts:1-40` :
+
+- un Comptable crée une entité auditée par `authedApiContext` (un contact), ouvre l'écran **par le
+  menu** (`nav-link-audit-log`) et voit la ligne `contact.created` ;
+- le filtre par type d'entité la garde, un filtre sur une autre période l'écarte ; l'URL survit au
+  rechargement ;
+- le détail se déplie et contient le JSON ;
+- l'export déclenche un téléchargement dont `suggestedFilename()` commence par `kesh-journal-audit-`
+  (patron `invoices_echeancier.spec.ts:186-191`) ;
+- ⛔ un utilisateur **Consultation** ne voit **pas** l'entrée de menu (`toHaveCount(0)`) et `/audit-log`
+  le **redirige** (patron `reports.spec.ts:421-446`).
+
+**21. Épreuve par mutation, résultats OBSERVÉS consignés** :
+
+| mutation | test attendu rouge |
+|---|---|
+| garde de `+page.ts` retirée | 19, redirection Consultation |
+| repli « code inconnu » retiré de `entityTypeLabel` | 19, code inconnu |
+| une entrée retirée de `ENTITY_TYPE_LABELS` sans toucher le catalogue | la cardinalité de `i18n-keys.test.ts` |
+| entrée de menu passée de `comptableOnly` à `items` | 20, Consultation voit le menu |
+| filtres non transmis à l'export | 19, API d'export |
+
+⛔ **Un test qui ne compile pas ne rougit pas : il se tait.**
+
+## Tasks / Subtasks
+
+- [ ] **T0 — Rebase** sur `main` après le merge de la 25-1c-a (ne pas implémenter avant) ; vérifier que
+      les deux routes répondent (`curl` authentifié) sur le backend local.
+- [ ] **T1 — Téléchargement partagé** (AC 9, 10), `npm run test:unit` vert **avant** d'aller plus loin.
+- [ ] **T2 — Types, API, libellés, paramètres d'URL** (AC 1-4) et leurs tests (AC 19).
+- [ ] **T3 — Page, garde, menu, sélecteurs** (AC 5-8) et tests de page (AC 19).
+- [ ] **T4 — i18n, quatre locales** (AC 11) — ⚠️ recompte depuis la source.
+- [ ] **T5 — Gardes i18n** (AC 12), compteurs **recomptés** et ventilés.
+- [ ] **T6 — Glossaire et alignement des deux clés** (AC 13).
+- [ ] **T7 — E2E** (AC 20). ⛔ *Une tâche qui décrit un test est une promesse ; la cocher sans l'avoir
+      écrit la transforme en mensonge.*
+- [ ] **T8 — Mutations** (AC 21), résultats observés.
+- [ ] **T9 — Manuels et README** (AC 14-18), PDF régénérés et vérifiés aplatis.
+- [ ] **T10 — Gates** : frontend complet (`npm run check`, `lint-i18n-ownership`, `test:unit`, `build`) ;
+      backend `cargo test -p kesh-i18n` (parité des catalogues) puis `scripts/test-fast.sh` ; **E2E
+      complète**, `kesh_e2e` reconstruite, **build frontend postérieur au dernier patch**.
+
+## Dev Notes
+
+### Ce que la story touche
+
+| zone | fichiers |
+|---|---|
+| feature neuve | `frontend/src/lib/features/audit-log/` — `audit-log.types.ts`, `audit-log.api.ts`, `entity-type-label.ts`, `query-helpers.ts`, tests |
+| écran | `frontend/src/routes/(app)/audit-log/+page.svelte`, `+page.ts`, test de page |
+| menu | `frontend/src/routes/(app)/+layout.svelte` |
+| téléchargement | `frontend/src/lib/shared/utils/download.ts` (neuf) ; `reports.api.ts`, `exports.api.ts`, `imported-supplier-invoices.api.ts`, `admin-backup.api.ts` |
+| gardes | `frontend/src/lib/shared/i18n-keys.test.ts`, `i18n-libelle-en-dur.test.ts` |
+| i18n | `crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl` |
+| E2E | `frontend/tests/e2e/audit-log.spec.ts` |
+| docs | `docs/i18n-glossaire.md`, `docs/manual/fr/{user,admin}-manual.tex` + PDF, `README.md` |
+
+⚠️ **Largeur** : un paquet frontend, un crate i18n, la documentation. Le volet C touche quatre features
+de façon **mécanique** — c'est lui qui sort en premier si la story résiste (AC 10).
+
+### Faits établis à la spécification, et non supposés
+
+- **28 types d'entité littéraux** — recomptés par script sur les appels
+  `NewAuditLogEntry::(user|for_actor|api_key|from_current_user)(` de `crates/*/src` hors tests. Le script
+  a rendu 29 littéraux, dont **`admin_break_glass_reset`, qui est une ACTION sans point**
+  (`auth/bootstrap.rs`, type `user`) ; et **trois sites sans type littéral** —
+  `repositories/email_templates.rs:32` (helper, type `email_template` porté ailleurs) et
+  `kesh-api/src/audit.rs:42,51` (le trait générique). ⇒ la carte a 28 entrées.
+- **Menu** : `navGroups` (`+layout.svelte:62-163`), listes `comptableOnly` (`:130-136`) et `adminOnly`
+  (`:138-161`), `isComptablePlus` (`:46-48`), testid `nav-link-` (`:258-261`).
+- **`triggerDownload`** : quatre copies, et le commentaire d'`exports.api.ts:89-92` qui planifiait
+  l'extraction.
+- **Glossaire** : partie A (attestés), partie B (sans précédent, arbitrage requis), et la règle 3 — un
+  terme de A ne s'installe qu'avec toutes ses occurrences.
+- **Pas de composant partagé** de pagination ni de datepicker : `Input type="date"`, pagination écrite
+  dans la page.
+- **E2E** : `workers: 1`, `locale: 'fr-CH'` (`playwright.config.ts:53,58`) — la suite ne tourne qu'en
+  français, d'où l'exigence de `data-testid`.
+
+### Ce qui ne bouge PAS
+
+- Le backend, hors catalogues FTL : la route est la 25-1c-a.
+- Les libellés **par action** — hors arbitrage 3 (« le code d'action tel quel »).
+- Le **nom** d'une clé API (la route ne renvoie que `actorApiKeyId`).
+- `website/`.
+
+### Intelligence des stories précédentes
+
+- **25-1c-a** (validation en 5 passes) : *une forme qui casse à chaque correction se retire, elle ne se
+  rapièce pas* ; *un arbitrage résumé est un arbitrage réécrit* — citer, ne pas paraphraser.
+- **25-1c-zero** : une assertion **vraie par construction** a traversé quatre passes ; pour chaque
+  assertion, se demander ce qui la rendrait fausse.
+- **23-3b / 23-1** : les clés de menu échappent aux gardes si `FAMILLES_RESOLUES` n'est pas tenu ; les
+  cartes de libellés se **lisent** depuis la production.
+- **Gate** : un build frontend **en retard d'un périmètre** a déjà faussé un gate E2E (24-4c, 25-1b).
+
+### Hors périmètre
+
+- Libellés par action, recherche plein texte dans `details`, nom des clés API.
+- **[#386]** (export de souveraineté) → 25-5.
+- **[#431]**, **[#434]**, **[#435]**.
+
+### References
+
+- `25-1c-a-journal-audit-route.md` — **le contrat de la route** (AC 5-14)
+- `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15*
+- `frontend/src/lib/features/imported-supplier-invoices/error-label.ts:28-39` — **patron de la carte de libellés** (la fonction `:41-48` a un repli différent)
+- `frontend/src/lib/features/journal-entries/query-helpers.ts`, `routes/(app)/journal-entries/+page.svelte` — **patron de la liste filtrée**
+- `frontend/src/routes/(app)/supplier-invoices/import/+page.ts` — **patron de la garde Comptable+**
+- `frontend/src/lib/shared/i18n-keys.test.ts:206-213,234-301,319-327` · `docs/i18n-glossaire.md` § A, B, *Comment s'en servir*
+- `CLAUDE.md` § *Test Locally First*, § *Propagation post-patch*, § *Le prompt d'une passe doit NOMMER le manuel*
+
+## Dev Agent Record
+
+### Agent Model Used
+
+### Debug Log References
+
+### Completion Notes List
+
+### File List
+
+## Change Log
+
+- **2026-09-15** — Spécification créée (`bmad-create-story`), après la validation de la 25-1c-a. Faits
+  recomptés depuis la source : **28 types d'entité** (29 littéraux dont une action sans point, et trois
+  sites non littéraux vérifiés), quatre copies de `triggerDownload`, deux clés i18n au vocabulaire
+  divergent. **Défaut préexistant constaté** : `user-manual.tex:502-503` affirme qu'aucun verrou de
+  période n'existe, alors que la 24-4c l'a livré — corrigé dans le même encadré (AC 14).
+- **2026-09-15** — Contrôle checklist, quatre références vérifiées au sol : `error-label.ts`,
+  `exports.api.ts`, `CANDIDATES_ATTENDUES` (`:115`), et l'apostrophe **typographique** du catalogue
+  français, qui rendait muet un grep sur la locution.
+
+### Passe 1 de `bmad-create-story validate` — deux lentilles, contexte frais
+
+Prompt versionné : `25-1c-b-validate-prompt-p1.md`.
+
+| Lentille | Modèle | Rendu brut | Après vérification au sol |
+|---|---|---|---|
+| Sonnet | Sonnet | 0 C, 0 H, 2 M, 2 L | **4 retenus** |
+| Haiku | Haiku 4.5 | 0 | 0 — deux axes survolés, couverts par Sonnet |
+
+**Bilan : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 2 LOW.**
+
+- **M1 — l'extraction oubliait la distribution propre de `parseContentDispositionFilename`** : une
+  **seconde copie exportée** dans `admin-backup.api.ts:55` (sous un commentaire `DC-B1` qui planifiait la
+  même extraction), et un **import croisé** dans `imported-supplier-invoices.api.ts:11` qui aurait cessé
+  de compiler au déplacement. → tableau des trois sites à l'AC 9, fusion des tests des deux copies.
+- **M2 — le « patron exact » contredisait l'exigence qui le suit** : `importErrorLabel` rend un message
+  traduit pour un code inconnu, l'AC 3 exige le code brut. → citation restreinte à la **carte**
+  (`:28-39`), divergence écrite.
+- **L3** — `exports.api.ts` fait 110 lignes (`:52-111` → `:52-110`).
+- **L4** — « Pièce importée » n'est attesté nulle part ; le catalogue dit « Facture importée »
+  (`imported-supplier-invoices-err-not-found`) → aligné.
+
+⚠️ **La lentille Haiku a déclaré ses neuf axes exercés**, mais a compté les 28 types **dans la spec**
+(14 lignes × 2) au lieu de les recompter **dans le code**, et n'a vérifié que l'existence des gardes i18n,
+pas leur mécanique. Ces deux axes ont été exercés par Sonnet : recompte par script et `diff` → **ensembles
+identiques, 28/28** ; lecture intégrale d'`error-label.ts`, `i18n-keys.test.ts` et
+`lint-i18n-ownership.js`.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p1.md b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p1.md
new file mode 100644
index 000000000..2d6c3fbd0
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p1.md
@@ -0,0 +1,74 @@
+# Prompt — passe 1 de `bmad-create-story validate`, Story 25-1c-b
+
+*Versionné le 2026-09-15. Deux lentilles en contexte frais (Sonnet, Haiku), orthogonales à l'auteur
+de la spec (Opus 5).*
+
+Tu es un **valideur adversarial** en contexte frais. Ton objet est le fichier
+`_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md`, dépôt
+`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. Ta mission n'est pas
+d'approuver : c'est de **trouver ce qui ferait échouer, dévier ou mentir** l'implémentation qui suivra.
+
+⛔ **Rien ne se croit sur parole, surtout pas cette spec.** Chaque affirmation — chemin, numéro de ligne,
+nombre, comportement — se **vérifie dans le code** avant d'être retenue ou contestée.
+
+⚠️ **Ne conteste PAS les arbitrages du Project Lead** (`epic-25-vague1-suite.md` § *Arbitrages du
+2026-09-15*) ni le **contrat de la route**, fixé par la spec validée
+`25-1c-a-journal-audit-route.md` (AC 5-14). Conteste la **mise en œuvre** côté écran, et la fidélité de
+cette spec au contrat qu'elle consomme.
+
+## Sources
+
+- `25-1c-a-journal-audit-route.md` (le contrat), `epic-25-vague1-suite.md`, l'issue **#378** ;
+- `CLAUDE.md` — § *E2E (Playwright)*, § *Propagation post-patch*, § *Synchroniser le planning du README*,
+  § *Le prompt d'une passe doit NOMMER le manuel* ; `docs/testing.md` ; `docs/i18n-glossaire.md` ;
+- **le code**, qui prime.
+
+## Les axes, et tu déclareras lesquels tu as exercés
+
+1. **Exactitude factuelle.** Chaque `fichier:ligne` de la spec, vérifié.
+2. **Fidélité au contrat de la 25-1c-a.** Les types de l'AC 1 correspondent-ils **champ par champ** au
+   DTO de l'AC 8 de la 25-1c-a (noms, casse, nullabilité, `actorType` en `"user"`/`"api_key"`) ? Les
+   paramètres, la pagination, les codes d'erreur (`RESULT_TOO_LARGE`, 400 sur `entityId` sans
+   `entityType`) sont-ils consommés comme la route les définit ?
+3. **Les 28 types d'entité (AC 3).** Recompte-les **toi-même** depuis `crates/*/src` (hors tests) par ta
+   propre méthode. La liste est-elle exacte ? Un type manque-t-il, un code est-il mal orthographié ? Les
+   replis français sont-ils cohérents avec la partie A du glossaire et les libellés déjà présents dans les
+   catalogues ?
+4. **Les gardes i18n (AC 7, 11, 12).** Les mises à jour prescrites de `i18n-keys.test.ts`
+   (`MOTIFS_DYNAMIQUES`, `CARDINALITES`, `FAMILLES_RESOLUES`, `ATTENDU`) et de
+   `i18n-libelle-en-dur.test.ts` sont-elles **exactement** celles que ces tests exigeront ? Lis la mécanique
+   réelle de ces tests — par exemple : comment `error-label.ts` appelle-t-il `i18nMsg`, et que compte
+   alors la garde ? `lint-i18n-ownership` accepte-t-il une feature `audit-log` et la clé `nav-audit-log` ?
+5. **L'écran (AC 5-8).** La garde, le menu `comptableOnly`, les `data-testid` : chaque exigence est-elle
+   décidable et testable ? L'heure locale à l'écran contre l'UTC du CSV est-elle cohérente et écrite au
+   manuel ? Un état non prévu (session expirée, 403 de la route pour un rôle rétrogradé) ?
+6. **Le téléchargement partagé (AC 9, 10).** Les quatre copies de `triggerDownload` sont-elles
+   **identiques** ? Leur remplacement change-t-il un comportement ou un test existant ?
+7. **Les manuels, le README, le glossaire (AC 13-18).** Que rend faux l'écran, que laisse-t-il vrai ?
+   Contrôle le **PDF aplati** (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) ; en LaTeX le souligné
+   s'écrit `\_` ; le catalogue français écrit l'apostrophe **typographique**. Cherche un site que la spec
+   **ne nomme pas** — manuels, README, `website/`, en français **et** en anglais, et les clés i18n qui
+   parlent d'audit.
+8. **Les tests et mutations (AC 19-21).** Chaque assertion **tranche**-t-elle ? Qu'est-ce qui la rendrait
+   fausse ? L'E2E est-il constructible avec les helpers existants (création d'un contact par
+   `authedApiContext`, utilisateur Consultation) ?
+9. **Cohérence interne, décomptes, périmètre** — recompte, ne relis pas ; rien qui touche le backend hors
+   catalogues ; la § *Règle de splitting* respectée.
+
+## Ce que tu rends
+
+- **Les findings**, chacun avec sévérité (`CRITICAL` / `HIGH` / `MEDIUM` / `LOW`), l'endroit exact, **la
+  commande ou l'extrait qui l'établit**, et le correctif. Pour un scénario, montre que **son état de
+  départ est atteignable**.
+- ⛔ **La liste des axes réellement exercés ET de ceux qui ne l'ont pas été**, avec pour chaque axe
+  exercé **au moins une commande réellement lancée**. Ne qualifie jamais de « vérifié » ce que tu n'as
+  pas exécuté.
+
+## Interdits
+
+⛔ **N'écris aucun fichier du dépôt et n'exécute aucune commande qui écrit dans le dépôt ou dans une base
+persistante** — nommément `scripts/prepare-release.sh` (il bumpe les versions Cargo),
+`scripts/regen-test-schema.sh`, `scripts/install-hooks.sh`, `scripts/test-fast.sh`,
+`scripts/mem-guard.sh`, `make` dans `docs/manual/`, tout `git commit`/`push`/`checkout`/`add`/`stash`/
+`reset`/`rebase`, `npm install`, `npm run build`, `sqlx migrate`, `cargo test`/`cargo nextest`.
+`npm run test:unit` et `npm run check` en lecture sont autorisés.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p2.md b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p2.md
new file mode 100644
index 000000000..307077034
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p2.md
@@ -0,0 +1,62 @@
+# Prompt — passe 2 de `bmad-create-story validate`, Story 25-1c-b
+
+*Versionné le 2026-09-15. Une lentille en contexte frais (Opus), orthogonale aux deux lentilles de la
+passe 1 (Sonnet, Haiku).*
+
+Tu es un **valideur adversarial** en contexte frais. Ton objet est le fichier
+`_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md`, dépôt
+`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. Ta mission n'est pas
+d'approuver : c'est de **trouver ce qui ferait échouer, dévier ou mentir** l'implémentation qui suivra.
+
+⛔ **Rien ne se croit sur parole** — ni la spec, ni son Change Log, ni les vérifications de la passe 1.
+
+⚠️ **Ne conteste PAS les arbitrages du Project Lead** ni le **contrat de la route** fixé par
+`25-1c-a-journal-audit-route.md` (spec validée). Conteste la mise en œuvre côté écran.
+
+## Où regarder d'abord
+
+La passe 1 a réécrit l'AC 9 (trois sites de `parseContentDispositionFilename`, fusion des tests), l'AC 3
+(patron restreint à la carte, repli sur code inconnu, « Facture importée ») et des références. **Ta base
+de comparaison est le commit de la spec** : `git diff 5ea84b05 -- _bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md`.
+Nomme-la dans ton rapport.
+
+## Les axes, et tu déclareras lesquels tu as exercés
+
+1. **La remédiation de la passe 1.** Le tableau des trois sites de l'AC 9 est-il complet — un **quatrième**
+   consommateur (import, ré-export, test, composant Svelte) ? La fusion des tests des deux copies est-elle
+   faisable sans perdre un cas ? Rejoue `npx vitest run` sur les fichiers concernés pour établir la base.
+2. **Les gardes i18n, par l'exécution.** La forme prescrite pour `entityTypeLabel` (carte + appel
+   `i18nMsg` à gabarit, repli code brut sans `i18nMsg`) : **que comptera exactement `i18n-keys.test.ts`** —
+   `sitesGabarit`, `SITES_GABARIT_ATTENDUS`, `sitesTotal`, `MOTIFS_DYNAMIQUES`, `CARDINALITES` ? Et
+   `i18n-libelle-en-dur.test.ts` : une fonction `entityTypeLabel` qui **rend un littéral** (le code brut)
+   est-elle une « candidate » ? Lis les extracteurs réels, et si besoin construis une **copie jetable** du
+   fichier dans le scratchpad pour observer ce que les compteurs rendent — jamais dans le dépôt.
+3. **L'écran contre le contrat** : pagination, `entityId` désactivé sans type, message `RESULT_TOO_LARGE`,
+   heure locale à l'écran / UTC au CSV ; un état que la spec n'a pas prévu (session expirée, 403 d'un rôle
+   rétrogradé en cours de session, route 25-1c-a absente) ?
+4. **L'E2E (AC 20)** : chaque scénario est-il constructible avec les helpers réels (`authedApiContext`,
+   création d'un utilisateur Consultation, `seedTestState`) ? Le `seed` E2E crée-t-il des entrées d'audit
+   parasites qui fausseraient les assertions ? `workers: 1` et la pollution d'état documentée dans
+   `docs/testing.md` § *Les échecs attendus* sont-ils pris en compte ?
+5. **Les manuels, le README, le glossaire (AC 13-18)** : la réécriture prescrite est-elle exacte ? Existe-t-il
+   un site non nommé — manuels (PDF aplati : `pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`, souligné LaTeX
+   `\_`), `README.md`, `website/`, catalogues i18n (apostrophe typographique) ?
+6. **Cohérence interne, décomptes, périmètre** — recompte, ne relis pas.
+
+## Ce que tu rends
+
+- **Les findings**, chacun avec sévérité (`CRITICAL` / `HIGH` / `MEDIUM` / `LOW`), l'endroit exact, **la
+  commande ou l'extrait qui l'établit**, et le correctif. Pour un scénario, montre que **son état de départ
+  est atteignable**.
+- ⛔ **La liste des axes réellement exercés ET de ceux qui ne l'ont pas été.** Ne qualifie jamais de
+  « vérifié » ce que tu n'as pas exécuté.
+
+## Interdits
+
+⛔ **N'écris aucun fichier du dépôt et n'exécute aucune commande qui écrit dans le dépôt ou dans une base
+persistante** — nommément `scripts/prepare-release.sh`, `scripts/regen-test-schema.sh`,
+`scripts/install-hooks.sh`, `scripts/test-fast.sh`, `scripts/mem-guard.sh`, `make`, tout
+`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`,
+`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : `npm run check`, `npx vitest run` en lecture, et
+une copie jetable de fichiers **dans le scratchpad**
+`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/`.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-r5-derive.md b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-r5-derive.md
new file mode 100644
index 000000000..4cf1651f8
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b-validate-prompt-r5-derive.md
@@ -0,0 +1,53 @@
+# Prompt — revalidation R5 « dérive », Stories 25-1c-b1 et 25-1c-b2
+
+*Versionné le 2026-09-26. **Une lentille par fiche** (Sonnet), contexte frais.*
+
+⛔ **Pourquoi cette passe.** Les deux fiches ont été **validées le 2026-09-15/16** (R4 close), sur un
+`main` où la route de consultation (**25-1c-a**) n'était encore qu'une spec. Elles ont dormi dix jours
+sur une branche locale jamais poussée. Depuis, sur `main` : la **25-1c-a a été implémentée, revue et
+mergée (PR #439)** — son contrat réel peut différer de sa spec —, puis la 25-2-b-1/b-2, la 25-2-c, la
+25-3-zero, 25-3-a-1, 25-3-a-2, 25-3-b et la 25-5-a, qui ont ajouté des actions d'audit, des clés i18n,
+des pages de manuel et bougé des lignes. **Une fiche validée contre un état périmé n'est plus validée.**
+
+Dépôt `/home/gcorbaz/devel/kesh`, branche `story/25-1c-b1-journal-audit-ecran` (= `main` à `0e4c2682`
++ les fiches). Ta fiche est indiquée dans ta consigne ; lis aussi l'autre (elles se livrent dans la
+**même PR**), la fiche mère `25-1c-b-journal-audit-ecran.md` et la fiche **mergée** de la route
+`25-1c-a-journal-audit-route.md` (son Dev Agent Record et son Change Log disent ce qui a réellement
+été livré). Arbitrages de Guy : § de la fiche de l'epic `_bmad-output/planning-artifacts/epic-25-vague1-suite.md`
+(2026-09-11, 2026-09-15 soir et fin de soirée) — **ne pas les contester**.
+
+## Ce que tu cherches — la DÉRIVE, pas la conception
+
+1. **Le contrat de la route, tel qu'il est** : `crates/kesh-api/src/routes/` (le module d'audit, sa
+   réponse JSON, ses paramètres de filtre et de pagination, son export CSV, ses rôles), et ce que la
+   fiche en suppose. Chaque nom de champ, de paramètre, de code d'erreur, de chemin.
+2. **Chaque référence `fichier:ligne`** de la fiche : existe-t-elle encore, et dit-elle encore ce que
+   la fiche affirme ? Recompte les lignes, relis le code.
+3. **Chaque décompte** (types d'entité, actions, libellés, `sitesTotal`, `CLES_RELEVEES`, clés FTL,
+   pages du manuel, tests) : recompté **depuis la source actuelle**, jamais relu. Plusieurs stories
+   mergées depuis ont ajouté des actions d'audit (`supplier_invoice.settlement_cancelled`,
+   `invoice_settlement.cancelled`, rapprochement annulé…) et des types d'entité.
+4. **Ce qui existe déjà** : une partie de ce que la fiche prescrit a-t-elle été livrée entre-temps
+   (module de téléchargement, libellés, entrée de menu, vocabulaire « journal d'audit », sections du
+   manuel) ? Une prescription devenue fausse, ou redondante ?
+5. **Les manuels** (`docs/manual/fr/*.tex` et leurs PDF aplatis :
+   `pdftotext  - | tr '\n' ' ' | tr -s ' '`, vers le scratchpad) : les phrases que la fiche
+   prescrit de changer existent-elles encore, aux lignes dites ?
+
+## Ce que tu rends
+
+- **Findings** : sévérité (CRITICAL / HIGH / MEDIUM / LOW), l'endroit exact de la fiche, **la preuve**
+  (commande et résultat, code lu), ce qu'il faut changer. Une référence décalée sans conséquence est
+  LOW ; un contrat de route faux, un décompte faux qu'un test figera, une prescription devenue fausse
+  sont au moins MEDIUM.
+- ⛔ **La liste des axes réellement exercés ET de ceux qui ne l'ont pas été.**
+
+## Interdits
+
+⛔ N'écris aucun fichier du dépôt, n'exécute aucune commande qui écrit dans le dépôt ou dans une base
+persistante — `scripts/prepare-release.sh`, `scripts/regen-test-schema.sh`, `scripts/install-hooks.sh`,
+`scripts/test-fast.sh`, `scripts/mem-guard.sh`, `make`, tout `git commit`/`push`/`add`/`stash`/`reset`/
+`rebase`/`checkout`/`switch`/`worktree`, `sqlx migrate`, `cargo test`/`cargo nextest`, `npm run`,
+`npx playwright`. Lecture, `grep`, `git log`/`show`/`diff`, `gh issue view`, `pdftotext` (vers
+`/tmp/claude-1000/-home-gcorbaz-devel-kesh/ca5ce2e2-67a3-4eeb-817f-c2de35620a1c/scratchpad/`) et
+`cargo check` sont autorisés.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md
new file mode 100644
index 000000000..82ef69ebe
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md
@@ -0,0 +1,963 @@
+# Story 25.1c-b1 : L'écran de consultation du journal d'audit
+
+Status: done
+
+⚠️ **RÉOUVERTE le 2026-09-15 au soir**, après sa validation en 7 passes : trois arbitrages du Project Lead
+changent le contrat de la 25-1c-a, donc ce que l'écran consomme et affiche (cf. « Réouverture » au Change
+Log). **Revalidation requise** avant implémentation.
+
+⚠️ **Issue du SPLIT de la 25-1c-b** (arbitrage du Project Lead du 2026-09-15, après la passe 2 de
+validation — sévérité MEDIUM → MEDIUM sur des défauts d'origine répartis dans cinq zones) :
+
+| | objet | état |
+|---|---|---|
+| 25-1c-zero | la colonne `audit_log.company_id` | done — PR #437 ouverte |
+| 25-1c-a | la route de consultation, son vocabulaire traduit et son export CSV — backend | ready-for-dev, réouverte |
+| **25-1c-b1** *(celle-ci)* | **l'écran : feature, page, garde, menu, gardes i18n, tests** | ready-for-dev, réouverte |
+| 25-1c-b2 | les textes : manuels, README, vocabulaire « journal d'audit » | ready-for-dev, réouverte |
+
+La fiche parente `25-1c-b-journal-audit-ecran.md` (statut `split`) reste la référence des passes 1 et
+2 ; **ne pas l'implémenter**.
+
+⛔ **Dépendance dure** : cet écran consomme `GET /api/v1/audit-log`, `/export.csv` et `/vocabulary`
+**tels que la 25-1c-a les livre** (`25-1c-a-journal-audit-route.md`, AC 5-17 — le comportement HTTP, les
+libellés traduits, et la clé du message de refus). **Ne pas implémenter avant le merge de la 25-1c-a** —
+la branche est empilée sur la sienne et se rebase sur `main` après.
+
+⛔ **Livrée dans la MÊME PR que la 25-1c-b2**, et c'est cette PR qui **ferme #378** : son titre porte
+`closes #378` (le dépôt merge en squash — § *Commits qui adressent une issue*). Sans la b2, le manuel
+continuerait d'affirmer que l'écran n'existe pas.
+
+⚠️ **Hors de cette story, par arbitrage** : le regroupement des sept copies existantes de la logique
+de téléchargement → **issue de dette [#438]**. Cette story crée le module partagé et ne migre aucune
+copie (AC 9).
+
+## Story
+
+**En tant que** comptable ou administrateur,
+**je veux** consulter le journal d'audit de ma société dans l'application — le filtrer, déplier le
+détail d'une entrée, l'exporter —, dans la langue de l'interface,
+**afin de** **produire** la trace des corrections apportées aux livres sans accès direct à la base —
+« *"apparent" et "archivé dans une table que personne ne peut lire" ne sont pas la même chose* » (#378).
+
+**Couvre** : [#378], volet écran.
+
+## ✅ Arbitrages du Project Lead — 2026-09-15 (`epic-25-vague1-suite.md`)
+
+1. **Qui consulte** : **Comptable** et **Admin** ; ni Consultation, ni clé API.
+2. **Filtre strict par société**, porté par la route — et il n'existe **pas** d'entrée sans société à
+   montrer (*« le cas réel n'existe pas […] le cas théorique est … théorique »*).
+3. **Affichage** — cité de l'epic : *« le code d'action tel quel, les types d'entité traduits, les
+   détails en JSON indenté »* ; **amendé au soir** : à la proposition d'afficher l'action traduite à
+   l'écran comme dans le CSV, *« ok »*. ⇒ **actions et types d'entité affichés traduits**, détails en JSON
+   indenté.
+4. **Vocabulaire** : **« journal d'audit »** — `Audit-Protokoll`, `registro di audit`, `audit log`.
+5. **Découpage** (soir du 2026-09-15) : écran (b1) et textes (b2) dans la même PR ; le téléchargement
+   partagé sort vers une issue de dette.
+6. **L'export n'est pas inscrit au journal** — sans effet sur l'écran, sinon qu'aucun type d'entité
+   `audit_log` n'existe.
+
+⛔ **L'écran ne traduit RIEN du vocabulaire lui-même.** La 25-1c-a est la source unique des libellés
+(son AC 16) : la liste rend `actionLabel` et `entityTypeLabel` (son AC 8), la route de vocabulaire rend les
+deux listes traduites (son AC 17). Aucune carte de codes, aucune clé `audit-log-entity-*` ni
+`audit-log-action-*` n'est appelée par le frontend.
+
+## Acceptance Criteria
+
+### Volet A — la feature `frontend/src/lib/features/audit-log/`
+
+**1. Les types** — `audit-log.types.ts` :
+
+- `AuditLogEntry` en miroir **exact** du DTO de la 25-1c-a (AC 8) — `id`, `createdAt` (chaîne ISO UTC
+  finissant par `Z`), `actorLabel`, `actorType: 'user' | 'api_key'`, `actorApiKeyId: number | null`,
+  `userId`, `action`, `actionLabel`, `entityType`, `entityTypeLabel`, `entityId`, `details: unknown`.
+  ⚠️ **Pas de `companyId`** : la route ne le rend plus ;
+- `AuditLogVocabularyItem { code: string; label: string }` et `AuditLogVocabulary { entityTypes:
+  AuditLogVocabularyItem[]; actions: AuditLogVocabularyItem[] }` (25-1c-a AC 17) ;
+- `AuditLogQuery` (`dateFrom`, `dateTo`, `entityType`, `entityId`, `action`, `offset`, `limit`) ;
+- liste : l'enveloppe `{ items, total, offset, limit }` déjà typée (`src/lib/shared/types/user.ts:22-27`).
+
+**2. L'API** — `audit-log.api.ts`, via `apiClient` :
+
+- `listAuditLog(query)` → `GET /api/v1/audit-log` ; **un paramètre vide ou absent n'est pas envoyé** ;
+- `getAuditLogVocabulary()` → `GET /api/v1/audit-log/vocabulary` ;
+- `exportAuditLogCsv(query)` → `apiClient.getBlob(url)` (`api-client.ts:526-529`), **l'URL portant les
+  mêmes filtres que la liste, `offset` et `limit` exceptés** ; nom de fichier lu dans
+  `Content-Disposition` par `parseContentDispositionFilename`, téléchargement par `triggerDownload` —
+  **tous deux importés du module partagé de l'AC 9**, jamais recopiés.
+
+**3. Les listes des filtres** — `vocabulary-options.ts`, fonction pure
+`toSelectOptions(items, locale, current)` :
+
+- **tri par libellé** avec `Intl.Collator(locale, { sensitivity: 'base' })` — la route rend l'ordre des
+  **codes** (25-1c-a AC 17), et un tri d'octets rangerait « Écriture » après « Utilisateur » ;
+- ⛔ **un code de l'URL absent du vocabulaire est ajouté comme option, libellée par son code** : un code
+  historique doit rester filtrable **et visible** dans la liste, sans quoi la liste afficherait « Tous »
+  alors que la page filtre sur ce code ;
+- `locale` est celle de l'interface — ⛔ **et le frontend la JETTE aujourd'hui** : `loadI18nMessages`
+  (`src/lib/shared/utils/i18n.svelte.ts:25-35`) reçoit `{ locale, messages }` et ne garde que `messages`.
+  ⇒ la story ajoute **`i18nLocale()`** au même module, qui conserve `data.locale` (`'fr-CH'` avant tout
+  chargement) ; le mock de `$lib/shared/utils/i18n.svelte` des tests de page l'exporte aussi (patron
+  `contacts-page.test.ts:21-25`). Sans cela, un développeur fixerait `'fr-CH'` en dur ou prendrait
+  `navigator.language`, et l'écran d'une installation allemande afficherait des dates françaises.
+
+**4. Les paramètres d'URL** — `query-helpers.ts` sur le patron de
+`journal-entries/query-helpers.ts:19-121` : sérialisation qui **omet** les valeurs par défaut, lecture qui
+**ignore** les valeurs invalides — date mal formée, `entityId` non numérique ou `<= 0`. `entityType` et
+`action` **hors vocabulaire sont conservés** (un code historique doit rester filtrable).
+
+⛔ **`entityId` n'a de sens qu'avec un `entityType`** — la route répond 400 sinon (25-1c-a). Donc :
+
+- à la **lecture** de l'URL, un `entityId` **sans** `entityType` est **ignoré** (un lien retouché à la
+  main ne doit pas produire un écran en erreur) ;
+- à la **sérialisation**, un `entityId` sans `entityType` **n'est pas écrit**.
+
+### Volet B — l'écran `frontend/src/routes/(app)/audit-log/`
+
+**5. La page** — `+page.svelte` :
+
+- **au montage**, le vocabulaire et la première page de la liste se chargent ; ⛔ un **échec du
+  vocabulaire** affiche l'**état d'erreur** de la page — des filtres vides laisseraient croire qu'il n'y a
+  rien à filtrer ;
+- **filtres** :
+  - **période** (`Input type="date"`, du / au), **libellée comme des jours UTC** — « Du (jour UTC) » /
+    « Au (jour UTC) », ou un libellé de groupe « Période (jours UTC) » ;
+  - **type d'entité** : liste « Tous » + les types du vocabulaire, triés par libellé (AC 3) ;
+  - **identifiant d'entité** (numérique, **désactivé tant qu'aucun type n'est choisi**) ;
+  - **action** : liste « Toutes » + les actions du vocabulaire, triées par libellé (AC 3) ;
+  - ⛔ **les deux listes sont des `` natif se pilote par **valeur**,
+    `selectOption({ value: code })`, sans dépendre du texte traduit. Le `Select` de bits-ui pourrait l'être
+    aussi (il pose `data-value` sur ses options, `bits-ui/dist/bits/select/select.svelte.js:909`), mais **aucun
+    E2E du dépôt ne le fait** : les précédents le pilotent par libellé (`accounts.spec.ts:136`) ou par
+    position. ⚠️ **C'est la spec, et non la garde, qui interdit le pilotage par libellé** :
+    `e2e-selecteurs-traduits.test.ts` laisse passer `selectOption({ label: … })` et un
+    `getByRole('option', { name })` sans accent ;
+  - tous **synchronisés dans l'URL** et relus au montage (patron `journal-entries/+page.svelte:138-183`) ;
+    bouton « Réinitialiser ».
+
+  ⛔ **Revenir à « Tous » VIDE l'identifiant d'entité** : désactiver le champ ne suffit pas, sa valeur
+  resterait dans l'état et partirait à la route, qui répondrait 400.
+
+  ⛔ **Pourquoi « jours UTC »** : la route borne les dates sur des **jours UTC**
+  (`date_from 00:00:00.000` à `date_to 23:59:59.999`, en UTC — 25-1c-a), alors que le tableau affiche
+  l'heure **locale**. Une modification faite à Zurich le 16 à 00:30 s'affiche le 16, mais tombe dans le
+  jour UTC du **15** : sans le libellé, le filtre « du 16 au 16 » l'écarterait sans explication. Le
+  contrat de la route est arbitré et ne se change pas ici ; l'écran **dit** ce qu'il filtre.
+- **tableau**, colonnes :
+  - **Date** : heure **locale** du navigateur,
+    `Intl.DateTimeFormat(i18nLocale(), { dateStyle: 'medium', timeStyle: 'short' })` (AC 3), depuis
+    `createdAt` UTC — la forme change réellement avec la langue : `16 sept. 2026, 14:26` en `fr-CH`,
+    `16.09.2026, 14:26` en `de-CH` (Node 22, ICU 78 ; **sans** ces options, `16.09.2026` et `16.9.2026`) ;
+  - **Auteur** : `actorLabel`, avec la mention « clé API » si `actorType === 'api_key'` ;
+  - **Action** : **`actionLabel`** ;
+  - **Type d'entité** : **`entityTypeLabel`** ;
+  - **N°** : `entityId`, « — » s'il vaut `0` ;
+  - **Détails** : bouton qui déplie un `
` de `JSON.stringify(details, null, 2)`, absent si
+    `details` est `null` ;
+  - chaque ligne porte **`data-action`** et **`data-entity-type`** avec les **codes** : c'est par eux, et
+    non par le texte traduit, que les tests identifient une ligne.
+
+  ⚠️ **Plus de colonne « Société »** : le filtre est strict (arbitrage 2).
+- **pagination** Précédent / Suivant, « X–Y sur N », `limit` 50, garde `if (loading) return` (patron
+  `journal-entries/+page.svelte:245-265`) ;
+- **trois états distincts** — chargement, **erreur** (message, jamais une liste vide silencieuse —
+  patron `supplier-invoices/+page.svelte:429-438`), vide. ⚠️ **L'erreur de la LISTE remplace le tableau
+  seul** : les filtres et « Réinitialiser » restent visibles, pour qu'on sorte d'une erreur de filtre. Des
+  valeurs valides en forme mais refusées par la route — une plage inversée, une date hors
+  `[1000-01-01, 9999-12-31]` dans l'URL, une action de plus de 64 caractères — produisent un 400 dont le
+  **message du serveur** s'affiche ; c'est **assumé**, et `query-helpers` ne les écarte pas ;
+- **export CSV** : bouton qui transmet **les filtres affichés**, désactivé pendant l'export. Le fichier
+  est traduit par le serveur, dans la langue de l'interface (25-1c-a AC 12). Le traitement des erreurs :
+  - un 400 dont `code === 'RESULT_TOO_LARGE'` affiche, **dans la page** (`audit-log-export-error`),
+    **le message rendu par le serveur** — la route le traduit déjà (`audit-log-export-error-too-large`,
+    argument `limit`, 25-1c-a AC 11 ; clé inscrite au catalogue par son AC 15), et `parseErrorResponse` le
+    transmet tel quel dans `ApiError.message` (`api-client.ts:227-229`). **Aucune clé frontend neuve**
+    pour ce message ;
+  - toute autre erreur passe par `notifyError`.
+
+**6. La garde** — `+page.ts`, patron **exact** de `supplier-invoices/import/+page.ts` : `ssr = false`,
+redirection 302 vers `/` si le rôle n'est ni `Admin` ni `Comptable`. ⚠️ Le masquage d'interface ne
+protège rien : c'est le 403 de la route qui fait foi.
+
+**7. Le menu** — entrée `{ i18nKey: 'nav-audit-log', fallback: "Journal d'audit", href: '/audit-log' }`
+dans la liste **`comptableOnly`** du groupe `administration` (`routes/(app)/+layout.svelte:130-136`).
+⚠️ **`'audit-log'` s'ajoute à `FAMILLES_RESOLUES['nav-']`** (`i18n-keys.test.ts`, bloc `FAMILLES_RESOLUES`, `:428-436` au 2026-09-26) : une clé de
+menu est une donnée lue par `getItemLabel`, et c'est ce trou qui avait laissé quatre entrées en
+français dans les quatre langues (commentaire `i18n-keys.test.ts:311-314`).
+
+**8. Les sélecteurs** — `data-testid` sur tout ce que les tests visent : `audit-log-table`,
+`audit-log-row`, `audit-log-row-action`, `audit-log-empty`, `audit-log-error`, `audit-log-loading`,
+`audit-log-export`, `audit-log-export-error`, `audit-log-filter-date-from`, `-date-to`, `-entity-type`,
+`-entity-id`, `-action`, `-reset`, `audit-log-details-toggle`, `audit-log-details`, `audit-log-prev`,
+`audit-log-next`. Le lien de menu reçoit `nav-link-audit-log` du gabarit existant
+(`+layout.svelte:258-261`). ⛔ **Aucun sélecteur par libellé**, `selectOption({ label })` compris — la
+spec l'interdit, même là où `e2e-selecteurs-traduits.test.ts` ne le voit pas (AC 5) ; `DETTE_CONNUE` ne
+s'allonge pas.
+
+### Volet C — le module de téléchargement partagé
+
+**9. `frontend/src/lib/shared/utils/download.ts`** (neuf) exporte `triggerDownload(blob, filename)` et
+`parseContentDispositionFilename(header)`, reprises **à l'identique** de
+`features/export/exports.api.ts:52-110` — la forme `try/finally` de `triggerDownload`, **pas** la
+libération différée des trois copies écrites dans les pages.
+
+- ⛔ **Aucune copie existante n'est migrée** : c'est l'objet de [#438], qui recense les sept copies
+  (quatre fonctions dans des `.api.ts`, trois écrites en ligne dans des pages, au comportement
+  différent) et les deux définitions de `parseContentDispositionFilename`.
+- **Les commentaires qui parlent de l'extraction sont mis à jour** pour dire que le module existe et que
+  la migration est suivie par #438 — sans quoi ils continueraient d'annoncer une extraction « reportée » à
+  un module déjà présent :
+  - **deux la planifient**, et se réécrivent : `admin-backup.api.ts:10-14` (DC-B1) et `exports.api.ts:89-92` ;
+  - **un la justifie par renvoi** à la décision d'`exports.api.ts` : `imported-supplier-invoices.api.ts:66-69`,
+    qui renvoie désormais à `download.ts` et à #438 ;
+  - `reports.api.ts:338-346` ne parle **pas** d'extraction (il justifie le `finally`) : **il ne se touche pas**.
+
+  **Le commentaire seulement** : aucune ligne de code de ces fichiers ne bouge.
+
+**Tests** — `download.test.ts` :
+
+- `parseContentDispositionFilename` : les cas de `exports.api.test.ts` (`describe
+  'parseContentDispositionFilename'`), **recopiés sans modification de leurs assertions** — leur
+  suppression dans `exports.api.test.ts` relève de #438 ;
+- **`triggerDownload`, testé directement** — ce qu'aucune des copies actuelles n'a : l'ancre reçoit
+  `download = filename` et un `href` d'URL objet, `click()` est appelé, puis l'ancre est retirée et
+  `revokeObjectURL` appelé **même si `click()` jette** (espions `URL.createObjectURL` /
+  `revokeObjectURL`, patron `exports.api.test.ts:82-100`).
+
+### Volet D — les libellés de l'écran
+
+**10. Les clés i18n de l'écran**, dans les quatre catalogues
+(`crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl`), en un bloc
+`# --- Journal d'audit — écran (Story 25-1c-b1) ---` :
+
+- `nav-audit-log` ;
+- titre et sous-titre de page ;
+- libellés des filtres — **dont la mention « jour(s) UTC » de la période** (AC 5), et les options
+  « Tous » / « Toutes » — et du bouton de réinitialisation ;
+- en-têtes de colonnes ;
+- mention « clé API » ;
+- bouton de détail ;
+- états vide et erreur ;
+- bouton d'export.
+
+⚠️ **Les clés du vocabulaire ne sont PAS de cette story** : les clés `audit-log-*` de la 25-1c-a (en-têtes du CSV,
+message de refus, types d'auteur, 28 types d'entité et les actions — **138** clés et **97** actions au 2026-09-26, 133 et 92 à la spécification : cinq stories mergées depuis en ont ajouté, et le nombre **dérivera encore**, d'où un décompte à refaire au développement et jamais figé ici) **existent déjà** et
+ne se recréent ni ne s'appellent ici. Le **total** de cette story se recompte depuis la source à
+l'implémentation et s'écrit avec sa ventilation.
+
+⛔ **Préfixe partagé** : les clés de l'écran commencent par `audit-log-`, comme celles du backend. Ne
+**jamais** ajouter `audit-log-` à `PREFIXES_A_COUVERTURE_CLOSE` (`i18n-keys.test.ts:497` au 2026-09-26) : les clés `audit-log-*`
+servies par le backend y deviendraient des orphelines, alors qu'elles ont un lecteur, la route.
+
+Les termes suivent `docs/i18n-glossaire.md` : partie A là où elle les atteste — notamment « journal
+d'audit », **dont l'inscription en partie A est le travail de la 25-1c-b2** (règle 3) ; un terme
+récurrent qu'elle n'atteste pas va en **partie B**, avec sa valeur proposée.
+
+**11. Les gardes i18n du frontend, recomptées et ventilées.** L'écran n'appelle plus aucune clé à gabarit
+ni aucune carte de libellés : les compteurs dynamiques **ne bougent pas**, et c'est à **vérifier**, pas à
+supposer.
+
+`src/lib/shared/i18n-keys.test.ts` :
+
+| site | attendu |
+|---|---|
+| `MOTIFS_DYNAMIQUES` (`:343`), `CARDINALITES` (`:401-410`) | **inchangés** — aucun préfixe dynamique neuf |
+| `ATTENDU.sitesGabarit` (`:319`), `SITES_GABARIT_ATTENDUS` (`:474-485`) | **inchangés** — 10 |
+| `FAMILLES_RESOLUES['nav-']` (`:428-436`) | **+ `'audit-log'`** (AC 7) |
+| `ATTENDU.sitesTotal`, `sitesNonResolus`, `relais` (`:316-318`) | **recomptés** — les appels littéraux de la page ajoutent des sites —, historique commenté prolongé |
+| `PREFIXES_A_COUVERTURE_CLOSE` (`:497`) | **inchangé** (AC 10) |
+
+`src/lib/shared/i18n-libelle-en-dur.test.ts` : `CANDIDATES_ATTENDUES` (`:144`) et la partition (`:661`)
+**inchangés** — sauf si le code déclare une fonction ou une variable dont le nom finit par `Label`,
+`Text` ou `Display` (`SUFFIXES`, `:72`), y compris un `$derived` (`:245`). ⚠️ `toSelectOptions` n'en est
+pas une ; un `let actionLabel = $derived(…)` en serait une. **Dans ce cas, recompter et ventiler.**
+
+⛔ **Si un compteur bouge sans que le code l'explique, c'est la forme du code qui diffère de la spec** — la
+corriger, ou écrire l'écart : **jamais** ajuster un compteur pour le faire passer.
+
+### Volet E — les tests
+
+**12. Vitest** :
+
+- `i18n.svelte` (`src/lib/shared/utils/i18n.svelte.test.ts`) : ⛔ **`i18nLocale()` rend la locale servie** —
+  le helper `servir` (`:28`), qui ne sert aujourd'hui que des messages, est étendu à la locale ; `'de-CH'`
+  servi ⇒ `i18nLocale()` rend `'de-CH'` ; avant tout chargement ⇒ `'fr-CH'` — ⛔ **lu sur un module FRAIS** (`vi.resetModules()` puis
+  `await import('./i18n.svelte')`) : ce fichier ne remet jamais son module à zéro, et l'état d'un chargement
+  précédent rendrait l'assertion vraie ou fausse **selon sa place dans le fichier** (exécuté en R3). ⚠️ **C'est le seul test qui
+  exerce la vraie fonction** : les tests de page mockent le module entier ;
+- `vocabulary-options` :
+  - tri par libellé : l'entrée arrive **dans l'ordre des codes**, comme la route la rend
+    (`invoice.validated` « Facture validée », puis `journal_entry.created` « Écriture créée ») ; la sortie
+    range « Écriture créée » **avant** « Facture validée ». ⚠️ Une entrée déjà rangée par libellé laisserait
+    passer l'absence de tri ;
+  - ⛔ **le paramètre `locale` se prouve par le résultat, à condition de choisir une locale et des libellés
+    qui trient autrement** : les quatre locales du projet rangent « Öffnung » avant « Zahlung », `sv-SE` fait
+    l'inverse (Node 22, ICU 78). ⇒ `toSelectOptions` avec `'sv-SE'` et ces deux libellés rend « Zahlung »
+    d'abord. *(Un espion `vi.spyOn(Intl, 'Collator')`, prescrit en R1, casse `.compare()` et a été retiré en
+    R2 ; l'angle mort écrit alors est levé en R3.)* ;
+  - ⛔ un code courant **absent** du vocabulaire est ajouté, libellé par son code ; un code présent n'est
+    pas dupliqué ;
+- `query-helpers` :
+  - aller-retour URL ;
+  - valeurs invalides ignorées ;
+  - `entityType` et `action` hors vocabulaire conservés ;
+  - ⛔ **un `entityId` sans `entityType` est ignoré à la lecture et omis à la sérialisation** (AC 4) ;
+- `audit-log.api` :
+  - un paramètre vide n'est pas envoyé ;
+  - `getAuditLogVocabulary` appelle la route de vocabulaire ;
+  - ⛔ **l'URL passée à `getBlob` porte les filtres** : `dateFrom`, `dateTo`, `entityType`,
+    `entityId`, `action`, sans `offset` ni `limit`. Aucun test du dépôt n'observe aujourd'hui l'URL de
+    `getBlob` ; patron `exports.api.test.ts:102-120`, qui intercepte `fetch` par `vi.stubGlobal` et lit
+    l'URL appelée ;
+  - le nom vient de `Content-Disposition` ;
+- `download.ts` : AC 9 ;
+- la page :
+  - `load()` **redirige** un rôle Consultation (patron `routes/(app)/users/users-page.test.ts:26-53`) ;
+  - API mockée (patron `contacts/contacts-page.test.ts:17-40`) : rendu des trois états, de « — » pour
+    `entityId = 0`, et du dépliage du détail ;
+  - ⛔ **la colonne Action affiche `actionLabel`, et non `action`** : l'entrée mockée porte des valeurs
+    **différentes** pour les deux, faute de quoi l'assertion serait vraie par construction ; même chose
+    pour `entityTypeLabel` ;
+  - ⛔ **un échec du vocabulaire affiche l'état d'erreur** ;
+  - ⛔ **la page passe réellement par `toSelectOptions`** : URL `?action=zz.legacy`, vocabulaire mocké sans
+    ce code et rendu **dans l'ordre des codes**. Le `` natifs existent dans **treize** pages de `routes/` ; le patron « natif + `data-testid` » est
+  `invoices/+page.svelte:314-318`, piloté par `invoices.spec.ts:167` (les filtres de `contacts/+page.svelte`
+  n'ont pas de `data-testid`).
+- **Locale** : `i18n.svelte.ts:25-35` jette `data.locale` ; aucun `Intl.DateTimeFormat` ni `Intl.Collator`
+  dans le frontend ; `app.html:2` porte `lang="fr"` en dur. Un store qui conserve la locale ne change aucun
+  compteur de garde (sonde de la revalidation R1).
+- **Erreurs d'API** : `parseErrorResponse` (`api-client.ts:208-240`) recopie `error.code` et
+  `error.message` du corps JSON ; le message d'un 400 de la route arrive donc **déjà traduit** par le
+  serveur. **Aucune page du frontend ne traite encore `RESULT_TOO_LARGE`** (grep vide).
+- **Menu** : `navGroups` (`+layout.svelte:62-163`), listes `comptableOnly` (`:130-136`) et `adminOnly`
+  (`:138-161`), `isComptablePlus` (`:46-48`), testid `nav-link-` (`:258-261`).
+- **Téléchargement** : aucun test du dépôt n'observe l'URL passée à `getBlob`, ni `triggerDownload`
+  directement. Inventaire des copies : [#438].
+- **E2E** : `workers: 1`, `locale: 'fr-CH'`, `timezoneId: 'Europe/Zurich'` (`playwright.config.ts:53,58-59`)
+  — la suite ne tourne qu'en français, d'où l'exigence de `data-testid` et des attributs de code.
+- **Pas de composant partagé** de pagination ni de datepicker : `Input type="date"`, pagination écrite
+  dans la page.
+
+### Ce qui ne bouge PAS
+
+- Le backend, hors catalogues FTL : les routes et le vocabulaire sont la 25-1c-a.
+- Le **nom** d'une clé API (la route ne renvoie que `actorApiKeyId`).
+- Le code des sept copies de téléchargement ([#438]).
+- Les manuels, le README, le glossaire partie A et les clés existantes au vocabulaire divergent : **25-1c-b2**.
+
+### Intelligence des stories précédentes
+
+- **25-1c-a** (validation en 5 passes) : *une forme qui casse à chaque correction se retire, elle ne se
+  rapièce pas* ; *un arbitrage résumé est un arbitrage réécrit* — citer, ne pas paraphraser.
+- **25-1c-zero** : une assertion **vraie par construction** a traversé quatre passes ; pour chaque
+  assertion, se demander ce qui la rendrait fausse — d'où les valeurs **différentes** de `action` et
+  `actionLabel` dans les données de test (AC 12).
+- **Cette story, passes 3 à 7** : trois corrections successives d'une même assertion E2E n'ont pas su la
+  rendre discriminante ; elle a été retirée au profit d'un test unitaire qui observait la chose.
+- **23-3b / 23-1** : les clés de menu échappent aux gardes si `FAMILLES_RESOLUES` n'est pas tenu.
+- **Gate** : un build frontend **en retard d'un périmètre** a déjà faussé un gate E2E (24-4c, 25-1b).
+
+### Hors périmètre
+
+- Recherche plein texte dans `details`, nom des clés API.
+- Conversion des jours locaux en bornes UTC : le contrat de la route prend des dates, il est arbitré.
+- Toute traduction du vocabulaire côté frontend : la 25-1c-a en est la source unique.
+- **[#438]** (regroupement du téléchargement), **[#386]** (export de souveraineté, 25-5), **[#431]**,
+  **[#434]**, **[#435]**.
+
+### References
+
+- `25-1c-a-journal-audit-route.md` — **le contrat des routes** (AC 5-17)
+- `25-1c-b-journal-audit-ecran.md` — la fiche parente, passes 1 et 2
+- `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15* et § *fin de soirée*
+- `frontend/src/lib/features/journal-entries/query-helpers.ts`, `routes/(app)/journal-entries/+page.svelte` — **patron de la liste filtrée**
+- `frontend/src/routes/(app)/invoices/+page.svelte:314-318` et `tests/e2e/invoices.spec.ts:167` — **patron du `` natifs pilotables
+  par valeur, identifiant désactivé sans type et vidé au retour à « Tous »), tableau (date locale
+  `Intl.DateTimeFormat(i18nLocale())`, `actionLabel`, `entityTypeLabel`, « — » pour 0, détail JSON
+  indenté, `data-action` / `data-entity-type`), trois états — l'échec du vocabulaire remplace la page,
+  celui de la liste le tableau seul —, pagination, export (`RESULT_TOO_LARGE` affiché dans la page, le
+  reste par `notifyError`). Garde `+page.ts` ; entrée de menu `comptableOnly`.
+- **i18n** : **26 clés** ×4 (`nav-audit-log` + 25 `audit-log-*` d'écran), bloc « Journal d'audit — écran » ;
+  total `audit-log-*` + `nav-audit-log` par catalogue : 138 + 26 = **164**, recompté (`grep -c`).
+  Vocabulaire arbitré : « journal d'audit » / Audit-Protokoll / registro di audit / audit log.
+- **Gardes i18n** : `'audit-log'` dans `FAMILLES_RESOLUES['nav-']` ; `sitesTotal` 1709 → **1738** (+29,
+  tous dans la page, ventilés en commentaire) ; `sitesNonResolus`, `relais`, `sitesGabarit`,
+  `MOTIFS_DYNAMIQUES`, `CARDINALITES`, `PREFIXES_A_COUVERTURE_CLOSE`, `CANDIDATES_ATTENDUES` **inchangés**.
+- **Tests** : vitest **+35** (download 8, i18n 2, options 4, query-helpers 5, API 3, page 13) ; E2E **+2**
+  (`audit-log.spec.ts` : parcours Comptable — menu, filtre isolé par type et identifiant, libellé égal au
+  vocabulaire de la route, URL rechargée, détail, export, période passée puis jour UTC — et Consultation).
+- **Mutations — 17 sur 17 tuées**, observées : garde retirée ; colonne rendue avec `action` ; tri par code ;
+  code hors vocabulaire non ajouté ; échec du vocabulaire avalé ; `vocabulary.actions` rendu sans
+  `toSelectOptions` ; `i18nLocale` toujours `'fr-CH'` ; valeur initiale vide ; `'fr-CH'` en dur au tri ;
+  date en `'fr-CH'` en dur ; `entityId` conservé (après renforcement) ; `entityId` sans type relu ; filtres
+  non sérialisés à l'export ; export appelé avec `{}` ; `revokeObjectURL` hors du `finally` ;
+  `nav-audit-log` retirée des quatre catalogues ; entrée de menu passée à la liste commune (E2E
+  Consultation, rouge sur `nav-link-audit-log` à `toHaveCount(0)`).
+
+### Gates *(b1 seule — à rejouer après la b2)*
+
+- Frontend : `check` 0 erreur (27 avertissements), `lint-i18n-ownership` PASS, `test:unit` **812 / 812**
+  (777 + 35), build OK.
+- Backend : `cargo test -p kesh-i18n` 29 / 29 (parité) ; base remise à zéro, `scripts/test-fast.sh`
+  **2463 / 2463**.
+- E2E : `kesh_e2e` reconstruite, montage complet, frontend buildé après le dernier patch, run à **12:38 UTC** :
+  **223 passés / 8 échoués / 19 ignorés, zéro régression** — 7 KF-029, `sidebar-navigation:75`.
+
+### File List
+
+| Fichier | Nature |
+|---|---|
+| `frontend/src/lib/shared/utils/download.ts` (+ `download.test.ts`) | **neufs** — téléchargement partagé |
+| `frontend/src/lib/features/export/exports.api.ts`, `admin-backup/admin-backup.api.ts`, `imported-supplier-invoices/imported-supplier-invoices.api.ts` | commentaires seulement |
+| `frontend/src/lib/shared/utils/i18n.svelte.ts` (+ `.test.ts`) | `i18nLocale()` |
+| `frontend/src/lib/features/audit-log/` — `audit-log.types.ts`, `audit-log.api.ts`, `vocabulary-options.ts`, `query-helpers.ts` + trois tests | **neufs** |
+| `frontend/src/routes/(app)/audit-log/+page.svelte`, `+page.ts`, `audit-log-page.test.ts` | **neufs** |
+| `frontend/src/routes/(app)/+layout.svelte` | entrée de menu |
+| `frontend/src/lib/shared/i18n-keys.test.ts` | famille `nav-`, `sitesTotal` |
+| `crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl` | 26 clés ×4 |
+| `frontend/tests/e2e/audit-log.spec.ts` | **neuf** |
+
+## Change Log
+
+- **2026-09-26** — **gates rejoués avec la b2** (T8) — sur la même branche : frontend 817/817, backend 2463/2463, E2E **224 / 7 / 19**, zéro régression (7 KF-029). Un défaut de la b1 corrigé par la b2 : le sous-titre italien de l'écran employait la forme de politesse (« vostra »), contre le registre du glossaire (2ᵉ personne du singulier). Story **done**.
+- **2026-09-26** — **review P2** — **Trois lentilles Haiku 4.5** en contexte frais, diffs **aplatis** (story complète et remédiation P1 seule), prompt versionné `25-1c-b1-review-prompt-p2.md`. **0 au-dessus de LOW.** *Blind* : chaque correctif P1 confirmé, replis mot pour mot, compteurs — ⚠️ n'a **pas** déclaré ses axes non exercés (règle du `CLAUDE.md`) : compté avec réserve, ses axes étant couverts par l'*Edge* sur le même diff. *Edge* : ordre montage / effet, jeton de requête, bornes de pagination, `entityIdValue`, contrat, `i18nLocale`, `download.ts`, E2E — 0. *Auditor* : les dix Review Findings de P1 vérifiés corrigés dans le code ; ⚠️ il recompte « +26 » tests vitest — **faux**, réfuté par recompte `grep -cE '^\s*it\('` : download 8, options 4, query-helpers 5, API 3, page 18, i18n 5 → 7, soit **+40** comme déclaré (et il cite encore 26 clés / 164 au lieu de 27 / 165) : faux positif de décompte Haiku, écarté. **Orchestrateur** : l'export ne contrôle pas la plage inversée et reçoit le refus non traduit de la route — cas couvert par **#469** (« tout appel direct »), non rouvert. ⇒ **Boucle close** (P1 Sonnet 2 HIGH / 4 MED → P2 Haiku 0 > LOW). ⚠️ **Gates complets à rejouer** sur la branche après la 25-1c-b2 (T8).
+- **2026-09-26** — **review P1** — **Trois lentilles Sonnet** en contexte frais (auteur : Opus 5.5), prompt versionné `25-1c-b1-review-prompt-p1.md`, axes déclarés. **2 HIGH, 4 MEDIUM, LOW divers, 1 report.** *Edge* : identifiant conservé d'un type à l'autre (HIGH) ; réponses concurrentes non ordonnées (HIGH) ; plage inversée refusée par la route en français (MEDIUM) ; page au-delà de la fin sans issue (MEDIUM) ; a aussi **vérifié dans le runtime Svelte** que `onMount`, déclaré avant l'effet d'URL, s'exécute avant lui. *Blind* : mock d'`URL` qui mutait le global (MEDIUM) ; ordre montage / effet non testable, `goto` inerte (MEDIUM). *Auditor* : 0 au-dessus de LOW, décomptes **recomptés** (26 ×4, 164, +29, +35, +2, 17 mutations jugées discriminantes), `download.ts` identique à l'octet. Correctifs : identifiant vidé à **tout** changement de type ; **jeton de requête** ; plage contrôlée dans la page (clé `audit-log-error-date-range` ×4, 27 clés d'écran, 165 par catalogue) et `min`/`max` ; « Précédent » dans l'état vide ; `FakeURL` sous-classe ; `goto` bouclé sur l'URL relue. **Cinq mutations, toutes tuées** (jeton retiré, vidage conditionnel, plage non contrôlée, « Précédent » absent, effet d'URL déplacé avant la lecture — 7 tests rougissent). `sitesTotal` 1738 → **1740** (+2, recomptés). **Reporté** : refus de validation non traduits côté route → **#469**. Gate ciblé : vitest `audit-log` 30/30, gardes `shared` 189/189, `cargo test -p kesh-i18n` 29/29, `check` 0 erreur, lint PASS. ⚠️ Gate complet et E2E à rejouer.
+- **2026-09-26** — **dev** — Implémentée (Opus 5.5) sur `main` à `0e4c2682` + fiches. Téléchargement partagé, `i18nLocale()`, feature `audit-log`, écran, garde, menu, 26 clés ×4, gardes i18n recomptées (`sitesTotal` 1738). **17 mutations sur 17 tuées** — une a d'abord survécu, le test a été renforcé. Gates sur la b1 seule : frontend 812/812, backend 2463/2463, E2E 223/8/19 sans régression. ⚠️ **La PR attend la 25-1c-b2** (fiche à réécrire, R5) : c'est elle qui porte `closes #378`.
+- **2026-09-26** — **revalidation R6 ciblée** — **Une lentille Haiku 4.5**, contexte frais, braquée sur la seule remédiation R5 (`git diff bc4fee4b 6358639e`), prompt `25-1c-b1-validate-prompt-r6-ciblee.md`, axes déclarés. **0 finding** : 28 types, 97 actions, 138 clés recomptés ; chaque numéro de ligne vérifié par `grep` ; « 133 » et « 92 » ne subsistent que dans l'historique. La remédiation ne touchait que la fiche ⇒ **boucle close**, fiche revalidée contre `main` à `0e4c2682`.
+- **2026-09-26** — **revalidation R5 « dérive »** — Reprise sur `main` à `0e4c2682` après dix jours sur une branche locale jamais poussée. **Une lentille Sonnet**, prompt `25-1c-b-validate-prompt-r5-derive.md`, axes déclarés (non exercés : manuels — délégués à la b2 —, contenu détaillé des specs E2E non modifiées depuis). **Contrat de la route mergée (PR #439) confirmé champ par champ** ; rien de ce que la fiche prescrit n'a été livré entre-temps ; sept copies de téléchargement toujours sept. **1 MEDIUM** : « 133 clés / 92 actions » périmé — **138 / 97** au 2026-09-26 (`account.retyped`, `invoice.unvalidated`, `invoice.settlement_cancelled`, `supplier_invoice.settlement_cancelled`, `reconciliation.cancelled`) ; la fiche ne fige plus le nombre, à recompter au développement. **3 LOW** : lignes de `i18n-keys.test.ts` (+109 à +120) et de `i18n-libelle-en-dur.test.ts` rafraîchies ; décalages d'une dizaine de lignes dans les patrons cités (`invoices/+page.svelte`, `invoices_echeancier.spec.ts`, commentaires de l'AC 9), contenu intact — non repris, T0 relit par contenu. #386 citée hors périmètre : désormais fermée, sans effet.
+- *(Les entrées du 2026-09-26 avaient d'abord été insérées par erreur dans un tableau de la liste ; remontées ici.)*
+- **2026-09-15** — **Fiche créée par le split de la 25-1c-b** (arbitrage du Project Lead), après la passe 2
+  de validation de la parente (Opus : 6 MEDIUM, 7 LOW, tous vérifiés au sol et retenus). Les AC de
+  l'écran, du menu, des gardes et des tests viennent de la parente ; les treize findings y sont appliqués
+  **ici** (M1-M4, L1, L3-L7) ou dans la **25-1c-b2** (M5, M6) :
+  - **M1** — le 29ᵉ type `audit_log`, créé par l'export de la 25-1c-a, manquait à la carte → ajouté
+    (AC 3), recompte en T0 ;
+  - **M2** — filtre en jours UTC, tableau en heure locale → libellé « jours UTC » (AC 5), E2E calculé en
+    jour UTC (AC 13, scénario 4) ;
+  - **M3** — `entityId` résiduel au retour à « Tous », et relu sans type depuis l'URL → vidé et ignoré
+    (AC 4, 5), tests et mutations ;
+  - **M4** — rien ne prouvait que l'export transmet les filtres → test de l'URL de `getBlob` et test de
+    page, sites de mutation nommés (AC 12, 14) ;
+  - **L1** — renvoi « module partagé de l'AC 11 » faux → AC 9 ;
+  - **L2** — sept copies de téléchargement et non quatre → **le volet d'extraction sort** (arbitrage) :
+    module neuf sans migration (AC 9), issue de dette **#438**, quatre commentaires mis à jour ;
+  - **L3** — conséquences exactes sur les gardes (`sitesGabarit` 11, `SITES_GABARIT_ATTENDUS`,
+    `CANDIDATES_ATTENDUES` 44, `conforme` 37) et trois textes devenus faux → tableau de l'AC 11 ;
+  - **L4** — message de `RESULT_TOO_LARGE` → **message du serveur réutilisé**, aucune clé neuve (AC 5) ;
+  - **L5** — E2E : groupe « Administration » replié, seed sans Comptable ni Consultation → AC 13 ;
+  - **L6** — les clés de la 25-1c-a sont 11 + 1 = 12 → AC 10 ;
+  - **L7** — renvoi de commentaire `:311-314` ; aucun test direct de `triggerDownload` → ajouté (AC 9) ;
+    le message d'erreur arrive traduit par le serveur → Dev Notes.
+
+### Passe 3 de `bmad-create-story validate` — une lentille Sonnet, contexte frais
+
+Prompt versionné : `25-1c-b1-validate-prompt-p3.md`. Base : `git diff 0b029e24 f764004d`.
+
+**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 2 LOW — tous vérifiés au sol et retenus. Après reclassement :
+1 MEDIUM, 3 LOW.**
+
+- **M1** — la dépendance à la 25-1c-a citait « AC 5-14 », alors que l'écran consomme aussi la clé
+  `audit-log-export-error-too-large` de son AC 15 → « AC 5-15 ».
+  *Reclassé LOW à la vérification* : les References disaient déjà 5-15.
+- **M2** — l'argument `limit` du refus `RESULT_TOO_LARGE` était attribué à l'AC 15 de la 25-1c-a ; il est
+  défini par son **AC 11** (« Plafond », `25-1c-a…:203-205`) → « AC 11 », au site de l'AC 5 de cette
+  fiche. L'autre mention
+  (`AC 10`, « 11 + 1, 25-1c-a AC 15 ») porte sur les **clés**, et reste juste.
+- **L3** — le scénario E2E 7 citait `reports.spec.ts:421-446` pour la redirection d'un Consultation ; ce
+  bloc n'ouvre aucune route réservée. **Aucune spec du dépôt ne teste une telle redirection** (grep
+  inventaire corrigé en passe 4 : les `goto` vers des routes gardées se font tous en
+  Admin, et les trois specs qui créent un Consultation — `reports.spec.ts:428`,
+  `homepage-reminders.spec.ts:66`, `invoice-send-email.spec.ts:144` — n'ouvrent que des routes ouvertes à
+  tous) → patron restreint à la création et connexion (`:421-439`),
+  assertion écrite, et une seconde assertion exigée pour que la redirection ne soit pas vraie par
+  construction.
+- **L4** — `uniqSuffix()` n'est exporté d'aucun helper (trois copies locales) → précisé.
+
+**Axe déclaré non exercé par la lentille, repris par l'orchestrateur** : la partition de
+`i18n-libelle-en-dur.test.ts`. `candidatesDe` (la fonction réelle, copiée dans le scratchpad) a été
+**exécutée** sur un `entity-type-label.ts` synthétique conforme à l'AC 3 et sur l'`error-label.ts` réel :
+une candidate chacun, `analysee: true`, `retours: []` ⇒ **conforme**, comme son modèle. Les valeurs 44 et
+37 de l'AC 11 sont confirmées par exécution. La lentille avait, elle, exécuté le moissonneur de gabarits
+(1 site, préfixe `audit-log-entity-` ⇒ `sitesGabarit` 11).
+
+Remédiation : texte de spec uniquement, aucune ligne de code.
+
+### Passe 4 CIBLÉE de `bmad-create-story validate` — une lentille Opus, contexte frais
+
+Prompt versionné : `25-1c-b1-validate-prompt-p4.md`. Base : `git diff f764004d 637c6508`.
+
+**Rendu : 0 CRITICAL, 0 HIGH, 1 MEDIUM, 2 LOW — tous sur le scénario E2E 7 écrit par la passe 3, tous
+vérifiés au sol et retenus.**
+
+- **M1** — l'assertion `audit-log-table` à `toHaveCount(0)`, ajoutée « pour que la redirection ne soit pas
+  vraie par construction », **l'était elle-même**. Une page qui ne charge pas garde l'URL `/audit-log`, donc
+  `toHaveURL('/')` suffit, et aucun autre chemin ne mène un Consultation sur `/` (vérifié :
+  `(app)/+layout.ts:11,40,43`). → retirée, remplacée par deux compléments qui discriminent : l'accueil
+  rendu, et aucune requête vers la route.
+- **L2** — le volet menu (`toHaveCount(0)`) pouvait passer avant le rendu de la barre latérale, alors que
+  le dépôt a deux patrons anti-vacuité → ancrage `nav-link-settings` à `toHaveCount(1)`.
+- **L3** — la preuve de « aucun précédent » (un grep sur le mot `redirect`) n'établissait pas une absence
+  → remplacée par l'inventaire des `goto` et des specs qui créent un Consultation.
+
+⛔ *Une assertion ajoutée pour empêcher un test vrai par construction peut l'être à son tour* — la leçon
+de la 25-1c-zero, reproduite une passe plus tard sur une spec.
+
+La passe confirme par ailleurs les références neuves de la passe 3 (AC 11, AC 15, lignes de
+`reports.spec.ts` et `company-contact-details.spec.ts`, `uniqSuffix`) et la cohérence des décomptes.
+Remédiation : texte de spec uniquement.
+
+### Passe 5 CIBLÉE de `bmad-create-story validate` — une lentille Sonnet, contexte frais
+
+Prompt versionné : `25-1c-b1-validate-prompt-p5.md`. Base : `git diff 637c6508` (remédiation de la passe 4).
+
+**Rendu : 0 CRITICAL, 0 HIGH, 1 MEDIUM, 0 LOW — vérifié au sol et retenu.**
+
+- **M1** — le complément « aucune requête » ne disait pas que le listener doit être attaché **avant** le
+  `goto`. Posé après `toHaveURL('/')` — l'ordre dans lequel le texte présentait les choses —, il passe à
+  vide contre une garde déplacée après le montage : la page monte, appelle la route, puis redirige.
+  Vérifié : le patron cité attache le listener (`homepage-reminders.spec.ts:77-80`) avant son `goto`
+  (`:82`), et c'est la seule occurrence de `page.on('request'` de la suite. → ordre exigé ; **la mutation
+  qui prouve ce complément est ajoutée à l'AC 14**, pour que l'implémentation montre qu'il rougit.
+
+⛔ **Deuxième passe de suite où l'assertion ajoutée pour garantir une autre était elle-même muette**
+(passe 4 : le tableau absent, écrit en passe 3 ; passe 5 : le compteur de requêtes, écrit en passe 4).
+*(Écrit « troisième » à la rédaction, en comptant deux fois la même assertion — corrigé en passe 6. Le
+prompt `p6` reprend l'erreur et reste tel qu'il a été envoyé.)* Le geste qui
+ferme le motif n'est pas une meilleure phrase : c'est **une mutation par assertion ajoutée**, observée
+rouge à l'implémentation.
+
+La passe confirme par ailleurs : `homepage-card-open-invoices` n'est pas conditionnée au rôle ;
+`nav-link-settings` est unique et reste dans le DOM d'un `
` replié ; `nav-link-audit-log` est +retiré par un `{#if}` réel ; le préchargement au survol (`app.html:10`) est sans effet, le lien étant +absent pour ce rôle. + +Remédiation : texte de spec uniquement. + +### Passe 6 CIBLÉE de `bmad-create-story validate` — une lentille Opus, contexte frais + +Prompt versionné : `25-1c-b1-validate-prompt-p6.md`. Base : `git diff c5d14f32` (remédiation de la passe 5). + +**Rendu : 0 CRITICAL, 0 HIGH, 1 MEDIUM, 2 LOW — vérifiés et retenus.** + +- **M1** — la mutation ajoutée en passe 5 (« garde déplacée dans le composant ») ne rougit l'assertion + qu'elle devait prouver que sous **une seule** des trois formes qu'on peut lui donner. Le patron de page + imposé par l'AC 5 déclenche au montage un `onMount` qui charge, **puis** un `$effect` qui fait + `goto(url, { replaceState: true })` ; SvelteKit abandonne une navigation dès qu'une plus récente la + suit (`client.js:1690-1692`). Donc : + - une garde écrite en tête de composant voit son `goto('/')` annulé, et c'est `toHaveURL('/')` qui + rougit — le compteur n'est jamais lu ; + - seule une garde déclarée **après** le `$effect` fait rougir le compteur. + + « Scénario 7 rouge » aurait été consigné sans que l'assertion visée ait tourné. +- **L1** — « une page montée puis renvoyée ailleurs en ferait une [requête] » était trop général. +- **L2** — « troisième passe de suite » comptait deux fois la même assertion → « deuxième ». + +**Décision de l'orchestrateur : le complément et sa mutation sont RETIRÉS, pas rapiécés.** C'est la +troisième correction sur le même objet (écrit en passe 4, corrigé en passe 5, mis en défaut en passe 6), +et la leçon de la 25-1c-a s'applique à la lettre : *une forme qui casse à chaque correction se retire*. +Ce qu'il prétendait prouver — la garde jette dans `load()`, avant montage — est **déjà observé +directement** par le test unitaire de l'AC 12, sans dépendre de l'ordre des effets d'un composant. +L'E2E garde `toHaveURL('/')` et l'accueil rendu, qui rougissent tous deux quand la garde est retirée. +L1 disparaît avec le complément. + +Remédiation : **retrait** de texte de spec ; aucune assertion ajoutée. + +### Passe 7 CIBLÉE de `bmad-create-story validate` — une lentille Sonnet, contexte frais — **BOUCLE CLOSE** + +Prompt versionné : `25-1c-b1-validate-prompt-p7.md`. Base : `git diff 32af4148` (remédiation de la passe 6). + +**Rendu : 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW.** Les quatre axes sont déclarés exercés, avec leurs +preuves : +- le patron unitaire `users-page.test.ts:26-53` exige une redirection 302 vers `/`, et toute mutation qui + sort la garde de `load()` le fait rougir ; +- la ligne fusionnée de l'AC 14 annonce des tests rouges réels ; +- le retrait est complet hors Change Log ; +- le Change Log de la passe 6 est fidèle au diff. + +**Vérification de l'orchestrateur** (un zéro se vérifie comme un finding) : le point porteur, relu à la +source. Dans `users-page.test.ts:26-53`, `load()` est appelé, et `expect.unreachable` jette s'il ne jette +pas lui-même. Cette erreur est rattrapée, puis l'assertion `status === 302` rougit. L'assertion est bien +discriminante. + +**Critère de clôture** : 0 finding au-dessus de LOW, et la dernière remédiation (passe 6) est un **retrait** +de texte de spec, sans assertion ajoutée ni ligne de code. + +**Trend de la boucle** (la parente pour les passes 1-2, cette fiche ensuite) : + +| passe | modèle | rendu (après reclassement) | +|---|---|---| +| 1 | Sonnet + Haiku | 2 M, 2 L | +| 2 | Opus | 6 M, 7 L → **split** | +| 3 | Sonnet | 1 M, 3 L | +| 4 | Opus, ciblée | 1 M, 2 L | +| 5 | Sonnet, ciblée | 1 M | +| 6 | Opus, ciblée | 1 M, 2 L → **retrait** | +| 7 | Sonnet, ciblée | **0** | + +Aucun défaut de conception d'origine après le split. **Chacun des MEDIUM des passes 3 à 6 a été introduit +par la remédiation précédente**, et les trois derniers portaient sur **le même objet** : l'assertion qui +devait garantir la redirection d'un Consultation. La boucle ne s'est pas close par une meilleure phrase, +mais par le **retrait** de cette assertion au profit d'un test qui observait déjà la chose directement. + +--- + +## Réouverture du 2026-09-15 (soir) — le contrat de la 25-1c-a a changé + +Trois arbitrages du Project Lead, rendus après la clôture de la boucle (`epic-25-vague1-suite.md` +§ *fin de soirée*), réécrivent le contrat que l'écran consomme : + +| arbitrage | ce qui change ici | +|---|---| +| filtre strict — *« le cas réel n'existe pas »* | plus de `companyId` (AC 1), plus de colonne « Société » ni de mention « société indéterminée » (AC 5, 10, 12) | +| l'export n'écrit pas d'entrée — *« non »* | le type `audit_log` n'existe plus ; l'ancien M1 de la parente (29ᵉ type) devient sans objet | +| actions et types traduits, écran et CSV, dans la langue de l'interface — *« ok »*, *« non, maintenant »* | **l'écran ne traduit plus rien lui-même** : il affiche `actionLabel` et `entityTypeLabel` (AC 5), charge le **vocabulaire** traduit (AC 2), et ses filtres type et action deviennent des **listes** triées par libellé (AC 3, 5) ; la carte `entity-type-label.ts`, ses 29 clés et ses conséquences sur les gardes **disparaissent** (AC 3, 10, 11, 12, 14) | + +**Choix de spécification qui en découlent, et qui sont à contester en revalidation** : + +- **`` natifs étaient inexacts : bits-ui est pilotable par + valeur, la garde E2E laisse passer `selectOption({ label })`, les natifs sont dans treize pages et le vrai + patron avec `data-testid` est `invoices`. Vérifié. → **choix maintenu**, justification réécrite, `