From bc4fee4b2dc5eca4527a48917d5f64dc4c25efa0 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 14:08:41 +0200 Subject: [PATCH 01/19] =?UTF-8?q?docs(25-1c-b1,=2025-1c-b2):=20reprise=20d?= =?UTF-8?q?es=20sp=C3=A9cifications=20valid=C3=A9es=20le=202026-09-16,=20s?= =?UTF-8?q?ur=20main=20actuel=20(refs=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les 34 commits de la branche locale story/25-1c-b-journal-audit-ecran, jamais poussée, rejouaient la 25-1c-a déjà squash-mergée (PR #439) : seuls les fichiers propres à b, b1 et b2 sont repris, la fiche de l'epic est fusionnée et le registre reçoit les trois entrées. L'ancienne branche reste en local sous archive/25-1c-b-specs-2026-09-16. Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b-journal-audit-ecran.md | 414 +++++++++ .../25-1c-b-validate-prompt-p1.md | 74 ++ .../25-1c-b-validate-prompt-p2.md | 62 ++ .../25-1c-b1-journal-audit-ecran.md | 875 ++++++++++++++++++ .../25-1c-b1-validate-prompt-p3.md | 65 ++ .../25-1c-b1-validate-prompt-p4.md | 49 + .../25-1c-b1-validate-prompt-p5.md | 59 ++ .../25-1c-b1-validate-prompt-p6.md | 44 + .../25-1c-b1-validate-prompt-p7.md | 44 + .../25-1c-b1-validate-prompt-r1.md | 62 ++ .../25-1c-b1-validate-prompt-r2.md | 47 + .../25-1c-b1-validate-prompt-r3.md | 42 + .../25-1c-b1-validate-prompt-r4.md | 45 + .../25-1c-b2-journal-audit-textes.md | 704 ++++++++++++++ .../25-1c-b2-validate-prompt-p3.md | 65 ++ .../25-1c-b2-validate-prompt-p4.md | 59 ++ .../25-1c-b2-validate-prompt-p5.md | 62 ++ .../25-1c-b2-validate-prompt-p6.md | 49 + .../25-1c-b2-validate-prompt-p7.md | 55 ++ .../25-1c-b2-validate-prompt-r1.md | 53 ++ .../25-1c-b2-validate-prompt-r2.md | 42 + .../25-1c-b2-validate-prompt-r3.md | 39 + .../25-1c-b2-validate-prompt-r4.md | 41 + .../sprint-status.yaml | 4 +- .../epic-25-vague1-suite.md | 54 +- 25 files changed, 3105 insertions(+), 4 deletions(-) create mode 100644 _bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p1.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b-validate-prompt-p2.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p3.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p4.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p5.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p6.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p7.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r1.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r2.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r3.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r4.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p3.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p4.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p5.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p6.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p7.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r1.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r2.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r3.md create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r4.md 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-b1-journal-audit-ecran.md b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md
new file mode 100644
index 000000000..748f991da
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md
@@ -0,0 +1,875 @@
+# Story 25.1c-b1 : L'écran de consultation du journal d'audit
+
+Status: ready-for-dev
+
+⚠️ **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: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 `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 133 clés de la 25-1c-a (en-têtes du CSV,
+message de refus, types d'auteur, 28 types d'entité, 92 actions, à la spécification) **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:388`) : les 133 clés
+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` (`:234`), `CARDINALITES` (`:292-301`) | **inchangés** — aucun préfixe dynamique neuf |
+| `ATTENDU.sitesGabarit` (`:210`), `SITES_GABARIT_ATTENDUS` (`:365-376`) | **inchangés** — 10 |
+| `FAMILLES_RESOLUES['nav-']` (`:321-327`) | **+ `'audit-log'`** (AC 7) |
+| `ATTENDU.sitesTotal`, `sitesNonResolus`, `relais` (`:207-209`) | **recomptés** — les appels littéraux de la page ajoutent des sites —, historique commenté prolongé |
+| `PREFIXES_A_COUVERTURE_CLOSE` (`:388`) | **inchangé** (AC 10) |
+
+`src/lib/shared/i18n-libelle-en-dur.test.ts` : `CANDIDATES_ATTENDUES` (`:115`) et la partition (`:632`)
+**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** pour les deux listes, et non le `Select` de bits-ui : le pilotage E2E par valeur
+  évite un sélecteur par libellé (AC 5) ;
+- **attributs `data-action` et `data-entity-type`** sur chaque ligne, pour identifier une ligne par son
+  code alors qu'elle affiche un texte traduit (AC 5, 13) ;
+- **un code d'URL hors vocabulaire devient une option** libellée par son code (AC 3) ;
+- **l'échec du vocabulaire est un état d'erreur** de la page (AC 5).
+
+**Ce qui tombe avec la réécriture** : les valeurs de gardes établies en passes 2 et 3 (`sitesGabarit` 11,
+`CANDIDATES_ATTENDUES` 44, `conforme` 37), qui ne valaient que pour la carte retirée — **les gardes
+restent désormais à leurs valeurs** (AC 11). Les AC cités dans le Change Log ci-dessus sont ceux de la
+version validée, et ne sont pas réécrits.
+
+**Revalidation requise** : passe complète sur les volets A, B, D et E, puis ciblée.
+
+### Revalidation R1 — une lentille Opus, contexte frais
+
+Prompt versionné : `25-1c-b1-validate-prompt-r1.md`. Base : `git diff 5760bab7 90a427bf`. Sondes : `Intl`
+sous Node 22 (ICU 78), copie jetable des gardes i18n avec une page synthétique conforme, regex de la garde
+E2E, source de bits-ui et de Svelte.
+
+**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 8 LOW — tous vérifiés au sol et retenus.**
+
+- **M1 — la « locale de l'interface » n'existe pas dans le frontend** : `i18n.svelte.ts` jette
+  `data.locale`. Vérifié. → `i18nLocale()` (AC 3), format de date sur elle (AC 5), mock, mutation ; la
+  phrase correspondante de la 25-1c-a (AC 17) est corrigée dans sa fiche.
+- **M2 — aucun test ne vérifiait que la page passe par `toSelectOptions`** : une page rendant le vocabulaire
+  brut passait tous les tests. → test de page (code d'URL hors vocabulaire, ordre par libellé), mutation.
+- **L1** — le test de tri laissait passer l'absence de tri et une locale ignorée → entrée dans l'ordre des
+  codes, espion `Intl.Collator`.
+- **L2** — oublier `'audit-log'` dans `FAMILLES_RESOLUES` ne fait rougir aucune garde (exécuté) → mutation
+  « `nav-audit-log` retirée des catalogues », qui rend l'ajout observable.
+- **L3** — les faits qui justifiaient les `` natif sous jsdom, espion `Intl.Collator`,
+tous exécutés sous vitest 4.1.3.
+
+**Rendu : 0 CRITICAL, 0 HIGH, 3 MEDIUM, 0 LOW — vérifiés au sol et retenus.** ⛔ **Deux des trois sont le motif
+de cette fiche** : une preuve ajoutée par la remédiation qui ne prouve rien.
+
+- **M1** — la mutation « `i18nLocale()` rend toujours `'fr-CH'` » était attribuée à deux tests qui **ne voient
+  pas la vraie fonction** : `toSelectOptions` reçoit la locale en paramètre, et le test de page mocke le module
+  entier. Exécuté : le test de page passe à l'identique sur la source correcte et sur la mutée. → **test direct**
+  dans `i18n.svelte.test.ts`, `servir` étendu à la locale, ligne de mutation réalignée.
+- **M2** — l'espion `vi.spyOn(Intl, 'Collator')` prescrit **casse `.compare()`** sous vitest, même sur une
+  implémentation correcte (exécuté). → **retiré, pas rapiécé** : la locale ne se prouve pas par le tri entre les
+  quatre langues du projet, et le contournement (`Reflect.construct`) ajouterait une assertion fragile à une
+  fiche où trois ont déjà dû être retirées. Angle mort écrit.
+- **M3** — le scénario E2E 1 ne connectait pas le Comptable : `authedApiContext` reprend la session de la page
+  (vérifié, `test-state.ts:174-186`), et tout le scénario aurait tourné sous l'Admin, autorisé lui aussi. →
+  connexion exigée.
+
+La passe confirme par ailleurs, par exécution : le test de page « la page passe par `toSelectOptions` » rougit
+sous sa mutation et reste vert sur une implémentation correcte ; aucun des 23 mocks existants du module i18n
+n'est cassé par l'ajout d'`i18nLocale` ; les faits réécrits en R1 (lignes, « treize pages ») sont exacts.
+
+Remédiation : texte de spec uniquement. **Revalidation R3 ciblée requise** (MEDIUM).
+
+### Revalidation R3 CIBLÉE — une lentille Opus, contexte frais
+
+Prompt versionné : `25-1c-b1-validate-prompt-r3.md`. Base : `git diff 0bbefc51 e9b15645`. Sondes exécutées
+sous vitest 4.1.3 : copies d'`i18n.svelte.ts` et de son test, trois emplacements du test, deux mutations ;
+page-sonde pour la date et le tri.
+
+**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 4 LOW, et 1 LOW hors périmètre — vérifiés et retenus.**
+
+- **M1** — l'assertion « avant tout chargement ⇒ `'fr-CH'` » ne discriminait que **placée en tête du
+  fichier** : le module garde son état d'un test à l'autre. En fin de fichier, elle restait verte sous la
+  mutation « valeur initiale vide », ou rougissait à tort. → **module frais** (`vi.resetModules()`), mutation
+  ajoutée.
+- **M2** — l'angle mort écrit en R2 était **trop étroit et évitable** : la colonne Date n'était plus observée
+  par aucun test, et une locale discriminante (`sv-SE`, « Öffnung » / « Zahlung ») prouve le tri sans espion.
+  Vérifié par l'orchestrateur sous Node 22 (`fr-CH` → « Öffnung » d'abord, `sv-SE` → « Zahlung » d'abord ;
+  `de-CH` en `dateStyle: 'medium'` → `16.09.2026, 14:26`). → deux tests de page, deux mutations, **angle mort
+  levé**.
+- **L1** — « comme au scénario 7 » renvoyait à un `login` à un seul paramètre → patron
+  `company-contact-details.spec.ts:39-49,249`, vérifié.
+- **L2** — `test-state.ts:174-186` → `:174-191`.
+- **L3** — les bases de la R2 (`2efbc76c`, `6cda8b89`) ne sont plus ancêtres de la branche après rebase ; leurs
+  équivalents `dd3cedab`, `5dfbafc2` donnent le même diff. Le prompt `r2` reste tel qu'il a été envoyé.
+- **L4** — « deux des trois » MEDIUM de la R2 relevaient du motif ; le troisième aussi (un scénario qui tourne
+  sous l'Admin sans que rien ne rougisse) → c'étaient **les trois**.
+- **Hors périmètre** — les exemples de date de l'AC 5 n'étaient justes qu'avec `dateStyle: 'medium'`, option que
+  la fiche ne prescrivait pas → option prescrite, exemples mesurés.
+
+⛔ **Le motif de la fiche, une fois de plus** : la remédiation R2 avait retiré un espion qui cassait, et écrit un
+angle mort **à la place d'un test qui existait** — il suffisait d'une locale qui trie autrement. *Retirer une
+assertion fragile n'oblige pas à renoncer à la preuve.*
+
+Remédiation : texte de spec uniquement. **Revalidation R4 ciblée requise** (MEDIUM).
+
+### Revalidation R4 CIBLÉE — une lentille Sonnet, contexte frais — **BOUCLE CLOSE**
+
+Prompt versionné : `25-1c-b1-validate-prompt-r4.md`. Base : `git diff 717f7763 44d638f9` — commits
+« docs(25-1c-b1): revalidation R2 ciblée » et « docs(25-1c-b1): revalidation R3 ciblée ».
+
+**Rendu : 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW.** Les cinq axes sont déclarés exercés, **par exécution** sous
+vitest 4.1.3 et Node 22 :
+- le test d'`i18nLocale()` sur module frais rougit sous « valeur initiale vide », **quelle que soit sa place**, et
+  le contrôle négatif sans `resetModules` reproduit le défaut que la R3 a corrigé ;
+- le tri sous `sv-SE` inverse bien l'ordre des quatre locales du projet, et la locale en dur rougit ; la CI épingle
+  Node 22 officiel, à ICU complète ;
+- la date `de-CH` rend `16.09.2026, 14:26` sous jsdom, et les mutations « `'fr-CH'` en dur » et
+  « `navigator.language` » rougissent ; la date reste le 16 en UTC comme à Zurich ;
+- le `login` à trois arguments et `test-state.ts:174-191` sont exacts ;
+- le Change Log R3 est fidèle, décomptes justes.
+
+**Vérification de l'orchestrateur** : la lentille a d'abord écrit ses sondes **dans le dépôt**
+(`frontend/src/lib/shared/utils/_r4probe_*`), en violation de l'interdit, puis les a retirées et refaites dans le
+scratchpad. Contrôlé avant clôture : aucun fichier de sonde, `git diff` vide, seuls les prompts non suivis. ⛔ *Un
+interdit écrit n'empêche pas l'écriture* : le motif de la passe 4 de la 24-5, reproduit en plus bénin.
+
+**Critère de clôture** : 0 finding, et la dernière remédiation ne touche que du texte de spec.
+
+**Trend de la boucle rouverte** (après la clôture en 7 passes de la version d'avant les arbitrages du soir) :
+
+| passe | modèle | rendu |
+|---|---|---|
+| R1 | Opus, complète | 2 M, 8 L |
+| R2 | Sonnet, ciblée | 3 M |
+| R3 | Opus, ciblée | 2 M, 5 L |
+| R4 | Sonnet, ciblée | **0** |
+
+**Ce que la boucle rouverte apprend** : les deux MEDIUM de R1 étaient **de conception** — la locale que le frontend
+jette, et une page que rien n'obligeait à passer par `toSelectOptions`. Tous les suivants venaient de la
+remédiation, sous la forme propre à cette fiche : **une preuve ajoutée qui ne prouvait rien** (un espion qui casse,
+un test dont le résultat dépend de sa place, un scénario joué sous le mauvais rôle). La boucle ne s'est close
+qu'une fois chaque preuve **exécutée** par la lentille sur sa mutation, et non plus relue.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p3.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p3.md
new file mode 100644
index 000000000..fcf74b58e
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p3.md
@@ -0,0 +1,65 @@
+# Prompt — passe 3 de `bmad-create-story validate`, Story 25-1c-b1
+
+*Versionné le 2026-09-15. Une lentille en contexte frais (Sonnet), orthogonale à la passe 2 (Opus). Première
+passe sur la fiche issue du split de la 25-1c-b.*
+
+Tu es un **valideur adversarial** en contexte frais. Ton objet est le fichier
+`_bmad-output/implementation-artifacts/25-1c-b1-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 fiche, ni son Change Log, ni les valeurs « établies par
+exécution » de la passe 2.
+
+⚠️ **Ne conteste PAS les arbitrages du Project Lead** (dont le découpage b1/b2 et la sortie du regroupement
+du téléchargement vers l'issue #438) 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 fiche vient d'être écrite par le commit `f764004d`, qui applique à la fois le split et onze findings de la
+passe 2 de la fiche parente (`25-1c-b-journal-audit-ecran.md`, statut `split`). **Le motif mesuré du projet :
+la sévérité se déplace vers ce qu'on vient d'écrire.** Relis en priorité ce qui est neuf par rapport à la
+parente : `git diff 0b029e24 f764004d -- _bmad-output/implementation-artifacts/` — nomme cette base dans ton
+rapport.
+
+## Les axes, et tu déclareras lesquels tu as exercés
+
+1. **Ce que le split a pu perdre.** Chaque AC de la parente (1-12, 19-21) se retrouve-t-il dans la b1 ou la
+   b2, sans trou ni doublon contradictoire ? Un renvoi de numéro d'AC resté faux après renumérotation ?
+2. **Le module `download.ts` sans migration (AC 9).** Crée-t-il un défaut qu'un gate verrait : `lint`,
+   `svelte-check`, un test de duplication, un import croisé ? Les tests « recopiés sans modification » des cas
+   `parseContentDispositionFilename` sont-ils recopiables tels quels (imports, helpers `fakeJwt`, `authState`) ?
+   Le test direct de `triggerDownload` est-il écrivable dans jsdom (`vi.stubGlobal('URL', …)` écrase-t-il le
+   constructeur `URL` dont d'autres modules ont besoin) ?
+3. **Les gardes i18n, par l'exécution (AC 11).** Les valeurs 11 / 44 / 37 et la ligne de
+   `SITES_GABARIT_ATTENDUS` : **que rendent réellement les extracteurs** pour la forme de code prescrite ?
+   Construis une **copie jetable** dans le scratchpad pour l'observer — jamais dans le dépôt. Le préfixe
+   `audit-log-entity-` ajouté à `MOTIFS_DYNAMIQUES` entre-t-il en conflit avec les 12 clés `audit-log-*` de
+   la 25-1c-a (orphelines, `lint-i18n-ownership`, préfixes « entièrement demandés ») ?
+4. **Les corrections M2-M4 contre le contrat** : « jours UTC » est-il exact au regard des bornes de la
+   25-1c-a ? Le vidage d'`entityId` et son rejet à la lecture couvrent-ils le bouton « Réinitialiser » et le
+   retour arrière du navigateur ? Le test de l'URL de `getBlob` est-il écrivable avec le patron cité ?
+   `RESULT_TOO_LARGE` : `parseErrorResponse` est-il bien le chemin de `getBlob` (lis `requestRaw`) ?
+5. **L'E2E (AC 13)** : chaque scénario est-il constructible avec les helpers réels ? Le rôle, le mot de passe,
+   la connexion d'un utilisateur créé par l'API, `clearAuthStorage` ? Le scénario 4 en jour UTC tient-il
+   si l'entrée est créée à 23:59:59 UTC et lue après minuit ?
+6. **Cohérence interne, décomptes, références de lignes** — recompte, ne relis pas. Grep `\b28\b` et `\b29\b`.
+
+## 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/p3-b1/`.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p4.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p4.md
new file mode 100644
index 000000000..ffab0cfa6
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p4.md
@@ -0,0 +1,49 @@
+# Prompt — passe 4 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b1
+
+*Versionné le 2026-09-15. Une seule lentille en contexte frais (Opus), orthogonale à la passe 3 (Sonnet).
+Passe ciblée au sens de `CLAUDE.md` § « La passe ciblée » : ce qu'il reste à relire n'est plus la story,
+c'est la dernière remédiation.*
+
+Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche
+`_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt `/home/gcorbaz/devel/kesh`,
+branche `story/25-1c-b-journal-audit-ecran`.
+
+**Ton périmètre est la remédiation**, et seulement elle :
+`git diff f764004d HEAD -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`. Nomme
+cette base dans ton rapport. Le motif mesuré du projet : *sur les huit passes cumulées de deux stories
+récentes, sept ont trouvé une régression du patch précédent et aucune un défaut de conception d'origine.*
+
+⛔ **Rien ne se croit sur parole** — ni le Change Log, ni les vérifications « par exécution » qu'il déclare.
+⚠️ **Ne conteste PAS les arbitrages du Project Lead** ni le contrat de `25-1c-a-journal-audit-route.md`.
+
+## Les axes, et tu déclareras lesquels tu as exercés
+
+1. **Chaque ligne changée dit-elle vrai ?** Rouvre la source de chaque référence neuve ou modifiée
+   (numéros d'AC de la 25-1c-a, lignes de `reports.spec.ts`, `company-contact-details.spec.ts`,
+   `journal_entries.rs`, etc.) et vérifie qu'elle désigne ce que la fiche prétend.
+2. **L'assertion E2E de redirection neuve (scénario 7)** : `toHaveURL('/')` après `page.goto('/audit-log')`
+   est-il atteignable avec la garde `+page.ts` prescrite (`redirect(302, '/')`, `ssr = false`) — l'URL
+   finale, un éventuel passage par `/login` ou `/onboarding`, le temps de chargement ? L'assertion
+   `audit-log-table` à `toHaveCount(0)` peut-elle être **vraie par construction** (vérifiée avant que la
+   page n'ait eu le temps de rendre) ? Lis `routes/(app)/+layout.ts` et le garde d'authentification réel.
+3. **La propagation** : un site non modifié par le patch dit-il encore l'ancienne chose (AC 5-14, AC 15 pour
+   `limit`, `421-446`, `uniqSuffix` présenté comme helper) ? Greppe les jetons, pas les phrases.
+4. **Le Change Log de la passe 3** : ses décomptes (rendu brut, après reclassement) sont-ils cohérents avec
+   sa propre liste ? Recompte.
+
+## 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.
+- ⛔ **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 : lecture, `grep`, `git diff`/`git show`, et une copie
+jetable **dans le scratchpad**
+`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p4-b1/`.
diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p5.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p5.md
new file mode 100644
index 000000000..ba0806c4d
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p5.md
@@ -0,0 +1,59 @@
+# Prompt — passe 5 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b1
+
+*Versionné le 2026-09-15. Une seule lentille en contexte frais (Sonnet), orthogonale à la passe 4 (Opus).
+Passe ciblée : `CLAUDE.md` § « La passe ciblée ».*
+
+Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche
+`_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt `/home/gcorbaz/devel/kesh`,
+branche `story/25-1c-b-journal-audit-ecran`.
+
+**Ton périmètre est la remédiation de la passe 4**, et seulement elle :
+`git diff 637c6508 -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md` (arbre de
+travail ou commit suivant, le contenu est le même). Nomme cette base dans ton rapport. Elle réécrit le
+**scénario E2E 7** (redirection d'un Consultation, entrée de menu absente) et le Change Log.
+
+⚠️ **Le motif à chasser est précis** : deux passes de suite, une assertion ajoutée pour empêcher un test
+*vrai par construction* l'était elle-même. Pour **chaque** assertion que le scénario 7 prescrit maintenant,
+construis l'état où elle **devrait rougir** et montre qu'elle rougit — sinon c'est un finding.
+
+⛔ **Rien ne se croit sur parole** — ni le Change Log, ni les lignes citées.
+⚠️ **Ne conteste PAS les arbitrages du Project Lead** ni le contrat de `25-1c-a-journal-audit-route.md`.
+
+## Les axes, et tu déclareras lesquels tu as exercés
+
+1. **Chaque assertion du scénario 7 discrimine-t-elle ?**
+   - `toHaveURL('/')` : quelles mutations la font rougir (garde retirée ; garde qui redirige ailleurs ;
+     login raté) ?
+   - `homepage-card-open-invoices` visible : un Consultation voit-il cette carte (lis
+     `routes/(app)/+page.svelte`, conditions de rôle) ? Si elle est masquée pour ce rôle, l'assertion
+     rougit **toujours**.
+   - **aucune requête** vers `/api/v1/audit-log` : est-ce vrai avec la garde `+page.ts` prescrite
+     (`ssr = false`, `redirect` dans `load`) ? SvelteKit précharge-t-il la page au survol du lien
+     (`data-sveltekit-preload-data` dans `app.html` ou le layout) — et le lien est-il seulement présent pour
+     ce rôle ? Le listener est-il posé avant `goto` ?
+   - `nav-link-settings` à `toHaveCount(1)` : ce testid existe-t-il exactement ainsi
+     (`+layout.svelte:258-261`, slug de `/settings`) ? Le groupe replié rend-il ses liens dans le DOM
+     (`
` fermé) ? Un autre lien produit-il le même testid (sous-routes `/settings/...`) ? +2. **Chaque référence neuve dit-elle vrai ?** `homepage-reminders.spec.ts:77-80,83-84,88`, + `invoice-send-email.spec.ts:154-157`, `+layout.svelte:126`, `(app)/+layout.ts:11,40,43`, les lignes + d'`api-client.ts` sur `/login` et `/setup`, les trois specs qui créent un Consultation. +3. **Propagation** : l'AC 14 (mutations) et le reste de la fiche sont-ils cohérents avec le scénario 7 + réécrit (la mutation « entrée de menu passée à `items` », la mutation « garde retirée ») ? +4. **Le Change Log de la passe 4** : décomptes et affirmations recomptés. + +## 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. +- ⛔ **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`, +`npm run test:e2e`, `sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, +`git diff`/`git show`, et une copie jetable **dans le scratchpad** +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p5-b1/`. diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p6.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p6.md new file mode 100644 index 000000000..183b617bb --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p6.md @@ -0,0 +1,44 @@ +# Prompt — passe 6 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b1 + +*Versionné le 2026-09-15. Une seule lentille en contexte frais (Opus), orthogonale à la passe 5 (Sonnet).* + +Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Ton périmètre est la remédiation de la passe 5**, et seulement elle : +`git diff c5d14f32 -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`. Nomme cette +base dans ton rapport. Elle touche le scénario E2E 7 (ordre du listener de requêtes), ajoute **une ligne +à la table des mutations de l'AC 14**, et le Change Log. + +⚠️ **Le motif à chasser** : trois passes de suite, l'assertion ajoutée pour en garantir une autre était +muette. La remédiation de la passe 5 répond par **une mutation**. Vérifie que cette mutation est **réelle** : + +1. **Est-elle écrivable et fait-elle ce qu'elle dit ?** Une garde déplacée de `load()` (`+page.ts`) dans le + composant (`onMount` qui fait `goto('/')`) : avec `ssr = false`, la page monte-t-elle, et l'appel + `listAuditLog` part-il **avant** le `goto` (ordre des `onMount`/`$effect` de Svelte 5, et où la page + prescrite par l'AC 5 déclenche-t-elle son premier chargement) ? Si le chargement part après la + redirection, ou n'a pas le temps de partir, la mutation **ne rougit pas** le test — et la ligne ajoutée + à l'AC 14 est fausse. +2. **Sa colonne « test attendu rouge » dit-elle vrai** : `toHaveURL('/')` reste-t-il vert sous cette + mutation, comme la ligne l'affirme ? +3. **Le listener « attaché avant le goto »** : la requête est-elle réellement observable par `page.on('request')` + (requête `fetch` du client vers la même origine) ? Un préchargement ou un cache pourrait-il la masquer ? +4. **Références neuves** (`homepage-reminders.spec.ts:77-82,88`) et cohérence du Change Log (décomptes). + +## 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. +- ⛔ **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`, +`npm run test:e2e`, `sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, +`git diff`/`git show`, et une copie jetable **dans le scratchpad** +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p6-b1/`. diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p7.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p7.md new file mode 100644 index 000000000..eb662625c --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-p7.md @@ -0,0 +1,44 @@ +# Prompt — passe 7 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b1 + +*Versionné le 2026-09-15. Une seule lentille en contexte frais (Sonnet), orthogonale à la passe 6 (Opus). +Avant-dernière passe autorisée par le plafond de 8.* + +Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Ton périmètre est la remédiation de la passe 6**, et seulement elle : +`git diff 32af4148 -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`. Nomme cette +base dans ton rapport. Cette remédiation **retire** une assertion E2E (un compteur de requêtes) et la +mutation qui la prouvait, et renvoie la preuve au test unitaire de l'AC 12. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Le retrait laisse-t-il un trou ?** Sans le compteur, quelle mutation plausible de la garde (AC 6) + n'est plus attrapée par **aucun** test — ni le test unitaire de `load()` (AC 12, patron + `frontend/src/routes/(app)/users/users-page.test.ts:26-53`), ni `toHaveURL('/')` et l'accueil rendu + (AC 13, scénario 7) ? Lis le patron unitaire : prouve-t-il vraiment que `load()` **jette** pour un + Consultation, ou seulement autre chose ? +2. **La table des mutations de l'AC 14 est-elle encore juste ?** La ligne « garde retirée » annonce deux + tests rouges (unitaire et E2E) : est-ce vrai des deux ? Une ligne a-t-elle perdu sa raison d'être, ou en + manque-t-il une ? +3. **Propagation** : le retrait est-il complet (grep `aucune requête`, `page.on`, `listener`, `onMount`, + `77-8` hors Change Log) ? Le texte neuf du scénario 7 se lit-il sans renvoi cassé ? +4. **Le Change Log de la passe 6** : décomptes, et fidélité de la décision au texte du corps. + +## 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. +- ⛔ **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`, +`npm run test:e2e`, `sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, +`git diff`/`git show`, `npx vitest run` en lecture, et une copie jetable **dans le scratchpad** +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p7-b1/`. diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r1.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r1.md new file mode 100644 index 000000000..d5649201f --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r1.md @@ -0,0 +1,62 @@ +# Prompt — revalidation R1 de `bmad-create-story validate`, Story 25-1c-b1 (réouverte) + +*Versionné le 2026-09-15. Une lentille en contexte frais (Opus), orthogonale à la passe 7 (Sonnet). Première +passe de la boucle rouverte : l'écran consomme désormais un vocabulaire traduit par la route.* + +Tu es un **valideur adversarial** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. Le contrat qu'elle consomme est +`25-1c-a-journal-audit-route.md` (même branche, rouvert lui aussi). + +**Base** : `git diff 5760bab7 HEAD -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`. +Nomme-la dans ton rapport. La section « Réouverture » du Change Log liste les choix de spécification **à +contester**. + +⛔ **Rien ne se croit sur parole.** ⚠️ **Ne conteste PAS les arbitrages du Project Lead** (actions et types +traduits à l'écran, filtre strict, export non audité, découpage b1/b2). + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Le contrat** : chaque champ, route, forme et numéro d'AC que la b1 attribue à la 25-1c-a existe-t-il + **exactement** dans la 25-1c-a rouverte (DTO de son AC 8, vocabulaire de son AC 17, refus de son AC 6, + plafond de son AC 11) ? +2. **Les `` natif pose-t-il un + problème d'accessibilité ou de cohérence visuelle que la fiche tait ? +3. **`toSelectOptions` (AC 3)** : d'où le frontend tire-t-il sa locale (`i18n.svelte.ts`) ? Le tri + `Intl.Collator(locale, { sensitivity: 'base' })` range-t-il « Écriture créée » avant « Facture validée » + en `fr-CH` sous jsdom/Node — **exécute-le** ? L'ajout d'un code hors vocabulaire est-il cohérent avec la + synchronisation d'URL et le bouton « Réinitialiser » ? +4. **Les gardes i18n (AC 10-11), par l'exécution** : la page prescrite laisse-t-elle vraiment + `MOTIFS_DYNAMIQUES`, `sitesGabarit`, `CANDIDATES_ATTENDUES` inchangés ? Les clés `audit-log-*` de l'écran, + dans une feature `audit-log`, passent-elles `lint-i18n-ownership` alors que le backend possède des clés + au même préfixe ? Construis une **copie jetable** dans le scratchpad pour observer ce que rendent les + extracteurs sur une page synthétique conforme. +5. **Les tests et l'E2E (AC 12-13)** : pour chaque assertion neuve, construis l'état où elle **devrait + rougir**. La preuve de traduction du scénario 1 (« cellule non vide et différente du code ») est-elle + discriminante — que se passe-t-il si la route renvoie un libellé égal au code pour un code inconnu ? + `selectOption({ value })` sur un `` natif lié par Svelte 5 ? Rougit-il sous la mutation + annoncée, et reste-t-il vert sur une implémentation correcte ? +4. **La preuve de traduction E2E (scénario 1)** : lire `/api/v1/audit-log/vocabulary` par `authedApiContext` + — quel rôle porte ce contexte (le Comptable créé, ou l'Admin du seed) ? Les deux sont autorisés (25-1c-a + AC 6) ? Le libellé lu est-il dans la même langue que l'écran ? +5. **Les faits réécrits (AC 5, Dev Notes)** : `invoices/+page.svelte:314-318`, `invoices.spec.ts:167`, + `bits-ui/dist/bits/select/select.svelte.js:909`, « treize pages », `i18n.svelte.ts:25-35`, `app.html:2`, + `api-client.ts:19-28`, `reports.api.ts:338-346`, `imported-supplier-invoices.api.ts:66-69` — recompte et + relis. +6. **Propagation et cohérence** : 133 / 92, AC 9 (trois commentaires) contre T1, l'erreur de liste (AC 5) + contre les tests (AC 12), le Change Log R1 (décomptes : 2 MEDIUM, 8 LOW). + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés** — rien de « vérifié » sans exécution. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`/`npm run test:e2e`, ni +`sqlx migrate`, ni `cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff`, `npx vitest run` en +lecture, Python/Node en lecture, copie jetable dans +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r2-b1/`. `grep` est +`ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r3.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r3.md new file mode 100644 index 000000000..4fa902e82 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r3.md @@ -0,0 +1,42 @@ +# Prompt — revalidation R3 CIBLÉE, Story 25-1c-b1 + +*Versionné le 2026-09-15. Une lentille Opus en contexte frais, orthogonale à R2 (Sonnet).* + +Chasseur de régressions. Objet : `_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt +`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. **Périmètre : la remédiation R2**, +`git diff 0bbefc51 e9b15645 -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`. Nomme +cette base. ⚠️ **Motif de cette fiche** : une preuve ajoutée par la remédiation qui ne prouve rien. Pour chaque +test prescrit ou modifié par R2, construis l'état où il **devrait rougir** et montre qu'il rougit. + +⛔ Rien ne se croit sur parole. ⚠️ Ne conteste PAS les arbitrages du Project Lead. + +## Axes (déclare lesquels tu as exercés) + +1. **Le test direct d'`i18nLocale()`** : étendre `servir` (`i18n.svelte.test.ts:28`) à la locale casse-t-il les + tests existants de ce fichier ? L'assertion « avant tout chargement ⇒ `'fr-CH'` » est-elle observable, + alors que l'état du module persiste entre tests (lis comment le fichier réinitialise son module) ? Rougit-il + sous la mutation « `i18nLocale()` rend toujours `'fr-CH'` » — **exécute-le** sur une copie jetable. +2. **L'angle mort de la locale du tri** : la fiche dit la provenance « prouvée par le test direct » et le + passage à `toSelectOptions` « relu en revue ». Une mutation plausible — la page passe `'fr-CH'` en dur à + `toSelectOptions`, ou utilise `navigator.language` pour la date — échappe-t-elle à **tous** les tests ? Si + oui, l'angle mort est-il écrit assez largement (la colonne Date aussi) ? +3. **La connexion du Comptable (scénario E2E 1)** : `clearAuthStorage` puis `login(page, username, password)` + — signatures réelles des helpers (`tests/e2e/helpers/`) ? L'utilisateur créé par l'API en Admin peut-il se + connecter aussitôt (mot de passe, changement forcé au premier login, onboarding) — cherche le patron du + scénario 7 (`reports.spec.ts:421-439`) et ce que fait `login` ? +4. **Propagation et Change Log R2** : la table des mutations, l'AC 12 et le scénario 1 disent-ils la même + chose ; décomptes (3 MEDIUM). + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés**. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`/`npm run test:e2e`, ni +`sqlx migrate`, ni `cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff`, `npx vitest run` en +lecture, Python/Node en lecture, copie jetable dans +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r3-b1/`. `grep` est +`ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r4.md b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r4.md new file mode 100644 index 000000000..776808ccb --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-validate-prompt-r4.md @@ -0,0 +1,45 @@ +# Prompt — revalidation R4 CIBLÉE, Story 25-1c-b1 + +*Versionné le 2026-09-15. Une lentille Sonnet en contexte frais, orthogonale à R3 (Opus).* + +Chasseur de régressions. Objet : `_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md`, dépôt +`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. **Périmètre : la remédiation R3**, +`git diff 717f7763 44d638f9 -- _bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md` — commits +« docs(25-1c-b1): revalidation R2 ciblée » et « docs(25-1c-b1): revalidation R3 ciblée ». Si un hash ne résout pas, +retrouve le commit par son message. Nomme la base. + +⚠️ **Motif de cette fiche** : une preuve ajoutée par la remédiation qui ne prouve rien. Pour chaque test prescrit ou +modifié par R3, **construis l'état où il devrait rougir et montre qu'il rougit — par exécution**, sous vitest ou +Node, sur une copie jetable. + +⛔ Rien ne se croit sur parole. ⚠️ Ne conteste PAS les arbitrages du Project Lead. + +## Axes (déclare lesquels tu as exercés) + +1. **Le test `i18nLocale()` sur module frais** : `vi.resetModules()` puis `await import('./i18n.svelte')` dans + `i18n.svelte.test.ts` — le `vi.mock` de l'`api-client` survit-il au reset ? Le module frais rend-il `'fr-CH'` ? + Rougit-il sous « valeur initiale vide » **quelle que soit sa place dans le fichier** ? +2. **Le tri sous `'sv-SE'`** (« Zahlung » avant « Öffnung ») : vrai sous l'ICU du **jsdom de vitest**, pas seulement + sous Node ? Et sur la machine de CI, dont l'ICU peut différer (Node du CI : lis `.github/workflows/ci.yml`) ? +3. **La date sous `'de-CH'`** (`16.09.2026`, sans `sept.`) : `Intl.DateTimeFormat('de-CH', { dateStyle: 'medium', + timeStyle: 'short' })` rend-il bien ce texte sous vitest/jsdom ? Le fuseau du test peut-il faire basculer la date + d'une entrée `2026-09-16T12:26:33.123Z` (fuseaux extrêmes) ? Les mutations `'fr-CH'` en dur et + `navigator.language` rougissent-elles ? +4. **Le `login` à trois arguments** (`company-contact-details.spec.ts:39-49`, appel `:249`) et `test-state.ts:174-191` + — relis. +5. **Propagation et Change Log R3** : la table des mutations, l'AC 12, l'AC 5 (date) disent-ils la même chose ; + décomptes (2 MEDIUM, 4 LOW, 1 hors périmètre). + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés**. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`/`npm run test:e2e`, ni +`sqlx migrate`, ni `cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff/log`, `npx vitest run` en +lecture, Python/Node en lecture, copie jetable dans +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r4-b1/`. `grep` est +`ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md new file mode 100644 index 000000000..927cee79e --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md @@ -0,0 +1,704 @@ +# Story 25.1c-b2 : Ce que l'écran du journal d'audit rend faux — manuels, README, vocabulaire + +Status: ready-for-dev + +⚠️ **RÉOUVERTE le 2026-09-15 au soir**, après sa validation en 7 passes : les arbitrages qui ont rouvert la +25-1c-a changent la section « Traçabilité » et le manuel d'administration (cf. « Réouverture » au Change +Log). **Revalidation ciblée en cours.** + +⚠️ **Issue du SPLIT de la 25-1c-b** (arbitrage du Project Lead du 2026-09-15, après la passe 2 de +validation) : + +| | 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 | l'écran : feature, page, garde, menu, gardes i18n, tests | ready-for-dev, réouverte | +| **25-1c-b2** *(celle-ci)* | **les textes : manuels, README, vocabulaire « journal d'audit »** | ready-for-dev, réouverte | + +⛔ **Livrée dans la MÊME PR que la 25-1c-b1, et implémentée APRÈS elle** sur la même branche : les +manuels décrivent l'écran **tel qu'il est construit** (menu, rôles, filtres), et ne se relisent contre +le code qu'une fois ce code écrit. C'est cette PR qui porte `closes #378`. + +⛔ **Dépendance à la 25-1c-a** : elle réécrit elle-même trois passages du manuel d'administration +(`admin-manual.tex:1585`, `:1590`, `:1803-1810`) et la phrase de `:1786`. **Les numéros de ligne de cette +fiche sont ceux de la branche `story/25-1c-b-journal-audit-ecran` au 2026-09-15** — empilée sur la +25-1c-zero, **avant** la 25-1c-a et la b1, et donc déjà différente de `main` (la 25-1c-zero a réécrit +`admin-manual.tex:1786`). Chaque site se retrouve **par sa phrase citée**, et se relit après le rebase. + +## Story + +**En tant que** comptable, administrateur ou lecteur de la documentation, +**je veux** que les manuels, le README et les libellés disent ce que le logiciel fait — un journal +d'audit **consultable** à l'écran, nommé d'un seul mot partout, +**afin de** ne pas lire qu'une fonction manque alors qu'elle existe, ni trois noms pour une même chose. + +**Couvre** : [#378], volet documentation. + +## ✅ Arbitrages du Project Lead — 2026-09-15 (`epic-25-vague1-suite.md`) + +- **Vocabulaire** : **« journal d'audit »** — `Audit-Protokoll`, `registro di audit`, `audit log`. +- **Qui consulte** : **Comptable** et **Admin** ; ni Consultation, ni clé API. +- **Découpage** (soir du 2026-09-15), cité : *« **25-1c-b2** (les textes : manuels, README, alignement du + vocabulaire « journal d'audit » dans les catalogues et le manuel) — livrées dans la même PR »*. + ⇒ **l'alignement du manuel est ARBITRÉ**, pas décidé par cette story (AC 3). +- **Au soir du 2026-09-15** (`epic-25-vague1-suite.md` § *fin de soirée*) : **filtre strict** par société + — il n'existe pas d'entrée sans société à expliquer ; l'**export n'est pas inscrit au journal** ; les + **types d'entité et les actions sont traduits**, à l'écran comme dans le CSV, dans la langue de + l'interface ; les lignes `:1796` et `:1778` du manuel d'administration se **corrigent** dans cette story + (réponse *« corrige »*, AC 6) ; `:1797`, trouvée ensuite, se corrige **par décision de la story**. + +## Acceptance Criteria + +### Volet A — le vocabulaire + +**1. Le glossaire** — `docs/i18n-glossaire.md`, **partie A**, une ligne : +« journal d'audit » | `Audit-Protokoll` | `registro di audit` | `audit log` | précédents **déjà livrés** : +`export-global-content-excludes` (`fr-CH:1241`, « journal d'audit interne » ; `en-CH:1184`, « internal +audit log ») et `fiscal-year-reopen-confirmation-body` (`de-CH:729`, « Audit-Protokoll ») ; **aucun en +italien** — la forme `registro di audit` vient de l'arbitrage du 2026-09-15. + +⚠️ **Ne pas citer `nav-audit-log` comme précédent** : la clé naît dans la même PR, et le glossaire pose +lui-même qu'une clé qu'on écrit n'est pas un précédent (ligne « modèle (d'e-mail) »). + +**Ce qui impose quoi — les trois sources ne se confondent pas** : + +| périmètre | source | +|---|---| +| les catalogues (AC 2) | **règle 3 du glossaire** (§ *Comment s'en servir*) : *« Un terme de la partie A ne se change pas dans une story de rollout. Il se change dans une story qui met AUSSI à jour toutes ses occurrences déjà livrées — sinon le produit dit deux mots pour une chose. »* **La ligne du glossaire ne s'écrit pas sans l'AC 2.** | +| les manuels (AC 3) | **l'arbitrage du soir** (« dans les catalogues et le manuel »). La règle 3, à la lettre, ne vise que les clés, et le glossaire distingue même le vocabulaire du manuel de celui de l'interface (`i18n-glossaire.md:180`) | +| la brochure et le README (AC 3) | **une extension décidée par cette story** — motif : le manuel et le README nomment l'écran « Journal d'audit », et ne peuvent pas désigner la même chose par un second mot | + +**2. Les catalogues** — deux clés s'écartent du terme, **sur cinq cellules** — trois sur huit sont déjà conformes : + +| 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 » ✓ | + +⇒ les cellules ✗ **alignées**, accords et articles ajustés à chaque langue. Lignes : `fr-CH:766` et +`:1241`, `de/it/en-CH:729` et `:1184` — **à retrouver par la clé**. + +⚠️ **Greper le mot `audit` seul, sans casse, jamais la locution** : +`grep -rniE "audit" crates/kesh-i18n/locales/*/messages.ftl | grep -vE ':[0-9]+:(nav-)?audit-log'` — ⚠️ **en excluant les clés +`^(nav-)?audit-log`**, dont le NOM contient le mot et qui, après la 25-1c-a, ajouteraient plus de 500 lignes +(vérifiées à part, ci-dessous). ⚠️ **Le filtre n'ôte que la LIGNE de clé** : les lignes de continuation +d'une valeur `audit-log-*`, ses attributs ou variantes, et les **en-têtes de blocs** neufs +(`# --- Journal d'audit — …`) sortent encore, et se trient comme « vérifiés à part ». 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 ; l'allemand écrit `Audit` avec majuscule. **Triés, à laisser** : + +- `bank-accounts-errors-has-transactions` (`fr-CH:986`, `de/it/en-CH:936`) — « audit comptable », + `Buchhaltungsprüfung`, « audit contabile », « accounting audit » : la **révision** des comptes, sans lien + avec `audit_log` (`errors.rs:1933`). Autre sens, dans les quatre langues ; +- les commentaires `# Story 14-2 — … audit …` (`fr-CH:763`, `de/it/en-CH:726`) : commentaires, pas + valeurs. + +⛔ **Les clés neuves ne sont PAS exemptées : elles sont VÉRIFIÉES.** Après la b1, chaque clé +`^(nav-)?audit-log` dont la valeur **nomme le journal** (`nav-audit-log`, et les clés d'écran de la b1 — +titre, sous-titre, état vide, bouton d'export — ; parmi les 133 clés de la 25-1c-a **à la spécification**, +tout libellé qui nommerait le journal) porte **exactement** la forme de sa langue — +« journal d'audit » / `Audit-Protokoll` / `registro di audit` / `audit log`. Motif : la 25-1c-a renvoie au +« vocabulaire de l'arbitrage » sans en écrire les valeurs, et le modèle allemand le plus proche à recopier, +`de-CH:1184`, dit « Audit-Log ». + +**Le procédé, exécutable** — en Python, **sur la VALEUR seule** : pour chaque `.ftl`, les lignes +`^((nav-)?audit-log[\w-]*)\s*=\s*(.*)`, et dans la valeur `audit|protokoll|registro|journal|piste|trail`, +sans casse ; puis tri à la main. ⚠️ **Une valeur Fluent peut continuer sur les lignes suivantes**, +indentées (patron `fr-CH:1330-1331`, `email-password-reset-body`) : la ligne de la clé rend alors une valeur +vide. Les lignes de continuation sont rattachées à leur clé avant la recherche. ⚠️ **Un grep sur « audit » ne suffit pas** : une variante qui ne contient +pas ce mot (« Protokoll » seul, « journal » seul) lui échapperait — un inventaire lexical se clôt sur ses +propres mots (passes 3 à 5). + +**3. Les manuels, la brochure et le README** (sources : tableau de l'AC 1). L'inventaire a été établi +par **deux procédés**, et c'est leur réunion qui fait foi : + +- **lexical** — `grep -rniE "piste d.audit|audit-trail|audit trail|audit log|piste de contr" docs/manual/fr/*.tex README.md` + rend **exactement** les douze premières lignes du tableau, rien à écarter ; +- **par le concept** (passe 4, PDF aplatis) — les phrases qui **désignent le journal** sans employer ces + mots : les cinq dernières lignes. + +| fichier | ligne | texte actuel | procédé | +|---|---|---|---| +| `user-manual.tex` | `:301` | « pour préserver l'audit-trail » | lexical | +| `user-manual.tex` | `:995` | « journalisée dans l'audit-trail » | lexical | +| `user-manual.tex` | `:1589` | `\section{Traçabilité (audit-trail)}` | lexical | +| `user-manual.tex` | `:1591` | « une \textbf{piste d'audit} (\texttt{audit\_log}) » | lexical | +| `user-manual.tex` | `:1603` | « Cette piste d'audit est \textbf{insert-only} » | lexical | +| `user-manual.tex` | `:1739` | `\item[Audit-trail (piste d'audit)]` — entrée du glossaire | lexical | +| `admin-manual.tex` | `:1141` | « l'audit-trail (\texttt{invoice.emailed}, …) » | lexical | +| `admin-manual.tex` | `:1780` | `\subsection{Audit-trail (audit\_log)}` | lexical | +| `admin-manual.tex` | `:2184` | `\item[Audit-trail]` — entrée du glossaire | lexical | +| `marketing-brochure.tex` | `:319` | « Audit-trail centralisé pour la responsabilité fiduciaire. » | lexical | +| `README.md` | `:29` | « audit log » (liste des fonctionnalités, en français) | lexical | +| `README.md` | `:219` | « la **piste de contrôle survit désormais à l'import d'une sauvegarde** » | lexical | +| `user-manual.tex` | `:858` | « nouvelle trace d'audit » → « nouvelle entrée au journal d'audit » | concept | +| `user-manual.tex` | `:1684` | « L'\texttt{audit\_log} garde toujours la trace » — le nom de table y sert de nom au concept | concept | +| `admin-manual.tex` | `:1088` | « La demande reste tracée dans l'audit » | concept | +| `admin-manual.tex` | `:1143` | « laisse sa propre trace d'audit » → « sa propre entrée au journal d'audit » | concept | +| `admin-manual.tex` | `:1935` | « La piste existe et est conservée, mais elle ne voyage pas avec l'export » — « piste » seul, abrégé de « piste d'audit » | concept | + +⇒ **« journal d'audit »** partout, soit **dix-sept sites**. **Triés autre sens, à laisser** (passes 4 et 5) : +`user-manual.tex:597` (« piste de correction »), `:1373`, `README.md:45`, `:213`, `:218` (« audit par +trois experts »), `marketing-brochure.tex:106`, `:209` (« Une piste pour les fiduciaires » — une piste commerciale), `:296`. +⚠️ En passe 4, le mot « piste » seul n'avait été cherché que dans les manuels : +`marketing-brochure.tex:209` a été trouvé en passe 5, par le rejeu du motif élargi de l'AC 8. + +Précisions : + +- **Le nom technique `audit\_log` ne change pas** là où il désigne la table (`:1591`, `:1780`, les + champs) ; il change là où il sert de nom au concept (`:1684`). +- **Les deux entrées de glossaire deviennent « Journal d'audit (angl. audit log) »** — l'anglais + **arbitré**, pas « audit-trail », que l'AC 2 retire du catalogue anglais — et **se déplacent à la + lettre J** (les glossaires des manuels sont alphabétiques), **à côté d'une entrée voisine qui ne désigne + pas la même chose** : dans `user-manual.tex`, **après** `\item[Journal]` (`:1754`, le journal des + écritures) ; dans `admin-manual.tex`, **avant** `\item[Journal entry]` (`:2189`). La définition doit + rendre la différence lisible. +- **Les renvois se suivent** : `user-manual.tex:1739` renvoie à la section « Traçabilité », dont le + titre change (`:1589`) ; toute mention textuelle de ces titres se met à jour. + +⛔ **L'inventaire se REJOUE après le rebase, sur le PDF aplati, avec un motif ÉLARGI** (AC 8) : un site +ajouté par la 25-1c-a ou la b1 n'y figure pas. + +### Volet B — ce que l'écran rend faux + +**4. Le manuel utilisateur, l'encadré « Ce que le logiciel ne fait pas encore à votre place »** +(`user-manual.tex:497-511`, du `\begin{keshwarning}` au `\end`) porte **cinq** énoncés : **quatre sont faux ou périmés, le premier est vrai mais incomplet** — il se +**nuance**, il ne se supprime pas : + +| lignes | énoncé | état | +|---|---|---| +| `:498-499` | « la modification et la suppression d'une écriture sont refusées » | **vrai pour la route des écritures, à nuancer** : une écriture disparaît encore avec la facture validée qu'on supprime (ci-dessous) | +| `:499-502` | « Ce qui manque encore, c'est la consultation du journal d'audit depuis l'application » | **faux** : l'écran existe (25-1c-b1) | +| `:502-503` | « Le verrou reste par ailleurs annuel : aucun verrou de période plus fin […] n'existe encore » | **déjà faux** : la Story 24-4c a livré le verrou de période (PR #425) | +| `:505-506` | « Une seule exception, déjà en vigueur : une écriture déjà contre-passée ne peut plus être supprimée » | **périmé** : la route des écritures refuse désormais **toute** suppression. La seule disparition d'une écriture passe par la suppression d'une facture validée, et ce n'est pas l'exception que l'énoncé décrit | +| `:508-510` | « préférez la contre-passation […], tant que le logiciel vous laisse le choix » | **périmé** : la contre-passation est **imposée**, le logiciel ne laisse plus le choix | + +⚠️ **Pas de formule absolue dans le texte de remplacement** : la suppression d'une **facture** validée +emporte son écriture — `crates/kesh-db/src/repositories/invoices.rs:1350` — ⚠️ le **repository**, et non +`crates/kesh-api/src/routes/invoices.rs`, homonyme — passe `enforce_immutability = false`, « seul site du dépôt » (commentaire +`crates/kesh-db/src/repositories/invoices.rs:1339-1340`, repris par `crates/kesh-db/src/repositories/journal_entries.rs:973-975`), résidu assumé que le commentaire du code rattache à #380 — **fermée le 2026-09-09** — et à #381, +ouverte. Le manuel utilisateur le décrit +déjà dans sa section sur la suppression d'une facture ; le nouveau texte ne doit pas écrire qu'une +écriture ne peut disparaître « en aucun cas ». + +⇒ L'encadré se **réécrit**, ou se **remplace par une note**. Dans les deux cas, le texte **conserve le +premier énoncé, nuancé** : la route des écritures refuse modification et suppression, mais la suppression +d'une facture validée emporte son écriture. Il dit aussi la contre-passation imposée. Ce n'est plus un +avertissement « ne fait pas encore » : l'encadré n'annonce plus de fonction manquante. + +**5. Le manuel utilisateur, section « Traçabilité »** (`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, **tel que la 25-1c-b1 le construit**. Critère : **chaque cas où +l'écran rend une liste vide ou un refus sans explication se dit dans le manuel**. + +- menu **Administration → Journal d'audit**, réservé au **Comptable** et à l'**Admin** ; +- le détail déplié, l'export CSV ; +- ⛔ **les dates** : le tableau affiche l'heure **locale** ; le filtre de période porte sur des **jours + UTC** — une opération faite peu après minuit, heure suisse, appartient au jour UTC **précédent** ; le + CSV écrit les heures en **UTC** ; +- **la langue** : les actions et les types d'entité s'affichent **traduits**, dans la langue de + l'interface de l'installation — et le CSV les écrit dans la même langue (25-1c-a AC 12) ; +- **les filtres « type d'entité » et « action » sont des listes** de libellés (25-1c-b1 AC 5) : on + choisit une action, on ne la tape pas ; +- **l'identifiant d'entité exige un type** : le champ reste inactif tant qu'aucun type n'est choisi ; +- **l'export est plafonné à 10 000 lignes** (25-1c-a AC 11) : au-delà, un message demande d'affiner + les filtres ; +- **une action historique**, écrite par une version antérieure et absente de la liste, s'affiche sous la + forme de son **code** (25-1c-a AC 16), et **n'est pas proposée dans la liste « Action »** (25-1c-b1 + AC 3 : seul un code déjà présent dans l'adresse de la page y est ajouté) — on la retrouve par le type et + l'identifiant d'entité. + +La phrase voisine sur le **centre de notifications** (`:1615-1616`) reste vraie et se conserve. + +**6. Le manuel d'administration, `admin-manual.tex:1786`** — l'item « Champs » dit aujourd'hui, sur la +branche, que le journal *« ne se \textbf{consulte} pas encore~: aucune route ni aucun écran ne permet de le +lire depuis l'application --- c'est le sujet de l'issue \#378 »* (graphie LaTeX exacte : un grep de « ne se +consulte » sur le `.tex` ne trouve rien), et la 25-1c-a le réécrit en « une route existe, l'écran reste à +venir ». → **l'écran existe** : accès Comptable et Admin, **refusé aux clés API** (la route rejette un +jeton d'accès personnel), et **ni la consultation ni l'export ne s'inscrivent au journal** (25-1c-a AC 9 +et 14) — un réviseur qui chercherait « qui a exporté le journal » doit savoir que le journal ne le dit pas. + +✅ **Trois défauts voisins du manuel d'administration, CORRIGÉS DANS CETTE STORY — à deux titres** : + +- `:1796` et `:1778`, trouvées en passe 4 : **par l'arbitrage du 2026-09-15 au soir** — la question portait + sur ces deux lignes, la réponse fut *« corrige »* ; +- `:1797`, trouvée en revalidation R1 : **par décision de la story**, que l'arbitrage ne nomme pas. Motif : la + nuance apportée à `:1796` rend `:1797` trompeuse, et l'une ne se corrige pas sans l'autre. + +Les consignes : + +- `:1796` — la formule absolue se **nuance** comme l'AC 4 le prescrit au manuel utilisateur : la route + des écritures refuse modification et suppression quel que soit le rôle ; la suppression d'une facture + validée emporte son écriture ; +- `:1778` — « Les 5 rôles standards » devient **trois**, en accord avec `:1345` ; +- `:1797`, **voisine de `:1796`** — « Il conserve aussi les modifications et suppressions **antérieures au + gel** » laisse entendre, une fois `:1796` nuancée, qu'aucune suppression postérieure n'est journalisée ; + or la suppression d'une facture validée inscrit `journal_entry.deleted` (`crates/kesh-db/src/repositories/invoices.rs:1350`, + `crates/kesh-db/src/repositories/journal_entries.rs:1061`) → à reformuler dans le même geste. + +| ligne | texte | ce que dit le code | +|---|---|---| +| `admin-manual.tex:1796` | « l'application refuse les deux, **sans exception et quel que soit le rôle** » (conformité OLICo) | faux : la suppression d'une facture validée emporte son écriture (`crates/kesh-db/src/repositories/invoices.rs:1350`) — la formule absolue que l'AC 4 interdit | +| `admin-manual.tex:1778` | « Les **5** rôles standards » | faux : trois rôles (`entities/user.rs:20-27`), ce que dit déjà `admin-manual.tex:1345` — et c'est la ligne qui précède la sous-section que l'AC 3 renomme | +| `admin-manual.tex:1797` | « Il conserve aussi les modifications et suppressions **antérieures au gel** » | trompeur une fois `:1796` nuancée : la suppression d'une facture validée journalise encore `journal_entry.deleted` après le gel (`crates/kesh-db/src/repositories/invoices.rs:1350`, `crates/kesh-db/src/repositories/journal_entries.rs:1061`) | + +**7. Le README**, feuille de route : + +- `README.md:218` — « le journal d'audit n'est consultable par aucun écran » ; +- `README.md:219` — « la **consultation depuis l'application** ([#378]), qui n'existe toujours pas ». + +→ mis à jour **dans le même commit** que l'écran (§ *Synchroniser le planning du README*), ainsi que les +deux sites de vocabulaire de l'AC 3. + +### Volet C — le contrôle + +**8. Régénération et vérification** : + +- `make fr` dans `docs/manual/`, PDF commités ; +- **PDF aplati vérifié** (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) — les anciennes phrases + absentes, les nouvelles présentes ; +- **inventaire rejoué sur les PDF aplatis et le README**, motif élargi puis tri à la main — **en + Python** : `grep` est ici `ugrep`, qui refuse ce motif avec son contexte `{0,60}`. Motif, sans casse : + `piste|trace d.audit|dans l.audit\b|audit-?trail|audit log`. + - **Recoller d'abord les césures** du texte aplati (`re.sub(r"(\w)- (\w)", r"\1\2", t)`) : le PDF du + manuel d'administration coupe des mots en fin de ligne, et un « jour- nal » ferait passer à tort le + contrôle « nouvelles phrases présentes ». + - Une occurrence peut apparaître **plusieurs fois** dans un PDF (sommaire, en-tête de page) : on compare + des **sites**, pas des nombres. + - **Reste attendu, et seulement lui, écrit en entier** : + - les deux « (angl. audit log) » des glossaires ; + - `user-manual.tex:597` (« piste de correction ») ; + - `marketing-brochure.tex:209` (« Une piste pour les fiduciaires »). + + Les six autres sites triés « autre sens » ne contiennent que « audit » ou « auditable » : le motif ne + les rend pas ; +- ⛔ en LaTeX le souligné s'écrit `\_` : un grep de `audit_log` sur le `.tex` ne voit rien. + +`website/index.html:106` et `roadmap.html:105` (« an audit log that a backup import no longer +replaces ») **restent vrais**, emploient déjà le terme `en-CH`, et ne se touchent pas. +`marketing-brochure.tex:139` dit déjà « journal d'audit ». + +**9. Les catalogues passent leurs gardes** : `cargo test -p kesh-i18n` (parité des quatre locales, +`loader.rs:693`) ; `npm run test:unit` — les deux clés de l'AC 2 changent de **valeur**, pas de nom, et +aucun compteur ne doit bouger ; s'il bouge, l'écrire. + +## Tasks / Subtasks + +- [ ] **T0 — Après la 25-1c-b1**, sur la même branche : relire chaque site **par sa phrase** ; rejouer + l'inventaire de l'AC 3 (motif élargi de l'AC 8) et le grep de l'AC 2. +- [ ] **T1 — Glossaire et catalogues** (AC 1, 2), dont la vérification des clés neuves. +- [ ] **T2 — Vocabulaire des manuels, de la brochure et du README** (AC 3), dix-sept sites. +- [ ] **T3 — Encadré et section « Traçabilité » du manuel utilisateur** (AC 4, 5) — contrôler chaque + affirmation neuve **contre le code de la b1**, pas contre cette fiche. +- [ ] **T4 — Manuel d'administration et README** (AC 6, 7), dont les trois défauts voisins + (`:1796`, `:1797`, `:1778`). +- [ ] **T5 — Régénération et contrôle aplati** (AC 8) ; gardes (AC 9). + +## Dev Notes + +### Ce que la story touche + +| zone | fichiers | +|---|---| +| vocabulaire | `docs/i18n-glossaire.md` ; `crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl` (deux clés, cinq cellules) | +| manuels | `docs/manual/fr/user-manual.tex`, `admin-manual.tex`, `marketing-brochure.tex` + les trois PDF | +| README | `README.md` | + +Aucun code applicatif. + +### Faits établis à la spécification, et non supposés + +- **L'inventaire de l'AC 3** : un grep exécuté sur la branche le 2026-09-15 (douze sites — la première + rédaction en annonçait onze, avec un motif qui ne pouvait pas trouver `README.md:29`), puis une + recherche par le concept sur les PDF aplatis en passe 4 (cinq de plus). La parente en citait trois. +- **Les sources de l'alignement** : la règle 3 pour les catalogues, l'arbitrage du soir pour le manuel, + une décision de la story pour la brochure et le README (AC 1). +- **L'encadré de l'AC 4** a été relu en entier (`user-manual.tex:497-511`) : la parente n'en corrigeait + que deux énoncés. +- **La suppression d'une facture validée emporte son écriture** : appel `crates/kesh-db/src/repositories/invoices.rs:1350`, commentaire `:1339-1340`, + `crates/kesh-db/src/repositories/journal_entries.rs:973-975`. + +### Ce qui ne bouge PAS + +- Le nom de la table `audit_log` là où il désigne la table, et les codes d'action. +- `website/` (terme `en-CH` déjà conforme). +- Les manuels `de`, `it`, `en` : vides en v0.1 (`CLAUDE.md` § *Synchroniser TOUTES les docs*). + +### Intelligence des stories précédentes + +- **25-1c-zero** : le manuel n'était vu d'aucun grep sur `company_id`, parce que LaTeX écrit + `company\_id` — il a fallu la revue pour le trouver. +- **Epic 24** (`CLAUDE.md` § *Le prompt d'une passe doit NOMMER le manuel*) : le manuel a été pris en + défaut six fois sous six formes ; c'est en le confrontant au code qu'on trouve ce que le code ne fait + pas. +- **Cette story, passes 3 et 4** : un inventaire lexical se clôt sur ses propres mots. *Deux ensembles de + même cardinal sont indétectables au nombre*, et un concept se désigne aussi sans son terme. + +### References + +- `25-1c-b1-journal-audit-ecran.md` — **l'écran que ces textes décrivent** +- `25-1c-a-journal-audit-route.md` — AC 2 (filtres), AC 6 (refus, dont les clés API), AC 8 et 17 (libellés traduits, vocabulaire), AC 9 et 14 (ni la consultation ni l'export ne sont tracés), AC 11 (plafond), AC 12 (langue du CSV), AC 15-16 (clés et repli sur le code), sites du manuel d'administration +- `25-1c-b-journal-audit-ecran.md` — la fiche parente, passes 1 et 2 +- `epic-25-vague1-suite.md` § *Arbitrage du 2026-09-15 (soir)* +- `docs/i18n-glossaire.md` § A, § *Comment s'en servir*, `:180` + +[#378]: https://github.com/guycorbaz/kesh/issues/378 + +## Dev Agent Record + +### Agent Model Used + +### Debug Log References + +### Completion Notes List + +### File List + +## Change Log + +- **2026-09-15** — **Fiche créée par le split de la 25-1c-b** (arbitrage du Project Lead). Les volets + « vocabulaire » et « ce que l'écran rend faux » viennent de la parente (AC 13-18) ; deux findings de sa + passe 2 sont appliqués **ici** : + - **M5** — l'encadré de `user-manual.tex:497-510` porte deux énoncés périmés de plus que ceux que la + parente corrigeait (« Une seule exception… », « …tant que le logiciel vous laisse le choix ») → les + quatre tabulés (AC 4) ; + - **M6** — la règle 3 s'applique aussi au manuel (`user-manual.tex:1591`, `:1603`, `:1739`) → **le + grep élargi en rend douze** (onze à la première rédaction, cf. passe 3), dont `audit-trail` dans les + deux manuels et la brochure, et « piste de contrôle » dans le README (AC 3). + + Précision ajoutée au passage : la section « Traçabilité » décrit la sémantique des dates (jours UTC au + filtre, heure locale à l'écran, UTC au CSV), conséquence du finding M2 appliqué dans la b1 (AC 5). + +### Passe 3 de `bmad-create-story validate` — une lentille Sonnet, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-p3.md`. Base : la parente à `0b029e24` (AC 13-18). + +**Rendu : 0 CRITICAL, 1 HIGH, 4 MEDIUM, 2 LOW — tous vérifiés au sol et retenus.** + +**Après vérification : 1 HIGH, 2 MEDIUM, 4 LOW.** Deux findings sont reclassés de MEDIUM à LOW (M4 et M5 +ci-dessous). + +- **H1 (HIGH)** — **l'inventaire de l'AC 3, écrit par la création de cette fiche, était faux**, et son + total juste par coïncidence. Le grep qu'il citait rendait bien 11 lignes, mais pas les mêmes que le + tableau : + - il trouvait `user-manual.tex:301` (« pour préserver l'audit-trail »), absent du tableau ; + - il ne pouvait pas trouver `README.md:29` (« audit log »), présent au tableau. + + Vérifié : `grep -c "audit-trail" user-manual.tex` rend 3 → ligne ajoutée, motif élargi à + `audit log`, **onze → douze** dans le corps et au Change Log. + + ⛔ *Deux ensembles différents de même cardinal sont indétectables à la relecture du nombre* — l'acquis 2 + de la 25-1b, reproduit par l'orchestrateur. +- **M2** — une troisième clé du catalogue contient « audit » : `bank-accounts-errors-has-transactions` + (« audit comptable », fr/it/en). Elle n'était ni alignée ni triée → **triée « autre sens »** (la + révision des comptes), écrite à l'AC 2. +- **M3** — « la règle 3 vaut aussi pour les manuels » excédait le glossaire, qui ne vise que les clés, et + distingue lui-même le manuel de l'interface (`i18n-glossaire.md:180`) → l'extension est désormais + écrite comme une **décision de cette story**, avec son motif : le manuel nomme l'écran. + *(Corrigé à son tour en passe 4 : pour le manuel, c'est un arbitrage.)* +- **M4 → LOW** — insertion dans les glossaires des manuels, où des entrées voisines existent : + `\item[Journal]` (`user-manual.tex:1754`) et `\item[Journal entry]` (`admin-manual.tex:2189`). Le point + d'insertion est précisé. +- **M5 → LOW** — les References citaient « AC 15 (clés) » de la 25-1c-a. C'était juste pour les clés + i18n, mais le refus des clés API, que l'AC 6 de cette fiche affirme, n'avait aucune référence → + ajouté « AC 6 (refus, dont les clés API) ». +- **L6** — la suppression d'une facture validée emporte encore son écriture (`journal_entries.rs:973-975`, + #380/#381) → le texte de remplacement de l'encadré ne doit pas être absolu (AC 4). +- **L7** — citation tronquée d'`admin-manual.tex:1786` → citée en entier. + +⚠️ **Signal de sévérité** : la parente avait terminé sa passe 2 à MEDIUM, et la première passe de cette +fiche remonte un HIGH. Ce n'est **pas** un défaut de conception d'origine qui résiste : **l'inventaire a +été écrit par la remédiation elle-même**. C'est le motif mesuré du projet, la sévérité se déplace vers ce +qu'on vient d'écrire. La story ne touche que du texte et ne se découpe pas davantage ; la boucle continue. + +Remédiation : texte de spec uniquement. + +### Passe 4 de `bmad-create-story validate` — une lentille Opus, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-p4.md`. Base : `git diff f764004d 637c6508`. + +**Rendu : 0 CRITICAL, 0 HIGH, 4 MEDIUM, 7 LOW, et 1 point hors périmètre — tous vérifiés au sol et +retenus.** Sévérité maximale **HIGH → MEDIUM** : la boucle converge. + +La passe confirme d'abord la correction du HIGH de la passe 3 : le grep de la fiche rend **exactement** +les douze lignes du tableau, ensemble contre ensemble. + +- **M1** — l'inventaire lexical ne voit pas les phrases qui désignent le journal **sans ses mots** : + `user-manual.tex:858`, `:1684`, `admin-manual.tex:1088`, `:1143`, `:1935` (« La piste existe… »). + Vérifié ligne à ligne → **dix-sept sites**, deux procédés, motif élargi pour le rejeu (AC 3, 8). +- **M2** — l'AC 4 se contredisait depuis la passe 3 : « **toute** suppression d'écriture est refusée » + à côté de l'avertissement sur la suppression de facture ; « deux lignes plus haut » était faux ; et + `admin-manual.tex:1796` écrit **déjà** la formule absolue interdite. Vérifié (`invoices.rs:1339`) → + justification réécrite, cinquième énoncé tabulé ; `:1796` **porté à l'arbitrage** (AC 6). +- **M3** — la passe 3 avait attribué à la story l'alignement du manuel, alors que **l'arbitrage du soir + le nomme** (« dans les catalogues et le manuel ») ; et l'AC 1 disait encore « les AC 2 et 3 sont ces + occurrences ». → tableau des trois sources à l'AC 1, arbitrage cité. + + ⛔ *Un arbitrage résumé est un arbitrage réécrit* : la leçon de la 25-1c-a, reproduite par la + remédiation d'un finding qui portait justement sur l'attribution d'une règle. +- **M4** — l'AC 2 exemptait de tout contrôle les clés `audit-log-*` neuves, les plus exposées à une + forme divergente → exemption remplacée par une vérification. +- **L1** — citation de `:1786` introuvable au grep (`\textbf{consulte}`), et lignes attribuées à `main` + alors qu'elles sont celles de la branche empilée → graphie exacte et en-tête corrigés. +- **L2** — l'encadré a **cinq** énoncés, bornes `497-511` → corrigé (AC 4, Dev Notes). +- **L3** — « audit-trail » gardé en synonyme contredisait l'AC 2 → « (angl. audit log) », reste attendu + écrit à l'AC 8. +- **L4** — `nav-audit-log` cité comme précédent d'une ligne de glossaire, alors que la clé naît dans la + PR → précédents déjà livrés cités. +- **L5** — trois cellules sur huit déjà conformes ; `de-CH:936` et les commentaires `# Story 14-2` + non triés → précisé. +- **L6** — le grep était décrit comme « écartant » des occurrences qu'il ne peut pas rendre → corrigé. +- **L7** — le manuel ne disait rien des cas où l'écran rend une liste vide ou un refus sans explication + (action en égalité exacte, identifiant sans type, plafond de 10 000, « société indéterminée ») → + ajoutés à l'AC 5, avec le critère. +- **Hors périmètre** — `admin-manual.tex:1778` annonce « 5 rôles » (il y en a trois, `user.rs:20-27` et + `:1345`) → **porté à l'arbitrage** avec `:1796` (AC 6). + +Remédiation : texte de spec uniquement. + +### Passe 5 de `bmad-create-story validate` — une lentille Sonnet, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-p5.md`. Base : `git diff 637c6508 c5d14f32`. + +**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 1 LOW — vérifiés au sol et retenus. Après reclassement : 1 MEDIUM, +2 LOW.** Les trois portent sur la remédiation de la passe 4. + +- **M1** — le motif élargi de l'AC 8, rejoué sur les sources actuelles, rend **18 lignes**, et l'une d'elles + n'est ni au tableau ni au tri : `marketing-brochure.tex:209`, « Une piste pour les fiduciaires », une + piste commerciale. Au rejeu après implémentation, elle aurait fait échouer à tort le critère « reste + attendu, et seulement lui ». La recherche par le concept n'avait couvert que les deux manuels. → + triée « autre sens ». + + La lentille note aussi que `user-manual.tex:1684` ne répond pas au motif. C'est sans conséquence : ce + site se contrôle par sa phrase (T0). +- **M2 → LOW** — l'introduction de l'AC 4 (« aucun ne reste vrai ») contredisait le cinquième énoncé, + ajouté par la même passe 4 et qualifié de « vrai, à nuancer ». *Reclassé* : la ligne du tableau prescrivait + déjà la nuance, et seule la phrase d'introduction se contredisait. → « quatre faux ou périmés, le premier + vrai mais incomplet ». +- **L3** — « six cellules » : le tableau de l'AC 2 compte 3 + 2 = **cinq** cellules à modifier → + corrigé. + +La passe confirme par ailleurs, ensemble contre ensemble : +- les douze sites lexicaux et les cinq sites « concept » ; +- les sept tris « autre sens » de la passe 4 (huit avec `marketing-brochure.tex:209`) ; +- les bornes de l'encadré ; +- les cellules de l'AC 2 et les deux défauts voisins ; +- la citation de l'arbitrage, les précédents de glossaire, et les énoncés de l'AC 5 confrontés au contrat + de la 25-1c-a et de la b1. + +Remédiation : texte de spec uniquement. + +### Passe 6 CIBLÉE de `bmad-create-story validate` — une lentille Opus, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-p6.md`. Base : `git diff c5d14f32` (remédiation de la passe 5). + +**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 3 LOW, plus 2 remarques hors périmètre — vérifiés au sol et +retenus.** + +⛔ **Les deux MEDIUM sont des défauts de PROPAGATION de l'orchestrateur** : la passe 5 a corrigé une phrase +sans corriger celle qui disait la même chose ailleurs. C'est la faute que la § *Propagation post-patch* +du `CLAUDE.md` décrit, commise pendant la boucle qui la cite. + +- **M1** — l'introduction de l'AC 4, réécrite en passe 5 (« le premier est vrai mais incomplet — il se + nuance »), contredisait la conclusion restée intacte (« l'encadré se réécrit **ou disparaît** […] s'il + reste quelque chose à dire »). Un encadré qui disparaît emporte la phrase vraie qu'on vient d'ordonner + de garder. → conclusion réécrite : le premier énoncé nuancé est **conservé**. +- **M2** — « six cellules » corrigé aux Dev Notes en passe 5, mais l'AC 2 disait « **trois** cellules + seulement » (`git log -S` : phrase de la passe 4, qui déformait « trois sur huit conformes ») → + « cinq cellules — trois sur huit sont déjà conformes ». +- **L3** — le « reste attendu » de l'AC 8 renvoyait aux huit tris, alors que le motif n'en rend que deux ; + une comparaison exacte aurait attendu six sites introuvables → reste écrit **en entier**. +- **L4** — la note de la passe 5 écrivait `:209` sans fichier, et attribuait à la passe 4 une recherche + « limitée aux manuels » que son prompt contredit → « le mot « piste » seul n'avait été cherché que dans + les manuels ». +- **L5** — « les sept tris » sans périmètre → « sept de la passe 4, huit avec `:209` ». +- **Hors périmètre, intégrés** : + - le PDF d'administration coupe des mots en fin de ligne, d'où un faux négatif possible sur « nouvelles + phrases présentes » → recollage des césures prescrit à l'AC 8 ; + - **#380 est fermée** (le 2026-09-09) → l'AC 4 ne la dit plus « en suivi ». + +Au passage, l'orchestrateur a constaté que le motif de l'AC 8 était prescrit en `grep -oiE` avec contexte +`{0,60}`, forme qu'`ugrep` refuse ; il est désormais prescrit en Python. + +La passe confirme par ailleurs le rejeu de l'AC 8 **sur les PDF aplatis** : aucun site n'échappe au +tableau, et les occurrences en plus viennent du sommaire et des en-têtes. + +Remédiation : texte de spec uniquement. + +### Passe 7 CIBLÉE de `bmad-create-story validate` — une lentille Sonnet, contexte frais — **BOUCLE CLOSE** + +Prompt versionné : `25-1c-b2-validate-prompt-p7.md`. Base : `git diff 06fda3b0` (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 : +- **propagation de chaque phrase modifiée** : AC 4 (introduction, conclusion, tableau, T3), AC 2 et Dev + Notes (3 ✗ + 2 ✗ = 5, `git log -S` sur l'ancienne phrase), AC 8 et tris de l'AC 3 (8 − 2 = 6), note sur + la brochure ; +- **le rejeu de l'AC 8 exécuté en Python** sur les trois PDF aplatis et le README, avec et sans recollage + des césures : décomptes identiques, et seuls les deux sites nommés du reste attendu y apparaissent ; +- **l'état des issues**, confirmé par `gh issue view` et par le commentaire de + `journal_entries.rs:965,978` ; +- **le Change Log de la passe 6**, recompté. + +**Vérification de l'orchestrateur** : l'état de #380 (fermée le 2026-09-09) et de #381 (ouverte) avait +été relevé par lui-même avant la remédiation (`gh issue view`). + +**Noté, non appliqué** (LOW latent, sans effet aujourd'hui) : le recollage `(\w)- (\w)` joint aussi les +**jonctions de colonnes** des tableaux du PDF d'administration (« para- Admin »), et pas seulement les +césures. Aucune ne croise le motif. À garder en tête si le rejeu de l'AC 8 rend un jour une occurrence +inattendue. + +**Critère de clôture** : 0 finding au-dessus de LOW, et la dernière remédiation (passe 6) ne touche +aucune ligne de code, pas plus que la story, qui n'est que texte. + +**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 H, 2 M, 4 L | +| 4 | Opus | 4 M, 7 L | +| 5 | Sonnet | 1 M, 2 L | +| 6 | Opus, ciblée | 2 M, 3 L | +| 7 | Sonnet, ciblée | **0** | + +**Ce que la boucle a appris**, et qui vaut au-delà de la story : +- **un inventaire lexical se clôt sur ses propres mots** (passes 3 à 5 : 11 → 12 → 17 sites, puis un + dix-huitième au tri) ; +- **deux ensembles de même cardinal sont indétectables au nombre** (passe 3) ; +- **la propagation se perd pendant la boucle même qui la cite** (passe 6 : deux phrases jumelles + laissées intactes par l'orchestrateur). + +### Réouverture du 2026-09-15 (soir) — la conception de la 25-1c-a a changé + +Trois arbitrages du Project Lead et la réponse *« corrige »*, rendus après la clôture de la boucle, changent ce que la section +« Traçabilité » et le manuel d'administration doivent dire : + +| arbitrage | ce qui change ici | +|---|---| +| filtre strict — *« le cas réel n'existe pas »* | la mention « société indéterminée » sort de l'AC 5 | +| l'export n'est pas inscrit au journal — *« non »* | AC 6 : le manuel dit que ni la consultation ni l'export ne sont tracés, au lieu de « l'export est tracé » | +| actions et types traduits, écran et CSV, langue de l'interface | AC 5 : filtres en listes de libellés, langue, repli sur le code d'une action historique ; AC 2 : la clé `audit-log-entity-audit-log` n'existe plus | +| *« corrige »* | AC 6 : `admin-manual.tex:1796` et `:1778` corrigés dans cette story (T4) | + +Les références à la 25-1c-a sont alignées sur sa numérotation réécrite (AC 8, 9, 11, 12, 14-17). +Le tableau d'en-tête et les arbitrages (ajout « au soir ») sont mis à jour dans le même geste. +**Revalidation requise**, ciblée sur ces changements. + +### Revalidation R1 CIBLÉE — une lentille Opus, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-r1.md`. Base : `git diff 5760bab7 90a427bf`. + +**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 6 LOW (dont 2 hors diff) — tous vérifiés au sol et retenus.** + +- **M1** — les réponses *« ok »* (écran traduit), *« non, maintenant »* et *« corrige »* étaient attribuées + à l'epic, qui ne les portait pas : leur **seule trace écrite était la story qu'elles élargissent**. + Vérifié (`grep` sur l'epic). → les trois questions et leurs réponses inscrites à l'epic, § *fin de + soirée* ; paragraphe périmé « société indéterminée » barré ; « 81 codes » → 82. +- **M2** — la vérification des catalogues n'était plus exécutable : le grep `audit` porte aussi sur les + **noms** de clés, et les 123 clés `audit-log-*` × 4 l'auraient noyé ; et un grep sur « audit » ne voit + pas « Protokoll » ou « journal » seul. → grep hérité restreint aux autres clés, procédé Python **sur la + valeur**, liste des clés d'écran de la b1 complétée, « à la spécification ». +- **L1** — une phrase attribuait à la 25-1c-a une prose sur « la piste » que sa réécriture a supprimée + (`grep -c "la piste"` = 0) → retirée. +- **L2** — `admin-manual.tex:1797` (« suppressions antérieures au gel ») devient trompeuse une fois `:1796` + nuancée → ajoutée à l'AC 6 et à T4. +- **L3** — une action historique s'affiche en code mais **n'est pas choisissable** dans la liste + « Action » → le manuel le dira (AC 5). +- **L4** — la fiche ne se disait pas réouverte ; « trois arbitrages » pour quatre lignes → bandeau et + tableau, Change Log précisé. +- **L5** — citation « seul site du dépôt » attribuée à `journal_entries.rs` → `invoices.rs:1339-1340`, + appel à `:1350`. +- **L6** (epic) — paragraphe contradictoire et titre « deux des trois » → corrigés avec M1. + +**Remarque laissée telle quelle** : `admin-manual.tex:1786` décrit la colonne `company_id` (« vide quand +elle est indéterminable ») ; c'est une description de la **table**, qui reste vraie, et non de l'écran. + +Remédiation : texte de spec et de planning uniquement. + +### Revalidation R2 CIBLÉE — une lentille Sonnet, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-r2.md`. Base : `git diff 2efbc76c 6cda8b89` et la section +*fin de soirée* de l'epic (commit `32ebe675`). + +**Rendu : 0 CRITICAL, 0 HIGH, 2 MEDIUM, 3 LOW — vérifiés au sol et retenus.** Sévérité maximale inchangée +(MEDIUM), et **les deux MEDIUM sont des propagations oubliées de la remédiation R1** — le même mode d'échec +que la passe 6. + +- **M1** — l'epic comptait encore **82** codes d'action, alors que la R1 de la 25-1c-a en a établi **92** ; + le commit `6cda8b89` se disait « comptes 133/92 propagés » sans toucher l'epic. → 92, avec la généalogie + des deux comptes faux. +- **M2** — la R1 a ajouté `:1797` aux défauts voisins sans propager « **deux** défauts » (trois sites) ni la + table de l'AC 6. → « trois », ligne ajoutée à la table. +- **L3** — le procédé Python ne lisait pas une valeur Fluent **multiligne** (exécuté sur un patron du dépôt) + → rattachement des lignes de continuation. +- **L4** — l'exclusion des clés `audit-log-*` du grep hérité n'était écrite comme aucune commande → + pipeline écrit. +- **L5** — `invoices.rs` et `journal_entries.rs` existent en **deux** exemplaires (routes et repositories) → + chemin complet du repository. + +**Noté sans suite** : les bases de la R1 (`5760bab7`, `90a427bf`) sont devenues des objets pendants au +rebase ; **le fichier** de la fiche y est identique à celui de `37596132` et `2efbc76c` *(« leurs arbres », +d'abord écrit, était faux — corrigé en R3)*. + +Remédiation : texte de spec et de planning uniquement. **Revalidation R3 ciblée requise** (MEDIUM). + +### Revalidation R3 CIBLÉE — une lentille Opus, contexte frais + +Prompt versionné : `25-1c-b2-validate-prompt-r3.md`. Base : `git diff 5dfbafc2 0bbefc51` (fiche et epic). +Sondes : pipeline exécuté sur les `.ftl` réels et sur une copie jetable enrichie de clés `audit-log-*`, +extraction positionnelle indépendante (92 actions confirmées), objets git et reflog. + +**Rendu : 0 CRITICAL, 0 HIGH, 1 MEDIUM, 3 LOW — vérifiés et retenus.** Les quatre viennent de la remédiation R2. + +- **M1** — la R2 rangeait `:1797` sous la réponse *« corrige »*, alors que la question ne portait que sur + `:1796` et `:1778` (epic, § *fin de soirée*) : *un arbitrage résumé est un arbitrage réécrit*, troisième + occurrence dans cette fiche. → `:1797` présentée comme **décision de la story**, avec son motif. +- **L1** — le filtre `grep -vE ':[0-9]+:(nav-)?audit-log'` n'ôte que la ligne de clé : continuations, + attributs, variantes et en-têtes de blocs neufs sortent encore (exécuté sur copie jetable) → écrit. +- **L2** — des renvois restaient sans chemin complet, dont un préfixe `crates/` manquant → **trois** sites + qualifiés *(« deux » d'abord écrit ici : deux symptômes, trois sites — recompté en R4)*. +- **L3** — la note « arbres identiques » était fausse (seul le fichier l'est), et **les bases citées par la R2 + elle-même** (`2efbc76c`, `6cda8b89`, `32ebe675`) ont disparu des branches au rebase suivant : équivalents + actuels `dd3cedab`, `5dfbafc2`, `7f8b0288`. Les prompts versionnés restent tels qu'envoyés. ⚠️ **Leçon de + méthode** : une base de revue citée par hash sur une branche empilée ne survit pas au rebase — citer aussi + le **message** du commit, qui, lui, survit. + +La passe confirme par ailleurs : les trois lignes du manuel et leurs citations, les deux repositories aux lignes +dites, la généalogie « 81, puis 82, puis 92 », et les décomptes du Change Log R2. + +Remédiation : texte de spec uniquement. **Revalidation R4 ciblée requise** (MEDIUM). + +### Revalidation R4 CIBLÉE — une lentille Sonnet, contexte frais — **BOUCLE CLOSE** + +Prompt versionné : `25-1c-b2-validate-prompt-r4.md`. Base : `git diff 44d638f9 8e729449` — le commit +« docs(25-1c-b2): revalidation R3 ciblée ». Sondes : pipeline rejoué sur copie jetable (valeur multiligne, +attribut, variante, en-tête de bloc), objets git et reflog. + +**Rendu : 0 CRITICAL, 0 HIGH, 0 MEDIUM, 2 LOW — vérifiés et appliqués.** Les quatre axes sont déclarés exercés. + +- **L1** — un renvoi `invoices.rs:1339-1340` restait sans chemin, dans la phrase même que la R3 qualifiait → + qualifié. +- **L2** — le Change Log R3 disait « deux renvois qualifiés » pour trois sites → recompté. + +La passe confirme : `:1797` n'est plus rattachée à *« corrige »* nulle part ; la phrase sur le filtre du grep +est exacte (exécutée) ; la note « le fichier est identique » et les équivalents de hash sont exacts. + +**Critère de clôture** : aucun finding au-dessus de LOW, et la dernière remédiation ne touche que du texte de +spec — la story, d'ailleurs, ne touche aucun code applicatif. + +**Trend de la boucle rouverte** (après la clôture en 7 passes de la version d'avant les arbitrages du soir) : + +| passe | modèle | rendu | +|---|---|---| +| R1 | Opus, ciblée | 2 M, 6 L | +| R2 | Sonnet, ciblée | 2 M, 3 L | +| R3 | Opus, ciblée | 1 M, 3 L | +| R4 | Sonnet, ciblée | **2 L** | + +**Ce que la boucle rouverte apprend** : aucun des MEDIUM de R1 à R3 ne portait sur la conception. Tous venaient +de la remédiation précédente, sous **deux** formes seulement : une **propagation oubliée** (un nombre corrigé à +un site, laissé à son jumeau), et un **arbitrage résumé** (une réponse du Project Lead étendue à ce qu'elle ne +nommait pas). La seconde forme est la plus coûteuse : elle se lit comme juste. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p3.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p3.md new file mode 100644 index 000000000..f13fca7d2 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p3.md @@ -0,0 +1,65 @@ +# Prompt — passe 3 de `bmad-create-story validate`, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille en contexte frais (Sonnet), orthogonale à la passe 2 (Opus). Première +passe sur la fiche issue du split de la 25-1c-b.* + +Tu es un **valideur adversarial** en contexte frais. Ton objet est le fichier +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.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 mentir les manuels, le README ou les catalogues** une fois la story implémentée. + +⛔ **Rien ne se croit sur parole** — ni la fiche, ni son Change Log, ni son inventaire. + +⚠️ **Ne conteste PAS les arbitrages du Project Lead** (vocabulaire « journal d'audit » / `Audit-Protokoll` / +`registro di audit` / `audit log`, découpage b1/b2 livrées dans la même PR). La fiche sœur +`25-1c-b1-journal-audit-ecran.md` décrit l'écran ; `25-1c-a-journal-audit-route.md` la route. + +## Où regarder d'abord + +La fiche vient d'être écrite par le commit `f764004d` (split de la 25-1c-b, findings M5 et M6 de sa passe 2). +Base de comparaison : la parente, `git show 0b029e24:_bmad-output/implementation-artifacts/25-1c-b-journal-audit-ecran.md` +(AC 13-18). Nomme-la dans ton rapport. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **L'inventaire de l'AC 3 est-il clos ?** Rejoue-le plus large que la fiche : `audit` seul, `trace`, + `traçabilité`, `piste`, `journal des modifications`, sur `docs/manual/fr/*.tex`, **les PDF aplatis** + (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`), `README.md`, `website/`, `docs/*.md` destinés au lecteur, + et les **quatre catalogues** (apostrophe typographique `’` en français). Trie : synonyme du concept à + aligner, ou autre sens (« audit par trois experts », « KF-002 audit ») à laisser. +2. **La règle 3 appliquée aux manuels est-elle ce que dit le glossaire ?** Lis `docs/i18n-glossaire.md` + en entier : la règle vise-t-elle le **produit** (catalogues) seulement, ou aussi la documentation ? La + fiche en tire-t-elle une obligation que le glossaire n'écrit pas — ou, à l'inverse, en oublie-t-elle une ? + « audit-trail » gardé en synonyme dans les glossaires des manuels est-il compatible avec la règle ? +3. **L'encadré de l'AC 4 et la section de l'AC 5, contre le CODE** : chacun des quatre énoncés est-il + réellement faux aujourd'hui ? Vérifie dans le backend que la suppression **et** la modification d'une + écriture sont refusées sans exception, que le verrou de période existe (24-4c), et ce que la + contre-passation laisse réellement au choix de l'utilisateur. Une affirmation neuve prescrite par l'AC 5 + (rôles, filtres, UTC, export tracé, refus des clés API) est-elle vraie au regard de la 25-1c-a et de la + b1 ? +4. **Le manuel d'administration et le README (AC 6-7)** : les phrases citées existent-elles mot pour mot ? + La 25-1c-a réécrit-elle déjà une partie de ces sites, et les deux fiches se contredisent-elles sur la + formulation cible ? `README.md:219` est-il le bon site pour « piste de contrôle », et d'autres sections + du README (Fonctionnalités, tableau des versions) deviennent-elles fausses ? +5. **Les renvois LaTeX** : `\label`/`\ref`/`\nameref` et mentions textuelles des titres qui changent + (`\section{Traçabilité (audit-trail)}`, `\subsection{Audit-trail (audit\_log)}`) ; l'ordre alphabétique + des deux glossaires ; un index (`\index`) éventuel. +6. **Cohérence interne, décomptes (« onze sites », « quatre énoncés »), références de lignes** — 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`, +`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, `pdftotext` vers la sortie +standard ou vers le scratchpad +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p3-b2/`. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p4.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p4.md new file mode 100644 index 000000000..5ccd73544 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p4.md @@ -0,0 +1,59 @@ +# Prompt — passe 4 de `bmad-create-story validate`, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille en contexte frais (Opus), orthogonale à la passe 3 (Sonnet). Passe +complète : la fiche n'a eu qu'une passe, et celle-ci a trouvé un HIGH sur son inventaire.* + +Tu es un **valideur adversarial** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. Ta mission : **trouver ce qui ferait mentir les manuels, le +README ou les catalogues** une fois la story implémentée. + +⛔ **Rien ne se croit sur parole** — ni la fiche, ni son Change Log, ni l'inventaire « douze sites » que la +passe 3 vient de corriger. + +⚠️ **Ne conteste PAS les arbitrages du Project Lead** (vocabulaire « journal d'audit » / `Audit-Protokoll` / +`registro di audit` / `audit log`, découpage b1/b2 dans la même PR). Fiches sœurs : +`25-1c-b1-journal-audit-ecran.md` (l'écran), `25-1c-a-journal-audit-route.md` (la route). + +## Où regarder d'abord + +La dernière remédiation : `git diff f764004d HEAD -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. +Nomme cette base dans ton rapport. Le HIGH de la passe 3 était **deux ensembles différents de même +cardinal** — la faute se reproduit facilement. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **L'inventaire de l'AC 3, par un procédé DIFFÉRENT de celui de la fiche.** Ne rejoue pas son grep : + aplatis les PDF (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) et cherche chaque désignation du **concept** + (la trace des opérations, qui/quand/quoi) — pas seulement les mots de la fiche. Compare **ensemble contre + ensemble**, ligne à ligne, jamais nombre contre nombre. Couvre aussi `docs/manual/fr/*.tex` qu'aucune ligne + du tableau ne cite, `website/`, et les catalogues (apostrophe `’`, et le **allemand**, où le mot s'écrit + avec une majuscule). +2. **Les décisions neuves de la passe 3** : l'extension de la règle 3 aux manuels « décidée par cette story » + est-elle compatible avec l'arbitrage 4 tel qu'écrit dans `epic-25-vague1-suite.md` (cite-le) ? Le triage + « autre sens » de `bank-accounts-errors-has-transactions` tient-il dans les **quatre** langues ? « audit-trail » + gardé en synonyme dans les glossaires des manuels contredit-il l'AC 3 elle-même ? +3. **Les énoncés à écrire contre le code** (AC 4, 5, 6) : chaque affirmation neuve prescrite — rôles, refus des + clés API, filtres, jours UTC, export tracé, suppression de facture qui emporte l'écriture — est-elle vraie + dans le code actuel ou dans le contrat validé de la 25-1c-a et de la b1 ? Cite la ligne. +4. **Ce que les trois stories font au même paragraphe** : la 25-1c-a, la b1 et la b2 réécrivent-elles un même + passage du manuel d'administration dans des sens contradictoires ? Lis les AC manuel de la 25-1c-a. +5. **Cohérence interne, décomptes (« douze », « quatre énoncés », tallies du Change Log), références de + lignes** — 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`, +`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, `git diff`/`git show`, +`pdftotext` vers la sortie standard ou vers le scratchpad +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p4-b2/`. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p5.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p5.md new file mode 100644 index 000000000..ab72184fc --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p5.md @@ -0,0 +1,62 @@ +# Prompt — passe 5 de `bmad-create-story validate`, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille en contexte frais (Sonnet), orthogonale à la passe 4 (Opus). La +remédiation de la passe 4 a réécrit la fiche presque entière : la passe porte sur ce qui a changé, et sur +tout ce qui en dépend.* + +Tu es un **valideur adversarial** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Base** : `git diff 637c6508 c5d14f32 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. +Nomme-la dans ton rapport. Le motif mesuré du projet : *la sévérité se déplace vers ce qu'on vient +d'écrire* — sur cette fiche, les passes 3 et 4 ont chacune trouvé un défaut **introduit par la remédiation +précédente**. + +⛔ **Rien ne se croit sur parole** — ni la fiche, ni son Change Log, ni les « vérifié » qu'il déclare. +⚠️ **Ne conteste PAS les arbitrages du Project Lead** (vocabulaire, découpage, texte du point 1 de +l'arbitrage du soir). Les deux défauts voisins marqués « EN ATTENTE D'ARBITRAGE » ne sont pas à trancher : +vérifie seulement qu'ils sont exacts. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Chaque ligne neuve des tableaux dit-elle vrai ?** Les cinq sites « concept » de l'AC 3, les sept sites + « autre sens », les cinq énoncés de l'AC 4, les cellules ✓/✗ de l'AC 2, les deux défauts voisins de + l'AC 6. Ouvre chaque ligne citée — `.tex` **et** PDF aplati (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`). + Un site trié « autre sens » désigne-t-il en fait le journal d'audit ? +2. **Le motif élargi de l'AC 8 et son « reste attendu »** : exécute-le sur les PDF aplatis et le README + **actuels**, et compare son rendu, ensemble contre ensemble, au tableau de l'AC 3 plus les sites triés. + Rend-il un site que ni le tableau ni le tri ne couvrent ? Le « reste attendu » après implémentation + est-il exactement celui qu'annonce l'AC 8 — ou d'autres occurrences légitimes (le mot « piste » dans un + autre sens, `audit_log` technique) y resteront-elles et le feront échouer à tort ? +3. **Les énoncés neufs de l'AC 5, contre le contrat** : égalité exacte du filtre d'action, identifiant + exigeant un type, plafond de 10 000 lignes et son message, sens de « société indéterminée ». Lis + `25-1c-a-journal-audit-route.md` (AC 2, 5, 11) et `25-1c-b1-journal-audit-ecran.md` (AC 5) : chaque + phrase que la b2 prescrit est-elle ce que le code livrera ? « Auteur dont la société n'a pas pu être + établie » est-il la définition exacte de `company_id` nul (25-1c-zero) ? +4. **Le tableau des trois sources de l'AC 1** : la citation de l'arbitrage du soir est-elle **mot pour + mot** celle de `epic-25-vague1-suite.md` ? Les précédents de glossaire cités (`fr-CH:1241`, `en-CH:1184`, + `de-CH:729`) attestent-ils bien le terme ? La vérification des clés neuves (AC 2) est-elle exécutable + telle qu'écrite ? +5. **Cohérence interne après réécriture** : décomptes (« dix-sept », « cinq énoncés », « trois cellules sur + huit », tallies des Change Logs), renvois entre AC, tâches qui exécutent chaque AC (une tâche oubliée ?), + References. 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`, +`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, `git diff`/`git show`, +`pdftotext` vers la sortie standard ou vers le scratchpad +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p5-b2/`. +⚠️ `grep` est ici `ugrep`, qui refuse les motifs trop complexes (`{0,160}` avec alternatives) : utilise +Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p6.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p6.md new file mode 100644 index 000000000..0c5f83c47 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p6.md @@ -0,0 +1,49 @@ +# Prompt — passe 6 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une seule lentille en contexte frais (Opus), orthogonale à la passe 5 (Sonnet). +Passe ciblée : les trois findings de la passe 5 portaient tous sur la remédiation de la passe 4.* + +Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Ton périmètre est la remédiation de la passe 5**, et seulement elle : +`git diff c5d14f32 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. Nomme cette +base dans ton rapport. Elle trie un site de la brochure, réécrit l'introduction de l'AC 4, corrige un +décompte, et ajoute le Change Log de la passe 5. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Le rejeu de l'AC 8 est-il maintenant exact, ensemble contre ensemble ?** Exécute le motif élargi + (`piste|trace d.audit|dans l.audit\b|audit-?trail|audit log`, sans casse) en Python sur les **PDF + aplatis** des trois documents (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) **et** sur `README.md`, pas + seulement sur les `.tex`. Chaque occurrence est-elle au tableau de l'AC 3 ou dans ses tris ? Un PDF + coupe-t-il un mot (`piste` en fin de ligne, césure) ou en fait-il apparaître un que le `.tex` ne + montre pas (macro, texte généré) ? +2. **L'introduction réécrite de l'AC 4** (« quatre faux ou périmés, le premier vrai mais incomplet — il se + nuance ») est-elle cohérente avec **chaque** ligne du tableau et avec la phrase de conclusion (« L'encadré + se réécrit ou disparaît ») ? Un encadré qui « disparaît » supprime-t-il la phrase vraie qu'on vient de dire + à nuancer ? +3. **Propagation** : un décompte touché (« cinq cellules », sites triés, « dix-sept ») est-il répété + ailleurs sous l'ancienne valeur — corps, tâches, Dev Notes, Change Logs antérieurs qui le présentent + comme actuel ? +4. **Le Change Log de la passe 5** : tallies brut et après reclassement, recomptés contre sa liste. + +## 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`, +`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, `git diff`/`git show`, Python en +lecture, `pdftotext` vers la sortie standard ou vers le scratchpad +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p6-b2/`. +⚠️ `grep` est ici `ugrep`, qui refuse les motifs trop complexes : utilise Python pour les extractions à +contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p7.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p7.md new file mode 100644 index 000000000..519dff74a --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-p7.md @@ -0,0 +1,55 @@ +# Prompt — passe 7 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une seule lentille en contexte frais (Sonnet), orthogonale à la passe 6 (Opus). +Avant-dernière passe autorisée par le plafond de 8.* + +Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Ton périmètre est la remédiation de la passe 6**, et seulement elle : +`git diff 06fda3b0 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. Nomme cette +base dans ton rapport. + +⚠️ **Le motif à chasser** : les deux MEDIUM de la passe 6 étaient des défauts de **propagation** — une +phrase corrigée, sa jumelle ailleurs laissée intacte. Pour **chaque** phrase modifiée par le diff, cherche +dans toute la fiche (corps, tâches, Dev Notes) **une autre phrase qui dit la même chose** et vérifie qu'elle +dit maintenant la même chose. Greppe les **valeurs et les notions**, pas les formulations. + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Propagation de chaque modification** : + - la conclusion de l'AC 4 (« se réécrit, ou se remplace par une note », premier énoncé conservé) contre + son introduction, le tableau et la tâche T3 ; + - le décompte de cellules de l'AC 2 contre les Dev Notes ; + - le reste attendu de l'AC 8 contre la liste des tris de l'AC 3 et la tâche T5 ; + - l'état de #380 contre toute autre mention d'issue ; + - la note de l'AC 3 sur la brochure. +2. **Le rejeu de l'AC 8 est-il exécutable tel qu'écrit ?** Exécute-le en Python sur les PDF aplatis actuels et + le README : le recollage `re.sub(r"(\w)- (\w)", r"\1\2", t)` recolle-t-il à tort un tiret légitime + (« Comptable- Admin », un intervalle, un tiret cadratin mal aplati) au point de créer ou masquer une + occurrence du motif ? Le « reste attendu » écrit en entier correspond-il, **aujourd'hui**, à ce que rend + le motif sur les sites qui ne doivent pas changer ? +3. **L'état des issues citées** : `gh issue view 380 --json state,closedAt` et `381` — la fiche dit-elle vrai ? + Le commentaire du code (`crates/kesh-db/src/repositories/journal_entries.rs` autour de 965-990) cite-t-il + bien ces deux numéros ? +4. **Le Change Log de la passe 6** : décomptes recomptés contre sa liste. + +## 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`add`/`stash`/`reset`/`rebase`, `gh issue edit`/`comment`/`close`, +`npm install`, `npm run build`, `sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, +`git diff`/`git show`, `gh issue view`, Python en lecture, `pdftotext` vers la sortie standard ou vers le +scratchpad `/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/p7-b2/`. +⚠️ `grep` est ici `ugrep`, qui refuse les motifs trop complexes : utilise Python pour les extractions à +contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r1.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r1.md new file mode 100644 index 000000000..875dbf383 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r1.md @@ -0,0 +1,53 @@ +# Prompt — revalidation R1 CIBLÉE de `bmad-create-story validate`, Story 25-1c-b2 (réouverte) + +*Versionné le 2026-09-15. Une seule lentille en contexte frais (Opus), orthogonale à la passe 7 (Sonnet). +Passe ciblée : la réouverture ne touche que quelques paragraphes de la fiche.* + +Tu es un **chasseur de régressions** en contexte frais. Ton objet est la fiche +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt `/home/gcorbaz/devel/kesh`, +branche `story/25-1c-b-journal-audit-ecran`. + +**Ton périmètre est la réouverture**, et seulement elle : +`git diff 5760bab7 HEAD -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. Nomme +cette base dans ton rapport. Les fiches sœurs rouvertes sont `25-1c-a-journal-audit-route.md` et +`25-1c-b1-journal-audit-ecran.md` (même branche). + +⛔ **Rien ne se croit sur parole.** ⚠️ **Ne conteste PAS les arbitrages du Project Lead** (filtre strict, +export non audité, vocabulaire traduit, correction de `:1796` et `:1778` dans cette story). + +## Les axes, et tu déclareras lesquels tu as exercés + +1. **Chaque énoncé neuf de l'AC 5 est-il vrai** au regard de la 25-1c-a rouverte (AC 8, 11, 12, 16, 17) et + de la b1 rouverte (AC 3, 5) — langue de l'interface, filtres en listes, repli sur le code d'une action + historique, plafond ? Un cas où l'écran rend encore une liste vide ou un refus sans explication, et que + le manuel ne dit pas ? +2. **L'AC 6** : « ni la consultation ni l'export ne s'inscrivent au journal » est-il exact contre la 25-1c-a + (AC 9, 14) ? Les consignes de correction de `admin-manual.tex:1796` et `:1778` sont-elles justes contre le + `.tex` actuel (cite les lignes) et contre le code (`crates/kesh-db/src/entities/user.rs`, + `repositories/invoices.rs` autour de 1339) ? La ligne `:1345` dit-elle bien « trois rôles » ? +3. **Propagation** : un paragraphe **non modifié** par le diff dit-il encore l'ancienne conception + (« société indéterminée », « export tracé », `audit_log.exported`, clé `audit-log-entity-audit-log`, + filtre d'action en champ libre, « en attente d'arbitrage ») ? Les renvois vers les AC de la 25-1c-a + rouverte sont-ils tous justes ? +4. **L'AC 2** : la vérification des « clés neuves » est-elle exécutable telle qu'écrite, maintenant que la + 25-1c-a crée 123 clés `audit-log-*` ? +5. **Le Change Log de la réouverture** : fidèle au diff, décomptes justes. + +## 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. +- ⛔ **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`, `latexmk`, tout +`git commit`/`push`/`checkout`/`switch`/`add`/`stash`/`reset`/`rebase`, `npm install`, `npm run build`, +`sqlx migrate`, `cargo test`/`cargo nextest`. Autorisés : lecture, `grep`, `git show`/`git diff`, Python en +lecture, `pdftotext` vers la sortie standard ou vers le scratchpad +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r1-b2/`. +⚠️ `grep` est ici `ugrep`, qui refuse les motifs trop complexes : utilise Python pour les extractions à +contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r2.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r2.md new file mode 100644 index 000000000..6f19ee718 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r2.md @@ -0,0 +1,42 @@ +# Prompt — revalidation R2 CIBLÉE, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille Sonnet en contexte frais, orthogonale à R1 (Opus).* + +Chasseur de régressions. Objet : `_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt +`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. **Périmètre : la remédiation R1**, +`git diff 2efbc76c 6cda8b89 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md` **et** la +section *fin de soirée* de `_bmad-output/planning-artifacts/epic-25-vague1-suite.md` modifiée par `32ebe675`. +Nomme ces bases. + +⛔ Rien ne se croit sur parole. ⚠️ Ne conteste PAS les arbitrages du Project Lead. + +## Axes (déclare lesquels tu as exercés) + +1. **Le procédé Python de l'AC 2** : exécute-le sur les quatre `.ftl` **actuels**. Est-il écrit sans ambiguïté + (motif, groupe de la valeur, casse) ? Que rendrait-il sur une clé multiligne Fluent ? Le grep hérité + « en excluant les clés `^(nav-)?audit-log` » est-il exécutable tel qu'écrit ? +2. **L'AC 6 et `admin-manual.tex:1797`** : la citation est-elle exacte ? `invoices.rs:1350` et + `journal_entries.rs:1061` disent-ils ce que la fiche affirme (suppression d'une facture validée ⇒ + `journal_entry.deleted`) ? La consigne de reformulation est-elle cohérente avec celle de `:1796` et avec + l'AC 4 ? +3. **L'AC 5, action historique** : « n'est pas proposée dans la liste », « seul un code déjà présent dans + l'adresse » — est-ce exactement ce que dit la b1 rouverte (AC 3) ? Un utilisateur retrouve-t-il vraiment + l'entrée « par le type et l'identifiant d'entité » ? +4. **L'epic** : les trois questions et réponses citées (*« ok »*, *« non, maintenant »*, *« corrige »*) sont-elles + rendues fidèlement, sans résumé qui réécrive l'arbitrage ? Le paragraphe barré est-il sans ambiguïté ? Les + « 82 codes » concordent-ils avec la 25-1c-a, qui en compte désormais **92** ? +5. **Propagation** : 133, `invoices.rs:1350` / `:1339-1340`, « réouverte », les renvois aux AC de la 25-1c-a ; + le Change Log R1 (décomptes 2 MEDIUM, 6 LOW). + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés** — rien de « vérifié » sans exécution. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `latexmk`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`, ni `sqlx migrate`, ni +`cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff`, Python en lecture, `pdftotext` vers la +sortie standard ou vers `/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r2-b2/`. +`grep` est `ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r3.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r3.md new file mode 100644 index 000000000..612af3f79 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r3.md @@ -0,0 +1,39 @@ +# Prompt — revalidation R3 CIBLÉE, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille Opus en contexte frais, orthogonale à R2 (Sonnet).* + +Chasseur de régressions. Objet : `_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt +`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. **Périmètre : la remédiation R2**, +`git diff 5dfbafc2 0bbefc51 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md _bmad-output/planning-artifacts/epic-25-vague1-suite.md`. +Nomme cette base. ⚠️ **Motif de cette fiche** : les passes 6, R1 et R2 ont chacune trouvé une **propagation +oubliée** par la remédiation précédente. Pour chaque valeur ou notion modifiée, cherche sa jumelle ailleurs. + +⛔ Rien ne se croit sur parole. ⚠️ Ne conteste PAS les arbitrages du Project Lead. + +## Axes (déclare lesquels tu as exercés) + +1. **« Trois défauts voisins »** : tous les sites qui en parlent (corps, tâches, Dev Notes, table, Change Log + courant) disent-ils trois, et la ligne `:1797` ajoutée à la table est-elle exacte contre `admin-manual.tex` + et contre `crates/kesh-db/src/repositories/{invoices,journal_entries}.rs` ? +2. **Le pipeline `grep … | grep -vE ':[0-9]+:(nav-)?audit-log'`** : exécute-le sur les `.ftl` actuels et sur une + copie jetable où tu ajoutes des clés `audit-log-*` ; exclut-il bien les clés, et elles seules (le format de + sortie de `ugrep` avec `-rn` a-t-il la forme `fichier:ligne:contenu` attendue) ? +3. **Le rattachement des valeurs Fluent multilignes** : la consigne est-elle implémentable et juste (lignes de + continuation indentées, lignes vides, commentaires, attributs `.attr`) ? +4. **L'epic** : « 92 codes d'action — 82 en littéral et 10 indirects » concorde-t-il exactement avec la 25-1c-a + (AC 15, AC 18, Dev Notes) ? La généalogie « 81, puis 82 » est-elle exacte ? +5. **Chemins qualifiés** : tous les renvois à `invoices.rs` et `journal_entries.rs` du **repository** sont-ils + désormais sans ambiguïté ; Change Log R2 fidèle, décomptes (2 MEDIUM, 3 LOW). + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés**. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `latexmk`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`, ni `sqlx migrate`, ni +`cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff`, Python en lecture, `pdftotext` vers la +sortie standard ou vers `/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r3-b2/`. +`grep` est `ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r4.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r4.md new file mode 100644 index 000000000..aa66f5e00 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r4.md @@ -0,0 +1,41 @@ +# Prompt — revalidation R4 CIBLÉE, Story 25-1c-b2 + +*Versionné le 2026-09-15. Une lentille Sonnet en contexte frais, orthogonale à R3 (Opus).* + +Chasseur de régressions. Objet : `_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`, dépôt +`/home/gcorbaz/devel/kesh`, branche `story/25-1c-b-journal-audit-ecran`. **Périmètre : la remédiation R3**, +`git diff 44d638f9 8e729449 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md` — le seul +commit « docs(25-1c-b2): revalidation R3 ciblée ». Si un hash ne résout pas, retrouve le commit par son message. +Nomme la base. + +⚠️ **Motif de cette fiche** : chaque remédiation depuis la passe 6 a laissé une propagation oubliée, ou réécrit un +arbitrage en le résumant. Pour chaque valeur ou notion modifiée, cherche sa jumelle ailleurs. + +⛔ Rien ne se croit sur parole. ⚠️ Ne conteste PAS les arbitrages du Project Lead. + +## Axes (déclare lesquels tu as exercés) + +1. **L'attribution de `:1797`** : l'AC 6 et l'arbitrage du soir (`:48-49`) disent-ils désormais exactement ce que + dit l'epic (§ *fin de soirée*, question et réponse *« corrige »* sur `:1796` et `:1778`) ? Un autre site de la + fiche (tâches, Dev Notes, Change Log « Réouverture », tableau d'en-tête) rattache-t-il encore `:1797` à + *« corrige »* ? +2. **La phrase sur le filtre du grep** (lignes de continuation, attributs, variantes, en-têtes de blocs) : exacte et + suffisante pour trier le rejeu de T0 ? Exécute le pipeline sur une copie jetable des `.ftl` enrichie d'un bloc + `audit-log-*` à valeur multiligne et d'un en-tête `# --- Journal d'audit — …`. +3. **Chemins qualifiés** : reste-t-il, hors Change Log, un renvoi `invoices.rs` ou `journal_entries.rs` ambigu, ou un + chemin sans `crates/` ? +4. **La note sur les bases** du Change Log R2 (« le fichier y est identique ») et la leçon du Change Log R3 : exactes + (`git cat-file`, blobs) ? Change Log R3 fidèle au diff, décomptes (1 MEDIUM, 3 LOW) ? + +## Rendu + +Findings avec sévérité, endroit, commande ou extrait probant, correctif ; **liste des axes exercés et non +exercés**. + +## Interdits + +⛔ Aucune écriture dans le dépôt, aucune commande mutante : ni `scripts/*`, ni `make`, ni `latexmk`, ni `git +commit/push/checkout/switch/add/stash/reset/rebase`, ni `npm install`/`npm run build`, ni `sqlx migrate`, ni +`cargo test/nextest`. Autorisés : lecture, `grep`, `git show/diff/log/cat-file`, Python en lecture, `pdftotext` vers +la sortie standard ou vers `/tmp/claude-1000/-home-gcorbaz-devel-kesh/cb9f9ce3-4808-472f-93d0-698c43110e0e/scratchpad/r4-b2/`. +`grep` est `ugrep` : Python pour les extractions à contexte. diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index 086ae4ce7..2097fb712 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -330,7 +330,9 @@ development_status: 25-1b-trous-alimentation: done # 2026-09-13 ✅ **GATE E2E DE CLOTURE — 216 passed / 19 skipped / 7 failed (8m42)**, montage COMPLET, base `kesh_e2e` DETRUITE ET RECONSTRUITE, `/health` controle avant lancement. ✅ **LES 7 ECHECS SONT EXACTEMENT LA BASELINE KF-029** — aucune pollution, aucune KF-045 (run d'apres-midi), aucune KF-046. Attendu 8 a 10, observe **7** : la baseline PURE, le meilleur resultat possible. Recoupement : 216+19+7 = 242 comme au run du 11 (214+19+9), les 2 tests de plus qui passent sont les 2 pollutions non declenchees ⇒ **ZERO REGRESSION**. ⚠️ **LE BUILD FRONTEND ETAIT EN RETARD ET A ETE REFAIT** : date du 11/09 11:40 alors que le dernier commit touchant `frontend/` (celui de la 25-1a, merge depuis dans `main`) est de 19:18. *Cette story ne touche pourtant PAS le frontend* — c'est le piege de la 24-5 : **un gate declare vert sur un perimetre qui a bouge n'est plus un gate**. **LES TROIS GATES SONT VERTS** : backend **2322/2322**, E2E **216/7-attendus**, frontend et i18n NON TOUCHES (verifie par `git diff`). **Reste : push + PR avec `closes #379` DANS LE TITRE** (squash-merge : le titre devient le message du commit). # 2026-09-13 2026-09-12 **IMPLEMENTEE** — **14 routes tracees**, gate backend COMPLET **2318/2318** (4 skipped, 90 s), fmt et clippy propres, frontend et i18n **NON TOUCHES (verifie)**. ⛔ **CINQ DEFAUTS QUE SEULE L'EXECUTION A REVELES, ET CE SONT TOUS DES DETECTEURS MAL FORMES — LES MIENS** : (1) `creditor_iban` n'est PAS `Option` — le compilateur a rejete le `is_some()` que la spec prescrivait ; (2) ⛔ **les 4 helpers d'audit CASSAIENT LE GATE** — `tests/common/mod.rs` est compile SEPAREMENT dans chaque binaire de test, donc un helper employe par 10 fichiers sur 40 est « jamais utilise » dans les 30 autres et `clippy -D warnings` refusait ; (3) ⛔ **mon extracteur du registre rendait un ensemble VIDE** sur `test_endpoints.rs` (forme `.route("/x", post(h))` et non `post(routes::m::h)`) et **comparait donc deux vides AVEC SUCCES** ⇒ garde contre l'ensemble vide ; (4) ⚠️ **mon controle de PDF a rendu un FAUX NEGATIF** sur une apostrophe TYPOGRAPHIQUE cherchee en apostrophe droite — le piege que le `CLAUDE.md` documente, pris sur le fait ; (5) ⛔ **ma remise a zero de la base de gate etait INCOMPLETE puis OMISE** : seed saute (29 tests tombes sur « need at least one Admin user in DB »), puis base non reconstruite entre deux runs alors que la regle est **INCONDITIONNELLE**. ⚠️ La 1re erreur a ete MASQUEE par une **verification mal formee** : elle comptait migrations et tables — ce qui etait commode — au lieu de ce dont les tests ont besoin. *Verifier n'est pas executer ; mais verifier la MAUVAISE CHOSE ne vaut pas mieux que ne rien verifier.* ✅ **Un test a rougi A RAISON, et c'etait LE TEST** : il comptait les traces globalement alors que le chemin d'amorçage ecrit desormais la sienne. Les 3 compteurs globaux du depot inspectes ensuite : aucun casse. ✅ **DEUX gardes eprouvees par MUTATION** : la garde no-op de `users` et celle du registre. ✅ **Trou de couverture ferme** : `contact_persons_e2e.rs` cree — ces 3 routes n'avaient AUCUN test. ✅ **Registre de 108 routes** (105 `lib.rs` + 3 `test_endpoints.rs`), diff ENSEMBLISTE par identite (105 couples verbe+handler, 0 doublon), ventilation recomptee : **87 tracees / 15 exemptees avec numero d'issue / 3 sans matiere**. ✅ **8 sites de manuel corriges**, 3 PDF regeneres, verifies sur les PDF **APLATIS**. **Decompte recoupe : 2307 + 11 tests neufs = 2318.** ⚠️ **E2E AU PUSH.** Prochaine : `bmad-code-review` passe 1, trois lentilles. # 2026-09-11 **VALIDATE 2026-09-11 **VALIDATE PASSE 1** (Sonnet 4.6 + Haiku 4.5, contextes frais, orthogonales a l'auteur Opus 5, prompt versionne) : **1 CRITICAL, 1 HIGH, 3 MEDIUM, 2 LOW** — tous verifies au sol, tous patches. ⇒ **12 AC (3 volets), 11 taches** (recomptes depuis la source : `grep -cE '^\*\*[0-9]+\. '`). ⛔ **LE CRITICAL EST UN INVARIANT INTENABLE SUR UNE SEULE ROUTE** : `imported_supplier_invoices::create` ecrit **sur le POOL** (`:61`) et **aucun `create_in_tx` n'existe** — a la difference de `mark_completed` et `mark_discarded`, tous deux transactionnels dans le MEME fichier. La spec ne prevoyait que le threading de l'acteur ⇒ la trace se serait ecrite dans une transaction DISTINCTE, apres l'autocommit : *exactement la fenetre non atomique que l'AC 9 ferme sur les douze autres routes*. Un developpeur ne l'aurait decouvert qu'en codant. ⛔ **LE HIGH VIENT DE L'ORCHESTRATEUR, EN VERIFIANT UN MEDIUM DE HAIKU** : la lentille signalait UN champ faux (`metadata_json`) au manuel ; il y en a **TROIS sur sept** — `company_id` **QUI N'EXISTE PAS** (et que l'arbitrage du jour vient de decider d'ajouter), `timestamp` pour `created_at`, `metadata_json` pour `details_json` — et **quatre manquent**. Surtout, la lentille n'a PAS releve l'affirmation qui OUVRE la section : `admin-manual.tex:1782` « **TOUTE** action metier mutante est enregistree » — faux pour 28 routes, et **TOUJOURS faux apres cette story** pour 15. **Le manuel se contredit lui-meme a 170 lignes d'ecart** (`:1956` avoue la couverture partielle) : motif exact paye par la 25-1a sur la section OLICo. ⇒ **AC 12 ajoute**. ⚠️ **MEDIUM de Sonnet sur l'AC 11 — le detecteur etait MAL FORME** : comparer un NOMBRE ne detecte pas une route retiree pendant qu'une autre est ajoutee ; `admin_pat_denied_e2e` peut compter parce qu'il opere sur un BLOC CLOS entre marqueurs, pas les 105 routes eparpillees ⇒ **diff ENSEMBLISTE par identite**. ⚠️ 2 LOW : deux references de ligne derivees (`companies.rs` le `if` est a `:181` pas dans 175-178 ; `audit.rs` le trait est a `:18`, son impl a `:33`). ✅ **1 finding de Haiku ECARTE** : `contact_persons.rs:5` etait **deja nomme par l'AC 2**. ✅ **Grep de propagation fait AVANT la passe 2** : symptome **circonscrit au manuel admin FR** (DE/EN/IT n'ont qu'un README). ⛔ **TROUVE EN CHEMIN, HORS PERIMETRE** : `user-manual.tex:503` affirme « aucun verrou de periode plus fin n'existe encore » alors que `:444-453` **le documente** — le manuel se contredit a 50 lignes d'ecart depuis la 24-4c. A TRACER. # 2026-09-11 **VALIDATE PASSE 2** (Opus 5, lentille unique, contexte frais, prompt versionne, braquee sur les 7 patches de la P1) : **0 CRIT, 3 HIGH, 5 MED, 6 LOW** — severite en RECUL (CRIT → HIGH) ⇒ **pas de split**. ⚠️ **13 findings sur 15 sont D'ORIGINE** — 1 HIGH et 1 LOW seulement nes d'un patch : *l'INVERSE du motif habituel du depot, la remediation n'a presque rien casse, c'est la conception qui etait incomplete*. ⛔ **P2-1 HIGH — UN TROISIEME CHEMIN D'ENTREE** : la REACTIVATION d'une piece ecartee (`inbox_import.rs:418-431`) rend `Accepted` **sans rien creer** ⇒ la piste garderait `discarded` pour derniere trace d'une piece `to_complete`. *Un journal qui affirme le contraire de la base est pire qu'un journal muet.* Et `reactivate_to_complete` porte le MEME defaut que `create` (`.execute(pool)`), a 60 lignes de la : **le patch de la P1 avait ferme la moitie du trou**. ⛔ **P2-2 HIGH — UNE JUSTIFICATION FONDEE SUR UN FAIT INEXISTANT** : la spec disait « la creation du premier admin n'est pas tracee ALORS QUE SON CHEMIN JUMEAU L'EST » ; `auth/bootstrap.rs:123` cree le meme admin et **n'ecrit rien** (le seul `insert_in_tx` du fichier, `:252`, est le break-glass). Et ce chemin **n'est pas une route** ⇒ hors des 105, hors [#434]/[#435] ⇒ **ecrit NULLE PART**. ⛔ **P2-3 HIGH, NE DU PATCH P1 — UN BALAYAGE DECLARE COMPLET NE L'ETAIT PAS** : la P1 avait ecrit « symptome circonscrit au manuel admin » ; il restait **`admin:2184`** (glossaire, meme promesse 400 lignes plus loin — la corriger seule REINSTALLE la contradiction) et **`user-manual:1591-1594`** (un AUTRE manuel). Le grep portait sur `metadata_json`, **UN des TROIS symptomes** de l'AC 12. *Greper un symptome n'est pas greper le defaut* — **sixieme fois** que ce geste est pris en defaut sur ces deux stories. ⇒ AC 12 passe a **CINQ sites, deux manuels**. ⚠️ P2-4 : l'AC 8 disait « trois routes », il y en a **QUATRE** (2 `users` + 2 `companies` partagent leur repository). ⛔ **P2-5 : LE COMMENTAIRE PUBLIE SUR [#431] ETAIT FAUX** — 35 sites annonces, **38** reels, sa propre ventilation sommant a 41 (les 3 de `mod tests` soustraits DEUX FOIS). **Rectifie sur l'issue le meme jour.** *Recompter depuis la source vaut aussi pour ce qu'on vient d'ecrire ailleurs.* ⚠️ P2-6 : deux silences, deux mecanismes — les `410`/`500` rollbackent, les quatre `400` sont AVANT le `begin`. ⚠️ P2-7 : **l'ensemble clos est celui de `lib.rs`, PAS du backend** — 3 routes mutantes vivent dans `test_endpoints.rs` via le `nest()` de `:995` ⇒ a exempter NOMMEMENT. ⚠️ **P2-8, ne du patch P1 : « transaction portee par `process_one_file` » ne disait pas OU** — pool a **5 connexions** (`main.rs:73`), `GET_LOCK` en tient deja une ⇒ la poser autour de la SEULE insertion (`:488-516`), sinon une connexion *idle-in-transaction* par fichier, jusqu'a 200 par run. ✅ Verifie en P2 et a ne pas re-debattre : le `GET_LOCK` n'interfere pas, la transaction PAR FICHIER est le bon grain, l'identite verbe+handler est discriminante (105 couples, 0 doublon). ⚠️ **Axe NON exerce declare par la lentille** : rien n'a ete COMPILE — la faisabilite des `_in_tx` reste raisonnee, pas prouvee par le compilateur ; et **la « 1 partielle » et les « 3 sans matiere » de l'inventaire ne sont nommees nulle part** (faiblesse reelle de l'inventaire). # 2026-09-12 **VALIDATE PASSE 3** (Sonnet 4.6, contexte frais, prompt versionne, DEUX FRONTS : les patches de la P2 **et les axes que la P2 declarait NON EXERCES**) : **0 CRIT, 1 HIGH, 2 MED, 2 LOW** — **TROISIEME RECUL CONSECUTIF** (1C/1H → 3H/5M → 1H/2M), convergence monotone, **les 5 sont D'ORIGINE**. ⛔ **P3-1 HIGH — T6 CONTREDISAIT L'AC 9 EN SILENCE** : elle ne disait ni d'extraire `update_step_in_tx`, ni qui mene la transaction. Pire : `onboarding::update_step` a **d'autres appelants** (le seed + 8 routes d'onboarding) ⇒ poser la trace DANS le repository ferait ecrire `installation.ui_mode_changed` **a chaque etape de l'onboarding**. *Une trace au mauvais etage ne manque pas : elle ment.* ⛔ **P3-2 — UN SIXIEME SITE DE MANUEL, ET IL PARLE D'ATTRIBUTION, PAS DE COUVERTURE** : `admin:1762` promet que les mutations par cle API sont tracees `actor_type='api_key'` — **faux pour les 38 sites de [#431]**, que cette story ne corrige pas. *Un inventaire construit sur une seule question ne trouve que les reponses a cette question* : cinq relectures cherchaient « ce qui est trace », celui-ci dit « comment c'est attribue ». **SEPTIEME defaut de manuel sur ces deux stories.** ⚠️ P3-3 : « 1 partielle » n'avait pas de PORTEE declaree — [#434] en documente une seconde (`onboarding/finalize`) ⇒ le chiffre a ete relu contre le mauvais perimetre. ⚠️ P3-5 : les 3 routes de `test_endpoints.rs` doivent sortir du DIFF AUTOMATIQUE (elles n'ont jamais ete dans `lib.rs`) — *un garde qui rougit toujours est un garde qu'on desactive*. ✅ **REPRENDRE LES AXES NON EXERCES PAIE** : l'axe « contenu de [#434] » a rendu P3-3, l'axe « faisabilite » a rendu P3-1. *Une passe qui declare honnetement ses manques rend la suivante plus efficace que si elle avait bluffe.* ✅ Verifie et a ne pas re-debattre : 105 routes **recomptees independamment** par script depuis `lib.rs` ; frontend et i18n **exerces, 0 finding** ; aucun changement de schema ; `cargo check --workspace --all-targets` propre ; les 5 sites de manuel exacts a la ligne. ⚠️ **Axes NON exerces declares** : les **73 routes reputees tracees** ne sont toujours pas verifiees UNE PAR UNE (coherence par fichier seulement) ; `cargo clippy` non lance ; la faisabilite du code a ecrire reste **raisonnee, non prototypee**. # 2026-09-12 **VALIDATE PASSE 4** (Haiku 4.5, contexte frais, prompt versionne) : **1 MEDIUM** apres reprise d'axe par l'orchestrateur. ⛔ **LA LENTILLE A RENDU 0 FINDING EN DECLARANT N'AVOIR TRAITE QUE 4 ROUTES SUR 73** — ni le 7e site du manuel, ni les recomptes ⇒ **NE COMPTE PAS COMME PASSE** (regle « une passe qui ne declare pas ses axes exerces »). ✅ **Mais sa FRANCHISE a fait son travail** : *un « 0 finding » adosse a la liste de ce qui n'a PAS ete fait vaut infiniment mieux qu'un « 0 finding » qui se tait* — contraire exact du precedent de la 24-5. Les 3 axes repris par l'orchestrateur. ✅ **AXE PRINCIPAL EXERCE AUTREMENT** : non route par route, mais par les **94 appels d'audit** du backend et leur gouvernance conditionnelle ⇒ **8 sous condition, les 8 justifies** (garde no-op, lot vide, verrou qui recule, best-effort, complementarite) ⇒ **aucun trou de couverture nouveau**. ⚠️ **Ce que la methode NE VOIT PAS, et c'est ecrit** : une route dont un chemin de succes CONTOURNERAIT le repository qui trace. ⛔ **P4-1 — UN SEPTIEME SITE DE MANUEL, ANGLE ENCORE INEDIT : L'EXPORTABILITE.** `admin:1935` gage la conformite **OLICo art. 9 al. 3** sur un ZIP d'export « garanti par `audit_log` » — or l'export porte **19 tables sur 38 et PAS `audit_log`** ([#386]). *Sept sites, SEPT QUESTIONS DIFFERENTES : couverture, champs, attribution, exportabilite — chacun trouve en posant une question que le precedent ne posait pas. Un inventaire documentaire ne se clot pas par l'epuisement des FICHIERS mais par celui des QUESTIONS.* ⛔ **HORS PERIMETRE, PORTE A [#431]** : `reconciliation_rule.deleted` ecrit son attribution **DIFFEREMMENT SELON L'EFFET** — `::user` quand la suppression aboutit (repo `:402`), `from_current_user` quand elle est no-op (route `:473`). La couverture est entiere, l'attribution non : *un reviseur qui filtre sur `actor_type='api_key'` verra les suppressions SANS effet et manquera celles qui en ont eu un.* **L'incoherence vaut pire que l'absence.** ⛔ **HORS PERIMETRE, PORTE A [#386]** : le site `:1935`, avec la correction INVERSE a prevoir quand l'export integrera la piste. **TREND : 1C/1H/3M/2L → 0C/3H/5M/6L → 0C/1H/2M/2L → 1M.** Convergence monotone depuis la P2. # 2026-09-12 **VALIDATE PASSE 5 CIBLEE** (Opus 5, contexte frais, prompt versionne, perimetre = LA REMEDIATION des P3/P4) : **0C, 0H, 4 MED, 5 LOW** ⇒ **LA BOUCLE NE PEUT PAS SE CLORE** (critere : plus rien au-dessus de LOW). ⛔ **P5-1, NE DU PATCH P3 — UN PATCH QUI ANNULE LE BENEFICE QU'IL INVOQUE** : sortir les 3 routes de `test_endpoints.rs` du diff ENTIER rend une QUATRIEME route de ce fichier aussi invisible qu'avant, alors que la phrase « sans quoi une 4e route ne ferait rougir aucun garde » a SURVECU au patch qui la rendait fausse ⇒ exclusion du **SECOND VOLET seulement**, l'extracteur lit les DEUX fichiers. *Corriger un mecanisme sans relire ce qui le motivait produit un texte qui se refute lui-meme.* ⛔ **P5-2 — « PROPAGE PARTOUT » EST UNE AFFIRMATION, DONC ELLE SE VERIFIE** : mon commit annonçait « propage aux SIX enonces », il y en avait **HUIT** — l'AC 7 n'assignait de constructeur qu'a TREIZE routes (la 14e, `complete`, absente du tableau) et T4 s'intitulait « deux routes » pour trois. *Celui qui corrige une derive de compteur n'est pas immunise contre elle* — c'est le defaut meme que la P2 avait reproche a la spec. ⛔ **P5-3 — UN HUITIEME SITE DE MANUEL, ET IL AMENDE LA LEÇON DE LA P4** : `user-manual:1733` (glossaire) porte la meme promesse que `:1591-1594` **plus un renvoi vers une section qui n'existe pas**. Les 7 premiers avaient chacun demande une QUESTION NEUVE ; le 8e repond a une question **DEJA POSEE, dans un fichier qu'elle n'avait pas visite**. ⇒ *Un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT**.* La leçon de la P4 etait vraie ET INCOMPLETE : elle orientait vers des themes neufs et detournait du balayage exhaustif. ⚠️ P5-4 : l'AC 12 s'intitulait « le manuel administrateur… TROIS affirmations » quand sa table en listait SEPT dans DEUX manuels — les P3 et P4 avaient incremente la ligne JUSTE EN DESSOUS sans toucher celle-la. ⚠️ P5-5 : « sept des quatorze chemins » → **SIX** (5 autres n'ouvrent AUCUNE transaction, 3 en ont deja une au handler : 6+5+3=14). ✅ **CONFIRME AU SOL par la P5** : les 94 appels / 8 conditionnels, les 19 tables d'export sans `audit_log`, la 2e partielle de [#434], la faisabilite de T6 (10 appelants, ZERO changement de signature), les 7 sites precedents, et **105 routes re-derivees independamment**. ⛔ **ARBITRAGE DEMANDE AU PROJECT LEAD** : la severite MAXIMALE recule encore (CRIT → HIGH → HIGH → MED) mais le NOMBRE de MED remonte (2 → 4). La § *Regle de splitting preventif* vise « une passe N+1 qui remonte une severite EGALE OU SUPERIEURE (e.g. MED → MED) ». **Recommandation de l'orchestrateur ET de la lentille : PAS de split** — aucun des 4 MED ne touche la conception (deux totaux non propages, un site documentaire, un garde dont le patch a vide la justification) ; c'est le motif Epic 16 *« le compte rendu est devenu le lieu du defaut, pas le fond »*. # 2026-09-12 **VALIDATE PASSE 6 CIBLEE** (Sonnet 4.6, contexte frais, prompt versionne, perimetre = LE SEUL COMMIT `2f87b1aa`) : **0C, 0H, 2 MED, 0 LOW** — **LES DEUX NES DU CORRECTIF DE LA P5**. ⛔ **P6-2 — UN PATCH MET A JOUR LE CRITERE ET OUBLIE LA TACHE QUI L'EXECUTE** : l'AC 12 comptait HUIT sites, **T10 en listait encore SEPT**. *Un developpeur suivant la tache a la lettre aurait corrige 7 sites sur 8 — et le gate serait reste VERT.* ⚠️ P6-1 : « quatre autres n'ouvrent aucune transaction » alors que le tableau JUSTE EN DESSOUS en liste CINQ (6+5+3=14). ✅ **Les deux verifications demandees sont FAVORABLES** : le constructeur de `complete` est EXACT (route dans `comptable_routes` ⇒ atteignable par jeton ⇒ `from_current_user`), et **AUCUN neuvieme site** — le manuel utilisateur ne promet rien sur les champs, l'attribution ni l'exportabilite. ⛔ **LE MOTIF ETAIT DEVENU CLAIR — la P5 corrige, la P6 trouve deux defauts DU PATCH P5 — et une passe de plus l'aurait RECONDUIT.** ⇒ § *Propagation post-patch* appliquee par l'orchestrateur sur **TOUS** les enonces numeriques du document. Elle a rendu : (a) l'ambiguite **105 vs 108** entrees au registre, que la P6 signalait sans la trancher — l'inventaire porte sur `lib.rs` (105), **le registre est plus large** (108), et c'est voulu ; (b) ⛔ **UN DERNIER PIEGE QUE SIX PASSES ONT LAISSE PASSER** : « les conventions, verifiees sur les **73 SITES** en place » — ce 73 designait les **LIBELLES D'ACTION**, pas les 73 routes tracees. **Deux grandeurs sans rapport portant le MEME NOMBRE sont indetectables a la relecture** ; seul un balayage qui interroge **ce que chaque nombre COMPTE**, et non sa valeur, pouvait le voir. *C'est la § « greper la VALEUR, pas la formulation » RETOURNEE : ici la valeur etait juste et l'OBJET faux.* L'enonce nomme desormais son objet. # 2026-09-12 ✅ **BOUCLE DE VALIDATE CLOSE — 7 PASSES** : **1C/1H/3M/2L → 3H/5M/6L → 1H/2M/2L → 1M → 4M/5L → 2M → 0**. **Severite maximale : CRIT → HIGH → HIGH → MED → MED → MED → RIEN**, decroissance STRICTE. Rotation complete (Sonnet+Haiku → Opus → Sonnet → Haiku → Opus → Sonnet → Haiku), **7 prompts versionnes**, 3 passes CIBLEES. **PASSE 7** (Haiku 4.5, ciblee sur `1f02097f`) : **0 finding**, 4 axes exerces, axes non exerces declares et tous LOGIQUEMENT IMPOSSIBLES a ce stade (verifier des PDF regeneres pour une spec non implementee). **Les 2 affirmations principales REPRISES par l'orchestrateur** — *un « 0 finding » se verifie comme un finding* : plus aucun « sept sites » hors Change Log, somme du tableau **6+5+3=14**. ⚠️ **Le controle a produit un FAUX POSITIF INSTRUCTIF** : un test cherchant « audit » sur `admin:1786` l'a signalee vide — elle enumere des NOMS DE COLONNES et ne contient pas le mot. *Un detecteur mal forme coute le meme diagnostic qu'un defaut reel* — jusque dans la verification d'une cloture. **CE QUE LA BOUCLE LAISSE AU-DELA DE LA STORY** : (1) ⛔ *un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT*** — 8 sites de manuel = 4 questions x 2 fichiers ; (2) ⛔ **deux grandeurs differentes portant le MEME NOMBRE sont indetectables a la relecture** (« 73 » = routes tracees OU libelles d'action) ⇒ interroger ce que chaque nombre COMPTE, pas sa valeur ; (3) ⛔ **un patch met a jour le CRITERE et oublie la TACHE qui l'execute** — 7 sites corriges sur 8, **gate vert** ; (4) ⛔ **quand une passe ne trouve plus que les defauts de son propre patch, ajouter une passe RECONDUIT le motif** — ajouter le GESTE, et le faire soi-meme ; (5) ✅ **une passe qui declare honnetement ce qu'elle n'a pas fait vaut mieux qu'une passe qui bluffe** (la P4 a designe ou chercher, 2 findings de la P3 en sont nes). **Statut `ready-for-dev`. Prochaine : `bmad-dev-story`.** ⛔ **L'INVENTAIRE DEPLACE LE PERIMETRE** : balayage de l'ENSEMBLE CLOS des **105 routes mutantes** du backend ⇒ **73 tracees, 28 NON tracees, 1 partielle, 3 sans matiere**. Le releve des six trous est EXACT mais INCOMPLET : deux familles entieres manquaient — **`onboarding` (11 routes)**, dont `accounts::bulk_create_from_chart` (non audite alors que les 4 autres mutations d'`accounts` le sont), la creation du compte bancaire, le seed et le reset — le code le sait, `routes/onboarding.rs:523-527` porte un TODO — et **`auth`/session (4 routes)**, ou `change_password` (`auth.rs:511`) n'ecrit RIEN alors que `reset_password`, EFFET MATERIEL IDENTIQUE, audite (`auth.rs:948`) ; y compris la revocation en masse pour VOL DE JETON (`auth.rs:382`). ⇒ **8 familles > 5 modules : la § Regle de splitting prefentif s'applique** ⇒ story tenue au perimetre de [#379] (6 familles, **13 routes**), les deux decouvertes sortent en issues propres. ✅ **ISSUES OUVERTES LE 2026-09-11 : [#434]** (onboarding, 11 routes) et **[#435]** (`auth`/session, 4 routes) — l'AC 11 les cite au registre des exemptions. ⚠️ **[#435] porte une question de conception** : tracer chaque `login`/`refresh` ferait de la piste un JOURNAL DE SESSIONS ; les 4 routes ne se valent pas (`change_password` et `theft_detected` sont rares et lourds, `login`/`refresh` sont du trafic). ⛔ **LE FAIT STRUCTURANT N'EST PAS DANS L'ISSUE** : `insert_in_tx` ne prend qu'une `&mut Transaction`, or **7 des 13 chemins** passent par un repository qui OUVRE ET COMMITE SEUL ⇒ le handler n'a rien ou greffer. Decision: extraire des variants `_in_tx` plutot que descendre `user_id` — parce que (1) `update_role_and_active` sert DEUX routes qui doivent ecrire DEUX libelles, (2) `companies::update` et `onboarding::update_step` ont d'autres appelants, (3) **descendre `user_id` NU est le mecanisme meme de [#431]**. ⚠️ **4 routes sont quasi gratuites** (`discard` et `setup/admin` : tx deja ouverte ; `POST /users` et `/reset-password` : variant `_in_tx` deja ecrit POUR CELA) — commencer par elles. ⛔ **Le critere du constructeur n'est PAS le role mais « un PAT peut-il atteindre ce chemin »** : `require_not_pat` est pose **UNE SEULE FOIS** (`lib.rs:330`, bloc `admin_routes`). ⇒ `::user` sur les 4 `users` + 2 `companies` ; `from_current_user` sur les 3 `contact_persons`, `discard`, `profile` ; `for_actor` threade sur `inbox-import` ; `::user(user.id)` sur `setup` (PRE-AUTH, acteur = la cible). ⚠️ **`user.role_changed` porte son propre libelle** — precedent `books.unlocked` vs `books.restored` : « confondre les deux rendrait le filtre inutilisable pour le reviseur ». ⚠️ **`profile/mode` trace l'INSTALLATION, pas l'utilisateur** : `onboarding_state` est MONO-LIGNE ET GLOBALE, sans `company_id` — un utilisateur bascule l'installation entiere en mode Expert. ⛔ **TROU DE COUVERTURE DECOUVERT** : les 3 routes `contact_persons` n'ont **AUCUN test**, ni integration ni E2E ⇒ fichier a creer (on n'ajoute pas une trace a du code que rien n'exerce). ⚠️ **AC 11 — registre des routes mutantes + garde** : la story ferme 13 des 28 sites ; les 15 restants deviennent des angles morts ASSUMES, et un angle mort ecrit nulle part redevient un oubli au premier ajout de route. Le test ne peut PAS verifier qu'une route est tracee (le tracage est au repository, a 3 appels de distance) — il verifie que toute route mutante a ete EXAMINEE, seule propriete decidable. ✅ **[#431] CORRIGEE le 2026-09-11** (commentaire) : **15 repositories de production et 35 sites**, l'issue en annoncait 10 ; +`audit_log.rs` dont les 3 occurrences sont dans `mod tests`. Les 6 ajoutes : `companies`, `contacts`, `invoices`, `invoice_settlements_write`, `reconciliation_rules`, `supplier_invoices`. ⚠️ `invoices.rs` porte **LES DEUX FORMES** (`::user` x5 ET `for_actor` `:2112`) — *aucun decompte par fichier ne montre une migration a moitie faite*. Les references de ligne de l'issue avaient derive (`require_not_pat` est a `lib.rs:330`, pas `:319`). Aucun changement de schema ⇒ ni P2-bis/P3/P5/P6/P7/P8. **Prochaine : `bmad-create-story validate` passe 1.** # [#379] + decouvertes de la passe 1 — **SIX trous, pas deux** : `users` (4 operations mutantes, 0 trace), `contact_persons` (3), `imported_supplier_invoices` (2), `profile`, `setup`, `companies::update`. ⚠️ **Le traçage se fait au REPOSITORY, pas a la route** — un `grep` au niveau du fichier de route conclut a un trou la ou il n'y en a pas, et l'inverse : `companies.rs` compte 2 insertions, mais dans `lock_books`/`unlock_books`, pas dans `update`. ⛔ Employer `NewAuditLogEntry::from_current_user` et non `::user` des que la route n'est pas admin-only. Convention des libelles relevee au sol : `.`. 25-1c-zero-audit-company-id: done # 2026-09-15 **REVUE DE CODE CLOSE EN 2 PASSES** : `1M → 1L`, 3 lentilles (Sonnet, Haiku, Sonnet) puis passe CIBLEE (Opus). Les deux findings retenus portent sur le MEME test de caracterisation (AC 8), aucune ligne de production prise en defaut ; le MEDIUM venait du montage prescrit par la spec. Gate de cloture **2330/2330**, base remise a zero. ✅ **GATE E2E DE CLOTURE — 214 passed / 19 skipped / 9 failed (8,3 min)**, montage COMPLET, `kesh_e2e` reconstruite (68 migrations), `/health` smtpConfigured:true ; **le binaire a demarre contre la migration sans erreur de checksum (P8)**. Les 9 : 7 KF-029 + 2 pollutions (`product-revenue-account:133`, `sidebar-navigation:75`) **vertes rejouees seules** ; branche sans aucun fichier frontend. **ZERO REGRESSION.** Prete pour le push. Aucune issue fermee (#378 appartient a la 25-1c). # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5). Gate backend **2330/2330** (4 skipped), base remise a zero et verifiee (68 migrations, 1 Admin) ; fmt + clippy verts. **+8 tests** (3 backfill, 3 alimentation, 2 import) + 1 assertion d'export ; **6 mutations sur 6 tuees par assertion**. Manuel admin corrige, PDF regeneres et verifies aplatis. `backup.rs`/`export.rs`/`import.rs` INTACTS. ⚠️ **E2E NON LANCEE — prerequis du push** (seule a voir un defaut P8). Prochaine etape : `bmad-code-review`. # 2026-09-15 **SPECIFIEE** — `25-1c-zero-audit-company-id.md`. Story-zero de la 25-1c, nee de l'arbitrage du 2026-09-11 (« la colonne et son backfill d'abord »). 18 AC en six volets, 10 taches. Colonne `BIGINT NULL` sans FK + index `(company_id, created_at)`, remplie par SOUS-SELECT dans `insert_in_tx` (aucun des 106 sites ne bouge — recompte de la passe 1 de validation ; l'epic disait 89). ⛔ **P7 : EXEMPTION PERISSABLE `PerishableSince(20260827000001)`**, precedent 24-5 — pas de rejeu, conformement a l'arbitrage ; fondement verifie par `git tag` (dernier tag v0.11.1, 2026-08-24). ⚠️ **Ecart releve avec l'epic** : apres restore, le point ouvert a DEUX sous-cas — identifiants differents (entrees montrees a personne) OU identiques (entrees d'une autre instance montrees a la societe restauree) ; l'epic ne nommait que le premier. Traite en CARACTERISATION ici, tranche en 25-1c. Aucune issue fermee. ✅ **ARBITRAGES RENDUS LE 2026-09-15 (Guy)** : (1) **AUCUNE RELEASE AVANT LE MERGE** de cette story — condition du fondement de l'exemption perissable ; (2) le partage mesure ici / tranche en 25-1c est valide. ✅ **BOUCLE DE VALIDATION CLOSE EN 4 PASSES** (2026-09-15) : max `MEDIUM → HIGH → HIGH → LOW`, soit `0C/0H/2M/1L → 0C/1H/4M/4L → 0C/1H/1M → 0C/0H/0M/1L` ; Sonnet+Haiku, Opus, Sonnet, Opus CIBLEE. ⚠️ Signal HIGH → HIGH de la regle de splitting SIGNALE au Project Lead : **pas de decoupage** (Guy, « continue »). ⛔ Le HIGH de la passe 2 : `admin-manual.tex:1786` affirme « il n'y a pas de colonne company_id » — l'AC 18 disait « aucun site », sur un grep aveugle au `\_` de LaTeX ; le manuel est desormais dans le perimetre (PDF a regenerer). Montage de l'AC 8 EXECUTE sur base jetable en passe 4. Prochaine etape : `bmad-dev-story`. 25-1c-a-journal-audit-route: done # 2026-09-19 **MERGEE — PR #439, squash `c7de37c4`**, trois checks verts ; #378 reste OUVERTE pour l'ecran (25-1c-b). 2026-09-16 **REVUE DE CODE : 7 PASSES · GATE DE CLOTURE EN COURS** — gate RELANCE EN ENTIER (la garde a ete reecrite 5 fois depuis T11, la cellule d'export a change, 4 documents et un PDF avec) : base remise a zero ET VERIFIEE (40 tables, 1 Admin), **backend 2372/2372** (fmt + clippy compris, 100,8 s), **frontend 0 erreur / 740 tests / build ✔**. ✅ **E2E 215 passed / 8 failed / 19 skipped (8,7 min)**, `kesh_e2e` reconstruite, montage COMPLET (avec `KESH_INBOX_DIR` et `KESH_DOCUMENTS_DIR` cette fois). Les 8 QUALIFIES UN PAR UN : **7 KF-029** + **`sidebar-navigation:75` = KF-046**, confirmee par REJEU ISOLE (elle echoue seule, donc pas de la pollution). KF-045 absente (run a 16:53 UTC) et `inbox-import:106` absent — la preuve que l'echec de l'apres-midi venait de MON montage. ⇒ **ZERO REGRESSION, zero pollution.** ⚠️ **REVUE CLOSE en 7 passes** (P8 non consommee, sur arbitrage du Project Lead : « clore la boucle »). Doc mise a jour : `user-manual.tex` (la consultation « ne peut pas encore etre produite » etait devenue faux — l'export CSV la produit), README (ligne v0.12.1), CHANGELOG (section `[Unreleased]` creee), `api-external.md`, manuel admin, glossaire. PDF regeneres et controles aplatis. **PRETE POUR LE PUSH** — rien n'est pousse, #378 reste OUVERTE (le `closes` appartient a la PR de l'ecran). — P7 CIBLEE (Sonnet) : **1 MEDIUM, zero CRITICAL ni HIGH** — une premiere depuis P1. Le MEDIUM : la distinction lifetime/litteral introduite a P6 n'etait gardee par RIEN (prouve par mutation EXECUTEE, invisible aux 10 tests comme aux 218 fichiers reels) ; nuance relevee par la lentille elle-meme : mon commit l'annoncait comme lacune assumee. ⛔ Ma PREMIERE tentative de correctif ne gardait rien non plus — l'epreuve l'a montre : sur `impl<'a> Chose<'a> {` le saut « large » va d'une apostrophe a l'autre SANS enjamber d'accolade, donc meme compte. ⇒ la propriete se teste DIRECTEMENT sur `accolades_hors_chaines`, avec le cas discriminant `impl<'a> Foo { fn g<'b>() {} }` = (2,2). EPROUVE : rouge. ⚠️ Lecon : tester une fonction par le detour de son appelant laisse passer ce que l'appelant ne distingue pas. **CLOTURE PROPOSEE au Project Lead** : P8 disponible mais rendement nul — le MEDIUM etait annonce par moi-meme, il est remedie et eprouve, la remediation ne touche AUCUNE ligne de production depuis P3, et le code de production n'est plus en cause depuis P2. # Avant : **6 PASSES** — P6 CIBLEE (Opus) : 3 HIGH, 2M, 1L, **tous sur ma garde, aucun sur le code de production**. ⛔ Ma garde symetrique etait VERTE PAR VACUITE : sur 99 fichiers portant `#[cfg(test)]`, **95** n'ont aucun item reconnu en colonne 0 apres l'attribut, et retirer le correctif qu'elle surveillait ne changeait RIEN sur le depot (0 fichier sur 218). ⛔ La VISIBILITE decidait de la protection : `pub(crate) fn` (28), `pub mod` (202), `pub use` (85) non reconnus. ⛔ Mon doc-comment affirmait « verifie » et disait FAUX : `loader.rs:386` porte `contains('{')` DANS son bloc de test, masque par ACCIDENT. ⇒ **SIMPLIFICATION** (recommandation de la lentille, annoncee au Project Lead a P5) : le balayage du depot est remplace par **4 entrees SYNTHETIQUES en dur**, decidables et independantes du depot. EPROUVEES : 2 mutations, 2 rouges, chacune nommant le cas. ⚠️ Verdict : « elargir l'enumeration ne ferait que deplacer la frontiere ; cout reel de deux passes, rendement mesure nul ». **P7 de confirmation, puis CLOTURE** — la remediation ne touche aucune ligne de production depuis P3. # Avant : **5 PASSES** — P5 CIBLEE (Sonnet, prompt VERSIONNE) : **1 CRITICAL** (l'angle que je lui avais donne, confirme par mutation compilee), 1M, 2L. ⛔ Mon appariement d'accolades ne sortait que si une accolade AVAIT ETE OUVERTE : un item garde sans bloc (`use …;`, `mod fixtures;`, `const …;`) faisait courir le saut jusqu'a la prochaine accolade du fichier — ou jusqu'a sa fin. Plus profond que la troncature que P4 avait remplacee ; dormant, mais `#[cfg(test)] mod fixtures;` est un idiome courant. ⛔ Et mon garde-fou restait VERT PAR EXCES : il ne verifiait que l'absence d'attribut residuel, or si le masquage avale tout il n'en reste aucun. ⇒ seconde sortie de boucle + garde SYMETRIQUE (le masquage ne retire aucun item de production). EPROUVE DANS LES DEUX SENS, une premiere dans cette boucle : correctif en place ⇒ l'action neuve est vue ; correctif retire ⇒ la garde nomme l'item perdu. ⛔ MEDIUM : SIXIEME decompte faux, dans le test cense fermer cette classe. ⚠️ **ARBITRAGE DE CONDUITE PORTE AU PROJECT LEAD** : severite non decroissante (HIGH en P4, CRITICAL en P5), production plus prise en defaut depuis P2, cinq passes sur UN SEUL fichier de test — ce detecteur vaut-il ce qu'il coute ? # Avant : **4 PASSES** — P4 CIBLEE (Opus, prompt VERSIONNE) : **1 HIGH**, 2M, 3L. ⛔ Ma coupe `#[cfg(test)]` fermait UNE variante du defaut et laissait l'autre : elle s'arretait au premier attribut QUEL QUE SOIT l'item garde. Or un attribut sur une METHODE (`invoice_email.rs:901`) ou un `mod tests` AU MILIEU (`version.rs:97`, `entities/user.rs:178`) sont ordinaires en Rust ⇒ **686, 256 et 75 lignes de PRODUCTION** hors balayage, dont `send_reminder_batch`, `check_downgrade_protection` (le garde-fou P2-bis) et `UserUpdate`. ⚠️ Et `invoice_email.rs` ECRIT DEJA de l'audit : la zone perdue appelait cette ecriture. ⛔ Le defaut n'etait pas la coupe seule mais MON DOC-COMMENT, qui declarait la classe fermee. ⇒ blocs desormais MASQUES par appariement d'accolades HORS CHAINES (ce que l'AC 18 demandait des l'origine). EPROUVE dans les DEUX variantes (2 mutations, 2 rouges) + controle que l'accolade d'un litteral ne casse rien. ⛔ MEDIUM : « six » corrige a UN site et laisse a DEUX AUTRES du meme fichier — regle de propagation enfreinte dans le commit qui corrigeait ce defaut ⇒ nombres doubles d'une ASSERTION. Garde 9/9. ⚠️ **P5 requise** (severite > LOW) ; la remediation ne touche toujours AUCUNE ligne de production. # Avant : **3 PASSES** — P3 CIBLEE (Sonnet, prompt VERSIONNE `25-1c-a-review-prompt-p3-ciblee.md`, sur le seul commit `9b47e3b8`) : **2 CRITICAL demontres par REPRODUCTION**, 1M, 2L — et les deux viennent de MON patch de P2. (1) le tamis appariait les guillemets sans suivre les ECHAPPEMENTS : un `\"` impair desynchronisait tout le reste du fichier, l'ensemble devenait VIDE, l'assertion ne s'executait plus et le trou se rouvrait, garde VERTE (reproduit : 7 litteraux → 0) ; (2) la coupe `#[cfg(test)]` portait sur la source BRUTE : `config.rs:406` porte cette chaine dans un DOC-COMMENT, 83% du fichier sortait du balayage — defaut DORMANT active par mon elargissement de P2. ⇒ PROPAGATION faite dans le meme patch (`relever()` portait le meme defaut sur TOUTES les crates) : une seule fonction `source_assainie`, coupe sur LIGNE ENTIERE. Les deux correctifs EPROUVES (2 mutations, 2 rouges). ⚠️ La 2e epreuve a d'abord rendu un FAUX NEGATIF : sonde posee en fin de fichier, donc dans les 93 lignes coupees — precaution prise pour config.rs et oubliee ici. ⚠️ **P4 requise** (severite > LOW) ; mais la remediation de P3 ne touche AUCUNE ligne de production, ce qui est le critere de cloture. # Avant : **2 PASSES** — P1 (Sonnet/Haiku/Opus, 3 lentilles) : 2H/5M/6L ; P2 (Opus/Sonnet/Haiku, braquee sur la REMEDIATION `8599035c..HEAD`) : 2H/4M/2L. ⛔ **Le HIGH de P2 porte sur MON correctif de P1** : il fermait le RENOMMAGE d'un code et laissait ouvert son AJOUT — une branche neuve passait les 7 tests, et la production aurait ecrit un code SANS LIBELLE. Le defaut de P1 deplace d'un cran, motif mesure du depot. ⇒ controle rendu BILATERAL, **eprouve par 3 mutations** (branche neuve, derivation dans un AUTRE fichier de route, code laisse en commentaire) : 3 rouges sur assertion. ⚠️ **Signal `HIGH → HIGH` SIGNALE au Project Lead** (regle de splitting) — mais les deux HIGH portent sur la remediation, non sur la conception. ⚠️ **P3 requise** (severite > LOW). ⚠️ **Deux arbitrages en attente** : `user-manual.tex:498-503` (« pas encore PRODUIRE » — l'export CSV le fait) et « on termine » = 25-1c-a seule ou #378 entiere. # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5) — les ONZE taches cochees, RIEN DE POUSSE. ⛔ Perimetre FIGE `main..8599035c` (cloture de l'implementation, avant la revue) : **25 commits, 29 fichiers, +5217/-41**. Decomptes recomptes depuis la source, perimetre `main..HEAD` : **+42 tests** (7 depot, 4 `util`, 5 module, **11 garde**, 15 E2E — recompte apres la passe 1 de revue, qui ajoute 3 tests a la garde ; la fiche a annonce « +35 » tant qu'elle en portait 4), **133 cles** par locale x 4, etat **DATE a la cloture de P5** (une mesure « actuelle » se perime a chaque commit — celle-ci l'avait deja fait deux fois) : 37 commits, 34 fichiers, +6218/-44. ⚠️ « 24 commits » et « +5206 » etaient FAUX des l'ecriture (25 et +5217) — releve par la passe 2. Gates : backend **2365/2365** (fmt + clippy compris), frontend **740/740** + build, base remise a zero ET verifiee (40 tables, 1 Admin). ✅ **E2E 214 passed / 9 failed / 19 skipped (8,3 min)** — les neuf qualifies UN PAR UN : 7 KF-029, `sidebar-navigation:75` = pollution (PASSE rejoue seul, donc pas la KF-046), et `inbox-import:106` = **defaut de MON montage** (KESH_INBOX_DIR/KESH_DOCUMENTS_DIR manquants ; diagnostic VERIFIE au log, spec rejouee ⇒ passe en 1,5 s). KF-045 absente, et c'est correct : run a 14:34 UTC. ⇒ **ZERO REGRESSION.** Epreuve par mutation : **9 mutations, 9 rouges SUR ASSERTION** (aucune sur compilation), fichiers restaures et identite verifiee. ⚠️ **Trois points laisses au Project Lead** : ecart garde/spec sur le decoupage de l'inventaire des sites indirects (six de part et d'autre, mais pas les memes six) ; libelle de `reconciliation.split_applied` (« Ventilation appliquee » arbitre vs « Eclater » au catalogue) ; emojis du manuel qui ne s'impriment pas (anterieur a la story). ⚠️ **Question en suspens : « on termine » = 25-1c-a seule, ou #378 entiere avec l'ecran ?** Prochaine etape : `bmad-code-review`. # Avant : **EN COURS** — PR #437 fusionnee (squash 842eaac0), pile rebasee, base de dev remise a zero et verifiee (40 tables, 1 Admin), revalidation close en 4 passes (1H/3M/3L -> 2M -> 1M/3L -> 1L), 122 libelles francais arbitres. Avant : **VALIDEE — boucle close en 5 passes** : `1C/1M/2L → 5M/8L → 1M/1L → 2M/2L (ciblee) → 1L (ciblee)` ; Sonnet+Haiku, Opus, Sonnet, Opus, Sonnet. ⛔ Stagnation MEDIUM sur 3 passes, toujours sur les bornes de date : la forme « created_at < date_to + 1 jour » a ete ABANDONNEE pour des bornes inclusives 23:59:59.999 (eprouvees sur base jetable). **Ne pas implementer avant le merge de #437** (rebase par SHA `42b6aac0`). **SPECIFIEE** — `25-1c-a-journal-audit-route.md`, 22 AC en six volets, 11 taches. `GET /api/v1/audit-log` + `/export.csv` dans `comptable_routes`, `ensure_not_pat` en handler, DTO dedie (`actorType` via `as_str`, `createdAt` en UTC explicite), `csv_sanitize` EXTRAITE d'`invoices.rs`, export audite `audit_log.exported`. ⚠️ **Trois** choix de conception laisses a confirmer en revue : inclusion des entrees sans societe, audit de l'export, codes bruts (non traduits) dans les cellules du CSV. Branche empilee sur la 25-1c-zero, a rebaser apres le merge de #437. [#378] — **issue du SPLIT de la 25-1c** (arbitrage de Guy, regle de splitting : 7 modules). La ROUTE de consultation du journal d'audit et son export CSV, cote backend. Acces **Comptable + Admin**, ni Consultation ni cle API ; filtre **strict par `company_id`** (l'import restaure la MEME installation) ; entrees sans societe montrees « societe indeterminee » (choix par defaut, non arbitre explicitement). Cf. `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15*. - 25-1c-b-journal-audit-ecran: backlog # 2026-09-15 [#378] — **issue du SPLIT de la 25-1c**. L'ECRAN : page, entree de menu, libelles des 28 types d'entite dans les 4 langues, E2E, manuels (qui promettent tous que la consultation « manque encore »). Affichage sobre : code d'action brut, detail en JSON indente. Vocabulaire fige : « journal d'audit » / Audit-Protokoll / registro di audit / audit log. **Portera le `closes #378`.** + 25-1c-b1-journal-audit-ecran: ready-for-dev # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en natifs sur le vocabulaire, plus de carte de libelles ni de colonne Societe. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : MED en passes 3 à 6, tous introduits par la remédiation précédente, puis 0 après le retrait d'une assertion E2E trois fois corrigée. **issue du SPLIT de la 25-1c-b** (arbitrage de Guy, signal MEDIUM → MEDIUM en passe 2 de validation). L'ECRAN : feature `audit-log`, page, garde Comptable+, menu, carte des **29** types d'entite (dont `audit_log`, cree par la 25-1c-a), i18n et gardes, tests unitaires et E2E. Module partage `download.ts` neuf ; le regroupement des 7 copies existantes part en issue de dette. Livree dans la MEME PR que la b2 ; porte `closes #378`. [#378] - 25-1c-b2-journal-audit-textes: ready-for-dev # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] + 25-1c-b1-journal-audit-ecran: ready-for-dev # 2026-09-26 **R5 DERIVE : 1 MEDIUM / 3 LOW corriges** (97 actions, 138 cles ; lignes des gardes i18n) ; contrat de la route mergee confirme. Prochaine etape : dev. # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en ` 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-15** — **Fiche créée par le split de la 25-1c-b** (arbitrage du Project Lead), après la passe 2 @@ -699,6 +764,7 @@ de texte de spec, sans assertion ajoutée ni ligne de code. | passe | modèle | rendu (après reclassement) | |---|---|---| +| 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. | | 1 | Sonnet + Haiku | 2 M, 2 L | diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index 2844905b7..7230e37a2 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -330,7 +330,7 @@ development_status: 25-1b-trous-alimentation: done # 2026-09-13 ✅ **GATE E2E DE CLOTURE — 216 passed / 19 skipped / 7 failed (8m42)**, montage COMPLET, base `kesh_e2e` DETRUITE ET RECONSTRUITE, `/health` controle avant lancement. ✅ **LES 7 ECHECS SONT EXACTEMENT LA BASELINE KF-029** — aucune pollution, aucune KF-045 (run d'apres-midi), aucune KF-046. Attendu 8 a 10, observe **7** : la baseline PURE, le meilleur resultat possible. Recoupement : 216+19+7 = 242 comme au run du 11 (214+19+9), les 2 tests de plus qui passent sont les 2 pollutions non declenchees ⇒ **ZERO REGRESSION**. ⚠️ **LE BUILD FRONTEND ETAIT EN RETARD ET A ETE REFAIT** : date du 11/09 11:40 alors que le dernier commit touchant `frontend/` (celui de la 25-1a, merge depuis dans `main`) est de 19:18. *Cette story ne touche pourtant PAS le frontend* — c'est le piege de la 24-5 : **un gate declare vert sur un perimetre qui a bouge n'est plus un gate**. **LES TROIS GATES SONT VERTS** : backend **2322/2322**, E2E **216/7-attendus**, frontend et i18n NON TOUCHES (verifie par `git diff`). **Reste : push + PR avec `closes #379` DANS LE TITRE** (squash-merge : le titre devient le message du commit). # 2026-09-13 2026-09-12 **IMPLEMENTEE** — **14 routes tracees**, gate backend COMPLET **2318/2318** (4 skipped, 90 s), fmt et clippy propres, frontend et i18n **NON TOUCHES (verifie)**. ⛔ **CINQ DEFAUTS QUE SEULE L'EXECUTION A REVELES, ET CE SONT TOUS DES DETECTEURS MAL FORMES — LES MIENS** : (1) `creditor_iban` n'est PAS `Option` — le compilateur a rejete le `is_some()` que la spec prescrivait ; (2) ⛔ **les 4 helpers d'audit CASSAIENT LE GATE** — `tests/common/mod.rs` est compile SEPAREMENT dans chaque binaire de test, donc un helper employe par 10 fichiers sur 40 est « jamais utilise » dans les 30 autres et `clippy -D warnings` refusait ; (3) ⛔ **mon extracteur du registre rendait un ensemble VIDE** sur `test_endpoints.rs` (forme `.route("/x", post(h))` et non `post(routes::m::h)`) et **comparait donc deux vides AVEC SUCCES** ⇒ garde contre l'ensemble vide ; (4) ⚠️ **mon controle de PDF a rendu un FAUX NEGATIF** sur une apostrophe TYPOGRAPHIQUE cherchee en apostrophe droite — le piege que le `CLAUDE.md` documente, pris sur le fait ; (5) ⛔ **ma remise a zero de la base de gate etait INCOMPLETE puis OMISE** : seed saute (29 tests tombes sur « need at least one Admin user in DB »), puis base non reconstruite entre deux runs alors que la regle est **INCONDITIONNELLE**. ⚠️ La 1re erreur a ete MASQUEE par une **verification mal formee** : elle comptait migrations et tables — ce qui etait commode — au lieu de ce dont les tests ont besoin. *Verifier n'est pas executer ; mais verifier la MAUVAISE CHOSE ne vaut pas mieux que ne rien verifier.* ✅ **Un test a rougi A RAISON, et c'etait LE TEST** : il comptait les traces globalement alors que le chemin d'amorçage ecrit desormais la sienne. Les 3 compteurs globaux du depot inspectes ensuite : aucun casse. ✅ **DEUX gardes eprouvees par MUTATION** : la garde no-op de `users` et celle du registre. ✅ **Trou de couverture ferme** : `contact_persons_e2e.rs` cree — ces 3 routes n'avaient AUCUN test. ✅ **Registre de 108 routes** (105 `lib.rs` + 3 `test_endpoints.rs`), diff ENSEMBLISTE par identite (105 couples verbe+handler, 0 doublon), ventilation recomptee : **87 tracees / 15 exemptees avec numero d'issue / 3 sans matiere**. ✅ **8 sites de manuel corriges**, 3 PDF regeneres, verifies sur les PDF **APLATIS**. **Decompte recoupe : 2307 + 11 tests neufs = 2318.** ⚠️ **E2E AU PUSH.** Prochaine : `bmad-code-review` passe 1, trois lentilles. # 2026-09-11 **VALIDATE 2026-09-11 **VALIDATE PASSE 1** (Sonnet 4.6 + Haiku 4.5, contextes frais, orthogonales a l'auteur Opus 5, prompt versionne) : **1 CRITICAL, 1 HIGH, 3 MEDIUM, 2 LOW** — tous verifies au sol, tous patches. ⇒ **12 AC (3 volets), 11 taches** (recomptes depuis la source : `grep -cE '^\*\*[0-9]+\. '`). ⛔ **LE CRITICAL EST UN INVARIANT INTENABLE SUR UNE SEULE ROUTE** : `imported_supplier_invoices::create` ecrit **sur le POOL** (`:61`) et **aucun `create_in_tx` n'existe** — a la difference de `mark_completed` et `mark_discarded`, tous deux transactionnels dans le MEME fichier. La spec ne prevoyait que le threading de l'acteur ⇒ la trace se serait ecrite dans une transaction DISTINCTE, apres l'autocommit : *exactement la fenetre non atomique que l'AC 9 ferme sur les douze autres routes*. Un developpeur ne l'aurait decouvert qu'en codant. ⛔ **LE HIGH VIENT DE L'ORCHESTRATEUR, EN VERIFIANT UN MEDIUM DE HAIKU** : la lentille signalait UN champ faux (`metadata_json`) au manuel ; il y en a **TROIS sur sept** — `company_id` **QUI N'EXISTE PAS** (et que l'arbitrage du jour vient de decider d'ajouter), `timestamp` pour `created_at`, `metadata_json` pour `details_json` — et **quatre manquent**. Surtout, la lentille n'a PAS releve l'affirmation qui OUVRE la section : `admin-manual.tex:1782` « **TOUTE** action metier mutante est enregistree » — faux pour 28 routes, et **TOUJOURS faux apres cette story** pour 15. **Le manuel se contredit lui-meme a 170 lignes d'ecart** (`:1956` avoue la couverture partielle) : motif exact paye par la 25-1a sur la section OLICo. ⇒ **AC 12 ajoute**. ⚠️ **MEDIUM de Sonnet sur l'AC 11 — le detecteur etait MAL FORME** : comparer un NOMBRE ne detecte pas une route retiree pendant qu'une autre est ajoutee ; `admin_pat_denied_e2e` peut compter parce qu'il opere sur un BLOC CLOS entre marqueurs, pas les 105 routes eparpillees ⇒ **diff ENSEMBLISTE par identite**. ⚠️ 2 LOW : deux references de ligne derivees (`companies.rs` le `if` est a `:181` pas dans 175-178 ; `audit.rs` le trait est a `:18`, son impl a `:33`). ✅ **1 finding de Haiku ECARTE** : `contact_persons.rs:5` etait **deja nomme par l'AC 2**. ✅ **Grep de propagation fait AVANT la passe 2** : symptome **circonscrit au manuel admin FR** (DE/EN/IT n'ont qu'un README). ⛔ **TROUVE EN CHEMIN, HORS PERIMETRE** : `user-manual.tex:503` affirme « aucun verrou de periode plus fin n'existe encore » alors que `:444-453` **le documente** — le manuel se contredit a 50 lignes d'ecart depuis la 24-4c. A TRACER. # 2026-09-11 **VALIDATE PASSE 2** (Opus 5, lentille unique, contexte frais, prompt versionne, braquee sur les 7 patches de la P1) : **0 CRIT, 3 HIGH, 5 MED, 6 LOW** — severite en RECUL (CRIT → HIGH) ⇒ **pas de split**. ⚠️ **13 findings sur 15 sont D'ORIGINE** — 1 HIGH et 1 LOW seulement nes d'un patch : *l'INVERSE du motif habituel du depot, la remediation n'a presque rien casse, c'est la conception qui etait incomplete*. ⛔ **P2-1 HIGH — UN TROISIEME CHEMIN D'ENTREE** : la REACTIVATION d'une piece ecartee (`inbox_import.rs:418-431`) rend `Accepted` **sans rien creer** ⇒ la piste garderait `discarded` pour derniere trace d'une piece `to_complete`. *Un journal qui affirme le contraire de la base est pire qu'un journal muet.* Et `reactivate_to_complete` porte le MEME defaut que `create` (`.execute(pool)`), a 60 lignes de la : **le patch de la P1 avait ferme la moitie du trou**. ⛔ **P2-2 HIGH — UNE JUSTIFICATION FONDEE SUR UN FAIT INEXISTANT** : la spec disait « la creation du premier admin n'est pas tracee ALORS QUE SON CHEMIN JUMEAU L'EST » ; `auth/bootstrap.rs:123` cree le meme admin et **n'ecrit rien** (le seul `insert_in_tx` du fichier, `:252`, est le break-glass). Et ce chemin **n'est pas une route** ⇒ hors des 105, hors [#434]/[#435] ⇒ **ecrit NULLE PART**. ⛔ **P2-3 HIGH, NE DU PATCH P1 — UN BALAYAGE DECLARE COMPLET NE L'ETAIT PAS** : la P1 avait ecrit « symptome circonscrit au manuel admin » ; il restait **`admin:2184`** (glossaire, meme promesse 400 lignes plus loin — la corriger seule REINSTALLE la contradiction) et **`user-manual:1591-1594`** (un AUTRE manuel). Le grep portait sur `metadata_json`, **UN des TROIS symptomes** de l'AC 12. *Greper un symptome n'est pas greper le defaut* — **sixieme fois** que ce geste est pris en defaut sur ces deux stories. ⇒ AC 12 passe a **CINQ sites, deux manuels**. ⚠️ P2-4 : l'AC 8 disait « trois routes », il y en a **QUATRE** (2 `users` + 2 `companies` partagent leur repository). ⛔ **P2-5 : LE COMMENTAIRE PUBLIE SUR [#431] ETAIT FAUX** — 35 sites annonces, **38** reels, sa propre ventilation sommant a 41 (les 3 de `mod tests` soustraits DEUX FOIS). **Rectifie sur l'issue le meme jour.** *Recompter depuis la source vaut aussi pour ce qu'on vient d'ecrire ailleurs.* ⚠️ P2-6 : deux silences, deux mecanismes — les `410`/`500` rollbackent, les quatre `400` sont AVANT le `begin`. ⚠️ P2-7 : **l'ensemble clos est celui de `lib.rs`, PAS du backend** — 3 routes mutantes vivent dans `test_endpoints.rs` via le `nest()` de `:995` ⇒ a exempter NOMMEMENT. ⚠️ **P2-8, ne du patch P1 : « transaction portee par `process_one_file` » ne disait pas OU** — pool a **5 connexions** (`main.rs:73`), `GET_LOCK` en tient deja une ⇒ la poser autour de la SEULE insertion (`:488-516`), sinon une connexion *idle-in-transaction* par fichier, jusqu'a 200 par run. ✅ Verifie en P2 et a ne pas re-debattre : le `GET_LOCK` n'interfere pas, la transaction PAR FICHIER est le bon grain, l'identite verbe+handler est discriminante (105 couples, 0 doublon). ⚠️ **Axe NON exerce declare par la lentille** : rien n'a ete COMPILE — la faisabilite des `_in_tx` reste raisonnee, pas prouvee par le compilateur ; et **la « 1 partielle » et les « 3 sans matiere » de l'inventaire ne sont nommees nulle part** (faiblesse reelle de l'inventaire). # 2026-09-12 **VALIDATE PASSE 3** (Sonnet 4.6, contexte frais, prompt versionne, DEUX FRONTS : les patches de la P2 **et les axes que la P2 declarait NON EXERCES**) : **0 CRIT, 1 HIGH, 2 MED, 2 LOW** — **TROISIEME RECUL CONSECUTIF** (1C/1H → 3H/5M → 1H/2M), convergence monotone, **les 5 sont D'ORIGINE**. ⛔ **P3-1 HIGH — T6 CONTREDISAIT L'AC 9 EN SILENCE** : elle ne disait ni d'extraire `update_step_in_tx`, ni qui mene la transaction. Pire : `onboarding::update_step` a **d'autres appelants** (le seed + 8 routes d'onboarding) ⇒ poser la trace DANS le repository ferait ecrire `installation.ui_mode_changed` **a chaque etape de l'onboarding**. *Une trace au mauvais etage ne manque pas : elle ment.* ⛔ **P3-2 — UN SIXIEME SITE DE MANUEL, ET IL PARLE D'ATTRIBUTION, PAS DE COUVERTURE** : `admin:1762` promet que les mutations par cle API sont tracees `actor_type='api_key'` — **faux pour les 38 sites de [#431]**, que cette story ne corrige pas. *Un inventaire construit sur une seule question ne trouve que les reponses a cette question* : cinq relectures cherchaient « ce qui est trace », celui-ci dit « comment c'est attribue ». **SEPTIEME defaut de manuel sur ces deux stories.** ⚠️ P3-3 : « 1 partielle » n'avait pas de PORTEE declaree — [#434] en documente une seconde (`onboarding/finalize`) ⇒ le chiffre a ete relu contre le mauvais perimetre. ⚠️ P3-5 : les 3 routes de `test_endpoints.rs` doivent sortir du DIFF AUTOMATIQUE (elles n'ont jamais ete dans `lib.rs`) — *un garde qui rougit toujours est un garde qu'on desactive*. ✅ **REPRENDRE LES AXES NON EXERCES PAIE** : l'axe « contenu de [#434] » a rendu P3-3, l'axe « faisabilite » a rendu P3-1. *Une passe qui declare honnetement ses manques rend la suivante plus efficace que si elle avait bluffe.* ✅ Verifie et a ne pas re-debattre : 105 routes **recomptees independamment** par script depuis `lib.rs` ; frontend et i18n **exerces, 0 finding** ; aucun changement de schema ; `cargo check --workspace --all-targets` propre ; les 5 sites de manuel exacts a la ligne. ⚠️ **Axes NON exerces declares** : les **73 routes reputees tracees** ne sont toujours pas verifiees UNE PAR UNE (coherence par fichier seulement) ; `cargo clippy` non lance ; la faisabilite du code a ecrire reste **raisonnee, non prototypee**. # 2026-09-12 **VALIDATE PASSE 4** (Haiku 4.5, contexte frais, prompt versionne) : **1 MEDIUM** apres reprise d'axe par l'orchestrateur. ⛔ **LA LENTILLE A RENDU 0 FINDING EN DECLARANT N'AVOIR TRAITE QUE 4 ROUTES SUR 73** — ni le 7e site du manuel, ni les recomptes ⇒ **NE COMPTE PAS COMME PASSE** (regle « une passe qui ne declare pas ses axes exerces »). ✅ **Mais sa FRANCHISE a fait son travail** : *un « 0 finding » adosse a la liste de ce qui n'a PAS ete fait vaut infiniment mieux qu'un « 0 finding » qui se tait* — contraire exact du precedent de la 24-5. Les 3 axes repris par l'orchestrateur. ✅ **AXE PRINCIPAL EXERCE AUTREMENT** : non route par route, mais par les **94 appels d'audit** du backend et leur gouvernance conditionnelle ⇒ **8 sous condition, les 8 justifies** (garde no-op, lot vide, verrou qui recule, best-effort, complementarite) ⇒ **aucun trou de couverture nouveau**. ⚠️ **Ce que la methode NE VOIT PAS, et c'est ecrit** : une route dont un chemin de succes CONTOURNERAIT le repository qui trace. ⛔ **P4-1 — UN SEPTIEME SITE DE MANUEL, ANGLE ENCORE INEDIT : L'EXPORTABILITE.** `admin:1935` gage la conformite **OLICo art. 9 al. 3** sur un ZIP d'export « garanti par `audit_log` » — or l'export porte **19 tables sur 38 et PAS `audit_log`** ([#386]). *Sept sites, SEPT QUESTIONS DIFFERENTES : couverture, champs, attribution, exportabilite — chacun trouve en posant une question que le precedent ne posait pas. Un inventaire documentaire ne se clot pas par l'epuisement des FICHIERS mais par celui des QUESTIONS.* ⛔ **HORS PERIMETRE, PORTE A [#431]** : `reconciliation_rule.deleted` ecrit son attribution **DIFFEREMMENT SELON L'EFFET** — `::user` quand la suppression aboutit (repo `:402`), `from_current_user` quand elle est no-op (route `:473`). La couverture est entiere, l'attribution non : *un reviseur qui filtre sur `actor_type='api_key'` verra les suppressions SANS effet et manquera celles qui en ont eu un.* **L'incoherence vaut pire que l'absence.** ⛔ **HORS PERIMETRE, PORTE A [#386]** : le site `:1935`, avec la correction INVERSE a prevoir quand l'export integrera la piste. **TREND : 1C/1H/3M/2L → 0C/3H/5M/6L → 0C/1H/2M/2L → 1M.** Convergence monotone depuis la P2. # 2026-09-12 **VALIDATE PASSE 5 CIBLEE** (Opus 5, contexte frais, prompt versionne, perimetre = LA REMEDIATION des P3/P4) : **0C, 0H, 4 MED, 5 LOW** ⇒ **LA BOUCLE NE PEUT PAS SE CLORE** (critere : plus rien au-dessus de LOW). ⛔ **P5-1, NE DU PATCH P3 — UN PATCH QUI ANNULE LE BENEFICE QU'IL INVOQUE** : sortir les 3 routes de `test_endpoints.rs` du diff ENTIER rend une QUATRIEME route de ce fichier aussi invisible qu'avant, alors que la phrase « sans quoi une 4e route ne ferait rougir aucun garde » a SURVECU au patch qui la rendait fausse ⇒ exclusion du **SECOND VOLET seulement**, l'extracteur lit les DEUX fichiers. *Corriger un mecanisme sans relire ce qui le motivait produit un texte qui se refute lui-meme.* ⛔ **P5-2 — « PROPAGE PARTOUT » EST UNE AFFIRMATION, DONC ELLE SE VERIFIE** : mon commit annonçait « propage aux SIX enonces », il y en avait **HUIT** — l'AC 7 n'assignait de constructeur qu'a TREIZE routes (la 14e, `complete`, absente du tableau) et T4 s'intitulait « deux routes » pour trois. *Celui qui corrige une derive de compteur n'est pas immunise contre elle* — c'est le defaut meme que la P2 avait reproche a la spec. ⛔ **P5-3 — UN HUITIEME SITE DE MANUEL, ET IL AMENDE LA LEÇON DE LA P4** : `user-manual:1733` (glossaire) porte la meme promesse que `:1591-1594` **plus un renvoi vers une section qui n'existe pas**. Les 7 premiers avaient chacun demande une QUESTION NEUVE ; le 8e repond a une question **DEJA POSEE, dans un fichier qu'elle n'avait pas visite**. ⇒ *Un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT**.* La leçon de la P4 etait vraie ET INCOMPLETE : elle orientait vers des themes neufs et detournait du balayage exhaustif. ⚠️ P5-4 : l'AC 12 s'intitulait « le manuel administrateur… TROIS affirmations » quand sa table en listait SEPT dans DEUX manuels — les P3 et P4 avaient incremente la ligne JUSTE EN DESSOUS sans toucher celle-la. ⚠️ P5-5 : « sept des quatorze chemins » → **SIX** (5 autres n'ouvrent AUCUNE transaction, 3 en ont deja une au handler : 6+5+3=14). ✅ **CONFIRME AU SOL par la P5** : les 94 appels / 8 conditionnels, les 19 tables d'export sans `audit_log`, la 2e partielle de [#434], la faisabilite de T6 (10 appelants, ZERO changement de signature), les 7 sites precedents, et **105 routes re-derivees independamment**. ⛔ **ARBITRAGE DEMANDE AU PROJECT LEAD** : la severite MAXIMALE recule encore (CRIT → HIGH → HIGH → MED) mais le NOMBRE de MED remonte (2 → 4). La § *Regle de splitting preventif* vise « une passe N+1 qui remonte une severite EGALE OU SUPERIEURE (e.g. MED → MED) ». **Recommandation de l'orchestrateur ET de la lentille : PAS de split** — aucun des 4 MED ne touche la conception (deux totaux non propages, un site documentaire, un garde dont le patch a vide la justification) ; c'est le motif Epic 16 *« le compte rendu est devenu le lieu du defaut, pas le fond »*. # 2026-09-12 **VALIDATE PASSE 6 CIBLEE** (Sonnet 4.6, contexte frais, prompt versionne, perimetre = LE SEUL COMMIT `2f87b1aa`) : **0C, 0H, 2 MED, 0 LOW** — **LES DEUX NES DU CORRECTIF DE LA P5**. ⛔ **P6-2 — UN PATCH MET A JOUR LE CRITERE ET OUBLIE LA TACHE QUI L'EXECUTE** : l'AC 12 comptait HUIT sites, **T10 en listait encore SEPT**. *Un developpeur suivant la tache a la lettre aurait corrige 7 sites sur 8 — et le gate serait reste VERT.* ⚠️ P6-1 : « quatre autres n'ouvrent aucune transaction » alors que le tableau JUSTE EN DESSOUS en liste CINQ (6+5+3=14). ✅ **Les deux verifications demandees sont FAVORABLES** : le constructeur de `complete` est EXACT (route dans `comptable_routes` ⇒ atteignable par jeton ⇒ `from_current_user`), et **AUCUN neuvieme site** — le manuel utilisateur ne promet rien sur les champs, l'attribution ni l'exportabilite. ⛔ **LE MOTIF ETAIT DEVENU CLAIR — la P5 corrige, la P6 trouve deux defauts DU PATCH P5 — et une passe de plus l'aurait RECONDUIT.** ⇒ § *Propagation post-patch* appliquee par l'orchestrateur sur **TOUS** les enonces numeriques du document. Elle a rendu : (a) l'ambiguite **105 vs 108** entrees au registre, que la P6 signalait sans la trancher — l'inventaire porte sur `lib.rs` (105), **le registre est plus large** (108), et c'est voulu ; (b) ⛔ **UN DERNIER PIEGE QUE SIX PASSES ONT LAISSE PASSER** : « les conventions, verifiees sur les **73 SITES** en place » — ce 73 designait les **LIBELLES D'ACTION**, pas les 73 routes tracees. **Deux grandeurs sans rapport portant le MEME NOMBRE sont indetectables a la relecture** ; seul un balayage qui interroge **ce que chaque nombre COMPTE**, et non sa valeur, pouvait le voir. *C'est la § « greper la VALEUR, pas la formulation » RETOURNEE : ici la valeur etait juste et l'OBJET faux.* L'enonce nomme desormais son objet. # 2026-09-12 ✅ **BOUCLE DE VALIDATE CLOSE — 7 PASSES** : **1C/1H/3M/2L → 3H/5M/6L → 1H/2M/2L → 1M → 4M/5L → 2M → 0**. **Severite maximale : CRIT → HIGH → HIGH → MED → MED → MED → RIEN**, decroissance STRICTE. Rotation complete (Sonnet+Haiku → Opus → Sonnet → Haiku → Opus → Sonnet → Haiku), **7 prompts versionnes**, 3 passes CIBLEES. **PASSE 7** (Haiku 4.5, ciblee sur `1f02097f`) : **0 finding**, 4 axes exerces, axes non exerces declares et tous LOGIQUEMENT IMPOSSIBLES a ce stade (verifier des PDF regeneres pour une spec non implementee). **Les 2 affirmations principales REPRISES par l'orchestrateur** — *un « 0 finding » se verifie comme un finding* : plus aucun « sept sites » hors Change Log, somme du tableau **6+5+3=14**. ⚠️ **Le controle a produit un FAUX POSITIF INSTRUCTIF** : un test cherchant « audit » sur `admin:1786` l'a signalee vide — elle enumere des NOMS DE COLONNES et ne contient pas le mot. *Un detecteur mal forme coute le meme diagnostic qu'un defaut reel* — jusque dans la verification d'une cloture. **CE QUE LA BOUCLE LAISSE AU-DELA DE LA STORY** : (1) ⛔ *un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT*** — 8 sites de manuel = 4 questions x 2 fichiers ; (2) ⛔ **deux grandeurs differentes portant le MEME NOMBRE sont indetectables a la relecture** (« 73 » = routes tracees OU libelles d'action) ⇒ interroger ce que chaque nombre COMPTE, pas sa valeur ; (3) ⛔ **un patch met a jour le CRITERE et oublie la TACHE qui l'execute** — 7 sites corriges sur 8, **gate vert** ; (4) ⛔ **quand une passe ne trouve plus que les defauts de son propre patch, ajouter une passe RECONDUIT le motif** — ajouter le GESTE, et le faire soi-meme ; (5) ✅ **une passe qui declare honnetement ce qu'elle n'a pas fait vaut mieux qu'une passe qui bluffe** (la P4 a designe ou chercher, 2 findings de la P3 en sont nes). **Statut `ready-for-dev`. Prochaine : `bmad-dev-story`.** ⛔ **L'INVENTAIRE DEPLACE LE PERIMETRE** : balayage de l'ENSEMBLE CLOS des **105 routes mutantes** du backend ⇒ **73 tracees, 28 NON tracees, 1 partielle, 3 sans matiere**. Le releve des six trous est EXACT mais INCOMPLET : deux familles entieres manquaient — **`onboarding` (11 routes)**, dont `accounts::bulk_create_from_chart` (non audite alors que les 4 autres mutations d'`accounts` le sont), la creation du compte bancaire, le seed et le reset — le code le sait, `routes/onboarding.rs:523-527` porte un TODO — et **`auth`/session (4 routes)**, ou `change_password` (`auth.rs:511`) n'ecrit RIEN alors que `reset_password`, EFFET MATERIEL IDENTIQUE, audite (`auth.rs:948`) ; y compris la revocation en masse pour VOL DE JETON (`auth.rs:382`). ⇒ **8 familles > 5 modules : la § Regle de splitting prefentif s'applique** ⇒ story tenue au perimetre de [#379] (6 familles, **13 routes**), les deux decouvertes sortent en issues propres. ✅ **ISSUES OUVERTES LE 2026-09-11 : [#434]** (onboarding, 11 routes) et **[#435]** (`auth`/session, 4 routes) — l'AC 11 les cite au registre des exemptions. ⚠️ **[#435] porte une question de conception** : tracer chaque `login`/`refresh` ferait de la piste un JOURNAL DE SESSIONS ; les 4 routes ne se valent pas (`change_password` et `theft_detected` sont rares et lourds, `login`/`refresh` sont du trafic). ⛔ **LE FAIT STRUCTURANT N'EST PAS DANS L'ISSUE** : `insert_in_tx` ne prend qu'une `&mut Transaction`, or **7 des 13 chemins** passent par un repository qui OUVRE ET COMMITE SEUL ⇒ le handler n'a rien ou greffer. Decision: extraire des variants `_in_tx` plutot que descendre `user_id` — parce que (1) `update_role_and_active` sert DEUX routes qui doivent ecrire DEUX libelles, (2) `companies::update` et `onboarding::update_step` ont d'autres appelants, (3) **descendre `user_id` NU est le mecanisme meme de [#431]**. ⚠️ **4 routes sont quasi gratuites** (`discard` et `setup/admin` : tx deja ouverte ; `POST /users` et `/reset-password` : variant `_in_tx` deja ecrit POUR CELA) — commencer par elles. ⛔ **Le critere du constructeur n'est PAS le role mais « un PAT peut-il atteindre ce chemin »** : `require_not_pat` est pose **UNE SEULE FOIS** (`lib.rs:330`, bloc `admin_routes`). ⇒ `::user` sur les 4 `users` + 2 `companies` ; `from_current_user` sur les 3 `contact_persons`, `discard`, `profile` ; `for_actor` threade sur `inbox-import` ; `::user(user.id)` sur `setup` (PRE-AUTH, acteur = la cible). ⚠️ **`user.role_changed` porte son propre libelle** — precedent `books.unlocked` vs `books.restored` : « confondre les deux rendrait le filtre inutilisable pour le reviseur ». ⚠️ **`profile/mode` trace l'INSTALLATION, pas l'utilisateur** : `onboarding_state` est MONO-LIGNE ET GLOBALE, sans `company_id` — un utilisateur bascule l'installation entiere en mode Expert. ⛔ **TROU DE COUVERTURE DECOUVERT** : les 3 routes `contact_persons` n'ont **AUCUN test**, ni integration ni E2E ⇒ fichier a creer (on n'ajoute pas une trace a du code que rien n'exerce). ⚠️ **AC 11 — registre des routes mutantes + garde** : la story ferme 13 des 28 sites ; les 15 restants deviennent des angles morts ASSUMES, et un angle mort ecrit nulle part redevient un oubli au premier ajout de route. Le test ne peut PAS verifier qu'une route est tracee (le tracage est au repository, a 3 appels de distance) — il verifie que toute route mutante a ete EXAMINEE, seule propriete decidable. ✅ **[#431] CORRIGEE le 2026-09-11** (commentaire) : **15 repositories de production et 35 sites**, l'issue en annoncait 10 ; +`audit_log.rs` dont les 3 occurrences sont dans `mod tests`. Les 6 ajoutes : `companies`, `contacts`, `invoices`, `invoice_settlements_write`, `reconciliation_rules`, `supplier_invoices`. ⚠️ `invoices.rs` porte **LES DEUX FORMES** (`::user` x5 ET `for_actor` `:2112`) — *aucun decompte par fichier ne montre une migration a moitie faite*. Les references de ligne de l'issue avaient derive (`require_not_pat` est a `lib.rs:330`, pas `:319`). Aucun changement de schema ⇒ ni P2-bis/P3/P5/P6/P7/P8. **Prochaine : `bmad-create-story validate` passe 1.** # [#379] + decouvertes de la passe 1 — **SIX trous, pas deux** : `users` (4 operations mutantes, 0 trace), `contact_persons` (3), `imported_supplier_invoices` (2), `profile`, `setup`, `companies::update`. ⚠️ **Le traçage se fait au REPOSITORY, pas a la route** — un `grep` au niveau du fichier de route conclut a un trou la ou il n'y en a pas, et l'inverse : `companies.rs` compte 2 insertions, mais dans `lock_books`/`unlock_books`, pas dans `update`. ⛔ Employer `NewAuditLogEntry::from_current_user` et non `::user` des que la route n'est pas admin-only. Convention des libelles relevee au sol : `.`. 25-1c-zero-audit-company-id: done # 2026-09-15 **REVUE DE CODE CLOSE EN 2 PASSES** : `1M → 1L`, 3 lentilles (Sonnet, Haiku, Sonnet) puis passe CIBLEE (Opus). Les deux findings retenus portent sur le MEME test de caracterisation (AC 8), aucune ligne de production prise en defaut ; le MEDIUM venait du montage prescrit par la spec. Gate de cloture **2330/2330**, base remise a zero. ✅ **GATE E2E DE CLOTURE — 214 passed / 19 skipped / 9 failed (8,3 min)**, montage COMPLET, `kesh_e2e` reconstruite (68 migrations), `/health` smtpConfigured:true ; **le binaire a demarre contre la migration sans erreur de checksum (P8)**. Les 9 : 7 KF-029 + 2 pollutions (`product-revenue-account:133`, `sidebar-navigation:75`) **vertes rejouees seules** ; branche sans aucun fichier frontend. **ZERO REGRESSION.** Prete pour le push. Aucune issue fermee (#378 appartient a la 25-1c). # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5). Gate backend **2330/2330** (4 skipped), base remise a zero et verifiee (68 migrations, 1 Admin) ; fmt + clippy verts. **+8 tests** (3 backfill, 3 alimentation, 2 import) + 1 assertion d'export ; **6 mutations sur 6 tuees par assertion**. Manuel admin corrige, PDF regeneres et verifies aplatis. `backup.rs`/`export.rs`/`import.rs` INTACTS. ⚠️ **E2E NON LANCEE — prerequis du push** (seule a voir un defaut P8). Prochaine etape : `bmad-code-review`. # 2026-09-15 **SPECIFIEE** — `25-1c-zero-audit-company-id.md`. Story-zero de la 25-1c, nee de l'arbitrage du 2026-09-11 (« la colonne et son backfill d'abord »). 18 AC en six volets, 10 taches. Colonne `BIGINT NULL` sans FK + index `(company_id, created_at)`, remplie par SOUS-SELECT dans `insert_in_tx` (aucun des 106 sites ne bouge — recompte de la passe 1 de validation ; l'epic disait 89). ⛔ **P7 : EXEMPTION PERISSABLE `PerishableSince(20260827000001)`**, precedent 24-5 — pas de rejeu, conformement a l'arbitrage ; fondement verifie par `git tag` (dernier tag v0.11.1, 2026-08-24). ⚠️ **Ecart releve avec l'epic** : apres restore, le point ouvert a DEUX sous-cas — identifiants differents (entrees montrees a personne) OU identiques (entrees d'une autre instance montrees a la societe restauree) ; l'epic ne nommait que le premier. Traite en CARACTERISATION ici, tranche en 25-1c. Aucune issue fermee. ✅ **ARBITRAGES RENDUS LE 2026-09-15 (Guy)** : (1) **AUCUNE RELEASE AVANT LE MERGE** de cette story — condition du fondement de l'exemption perissable ; (2) le partage mesure ici / tranche en 25-1c est valide. ✅ **BOUCLE DE VALIDATION CLOSE EN 4 PASSES** (2026-09-15) : max `MEDIUM → HIGH → HIGH → LOW`, soit `0C/0H/2M/1L → 0C/1H/4M/4L → 0C/1H/1M → 0C/0H/0M/1L` ; Sonnet+Haiku, Opus, Sonnet, Opus CIBLEE. ⚠️ Signal HIGH → HIGH de la regle de splitting SIGNALE au Project Lead : **pas de decoupage** (Guy, « continue »). ⛔ Le HIGH de la passe 2 : `admin-manual.tex:1786` affirme « il n'y a pas de colonne company_id » — l'AC 18 disait « aucun site », sur un grep aveugle au `\_` de LaTeX ; le manuel est desormais dans le perimetre (PDF a regenerer). Montage de l'AC 8 EXECUTE sur base jetable en passe 4. Prochaine etape : `bmad-dev-story`. 25-1c-a-journal-audit-route: done # 2026-09-19 **MERGEE — PR #439, squash `c7de37c4`**, trois checks verts ; #378 reste OUVERTE pour l'ecran (25-1c-b). 2026-09-16 **REVUE DE CODE : 7 PASSES · GATE DE CLOTURE EN COURS** — gate RELANCE EN ENTIER (la garde a ete reecrite 5 fois depuis T11, la cellule d'export a change, 4 documents et un PDF avec) : base remise a zero ET VERIFIEE (40 tables, 1 Admin), **backend 2372/2372** (fmt + clippy compris, 100,8 s), **frontend 0 erreur / 740 tests / build ✔**. ✅ **E2E 215 passed / 8 failed / 19 skipped (8,7 min)**, `kesh_e2e` reconstruite, montage COMPLET (avec `KESH_INBOX_DIR` et `KESH_DOCUMENTS_DIR` cette fois). Les 8 QUALIFIES UN PAR UN : **7 KF-029** + **`sidebar-navigation:75` = KF-046**, confirmee par REJEU ISOLE (elle echoue seule, donc pas de la pollution). KF-045 absente (run a 16:53 UTC) et `inbox-import:106` absent — la preuve que l'echec de l'apres-midi venait de MON montage. ⇒ **ZERO REGRESSION, zero pollution.** ⚠️ **REVUE CLOSE en 7 passes** (P8 non consommee, sur arbitrage du Project Lead : « clore la boucle »). Doc mise a jour : `user-manual.tex` (la consultation « ne peut pas encore etre produite » etait devenue faux — l'export CSV la produit), README (ligne v0.12.1), CHANGELOG (section `[Unreleased]` creee), `api-external.md`, manuel admin, glossaire. PDF regeneres et controles aplatis. **PRETE POUR LE PUSH** — rien n'est pousse, #378 reste OUVERTE (le `closes` appartient a la PR de l'ecran). — P7 CIBLEE (Sonnet) : **1 MEDIUM, zero CRITICAL ni HIGH** — une premiere depuis P1. Le MEDIUM : la distinction lifetime/litteral introduite a P6 n'etait gardee par RIEN (prouve par mutation EXECUTEE, invisible aux 10 tests comme aux 218 fichiers reels) ; nuance relevee par la lentille elle-meme : mon commit l'annoncait comme lacune assumee. ⛔ Ma PREMIERE tentative de correctif ne gardait rien non plus — l'epreuve l'a montre : sur `impl<'a> Chose<'a> {` le saut « large » va d'une apostrophe a l'autre SANS enjamber d'accolade, donc meme compte. ⇒ la propriete se teste DIRECTEMENT sur `accolades_hors_chaines`, avec le cas discriminant `impl<'a> Foo { fn g<'b>() {} }` = (2,2). EPROUVE : rouge. ⚠️ Lecon : tester une fonction par le detour de son appelant laisse passer ce que l'appelant ne distingue pas. **CLOTURE PROPOSEE au Project Lead** : P8 disponible mais rendement nul — le MEDIUM etait annonce par moi-meme, il est remedie et eprouve, la remediation ne touche AUCUNE ligne de production depuis P3, et le code de production n'est plus en cause depuis P2. # Avant : **6 PASSES** — P6 CIBLEE (Opus) : 3 HIGH, 2M, 1L, **tous sur ma garde, aucun sur le code de production**. ⛔ Ma garde symetrique etait VERTE PAR VACUITE : sur 99 fichiers portant `#[cfg(test)]`, **95** n'ont aucun item reconnu en colonne 0 apres l'attribut, et retirer le correctif qu'elle surveillait ne changeait RIEN sur le depot (0 fichier sur 218). ⛔ La VISIBILITE decidait de la protection : `pub(crate) fn` (28), `pub mod` (202), `pub use` (85) non reconnus. ⛔ Mon doc-comment affirmait « verifie » et disait FAUX : `loader.rs:386` porte `contains('{')` DANS son bloc de test, masque par ACCIDENT. ⇒ **SIMPLIFICATION** (recommandation de la lentille, annoncee au Project Lead a P5) : le balayage du depot est remplace par **4 entrees SYNTHETIQUES en dur**, decidables et independantes du depot. EPROUVEES : 2 mutations, 2 rouges, chacune nommant le cas. ⚠️ Verdict : « elargir l'enumeration ne ferait que deplacer la frontiere ; cout reel de deux passes, rendement mesure nul ». **P7 de confirmation, puis CLOTURE** — la remediation ne touche aucune ligne de production depuis P3. # Avant : **5 PASSES** — P5 CIBLEE (Sonnet, prompt VERSIONNE) : **1 CRITICAL** (l'angle que je lui avais donne, confirme par mutation compilee), 1M, 2L. ⛔ Mon appariement d'accolades ne sortait que si une accolade AVAIT ETE OUVERTE : un item garde sans bloc (`use …;`, `mod fixtures;`, `const …;`) faisait courir le saut jusqu'a la prochaine accolade du fichier — ou jusqu'a sa fin. Plus profond que la troncature que P4 avait remplacee ; dormant, mais `#[cfg(test)] mod fixtures;` est un idiome courant. ⛔ Et mon garde-fou restait VERT PAR EXCES : il ne verifiait que l'absence d'attribut residuel, or si le masquage avale tout il n'en reste aucun. ⇒ seconde sortie de boucle + garde SYMETRIQUE (le masquage ne retire aucun item de production). EPROUVE DANS LES DEUX SENS, une premiere dans cette boucle : correctif en place ⇒ l'action neuve est vue ; correctif retire ⇒ la garde nomme l'item perdu. ⛔ MEDIUM : SIXIEME decompte faux, dans le test cense fermer cette classe. ⚠️ **ARBITRAGE DE CONDUITE PORTE AU PROJECT LEAD** : severite non decroissante (HIGH en P4, CRITICAL en P5), production plus prise en defaut depuis P2, cinq passes sur UN SEUL fichier de test — ce detecteur vaut-il ce qu'il coute ? # Avant : **4 PASSES** — P4 CIBLEE (Opus, prompt VERSIONNE) : **1 HIGH**, 2M, 3L. ⛔ Ma coupe `#[cfg(test)]` fermait UNE variante du defaut et laissait l'autre : elle s'arretait au premier attribut QUEL QUE SOIT l'item garde. Or un attribut sur une METHODE (`invoice_email.rs:901`) ou un `mod tests` AU MILIEU (`version.rs:97`, `entities/user.rs:178`) sont ordinaires en Rust ⇒ **686, 256 et 75 lignes de PRODUCTION** hors balayage, dont `send_reminder_batch`, `check_downgrade_protection` (le garde-fou P2-bis) et `UserUpdate`. ⚠️ Et `invoice_email.rs` ECRIT DEJA de l'audit : la zone perdue appelait cette ecriture. ⛔ Le defaut n'etait pas la coupe seule mais MON DOC-COMMENT, qui declarait la classe fermee. ⇒ blocs desormais MASQUES par appariement d'accolades HORS CHAINES (ce que l'AC 18 demandait des l'origine). EPROUVE dans les DEUX variantes (2 mutations, 2 rouges) + controle que l'accolade d'un litteral ne casse rien. ⛔ MEDIUM : « six » corrige a UN site et laisse a DEUX AUTRES du meme fichier — regle de propagation enfreinte dans le commit qui corrigeait ce defaut ⇒ nombres doubles d'une ASSERTION. Garde 9/9. ⚠️ **P5 requise** (severite > LOW) ; la remediation ne touche toujours AUCUNE ligne de production. # Avant : **3 PASSES** — P3 CIBLEE (Sonnet, prompt VERSIONNE `25-1c-a-review-prompt-p3-ciblee.md`, sur le seul commit `9b47e3b8`) : **2 CRITICAL demontres par REPRODUCTION**, 1M, 2L — et les deux viennent de MON patch de P2. (1) le tamis appariait les guillemets sans suivre les ECHAPPEMENTS : un `\"` impair desynchronisait tout le reste du fichier, l'ensemble devenait VIDE, l'assertion ne s'executait plus et le trou se rouvrait, garde VERTE (reproduit : 7 litteraux → 0) ; (2) la coupe `#[cfg(test)]` portait sur la source BRUTE : `config.rs:406` porte cette chaine dans un DOC-COMMENT, 83% du fichier sortait du balayage — defaut DORMANT active par mon elargissement de P2. ⇒ PROPAGATION faite dans le meme patch (`relever()` portait le meme defaut sur TOUTES les crates) : une seule fonction `source_assainie`, coupe sur LIGNE ENTIERE. Les deux correctifs EPROUVES (2 mutations, 2 rouges). ⚠️ La 2e epreuve a d'abord rendu un FAUX NEGATIF : sonde posee en fin de fichier, donc dans les 93 lignes coupees — precaution prise pour config.rs et oubliee ici. ⚠️ **P4 requise** (severite > LOW) ; mais la remediation de P3 ne touche AUCUNE ligne de production, ce qui est le critere de cloture. # Avant : **2 PASSES** — P1 (Sonnet/Haiku/Opus, 3 lentilles) : 2H/5M/6L ; P2 (Opus/Sonnet/Haiku, braquee sur la REMEDIATION `8599035c..HEAD`) : 2H/4M/2L. ⛔ **Le HIGH de P2 porte sur MON correctif de P1** : il fermait le RENOMMAGE d'un code et laissait ouvert son AJOUT — une branche neuve passait les 7 tests, et la production aurait ecrit un code SANS LIBELLE. Le defaut de P1 deplace d'un cran, motif mesure du depot. ⇒ controle rendu BILATERAL, **eprouve par 3 mutations** (branche neuve, derivation dans un AUTRE fichier de route, code laisse en commentaire) : 3 rouges sur assertion. ⚠️ **Signal `HIGH → HIGH` SIGNALE au Project Lead** (regle de splitting) — mais les deux HIGH portent sur la remediation, non sur la conception. ⚠️ **P3 requise** (severite > LOW). ⚠️ **Deux arbitrages en attente** : `user-manual.tex:498-503` (« pas encore PRODUIRE » — l'export CSV le fait) et « on termine » = 25-1c-a seule ou #378 entiere. # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5) — les ONZE taches cochees, RIEN DE POUSSE. ⛔ Perimetre FIGE `main..8599035c` (cloture de l'implementation, avant la revue) : **25 commits, 29 fichiers, +5217/-41**. Decomptes recomptes depuis la source, perimetre `main..HEAD` : **+42 tests** (7 depot, 4 `util`, 5 module, **11 garde**, 15 E2E — recompte apres la passe 1 de revue, qui ajoute 3 tests a la garde ; la fiche a annonce « +35 » tant qu'elle en portait 4), **133 cles** par locale x 4, etat **DATE a la cloture de P5** (une mesure « actuelle » se perime a chaque commit — celle-ci l'avait deja fait deux fois) : 37 commits, 34 fichiers, +6218/-44. ⚠️ « 24 commits » et « +5206 » etaient FAUX des l'ecriture (25 et +5217) — releve par la passe 2. Gates : backend **2365/2365** (fmt + clippy compris), frontend **740/740** + build, base remise a zero ET verifiee (40 tables, 1 Admin). ✅ **E2E 214 passed / 9 failed / 19 skipped (8,3 min)** — les neuf qualifies UN PAR UN : 7 KF-029, `sidebar-navigation:75` = pollution (PASSE rejoue seul, donc pas la KF-046), et `inbox-import:106` = **defaut de MON montage** (KESH_INBOX_DIR/KESH_DOCUMENTS_DIR manquants ; diagnostic VERIFIE au log, spec rejouee ⇒ passe en 1,5 s). KF-045 absente, et c'est correct : run a 14:34 UTC. ⇒ **ZERO REGRESSION.** Epreuve par mutation : **9 mutations, 9 rouges SUR ASSERTION** (aucune sur compilation), fichiers restaures et identite verifiee. ⚠️ **Trois points laisses au Project Lead** : ecart garde/spec sur le decoupage de l'inventaire des sites indirects (six de part et d'autre, mais pas les memes six) ; libelle de `reconciliation.split_applied` (« Ventilation appliquee » arbitre vs « Eclater » au catalogue) ; emojis du manuel qui ne s'impriment pas (anterieur a la story). ⚠️ **Question en suspens : « on termine » = 25-1c-a seule, ou #378 entiere avec l'ecran ?** Prochaine etape : `bmad-code-review`. # Avant : **EN COURS** — PR #437 fusionnee (squash 842eaac0), pile rebasee, base de dev remise a zero et verifiee (40 tables, 1 Admin), revalidation close en 4 passes (1H/3M/3L -> 2M -> 1M/3L -> 1L), 122 libelles francais arbitres. Avant : **VALIDEE — boucle close en 5 passes** : `1C/1M/2L → 5M/8L → 1M/1L → 2M/2L (ciblee) → 1L (ciblee)` ; Sonnet+Haiku, Opus, Sonnet, Opus, Sonnet. ⛔ Stagnation MEDIUM sur 3 passes, toujours sur les bornes de date : la forme « created_at < date_to + 1 jour » a ete ABANDONNEE pour des bornes inclusives 23:59:59.999 (eprouvees sur base jetable). **Ne pas implementer avant le merge de #437** (rebase par SHA `42b6aac0`). **SPECIFIEE** — `25-1c-a-journal-audit-route.md`, 22 AC en six volets, 11 taches. `GET /api/v1/audit-log` + `/export.csv` dans `comptable_routes`, `ensure_not_pat` en handler, DTO dedie (`actorType` via `as_str`, `createdAt` en UTC explicite), `csv_sanitize` EXTRAITE d'`invoices.rs`, export audite `audit_log.exported`. ⚠️ **Trois** choix de conception laisses a confirmer en revue : inclusion des entrees sans societe, audit de l'export, codes bruts (non traduits) dans les cellules du CSV. Branche empilee sur la 25-1c-zero, a rebaser apres le merge de #437. [#378] — **issue du SPLIT de la 25-1c** (arbitrage de Guy, regle de splitting : 7 modules). La ROUTE de consultation du journal d'audit et son export CSV, cote backend. Acces **Comptable + Admin**, ni Consultation ni cle API ; filtre **strict par `company_id`** (l'import restaure la MEME installation) ; entrees sans societe montrees « societe indeterminee » (choix par defaut, non arbitre explicitement). Cf. `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15*. - 25-1c-b1-journal-audit-ecran: ready-for-dev # 2026-09-26 **R5 DERIVE : 1 MEDIUM / 3 LOW corriges** (97 actions, 138 cles ; lignes des gardes i18n) ; contrat de la route mergee confirme. Prochaine etape : dev. # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en natifs sur le vocabulaire, plus de carte de libelles ni de colonne Societe. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : MED en passes 3 à 6, tous introduits par la remédiation précédente, puis 0 après le retrait d'une assertion E2E trois fois corrigée. **issue du SPLIT de la 25-1c-b** (arbitrage de Guy, signal MEDIUM → MEDIUM en passe 2 de validation). L'ECRAN : feature `audit-log`, page, garde Comptable+, menu, carte des **29** types d'entite (dont `audit_log`, cree par la 25-1c-a), i18n et gardes, tests unitaires et E2E. Module partage `download.ts` neuf ; le regroupement des 7 copies existantes part en issue de dette. Livree dans la MEME PR que la b2 ; porte `closes #378`. [#378] 25-1c-b2-journal-audit-textes: ready-for-dev # 2026-09-26 ⛔ **R5 DERIVE : 1 CRITICAL / 4 HIGH / 4 MEDIUM** — fiche a REECRIRE en T0 contre le texte actuel, APRES la b1, puis revalider (la facture validee se devalide, elle ne se supprime plus ; plusieurs textes deja reecrits par 25-1c-a, 25-2-b, 25-5-a). # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] 25-1c-b-journal-audit-ecran: split # 2026-09-15 **DECOUPEE en 25-1c-b1 (ecran) + 25-1c-b2 (textes)** — arbitrage de Guy apres la passe 2 de validation. Fiche conservee comme reference (passes 1-2). **SPECIFIEE** — `25-1c-b-journal-audit-ecran.md`, 21 AC en six volets, 11 taches, PAS ENCORE VALIDEE. Feature `audit-log`, carte des **28 types d'entite** recomptes depuis la source (29 litteraux dont l'action `admin_break_glass_reset`), garde Comptable+, menu `comptableOnly`, `triggerDownload` extrait (4 copies), glossaire « journal d'audit » en partie A avec alignement de 2 cles divergentes. ⚠️ Defaut PREEXISTANT constate : `user-manual.tex:502-503` nie le verrou de periode livre par la 24-4c. Ne pas implementer avant le merge de la 25-1c-a. [#378] — **issue du SPLIT de la 25-1c**. L'ECRAN : page, entree de menu, libelles des 28 types d'entite dans les 4 langues, E2E, manuels (qui promettent tous que la consultation « manque encore »). Affichage sobre : code d'action brut, detail en JSON indente. Vocabulaire fige : « journal d'audit » / Audit-Protokoll / registro di audit / audit log. **Portera le `closes #378`.** 25-1c-piste-consultable: split # 2026-09-15 **DECOUPEE en 25-1c-a (route) + 25-1c-b (ecran)** — arbitrage de Guy. [#378] — route + ecran de consultation. ✅ **ARBITRAGE RENDU LE 2026-09-11 (Guy) : AJOUTER LA COLONNE `company_id`** — ni restreindre la route, ni documenter la limite. ⚠️ **C'est une migration AVEC BACKFILL** ⇒ les garde-fous **P2-bis, P3, P5, P6, P7 et P8** s'arment tous, alors que la 25-1b n'en arme aucun. ✅ **MECANISME ARRETE LE 2026-09-11 apres lecture ciblee : LE SOUS-SELECT**, patron exact d'`actor_label`. Colonne **`BIGINT NULL`, SANS FK**. ⇒ **Aucun des 106 sites d'ecriture ne bouge** ⇒ **L'ORDRE REDEVIENT INDIFFERENT** : la 25-1b n'attend pas la colonne. **Verifie et non suppose** : `users.company_id` est NOT NULL, ecrit une fois, jamais modifie, aucune table de jonction ; **AUCUNE route n'ecrit un audit sur une entite d'une AUTRE societe** (aucun handler ne reçoit de `company_id` de la requete, tout ecart devient 404) ; et le seul site ou l'acteur n'est pas `current_user` — `admin.full_import`, qui prend le plus petit Admin du JEU RESTAURE — est celui ou le sous-SELECT est **PLUS JUSTE** qu'un champ, lequel propagerait l'id d'une societe que le restore vient de detruire. `routes/exports.rs:137-139` l'assumait deja par ecrit. ⚠️ **L'ESTIMATION DE COUT QUI FONDAIT L'HESITATION ETAIT FAUSSE D'UN FACTEUR 2,5** : « ~30 sites » annonces, **106** reels sur 38 fichiers *(recompte le 2026-09-15, passe 1 de validation de la 25-1c-zero : le « 89 sur 32 » d'abord ecrit ici etait lui-meme faux, et le facteur est donc de 3,5 et non de 2,5)*, dont ~40 dans des repositories qui ne reçoivent meme pas de `company_id`. ⛔ **TROIS CONTRAINTES, chacune adossee a un mode d'echec du depot** : (1) **NULL et jamais NOT NULL sans defaut** — `check_schema_compat` (`import.rs:195-236`) rendrait INIMPORTABLES TOUTES les sauvegardes existantes, c'est pourquoi `actor_label` est `NOT NULL DEFAULT ''` ; (2) **aucune FK vers `companies`** — `companies` EST remplacee au restore (`backup.rs:76`) alors que les entrees locales sont CONSERVEES depuis la 25-1a : c'est litteralement le scenario qui a fait retirer `fk_audit_log_user` ; (3) **ni COALESCE ni rejeu post-restore** — `NULL` est un etat LEGITIME ET PERMANENT (« societe indeterminable »), pas un trou a combler ; la garde d'`actor_label` reposait sur une sentinelle TEXTUELLE, `NULL` n'en est pas une. ⛔ **POINT OUVERT, A TRAITER DANS CETTE STORY-CI** : apres un restore, les entrees locales CONSERVEES portent le `company_id` d'une societe DETRUITE — une consultation scopee ne les montrerait A PERSONNE. *La 25-1a s'est battue pour que ces entrees survivent a l'import ; l'ecran qui les rend lisibles ne doit pas etre celui qui les cache.* ⇒ **A DECOUPER** : la colonne et son backfill d'un cote, la route et l'ecran de l'autre. *(Motif de l'arbitrage : `audit_log` n'a **pas de `company_id`** — verifie sur `20260413000001_audit_log.sql` —, la table est GLOBALE alors que Kesh est multi-societe, et une route de consultation exposerait les traces de TOUTES les societes a l'admin d'une seule.)* ⚠️ Le repository n'a que `insert_in_tx` et `find_by_entity` — une methode filtrable/paginee est a ecrire (S5). ⚠️ Le manuel annonce cette page « prevue pour une version ulterieure » (`user-manual.tex:1597-1603`) : c'est ICI qu'elle se livre, pas en 25-1a. ⛔ **NE PAS LA LIVRER AVANT LA 25-1a** : un ecran qui donne a voir une trace qu'un `reset` dement serait une promesse de plus. diff --git a/crates/kesh-i18n/locales/de-CH/messages.ftl b/crates/kesh-i18n/locales/de-CH/messages.ftl index cc6ae93e5..ff2dc95e1 100644 --- a/crates/kesh-i18n/locales/de-CH/messages.ftl +++ b/crates/kesh-i18n/locales/de-CH/messages.ftl @@ -2097,6 +2097,34 @@ audit-log-csv-header-api-key-id = API-Schlüssel audit-log-csv-header-details = Details audit-log-export-error-too-large = Zu viele Ergebnisse (> { $limit }). Bitte die Filter verfeinern (z. B. Datumsbereich, Aktion oder Entitätstyp), bevor der Export erneut gestartet wird. +# --- Journal d'audit — écran (Story 25-1c-b1) --- +nav-audit-log = Audit-Protokoll +audit-log-title = Audit-Protokoll +audit-log-subtitle = Die Spur jedes Vorgangs, der die Bücher Ihres Unternehmens verändert: wer, was, wann. +audit-log-filter-date-from = Von (UTC-Tag) +audit-log-filter-date-to = Bis (UTC-Tag) +audit-log-filter-entity-type = Entitätstyp +audit-log-filter-entity-type-all = Alle +audit-log-filter-entity-id = Entitäts-Nr. +audit-log-filter-action = Aktion +audit-log-filter-action-all = Alle +audit-log-filter-reset = Zurücksetzen +audit-log-export = Als CSV exportieren +audit-log-col-date = Datum +audit-log-col-actor = Urheber +audit-log-col-action = Aktion +audit-log-col-entity-type = Entitätstyp +audit-log-col-entity-id = Nr. +audit-log-col-details = Details +audit-log-api-key = API-Schlüssel +audit-log-details-show = Anzeigen +audit-log-details-hide = Ausblenden +audit-log-empty = Keine Einträge für diese Filter. +audit-log-error = Das Audit-Protokoll konnte nicht geladen werden. +audit-log-prev = Zurück +audit-log-next = Weiter +audit-log-range = { $from }–{ $to } von { $total } + # --- Story 25-2-b-2 (#440) : Freigabe aufheben, und die bewusste Lücke --- invoice-unvalidate-button = Freigabe aufheben invoice-unvalidate-confirm-title = Freigabe der Rechnung aufheben? diff --git a/crates/kesh-i18n/locales/en-CH/messages.ftl b/crates/kesh-i18n/locales/en-CH/messages.ftl index 3f5ad7545..f169e2796 100644 --- a/crates/kesh-i18n/locales/en-CH/messages.ftl +++ b/crates/kesh-i18n/locales/en-CH/messages.ftl @@ -2103,6 +2103,34 @@ audit-log-csv-header-api-key-id = API key audit-log-csv-header-details = Details audit-log-export-error-too-large = Too many results (> { $limit }). Please refine your filters (e.g. date range, action or entity type) before exporting. +# --- Journal d'audit — écran (Story 25-1c-b1) --- +nav-audit-log = Audit log +audit-log-title = Audit log +audit-log-subtitle = The trace of every operation that changes your company's books: who, what, when. +audit-log-filter-date-from = From (UTC day) +audit-log-filter-date-to = To (UTC day) +audit-log-filter-entity-type = Entity type +audit-log-filter-entity-type-all = All +audit-log-filter-entity-id = Entity no. +audit-log-filter-action = Action +audit-log-filter-action-all = All +audit-log-filter-reset = Reset +audit-log-export = Export as CSV +audit-log-col-date = Date +audit-log-col-actor = Author +audit-log-col-action = Action +audit-log-col-entity-type = Entity type +audit-log-col-entity-id = No. +audit-log-col-details = Details +audit-log-api-key = API key +audit-log-details-show = Show +audit-log-details-hide = Hide +audit-log-empty = No entries match these filters. +audit-log-error = The audit log could not be loaded. +audit-log-prev = Previous +audit-log-next = Next +audit-log-range = { $from }–{ $to } of { $total } + # --- Story 25-2-b-2 (#440): unvalidate from the screen, and the assumed gap --- invoice-unvalidate-button = Unvalidate invoice-unvalidate-confirm-title = Unvalidate the invoice? diff --git a/crates/kesh-i18n/locales/fr-CH/messages.ftl b/crates/kesh-i18n/locales/fr-CH/messages.ftl index 3fc3f9817..9ae44bdfb 100644 --- a/crates/kesh-i18n/locales/fr-CH/messages.ftl +++ b/crates/kesh-i18n/locales/fr-CH/messages.ftl @@ -2151,6 +2151,34 @@ audit-log-csv-header-api-key-id = Clé API audit-log-csv-header-details = Détails audit-log-export-error-too-large = Trop de résultats (> { $limit }). Veuillez affiner vos filtres (par ex. plage de dates, action ou type d'entité) avant de relancer l'export. +# --- Journal d'audit — écran (Story 25-1c-b1) --- +nav-audit-log = Journal d'audit +audit-log-title = Journal d'audit +audit-log-subtitle = La trace de chaque opération qui modifie les livres de votre société : qui, quoi, quand. +audit-log-filter-date-from = Du (jour UTC) +audit-log-filter-date-to = Au (jour UTC) +audit-log-filter-entity-type = Type d'entité +audit-log-filter-entity-type-all = Tous +audit-log-filter-entity-id = N° d'entité +audit-log-filter-action = Action +audit-log-filter-action-all = Toutes +audit-log-filter-reset = Réinitialiser +audit-log-export = Exporter en CSV +audit-log-col-date = Date +audit-log-col-actor = Auteur +audit-log-col-action = Action +audit-log-col-entity-type = Type d'entité +audit-log-col-entity-id = N° +audit-log-col-details = Détails +audit-log-api-key = clé API +audit-log-details-show = Afficher +audit-log-details-hide = Masquer +audit-log-empty = Aucune entrée ne correspond à ces filtres. +audit-log-error = Le journal d'audit n'a pas pu être chargé. +audit-log-prev = Précédent +audit-log-next = Suivant +audit-log-range = { $from }–{ $to } sur { $total } + # --- Story 25-2-b-2 (#440) : dévalidation depuis l'écran, et le trou assumé --- invoice-unvalidate-button = Dévalider invoice-unvalidate-confirm-title = Dévalider la facture ? diff --git a/crates/kesh-i18n/locales/it-CH/messages.ftl b/crates/kesh-i18n/locales/it-CH/messages.ftl index 34171c702..b9c30f768 100644 --- a/crates/kesh-i18n/locales/it-CH/messages.ftl +++ b/crates/kesh-i18n/locales/it-CH/messages.ftl @@ -2098,6 +2098,34 @@ audit-log-csv-header-api-key-id = Chiave API audit-log-csv-header-details = Dettagli audit-log-export-error-too-large = Troppi risultati (> { $limit }). Affinare i filtri (intervallo date, azione o tipo di entità) prima di esportare. +# --- Journal d'audit — écran (Story 25-1c-b1) --- +nav-audit-log = Registro di audit +audit-log-title = Registro di audit +audit-log-subtitle = La traccia di ogni operazione che modifica i libri della vostra società: chi, cosa, quando. +audit-log-filter-date-from = Dal (giorno UTC) +audit-log-filter-date-to = Al (giorno UTC) +audit-log-filter-entity-type = Tipo di entità +audit-log-filter-entity-type-all = Tutti +audit-log-filter-entity-id = N. entità +audit-log-filter-action = Azione +audit-log-filter-action-all = Tutte +audit-log-filter-reset = Reimposta +audit-log-export = Esporta in CSV +audit-log-col-date = Data +audit-log-col-actor = Autore +audit-log-col-action = Azione +audit-log-col-entity-type = Tipo di entità +audit-log-col-entity-id = N. +audit-log-col-details = Dettagli +audit-log-api-key = chiave API +audit-log-details-show = Mostra +audit-log-details-hide = Nascondi +audit-log-empty = Nessuna voce corrisponde a questi filtri. +audit-log-error = Il registro di audit non ha potuto essere caricato. +audit-log-prev = Precedente +audit-log-next = Successivo +audit-log-range = { $from }–{ $to } di { $total } + # --- Story 25-2-b-2 (#440): annullare la convalida, e il vuoto assunto --- invoice-unvalidate-button = Annulla convalida invoice-unvalidate-confirm-title = Annullare la convalida della fattura? diff --git a/frontend/src/lib/features/admin-backup/admin-backup.api.ts b/frontend/src/lib/features/admin-backup/admin-backup.api.ts index 67a697e54..9f26ad512 100644 --- a/frontend/src/lib/features/admin-backup/admin-backup.api.ts +++ b/frontend/src/lib/features/admin-backup/admin-backup.api.ts @@ -8,10 +8,9 @@ // l'export per-company CSV (`/exports/global.zip`). Ici = installation complète. // // DC-B1 (cf. story 17-3b) : `parseContentDispositionFilename` + `triggerDownload` -// sont **dupliqués localement** depuis `exports.api.ts` (~30 lignes) plutôt -// qu'importés cross-feature (anti-pattern) ou extraits maintenant. L'extraction -// vers `lib/shared/utils/download.ts` (3ᵉ feature à dupliquer) est tracée comme -// cleanup v0.2 / Epic 15 (note umbrella 9-2b §triggerDownload-reuse). +// sont **dupliqués localement** depuis `exports.api.ts` (~30 lignes). Le module +// partagé `lib/shared/utils/download.ts` existe désormais (Story 25-1c-b1) ; la +// migration de cette copie et des six autres est suivie par l'issue #438. import { apiClient } from '$lib/shared/utils/api-client'; diff --git a/frontend/src/lib/features/audit-log/audit-log.api.test.ts b/frontend/src/lib/features/audit-log/audit-log.api.test.ts new file mode 100644 index 000000000..54e4ff1ad --- /dev/null +++ b/frontend/src/lib/features/audit-log/audit-log.api.test.ts @@ -0,0 +1,99 @@ +/** + * L'API du journal d'audit — Story 25-1c-b1 (#378). + * + * Patron : `features/export/exports.api.test.ts` — `fetch` intercepté par + * `vi.stubGlobal`, l'URL appelée relue. ⚠️ C'est le premier test du dépôt qui + * observe l'URL passée à `getBlob`. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { authState } from '$lib/app/stores/auth.svelte'; +import { exportAuditLogCsv, getAuditLogVocabulary, listAuditLog } from './audit-log.api'; + +let fetchMock: ReturnType; + +function jsonResponse(body: unknown) { + return { + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(body), + text: () => Promise.resolve(JSON.stringify(body)), + } as unknown as Response; +} + +beforeEach(() => { + authState.clearSession(); + authState.login({ userId: '1', username: 'test', role: 'Comptable', expiresIn: 900 }); + fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + authState.clearSession(); +}); + +function calledUrl(): URL { + return new URL(fetchMock.mock.calls[0][0] as string, 'http://localhost'); +} + +describe('audit-log.api', () => { + it("n'envoie pas un paramètre vide", async () => { + fetchMock.mockResolvedValue(jsonResponse({ items: [], total: 0, offset: 0, limit: 50 })); + await listAuditLog({ action: '', entityType: '', dateFrom: '2026-09-01' }); + const url = calledUrl(); + expect(url.pathname).toBe('/api/v1/audit-log'); + expect(url.searchParams.has('action')).toBe(false); + expect(url.searchParams.has('entityType')).toBe(false); + expect(url.searchParams.get('dateFrom')).toBe('2026-09-01'); + }); + + it('getAuditLogVocabulary appelle la route de vocabulaire', async () => { + fetchMock.mockResolvedValue(jsonResponse({ entityTypes: [], actions: [] })); + await getAuditLogVocabulary(); + expect(calledUrl().pathname).toBe('/api/v1/audit-log/vocabulary'); + }); + + it('⛔ l’URL passée à getBlob porte les filtres, sans offset ni limit (mutation : filtres non sérialisés)', async () => { + vi.stubGlobal('URL', Object.assign(URL, { + createObjectURL: vi.fn().mockReturnValue('blob:x'), + revokeObjectURL: vi.fn(), + })); + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + blob: () => Promise.resolve(new Blob(['a'])), + headers: new Headers({ + 'content-disposition': 'attachment; filename="kesh-journal-audit-ci-2026-09-16.csv"', + }), + } as unknown as Response); + let named = ''; + vi.spyOn(document.body, 'appendChild').mockImplementation((n: T) => { + named = (n as unknown as HTMLAnchorElement).download; + return n; + }); + + await exportAuditLogCsv({ + dateFrom: '2026-09-01', + dateTo: '2026-09-16', + entityType: 'contact', + entityId: 5, + action: 'contact.created', + offset: 100, + limit: 50, + }); + + const url = new URL(fetchMock.mock.calls[0][0] as string, 'http://localhost'); + expect(url.pathname).toBe('/api/v1/audit-log/export.csv'); + expect(Object.fromEntries(url.searchParams)).toEqual({ + dateFrom: '2026-09-01', + dateTo: '2026-09-16', + entityType: 'contact', + entityId: '5', + action: 'contact.created', + }); + // Le nom vient de `Content-Disposition`. + expect(named).toBe('kesh-journal-audit-ci-2026-09-16.csv'); + }); +}); diff --git a/frontend/src/lib/features/audit-log/audit-log.api.ts b/frontend/src/lib/features/audit-log/audit-log.api.ts new file mode 100644 index 000000000..a389dd7aa --- /dev/null +++ b/frontend/src/lib/features/audit-log/audit-log.api.ts @@ -0,0 +1,52 @@ +/** + * API du journal d'audit — Story 25-1c-b1 (#378). + * + * Consomme les trois routes de la Story 25-1c-a (Comptable et Administrateur, + * refusées aux clés API) : la liste paginée, le vocabulaire traduit, et + * l'export CSV. ⚠️ Le frontend ne traduit RIEN du vocabulaire : la route en est + * la source unique. + */ + +import { apiClient } from '$lib/shared/utils/api-client'; +import { parseContentDispositionFilename, triggerDownload } from '$lib/shared/utils/download'; +import type { AuditLogListResponse, AuditLogQuery, AuditLogVocabulary } from './audit-log.types'; +import { filtersOnly, serializeQuery } from './query-helpers'; + +const BASE = '/api/v1/audit-log'; + +/** Les paramètres d'une requête ; un paramètre vide ou absent n'est pas envoyé. */ +function withParams(url: string, params: URLSearchParams): string { + const qs = params.toString(); + return qs ? `${url}?${qs}` : url; +} + +/** `GET /api/v1/audit-log` — une page de la liste filtrée. */ +export function listAuditLog(query: AuditLogQuery): Promise { + const params = serializeQuery(query); + // La route attend `offset` et `limit` ; `serializeQuery` omet leurs défauts + // pour l'URL de la page, on les rend explicites ici. + params.set('offset', String(query.offset ?? 0)); + params.set('limit', String(query.limit ?? 50)); + return apiClient.get(withParams(BASE, params)); +} + +/** `GET /api/v1/audit-log/vocabulary` — types d'entité et actions, traduits. */ +export function getAuditLogVocabulary(): Promise { + return apiClient.get(`${BASE}/vocabulary`); +} + +/** + * `GET /api/v1/audit-log/export.csv` — l'export des filtres affichés + * (`offset` et `limit` exceptés), traduit par le serveur dans la langue de + * l'interface. Le nom du fichier vient de `Content-Disposition`. + */ +export async function exportAuditLogCsv(query: AuditLogQuery): Promise { + const response = await apiClient.getBlob( + withParams(`${BASE}/export.csv`, serializeQuery(filtersOnly(query))), + ); + const blob = await response.blob(); + const filename = + parseContentDispositionFilename(response.headers.get('Content-Disposition')) ?? + 'kesh-journal-audit.csv'; + triggerDownload(blob, filename); +} diff --git a/frontend/src/lib/features/audit-log/audit-log.types.ts b/frontend/src/lib/features/audit-log/audit-log.types.ts new file mode 100644 index 000000000..1b53966b2 --- /dev/null +++ b/frontend/src/lib/features/audit-log/audit-log.types.ts @@ -0,0 +1,62 @@ +/** + * Types du journal d'audit — Story 25-1c-b1 (#378). + * + * Miroir **exact** des DTO de la route (Story 25-1c-a, + * `crates/kesh-api/src/routes/audit_log.rs`). ⚠️ Pas de `companyId` : la route + * filtre strictement par société et ne le rend pas. + */ + +/** Une entrée du journal, telle que `GET /api/v1/audit-log` la rend. */ +export interface AuditLogEntry { + id: number; + /** Horodatage ISO 8601 en UTC, finissant par `Z`. */ + createdAt: string; + /** Le nom de l'auteur AU MOMENT de l'écriture (instantané, pas une jointure). */ + actorLabel: string; + actorType: 'user' | 'api_key'; + actorApiKeyId: number | null; + userId: number; + /** Le code technique de l'action (`contact.created`) — sert aux sélecteurs, jamais affiché. */ + action: string; + /** L'action traduite dans la langue de l'interface — c'est elle qui s'affiche. */ + actionLabel: string; + entityType: string; + entityTypeLabel: string; + /** `0` quand l'entrée ne vise pas une entité précise. */ + entityId: number; + details: unknown; +} + +/** Une entrée du vocabulaire : un code et son libellé traduit. */ +export interface AuditLogVocabularyItem { + code: string; + label: string; +} + +/** `GET /api/v1/audit-log/vocabulary` — rendu dans l'ordre des CODES. */ +export interface AuditLogVocabulary { + entityTypes: AuditLogVocabularyItem[]; + actions: AuditLogVocabularyItem[]; +} + +/** Les filtres et la pagination de la liste. */ +export interface AuditLogQuery { + /** Jour UTC, `AAAA-MM-JJ`. */ + dateFrom?: string; + /** Jour UTC, `AAAA-MM-JJ`, inclus. */ + dateTo?: string; + entityType?: string; + /** N'a de sens qu'avec `entityType` : la route répond 400 sinon. */ + entityId?: number; + action?: string; + offset?: number; + limit?: number; +} + +/** L'enveloppe paginée de la liste. */ +export interface AuditLogListResponse { + items: AuditLogEntry[]; + total: number; + offset: number; + limit: number; +} diff --git a/frontend/src/lib/features/audit-log/query-helpers.test.ts b/frontend/src/lib/features/audit-log/query-helpers.test.ts new file mode 100644 index 000000000..f37ac92e4 --- /dev/null +++ b/frontend/src/lib/features/audit-log/query-helpers.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { filtersOnly, parseQueryFromUrl, serializeQuery } from './query-helpers'; + +describe('query-helpers du journal d’audit', () => { + it('aller-retour URL', () => { + const q = { + dateFrom: '2026-09-01', + dateTo: '2026-09-16', + entityType: 'contact', + entityId: 12, + action: 'contact.created', + offset: 50, + limit: 20, + }; + expect(parseQueryFromUrl(serializeQuery(q))).toEqual(q); + }); + + it('omet les valeurs par défaut et vides', () => { + expect(serializeQuery({ offset: 0, limit: 50, action: '' }).toString()).toBe(''); + }); + + it('ignore les valeurs invalides', () => { + const q = parseQueryFromUrl( + new URLSearchParams( + 'dateFrom=16.09.2026&dateTo=2026-9-1&entityType=contact&entityId=abc&offset=-3&limit=0', + ), + ); + expect(q).toEqual({ entityType: 'contact' }); + expect( + parseQueryFromUrl(new URLSearchParams('entityType=contact&entityId=0')).entityId, + ).toBeUndefined(); + }); + + it('conserve entityType et action hors vocabulaire (code historique)', () => { + const q = parseQueryFromUrl(new URLSearchParams('entityType=zz_old&action=zz.legacy')); + expect(q).toEqual({ entityType: 'zz_old', action: 'zz.legacy' }); + }); + + it('⛔ un entityId SANS entityType est ignoré à la lecture et omis à la sérialisation', () => { + expect(parseQueryFromUrl(new URLSearchParams('entityId=7'))).toEqual({}); + expect(serializeQuery({ entityId: 7 }).has('entityId')).toBe(false); + expect(filtersOnly({ entityId: 7 })).toEqual({}); + }); +}); diff --git a/frontend/src/lib/features/audit-log/query-helpers.ts b/frontend/src/lib/features/audit-log/query-helpers.ts new file mode 100644 index 000000000..3f0ac57c2 --- /dev/null +++ b/frontend/src/lib/features/audit-log/query-helpers.ts @@ -0,0 +1,84 @@ +/** + * Les filtres du journal d'audit dans l'URL — Story 25-1c-b1 (#378). + * + * Patron : `features/journal-entries/query-helpers.ts`. La sérialisation omet + * les valeurs par défaut ; la lecture ignore les valeurs invalides. + * + * ⛔ **`entityId` n'a de sens qu'avec un `entityType`** : la route répond 400 + * sinon. Un `entityId` seul est donc ignoré à la lecture (un lien retouché à la + * main ne doit pas produire un écran en erreur) et omis à la sérialisation. + * + * ⚠️ `entityType` et `action` hors vocabulaire sont CONSERVÉS : un code + * historique, écrit par une version antérieure, doit rester filtrable. + */ + +import type { AuditLogQuery } from './audit-log.types'; + +/** La taille de page par défaut, omise de l'URL. */ +export const DEFAULT_LIMIT = 50; + +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +/** Les filtres, sans la pagination — ce que l'export transmet. */ +export function filtersOnly(query: AuditLogQuery): AuditLogQuery { + const f: AuditLogQuery = {}; + if (query.dateFrom) f.dateFrom = query.dateFrom; + if (query.dateTo) f.dateTo = query.dateTo; + if (query.entityType) { + f.entityType = query.entityType; + if (query.entityId !== undefined && query.entityId > 0) f.entityId = query.entityId; + } + if (query.action) f.action = query.action; + return f; +} + +/** Sérialise les filtres et la pagination ; les valeurs vides et par défaut sont omises. */ +export function serializeQuery(query: AuditLogQuery): URLSearchParams { + const params = new URLSearchParams(); + const f = filtersOnly(query); + if (f.dateFrom) params.set('dateFrom', f.dateFrom); + if (f.dateTo) params.set('dateTo', f.dateTo); + if (f.entityType) params.set('entityType', f.entityType); + if (f.entityId !== undefined) params.set('entityId', String(f.entityId)); + if (f.action) params.set('action', f.action); + if (query.offset !== undefined && query.offset > 0) params.set('offset', String(query.offset)); + if (query.limit !== undefined && query.limit !== DEFAULT_LIMIT) { + params.set('limit', String(query.limit)); + } + return params; +} + +/** Relit les filtres depuis l'URL ; toute valeur invalide est ignorée. */ +export function parseQueryFromUrl(searchParams: URLSearchParams): AuditLogQuery { + const query: AuditLogQuery = {}; + + const dateFrom = searchParams.get('dateFrom'); + if (dateFrom && DATE_RE.test(dateFrom)) query.dateFrom = dateFrom; + const dateTo = searchParams.get('dateTo'); + if (dateTo && DATE_RE.test(dateTo)) query.dateTo = dateTo; + + const entityType = searchParams.get('entityType'); + if (entityType && entityType.trim() !== '') query.entityType = entityType.trim(); + + // ⛔ Sans type, l'identifiant est ignoré. + const entityId = searchParams.get('entityId'); + if (query.entityType && entityId !== null) { + const n = Number(entityId); + if (Number.isInteger(n) && n > 0) query.entityId = n; + } + + const action = searchParams.get('action'); + if (action && action.trim() !== '') query.action = action.trim(); + + const offset = searchParams.get('offset'); + if (offset !== null) { + const n = Number(offset); + if (Number.isInteger(n) && n >= 0) query.offset = n; + } + const limit = searchParams.get('limit'); + if (limit !== null) { + const n = Number(limit); + if (Number.isInteger(n) && n > 0) query.limit = n; + } + return query; +} diff --git a/frontend/src/lib/features/audit-log/vocabulary-options.test.ts b/frontend/src/lib/features/audit-log/vocabulary-options.test.ts new file mode 100644 index 000000000..1597b11be --- /dev/null +++ b/frontend/src/lib/features/audit-log/vocabulary-options.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { toSelectOptions } from './vocabulary-options'; + +// ⚠️ L'entrée arrive dans l'ordre des CODES, comme la route la rend : une entrée +// déjà rangée par libellé laisserait passer l'absence de tri. +const PAR_CODE = [ + { code: 'invoice.validated', label: 'Facture validée' }, + { code: 'journal_entry.created', label: 'Écriture créée' }, +]; + +describe('toSelectOptions', () => { + it('range par libellé, non par code (mutation : tri par code)', () => { + const out = toSelectOptions(PAR_CODE, 'fr-CH', undefined); + expect(out.map((o) => o.label)).toEqual(['Écriture créée', 'Facture validée']); + }); + + it("le paramètre `locale` compte : sv-SE range « Zahlung » avant « Öffnung » (mutation : locale en dur)", () => { + const items = [ + { code: 'a', label: 'Öffnung' }, + { code: 'b', label: 'Zahlung' }, + ]; + expect(toSelectOptions(items, 'fr-CH', undefined).map((o) => o.label)).toEqual([ + 'Öffnung', + 'Zahlung', + ]); + expect(toSelectOptions(items, 'sv-SE', undefined).map((o) => o.label)).toEqual([ + 'Zahlung', + 'Öffnung', + ]); + }); + + it('un code courant ABSENT du vocabulaire est ajouté, libellé par son code (mutation : non ajouté)', () => { + const out = toSelectOptions(PAR_CODE, 'fr-CH', 'zz.legacy'); + expect(out).toContainEqual({ value: 'zz.legacy', label: 'zz.legacy' }); + expect(out).toHaveLength(3); + }); + + it("un code courant PRÉSENT n'est pas dupliqué", () => { + expect(toSelectOptions(PAR_CODE, 'fr-CH', 'invoice.validated')).toHaveLength(2); + }); +}); diff --git a/frontend/src/lib/features/audit-log/vocabulary-options.ts b/frontend/src/lib/features/audit-log/vocabulary-options.ts new file mode 100644 index 000000000..5ef73068b --- /dev/null +++ b/frontend/src/lib/features/audit-log/vocabulary-options.ts @@ -0,0 +1,36 @@ +/** + * Les options des listes « Type d'entité » et « Action » — Story 25-1c-b1 (#378). + * + * ⚠️ La route rend le vocabulaire dans l'ordre des **codes** : les options se + * rangent ici par **libellé**, avec `Intl.Collator` dans la locale de + * l'interface — un tri d'octets rangerait « Écriture » après « Utilisateur ». + * + * ⛔ Un code de l'URL absent du vocabulaire (code historique) est AJOUTÉ comme + * option, libellée par son code : sans lui, la liste afficherait « Tous » alors + * que la page filtre sur ce code. + */ + +import type { AuditLogVocabularyItem } from './audit-log.types'; + +/** Une option de ` + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + {#if exportError} + + {/if} + + + {#if loading} +

+ {i18nMsg('common-loading', 'Chargement…')} +

+ {:else if listError} + + {:else if entries.length === 0} +

+ {i18nMsg('audit-log-empty', 'Aucune entrée ne correspond à ces filtres.')} +

+ {:else} + + + + + + + + + + + + + {#each entries as e (e.id)} + + + + + + + + + {/each} + +
{i18nMsg('audit-log-col-date', 'Date')}{i18nMsg('audit-log-col-actor', 'Auteur')}{i18nMsg('audit-log-col-action', 'Action')}{i18nMsg('audit-log-col-entity-type', "Type d'entité")}{i18nMsg('audit-log-col-entity-id', 'N°')}{i18nMsg('audit-log-col-details', 'Détails')}
{dateFormat.format(new Date(e.createdAt))} + {e.actorLabel} + {#if e.actorType === 'api_key'} + + ({i18nMsg('audit-log-api-key', 'clé API')}) + + {/if} + {e.actionLabel}{e.entityTypeLabel}{e.entityId === 0 ? '—' : e.entityId} + {#if e.details !== null && e.details !== undefined} + + {#if expanded.has(e.id)} +
{JSON.stringify(e.details, null, 2)}
+ {/if} + {/if} +
+ +
+ + + {i18nMsg('audit-log-range', '{ $from }–{ $to } sur { $total }', { + from: offset + 1, + to: Math.min(offset + limit, total), + total, + })} + + +
+ {/if} +{/if} diff --git a/frontend/src/routes/(app)/audit-log/+page.ts b/frontend/src/routes/(app)/audit-log/+page.ts new file mode 100644 index 000000000..3724ddecc --- /dev/null +++ b/frontend/src/routes/(app)/audit-log/+page.ts @@ -0,0 +1,18 @@ +import { browser } from '$app/environment'; +import { redirect } from '@sveltejs/kit'; +import { authState } from '$lib/app/stores/auth.svelte'; + +export const ssr = false; + +/** + * Garde Comptable+ (Story 25-1c-b1, #378) : le journal d'audit se consulte par + * les rôles Admin et Comptable. Un rôle Consultation est redirigé vers + * l'accueil. ⚠️ Le masquage d'interface ne protège rien : c'est le 403 de la + * route (`/api/v1/audit-log`) qui fait foi. + */ +export function load() { + const role = authState.currentUser?.role; + if (browser && role !== 'Admin' && role !== 'Comptable') { + throw redirect(302, '/'); + } +} diff --git a/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts b/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts new file mode 100644 index 000000000..6647b6259 --- /dev/null +++ b/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts @@ -0,0 +1,248 @@ +/** + * L'écran du journal d'audit — Story 25-1c-b1 (#378). + * + * ⚠️ Chaque test nomme la MUTATION qu'il attrape. Patron : + * `contacts/contacts-page.test.ts` — mocks hoistés AVANT l'import du composant. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, fireEvent, cleanup, waitFor } from '@testing-library/svelte'; +import type { AuditLogEntry, AuditLogVocabulary } from '$lib/features/audit-log/audit-log.types'; + +vi.mock('$app/environment', () => ({ browser: true })); +vi.mock('$app/navigation', () => ({ goto: vi.fn() })); +const url = { value: new URL('http://localhost/audit-log') }; +vi.mock('$app/state', () => ({ + page: { + get url() { + return url.value; + }, + }, +})); + +const locale = { value: 'fr-CH' }; +vi.mock('$lib/shared/utils/i18n.svelte', () => ({ + i18nMsg: (_k: string, fallback: string, args?: Record) => + args ? fallback.replace(/\{\s*\$(\w+)\s*\}/g, (_, n) => String(args[n] ?? '')) : fallback, + i18nLocale: () => locale.value, +})); + +const notifyErrorMock = vi.fn(); +vi.mock('$lib/shared/utils/notify', () => ({ + notifyError: (m: string) => notifyErrorMock(m), + notifySuccess: vi.fn(), +})); + +const listMock = vi.fn(); +const vocabMock = vi.fn(); +const exportMock = vi.fn(); +vi.mock('$lib/features/audit-log/audit-log.api', () => ({ + listAuditLog: (q: unknown) => listMock(q), + getAuditLogVocabulary: () => vocabMock(), + exportAuditLogCsv: (q: unknown) => exportMock(q), +})); + +import Page from './+page.svelte'; +import { authState } from '$lib/app/stores/auth.svelte'; + +/** Le vocabulaire, rendu dans l'ORDRE DES CODES comme la route le rend. */ +const VOCAB: AuditLogVocabulary = { + entityTypes: [ + { code: 'contact', label: 'Contact' }, + { code: 'journal_entry', label: 'Écriture' }, + ], + actions: [ + { code: 'contact.created', label: 'Contact créé' }, + { code: 'invoice.validated', label: 'Facture validée' }, + { code: 'journal_entry.created', label: 'Écriture créée' }, + ], +}; + +function entry(partial: Partial = {}): AuditLogEntry { + return { + id: 1, + createdAt: '2026-09-16T12:26:33.123Z', + actorLabel: 'comptable', + actorType: 'user', + actorApiKeyId: null, + userId: 2, + // ⚠️ Code et libellé DIFFÉRENTS : sinon l'assertion sur la colonne + // serait vraie par construction. + action: 'contact.created', + actionLabel: 'Contact créé (libellé)', + entityType: 'contact', + entityTypeLabel: 'Contact (libellé)', + entityId: 12, + details: { name: 'Dupont SA' }, + ...partial, + }; +} + +function page1(items: AuditLogEntry[]) { + return { items, total: items.length, offset: 0, limit: 50 }; +} + +beforeEach(() => { + url.value = new URL('http://localhost/audit-log'); + locale.value = 'fr-CH'; + vocabMock.mockResolvedValue(VOCAB); + listMock.mockResolvedValue(page1([entry()])); +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe('la garde', () => { + afterEach(async () => { + await authState.logout(); + }); + + it('load() redirige un rôle Consultation vers / (mutation : garde retirée)', async () => { + authState.login({ userId: '1', username: 'c', role: 'Consultation', expiresIn: 3600 }); + const { load } = await import('./+page'); + try { + load(); + expect.unreachable('redirection attendue'); + } catch (err: unknown) { + const e = err as { status: number; location: string }; + expect(e.status).toBe(302); + expect(e.location).toBe('/'); + } + }); + + it('load() laisse passer un Comptable', async () => { + authState.login({ userId: '1', username: 'c', role: 'Comptable', expiresIn: 3600 }); + const { load } = await import('./+page'); + expect(() => load()).not.toThrow(); + }); +}); + +describe('la page', () => { + it('affiche actionLabel et entityTypeLabel, jamais les codes (mutation : colonne rendue avec `action`)', async () => { + const { findByTestId, getByTestId } = render(Page); + const cell = await findByTestId('audit-log-row-action'); + expect(cell.textContent).toBe('Contact créé (libellé)'); + expect(getByTestId('audit-log-row').textContent).toContain('Contact (libellé)'); + expect(getByTestId('audit-log-row').getAttribute('data-action')).toBe('contact.created'); + }); + + it('rend « — » pour un entityId à 0, et déplie le détail en JSON indenté', async () => { + listMock.mockResolvedValue(page1([entry({ entityId: 0 })])); + const { findByTestId, getByTestId } = render(Page); + const row = await findByTestId('audit-log-row'); + expect(row.textContent).toContain('—'); + await fireEvent.click(getByTestId('audit-log-details-toggle')); + expect(getByTestId('audit-log-details').textContent).toBe( + JSON.stringify({ name: 'Dupont SA' }, null, 2), + ); + }); + + it('état vide', async () => { + listMock.mockResolvedValue(page1([])); + const { findByTestId } = render(Page); + expect(await findByTestId('audit-log-empty')).toBeTruthy(); + }); + + it("⛔ un échec du VOCABULAIRE affiche l'état d'erreur (mutation : échec avalé, filtres vides)", async () => { + vocabMock.mockRejectedValue({ code: 'X', message: 'vocabulaire indisponible', status: 500 }); + const { findByTestId, queryByTestId } = render(Page); + expect((await findByTestId('audit-log-error')).textContent).toContain( + 'vocabulaire indisponible', + ); + expect(queryByTestId('audit-log-filter-action')).toBeNull(); + }); + + it("l'erreur de la LISTE laisse les filtres et « Réinitialiser » visibles", async () => { + listMock.mockRejectedValue({ code: 'VALIDATION_ERROR', message: 'plage invalide', status: 400 }); + const { findByTestId, getByTestId } = render(Page); + expect((await findByTestId('audit-log-error')).textContent).toContain('plage invalide'); + expect(getByTestId('audit-log-filter-action')).toBeTruthy(); + expect(getByTestId('audit-log-filter-reset')).toBeTruthy(); + }); + + it('⛔ la page passe par toSelectOptions : code historique ajouté, options triées par libellé (mutation : vocabulary.actions rendu tel quel)', async () => { + url.value = new URL('http://localhost/audit-log?action=zz.legacy'); + const { findByTestId } = render(Page); + const select = (await findByTestId('audit-log-filter-action')) as HTMLSelectElement; + await waitFor(() => expect(select.options.length).toBe(5)); + expect(select.value).toBe('zz.legacy'); + const labels = Array.from(select.options).map((o) => o.text); + expect(labels).toContain('zz.legacy'); + // Après « Toutes », le premier PAR LIBELLÉ — « Contact créé » —, puis + // « Écriture créée » avant « Facture validée » (l'ordre des codes les inverserait). + expect(labels.slice(1, 4)).toEqual(['Contact créé', 'Écriture créée', 'Facture validée']); + }); + + it("⛔ la page transmet i18nLocale() au tri : sous sv-SE, « Zahlung » avant « Öffnung » (mutation : 'fr-CH' en dur)", async () => { + locale.value = 'sv-SE'; + vocabMock.mockResolvedValue({ + entityTypes: [], + actions: [ + { code: 'a', label: 'Öffnung' }, + { code: 'b', label: 'Zahlung' }, + ], + }); + const { findByTestId } = render(Page); + const select = (await findByTestId('audit-log-filter-action')) as HTMLSelectElement; + await waitFor(() => expect(select.options.length).toBe(3)); + expect(Array.from(select.options).map((o) => o.text).slice(1)).toEqual(['Zahlung', 'Öffnung']); + }); + + it("⛔ la date suit i18nLocale() : sous de-CH, « 16.09.2026 » et pas « sept. » (mutation : navigator.language ou 'fr-CH' en dur)", async () => { + locale.value = 'de-CH'; + const { findByTestId } = render(Page); + const row = await findByTestId('audit-log-row'); + expect(row.textContent).toContain('16.09.2026'); + expect(row.textContent).not.toContain('sept.'); + }); + + it("⛔ revenir à « Tous » VIDE l'identifiant d'entité (mutation : entityId conservé)", async () => { + url.value = new URL('http://localhost/audit-log?entityType=contact&entityId=12'); + const { findByTestId, getByTestId } = render(Page); + const select = (await findByTestId('audit-log-filter-entity-type')) as HTMLSelectElement; + await waitFor(() => expect(select.value).toBe('contact')); + expect(listMock.mock.calls[0][0]).toMatchObject({ entityType: 'contact', entityId: 12 }); + select.value = ''; + await fireEvent.change(select); + await waitFor(() => expect(listMock).toHaveBeenCalledTimes(2)); + const last = listMock.mock.calls[1][0] as Record; + expect(last.entityType).toBeUndefined(); + expect(last.entityId).toBeUndefined(); + // ⚠️ La requête seule ne suffit pas : `buildQuery` n'envoie déjà jamais + // un identifiant sans type. C'est le CHAMP qui doit être vidé — sinon + // l'ancien identifiant revient dès qu'on rechoisit un type. + const input = getByTestId('audit-log-filter-entity-id') as HTMLInputElement; + expect(input.value).toBe(''); + select.value = 'contact'; + await fireEvent.change(select); + await waitFor(() => expect(listMock).toHaveBeenCalledTimes(3)); + expect((listMock.mock.calls[2][0] as Record).entityId).toBeUndefined(); + }); + + it("⛔ l'export transmet les filtres affichés (mutation : export appelé avec {})", async () => { + url.value = new URL('http://localhost/audit-log?action=contact.created&dateFrom=2026-09-01'); + exportMock.mockResolvedValue(undefined); + const { findByTestId } = render(Page); + await fireEvent.click(await findByTestId('audit-log-export')); + await waitFor(() => expect(exportMock).toHaveBeenCalledOnce()); + expect(exportMock.mock.calls[0][0]).toMatchObject({ + action: 'contact.created', + dateFrom: '2026-09-01', + }); + }); + + it('un refus RESULT_TOO_LARGE affiche le message du serveur dans la page, sans notifyError', async () => { + exportMock.mockRejectedValue({ + code: 'RESULT_TOO_LARGE', + message: 'Trop de résultats (> 10000).', + status: 400, + }); + const { findByTestId } = render(Page); + await fireEvent.click(await findByTestId('audit-log-export')); + expect((await findByTestId('audit-log-export-error')).textContent).toContain( + 'Trop de résultats', + ); + expect(notifyErrorMock).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/tests/e2e/audit-log.spec.ts b/frontend/tests/e2e/audit-log.spec.ts new file mode 100644 index 000000000..c26b9be3e --- /dev/null +++ b/frontend/tests/e2e/audit-log.spec.ts @@ -0,0 +1,166 @@ +import { expect, test } from '@playwright/test'; +import { + seedTestState, + clearAuthStorage, + authedApiContext, + disposeContextSafe, +} from './helpers/test-state'; + +/** + * Tests E2E — l'écran du journal d'audit (Story 25-1c-b1, #378). + * + * # Ce que ces tests prouvent, et que rien d'autre ne prouve + * + * Que l'écran, ouvert **par le menu** par un **Comptable**, affiche une entrée + * réellement écrite par le backend, **traduite** (le libellé que la route + * elle-même rend pour ce code), qu'il filtre sur des jours **UTC**, déplie le + * détail, exporte, et qu'un rôle **Consultation** n'y a pas accès. + * + * ⚠️ **La suite partage une base** : une ligne s'isole par le type d'entité et + * l'identifiant du contact créé, jamais par sa position dans la liste. Les + * filtres se pilotent **par valeur** (`selectOption({ value })`), jamais par + * libellé : la suite ne tourne qu'en français, mais un sélecteur traduit est + * une dette. + */ + +test.beforeAll(async () => { + await seedTestState('with-company'); +}); + +test.afterEach(async ({ page }) => { + await clearAuthStorage(page); +}); + +/** Connexion d'un utilisateur NOMMÉ (patron `company-contact-details.spec.ts`). */ +async function login( + page: import('@playwright/test').Page, + username = 'admin', + password = 'admin123', +) { + await page.goto('/login'); + await page.fill('#username', username); + await page.fill('#password', password); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL('/'); +} + +function uniqSuffix(): string { + return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`; +} + +/** Crée un utilisateur du rôle donné, en Admin ; rend son nom. */ +async function createUser(page: import('@playwright/test').Page, role: string): Promise { + await login(page); + const ctx = await authedApiContext(page); + const username = `${role.toLowerCase()}-${uniqSuffix()}`; + try { + const res = await ctx.post('/api/v1/users', { + data: { username, password: 'MotDePasse12345', role }, + }); + expect(res.ok(), `création de l'utilisateur ${role} : ${res.status()}`).toBeTruthy(); + } finally { + await disposeContextSafe(ctx); + } + await clearAuthStorage(page); + return username; +} + +/** Le jour UTC courant, `AAAA-MM-JJ` — la route filtre sur des jours UTC. */ +function todayUtc(): string { + return new Date().toISOString().slice(0, 10); +} + +test('un Comptable ouvre le journal par le menu, filtre, déplie et exporte', async ({ page }) => { + const comptable = await createUser(page, 'Comptable'); + await login(page, comptable, 'MotDePasse12345'); + + // ⚠️ `authedApiContext` reprend la session de la PAGE : c'est le Comptable + // qui crée le contact, et c'est lui que la route doit servir. + const ctx = await authedApiContext(page); + let contactId: number; + let label: string; + try { + const res = await ctx.post('/api/v1/contacts', { + data: { + contactType: 'Entreprise', + name: `Audit ${uniqSuffix()}`, + isClient: true, + isSupplier: false, + addressStructured: { street: '', building: '', postalCode: '', city: '', country: 'CH' }, + }, + }); + expect(res.ok(), `création du contact : ${res.status()}`).toBeTruthy(); + contactId = (await res.json()).id as number; + const vocab = await (await ctx.get('/api/v1/audit-log/vocabulary')).json(); + label = (vocab.actions as { code: string; label: string }[]).find( + (a) => a.code === 'contact.created', + )!.label; + } finally { + await disposeContextSafe(ctx); + } + + // 1. Par le menu — le groupe « Administration » est replié. + await page.goto('/'); + await page.locator('[data-testid="nav-group-administration"] summary').click(); + await page.getByTestId('nav-link-audit-log').click(); + await expect(page).toHaveURL(/\/audit-log/); + + await page.getByTestId('audit-log-filter-entity-type').selectOption({ value: 'contact' }); + await page.getByTestId('audit-log-filter-entity-id').fill(String(contactId)); + await page.getByTestId('audit-log-filter-entity-id').press('Enter'); + await page.getByTestId('audit-log-filter-entity-id').blur(); + + const row = page.locator('[data-testid="audit-log-row"][data-action="contact.created"]'); + await expect(row).toHaveCount(1); + // ⛔ La cellule vaut le libellé que la route rend pour ce code : l'écran + // affiche la traduction, exacte, quelle que soit la langue. + await expect(row.getByTestId('audit-log-row-action')).toHaveText(label); + + // 2. L'URL survit au rechargement. + await expect(page).toHaveURL(new RegExp(`entityId=${contactId}`)); + await page.reload(); + await expect(row).toHaveCount(1); + + // 5. Le détail se déplie et contient le JSON. + await row.getByTestId('audit-log-details-toggle').click(); + await expect(row.getByTestId('audit-log-details')).toContainText('{'); + + // 6. L'export télécharge le fichier nommé par la route. + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.getByTestId('audit-log-export').click(), + ]); + expect(download.suggestedFilename()).toMatch(/^kesh-journal-audit-/); + + // 3. Une période PASSÉE l'écarte. + await page.getByTestId('audit-log-filter-date-from').fill('2000-01-01'); + await page.getByTestId('audit-log-filter-date-to').fill('2000-01-02'); + await page.getByTestId('audit-log-filter-date-to').blur(); + await expect(page.getByTestId('audit-log-empty')).toBeVisible(); + await expect(row).toHaveCount(0); + + // 4. ⛔ La période qui la GARDE se calcule en jour UTC, non en jour local : + // entre 00:00 et 02:00 à Zurich, le jour local précède le jour UTC. + await page.getByTestId('audit-log-filter-date-from').fill(todayUtc()); + await page.getByTestId('audit-log-filter-date-to').fill(todayUtc()); + await page.getByTestId('audit-log-filter-date-to').blur(); + await expect(row).toHaveCount(1); +}); + +test("un Consultation ne voit pas l'entrée de menu, et /audit-log le redirige", async ({ page }) => { + const lecteur = await createUser(page, 'Consultation'); + await login(page, lecteur, 'MotDePasse12345'); + + await page.locator('[data-testid="nav-group-administration"] summary').click(); + // Anti-vacuité : une entrée du même groupe, visible de tous, est rendue + // AVANT qu'on constate l'absence. + await expect(page.getByTestId('nav-link-settings')).toHaveCount(1); + await expect(page.getByTestId('nav-link-audit-log')).toHaveCount(0); + + // La redirection porte à elle seule la preuve : une page qui ne se charge + // pas garde l'URL `/audit-log`, et rien d'autre que la garde ne mène un + // Consultation sur `/`. + await page.goto('/audit-log'); + await expect(page).toHaveURL('/'); + await expect(page.getByTestId('homepage-card-open-invoices')).toBeVisible(); +}); From f7838a9774b989f8412f5a1367393dbf37de2791 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 14:48:55 +0200 Subject: [PATCH 05/19] docs(25-1c-b1): prompt de la passe 1 de revue (refs #378) Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b1-review-prompt-p1.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p1.md diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p1.md b/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p1.md new file mode 100644 index 000000000..20ce5053c --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p1.md @@ -0,0 +1,67 @@ +# Prompt — passe 1 de `bmad-code-review`, Story 25-1c-b1 + +*Versionné le 2026-09-26. Trois lentilles en contexte frais (Sonnet), orthogonales à l'auteur du +code (Opus 5.5). Protocole du workflow : Blind Hunter, Edge Case Hunter, Acceptance Auditor.* + +Dépôt `/home/gcorbaz/devel/kesh`, branche `story/25-1c-b1-journal-audit-ecran`, tête `6c169e07`. +**Diff revu** : `git diff main..HEAD` sur le code (PDF et `_bmad-output/` exclus), enregistré dans +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/ca5ce2e2-67a3-4eeb-817f-c2de35620a1c/scratchpad/25-1c-b1-code.diff`. +Fiche : `_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md` — AC 1 à 14, arbitrages +du Project Lead (**ne pas les contester**, en contester la mise en œuvre), Dev Agent Record. Contrat de la +route consommée : `crates/kesh-api/src/routes/audit_log.rs` (Story 25-1c-a, mergée). + +Objet : l'**écran de consultation du journal d'audit** (#378) — feature `frontend/src/lib/features/audit-log/`, +page `frontend/src/routes/(app)/audit-log/`, module partagé `frontend/src/lib/shared/utils/download.ts`, +`i18nLocale()` dans le store i18n, entrée de menu, 26 clés ×4, gardes i18n, spec E2E. ⚠️ Les manuels et +le vocabulaire des catalogues sont la **25-1c-b2**, hors de cette revue. + +## Lentille 1 — Blind Hunter (diff SEUL) + +Revue adversariale générale : logique, réactivité Svelte 5 (`$state`, `$derived`, `$effect` — boucle +possible entre l'effet qui écrit l'URL et `onMount` qui la lit ?), gestion d'erreurs, tests qui ne +prouvent rien ou passent à vide, incohérences code / commentaires / textes, clés i18n (quatre locales, +replis **mot pour mot** le FTL fr-CH, vocabulaire arbitré), sélecteurs E2E par libellé. + +## Lentille 2 — Edge Case Hunter (diff + dépôt) + +Chaque branche du code neuf, en lisant l'appelé et l'appelant. Priorités : +1. **La page** : ordre des effets au montage (l'effet d'URL s'exécute-t-il avant que `onMount` ait lu + l'URL, et l'écrase-t-il ?), double chargement, courses entre deux `loadList` (un filtre changé pendant + un chargement — la réponse la plus ancienne peut-elle écraser la plus récente ?), `entityIdValue` lié à + un `` (nombre ou chaîne ? `null` quand on efface ?), pagination aux bornes, + `details` à `null` / chaîne / tableau, `createdAt` invalide. +2. **Le contrat** : chaque paramètre envoyé à `/api/v1/audit-log`, `/vocabulary`, `/export.csv` contre + `ListAuditLogQuery` (noms, valeurs par défaut, `offset`/`limit` obligatoires ou non) ; chaque champ lu + contre `AuditLogEntryResponse`. Une réponse 400 de la route (plage inversée, date hors bornes, action + > 64 caractères, `entityId` sans type) est-elle atteignable depuis l'écran, et que voit l'utilisateur ? +3. **`i18nLocale()`** : qui d'autre charge les messages (`loadI18nMessages`) — la locale peut-elle rester + `'fr-CH'` sur une installation allemande (écran ouvert avant le chargement) ? +4. **`download.ts`** : strictement identique à l'original d'`exports.api.ts` ? Les commentaires + modifiés disent-ils vrai ? +5. **Les gardes i18n** : les compteurs modifiés, recomptés depuis la source ; `FAMILLES_RESOLUES`. +6. **L'E2E** : chaque scénario peut-il passer à vide (isolation par type et identifiant, attente de la + liste avant l'assertion, fuseau `Europe/Zurich`, jour UTC) ? + +## Lentille 3 — Acceptance Auditor (diff + fiche) + +Chaque AC (1 à 14) contre le diff, sous-point par sous-point. Les décomptes du Dev Agent Record +**recomptés** depuis la source : 26 clés ×4, total 164 par catalogue, `sitesTotal` 1709 → 1738 (+29), +vitest +35 (et sa ventilation), E2E +2, 17 mutations (juge, en lisant le code et les tests, que chacune +aurait bien rougi). Les `data-testid` de l'AC 8 tous présents ? Aucun sélecteur par libellé ? Les trois +commentaires de l'AC 9 réécrits et `reports.api.ts` intact ? Les termes du glossaire +(`docs/i18n-glossaire.md`) respectés dans les quatre langues ? + +## Ce que tu rends + +- Findings : sévérité (CRITICAL / HIGH / MEDIUM / LOW), `fichier:ligne`, **preuve** (code lu, commande + et résultat), correction. Pour tout CRITICAL ou HIGH affirmant qu'une chose est absente ou présente : + la commande `grep -nF` exécutée et son résultat. +- ⛔ **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 vitest`, `npx playwright`. Lecture, `grep`, `git log`/`show`/`diff` et `cargo check` sont autorisés. From 576464df299076508d3aab8a2e347e387b35fb24 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:02:57 +0200 Subject: [PATCH 06/19] =?UTF-8?q?fix(25-1c-b1):=20revue=20P1=20=E2=80=94?= =?UTF-8?q?=20identifiant=20vid=C3=A9=20=C3=A0=20tout=20changement=20de=20?= =?UTF-8?q?type,=20jeton=20de=20requ=C3=AAte,=20plage=20contr=C3=B4l=C3=A9?= =?UTF-8?q?e,=20page=20vide=20paginable=20(refs=20#378,=20refs=20#469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b1-journal-audit-ecran.md | 17 +++++ crates/kesh-i18n/locales/de-CH/messages.ftl | 1 + crates/kesh-i18n/locales/en-CH/messages.ftl | 1 + crates/kesh-i18n/locales/fr-CH/messages.ftl | 1 + crates/kesh-i18n/locales/it-CH/messages.ftl | 1 + .../features/audit-log/audit-log.api.test.ts | 12 ++-- frontend/src/lib/shared/i18n-keys.test.ts | 4 +- .../src/routes/(app)/audit-log/+page.svelte | 43 ++++++++++-- .../(app)/audit-log/audit-log-page.test.ts | 70 ++++++++++++++++++- frontend/tests/e2e/audit-log.spec.ts | 8 +-- 10 files changed, 144 insertions(+), 14 deletions(-) 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 index 19dcceafd..0ab17d9db 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md +++ b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md @@ -444,6 +444,22 @@ Les scénarios : - 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**. +### Review Findings + +*Passe 1, 2026-09-26, prompt `25-1c-b1-review-prompt-p1.md`.* + +- [x] [Review][Patch] **HIGH** — Passer d'un type d'entité à un AUTRE conservait l'identifiant : filtrage silencieux sur l'entité d'un autre type [`+page.svelte`, `onEntityTypeChange`] +- [x] [Review][Patch] **HIGH** — Deux chargements concurrents : une réponse périmée pouvait remplacer la plus récente [`+page.svelte`, `loadList`] — jeton de requête +- [x] [Review][Patch] **MEDIUM** — Plage de dates inversée : refus de la route, non traduit — contrôlée désormais dans la page, message traduit ×4 ; `min`/`max` sur les dates +- [x] [Review][Patch] **MEDIUM** — Une URL au-delà de la dernière page laissait une liste vide sans « Précédent » [`+page.svelte`] +- [x] [Review][Patch] **MEDIUM** — `Object.assign(URL, …)` mutait le vrai `URL`, que `vi.unstubAllGlobals()` ne restaure pas [`audit-log.api.test.ts`] — sous-classe +- [x] [Review][Patch] **MEDIUM** — L'ordre montage / effet d'URL n'était vérifiable par aucun test (mock de `goto` inerte) — `goto` boucle désormais sur l'URL relue, test ajouté, mutation tuée +- [x] [Review][Patch] **LOW** — Numérotation des étapes de la spec E2E dans le désordre +- [x] [Review][Defer] **MEDIUM** — Les refus de validation de la route du journal d'audit ne sont pas traduits (backend, 25-1c-a) → **#469** ; la fiche affirmait à tort qu'un 400 de la route arrive déjà traduit +- [x] [Review][Dismiss] **LOW** — `browser &&` redondant sous `ssr = false` : la garde est un patron **exact** imposé par l'AC 6 +- [x] [Review][Dismiss] **LOW** — `createdAt` invalide : format garanti par la route (`chrono`) +- [x] [Review][Dismiss] **LOW** — L'AC 12 décrit `servir` comme étendu à la locale ; les tests neufs appellent le chargement directement — le résultat prescrit est obtenu + ## Dev Notes ### Ce que la story touche @@ -764,6 +780,7 @@ de texte de spec, sans assertion ajoutée ni ligne de code. | passe | modèle | rendu (après reclassement) | |---|---|---| +| 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. | diff --git a/crates/kesh-i18n/locales/de-CH/messages.ftl b/crates/kesh-i18n/locales/de-CH/messages.ftl index ff2dc95e1..2dce6fe05 100644 --- a/crates/kesh-i18n/locales/de-CH/messages.ftl +++ b/crates/kesh-i18n/locales/de-CH/messages.ftl @@ -2121,6 +2121,7 @@ audit-log-details-show = Anzeigen audit-log-details-hide = Ausblenden audit-log-empty = Keine Einträge für diese Filter. audit-log-error = Das Audit-Protokoll konnte nicht geladen werden. +audit-log-error-date-range = Das Anfangsdatum muss vor dem Enddatum liegen oder ihm entsprechen. audit-log-prev = Zurück audit-log-next = Weiter audit-log-range = { $from }–{ $to } von { $total } diff --git a/crates/kesh-i18n/locales/en-CH/messages.ftl b/crates/kesh-i18n/locales/en-CH/messages.ftl index f169e2796..0243f2bd3 100644 --- a/crates/kesh-i18n/locales/en-CH/messages.ftl +++ b/crates/kesh-i18n/locales/en-CH/messages.ftl @@ -2127,6 +2127,7 @@ audit-log-details-show = Show audit-log-details-hide = Hide audit-log-empty = No entries match these filters. audit-log-error = The audit log could not be loaded. +audit-log-error-date-range = The start date must be on or before the end date. audit-log-prev = Previous audit-log-next = Next audit-log-range = { $from }–{ $to } of { $total } diff --git a/crates/kesh-i18n/locales/fr-CH/messages.ftl b/crates/kesh-i18n/locales/fr-CH/messages.ftl index 9ae44bdfb..79d3f6c17 100644 --- a/crates/kesh-i18n/locales/fr-CH/messages.ftl +++ b/crates/kesh-i18n/locales/fr-CH/messages.ftl @@ -2175,6 +2175,7 @@ audit-log-details-show = Afficher audit-log-details-hide = Masquer audit-log-empty = Aucune entrée ne correspond à ces filtres. audit-log-error = Le journal d'audit n'a pas pu être chargé. +audit-log-error-date-range = La date de début doit précéder ou égaler la date de fin. audit-log-prev = Précédent audit-log-next = Suivant audit-log-range = { $from }–{ $to } sur { $total } diff --git a/crates/kesh-i18n/locales/it-CH/messages.ftl b/crates/kesh-i18n/locales/it-CH/messages.ftl index b9c30f768..ae1f70a80 100644 --- a/crates/kesh-i18n/locales/it-CH/messages.ftl +++ b/crates/kesh-i18n/locales/it-CH/messages.ftl @@ -2122,6 +2122,7 @@ audit-log-details-show = Mostra audit-log-details-hide = Nascondi audit-log-empty = Nessuna voce corrisponde a questi filtri. audit-log-error = Il registro di audit non ha potuto essere caricato. +audit-log-error-date-range = La data d'inizio deve precedere o essere uguale alla data di fine. audit-log-prev = Precedente audit-log-next = Successivo audit-log-range = { $from }–{ $to } di { $total } diff --git a/frontend/src/lib/features/audit-log/audit-log.api.test.ts b/frontend/src/lib/features/audit-log/audit-log.api.test.ts index 54e4ff1ad..b3d9cb331 100644 --- a/frontend/src/lib/features/audit-log/audit-log.api.test.ts +++ b/frontend/src/lib/features/audit-log/audit-log.api.test.ts @@ -55,10 +55,14 @@ describe('audit-log.api', () => { }); it('⛔ l’URL passée à getBlob porte les filtres, sans offset ni limit (mutation : filtres non sérialisés)', async () => { - vi.stubGlobal('URL', Object.assign(URL, { - createObjectURL: vi.fn().mockReturnValue('blob:x'), - revokeObjectURL: vi.fn(), - })); + // Une SOUS-CLASSE, jamais `Object.assign(URL, …)` : muter le vrai `URL` + // survivrait à `vi.unstubAllGlobals()` (revue P1). `new URL(...)` reste + // utilisable plus bas. + class FakeURL extends URL { + static createObjectURL = vi.fn().mockReturnValue('blob:x'); + static revokeObjectURL = vi.fn(); + } + vi.stubGlobal('URL', FakeURL); vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); fetchMock.mockResolvedValue({ ok: true, diff --git a/frontend/src/lib/shared/i18n-keys.test.ts b/frontend/src/lib/shared/i18n-keys.test.ts index 9c1f37b34..4e337e57e 100644 --- a/frontend/src/lib/shared/i18n-keys.test.ts +++ b/frontend/src/lib/shared/i18n-keys.test.ts @@ -235,6 +235,8 @@ const RACINE_FTL = '../crates/kesh-i18n/locales'; * faux avec l'assurance de l'avoir mesuré.* */ const ATTENDU = { + // Story 25-1c-b1 (#378), revue P1 : 1738 → 1740, soit **+2** — le message de plage + // inversée (`audit-log-error-date-range`) et le « Précédent » de l'état vide. // Story 25-1c-b1 (#378) : 1709 → 1738, soit **+29**, recomptés depuis la source // (`grep -o 'i18nMsg('`, `main` contre l'arbre) — tous dans // `routes/(app)/audit-log/+page.svelte` (0 → 29) : titre ×2 (en-tête et @@ -324,7 +326,7 @@ const ATTENDU = { // trouve rien là où l'appel n'existe pas encore : chercher les clés mal // traduites ne révèle jamais celles qui ne sont pas appelées du tout — et // partir d'une liste de clés ne révèle jamais celles que la liste omet.* - sitesTotal: 1738, + sitesTotal: 1740, sitesNonResolus: 34, relais: 7, sitesGabarit: 10, diff --git a/frontend/src/routes/(app)/audit-log/+page.svelte b/frontend/src/routes/(app)/audit-log/+page.svelte index 69c396a75..8febd7c9d 100644 --- a/frontend/src/routes/(app)/audit-log/+page.svelte +++ b/frontend/src/routes/(app)/audit-log/+page.svelte @@ -77,21 +77,42 @@ return q; } + // ⛔ Jeton de requête (revue P1) : deux chargements peuvent se chevaucher — + // un filtre changé pendant un chargement. Seule la réponse du DERNIER + // chargement s'affiche : sans cela, une réponse lente à un filtre périmé + // écraserait celle du filtre affiché, sans rien signaler. + let requestSeq = 0; + async function loadList() { + const seq = ++requestSeq; loading = true; listError = ''; + // Une plage inversée est refusée ICI, dans la langue de l'interface : + // la route la refuserait aussi, mais avec un message non traduit. + if (dateFrom && dateTo && dateFrom > dateTo) { + entries = []; + total = 0; + listError = i18nMsg( + 'audit-log-error-date-range', + 'La date de début doit précéder ou égaler la date de fin.', + ); + loading = false; + return; + } try { const res = await listAuditLog(buildQuery()); + if (seq !== requestSeq) return; entries = res.items; total = res.total; } catch (err) { + if (seq !== requestSeq) return; entries = []; total = 0; listError = isApiError(err) ? err.message : i18nMsg('audit-log-error', "Le journal d'audit n'a pas pu être chargé."); } finally { - loading = false; + if (seq === requestSeq) loading = false; } } @@ -134,9 +155,12 @@ } function onEntityTypeChange() { - // ⛔ Revenir à « Tous » VIDE l'identifiant : désactiver le champ ne - // suffit pas, sa valeur partirait à la route, qui répondrait 400. - if (!entityType) entityIdValue = ''; + // ⛔ Tout changement de type VIDE l'identifiant : un identifiant ne + // désigne une entité que dans SON type. Revenir à « Tous » l'exige + // (la route répondrait 400) ; passer d'un type à un autre aussi — sinon + // l'écran filtrerait, sans rien dire, sur l'entité d'un autre type + // (revue P1). + entityIdValue = ''; applyFilters(); } @@ -215,6 +239,8 @@ id="audit-log-filter-date-from" data-testid="audit-log-filter-date-from" type="date" + min="1000-01-01" + max="9999-12-31" bind:value={dateFrom} onchange={applyFilters} /> @@ -227,6 +253,8 @@ id="audit-log-filter-date-to" data-testid="audit-log-filter-date-to" type="date" + min="1000-01-01" + max="9999-12-31" bind:value={dateTo} onchange={applyFilters} /> @@ -306,6 +334,13 @@

{i18nMsg('audit-log-empty', 'Aucune entrée ne correspond à ces filtres.')}

+ + {#if offset > 0} + + {/if} {:else} diff --git a/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts b/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts index 6647b6259..2eff29881 100644 --- a/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts +++ b/frontend/src/routes/(app)/audit-log/audit-log-page.test.ts @@ -9,7 +9,12 @@ import { render, fireEvent, cleanup, waitFor } from '@testing-library/svelte'; import type { AuditLogEntry, AuditLogVocabulary } from '$lib/features/audit-log/audit-log.types'; vi.mock('$app/environment', () => ({ browser: true })); -vi.mock('$app/navigation', () => ({ goto: vi.fn() })); +// ⚠️ `goto` BOUCLE sur l'URL que la page relit (revue P1) : un mock inerte +// masquerait un effet qui écraserait l'URL avant que `onMount` ne la lise. +const gotoMock = vi.fn((u: URL | string) => { + url.value = new URL(String(u), 'http://localhost'); +}); +vi.mock('$app/navigation', () => ({ goto: (u: URL | string) => gotoMock(u) })); const url = { value: new URL('http://localhost/audit-log') }; vi.mock('$app/state', () => ({ page: { @@ -246,3 +251,66 @@ describe('la page', () => { expect(notifyErrorMock).not.toHaveBeenCalled(); }); }); + +describe('revue P1', () => { + it("⛔ passer d'un type à un AUTRE vide aussi l'identifiant (mutation : vidé seulement au retour à « Tous »)", async () => { + url.value = new URL('http://localhost/audit-log?entityType=contact&entityId=12'); + const { findByTestId, getByTestId } = render(Page); + const select = (await findByTestId('audit-log-filter-entity-type')) as HTMLSelectElement; + await waitFor(() => expect(select.value).toBe('contact')); + select.value = 'journal_entry'; + await fireEvent.change(select); + await waitFor(() => expect(listMock).toHaveBeenCalledTimes(2)); + expect(listMock.mock.calls[1][0]).toMatchObject({ entityType: 'journal_entry' }); + expect((listMock.mock.calls[1][0] as Record).entityId).toBeUndefined(); + expect((getByTestId('audit-log-filter-entity-id') as HTMLInputElement).value).toBe(''); + }); + + it('⛔ une réponse PÉRIMÉE ne remplace pas la plus récente (mutation : jeton de requête retiré)', async () => { + let resolveFirst!: (v: unknown) => void; + listMock + .mockImplementationOnce(() => new Promise((r) => (resolveFirst = r))) + .mockResolvedValueOnce(page1([entry({ id: 2, actionLabel: 'RÉCENTE' })])); + const { findByTestId, getByTestId } = render(Page); + const select = (await findByTestId('audit-log-filter-action')) as HTMLSelectElement; + await waitFor(() => expect(select.options.length).toBe(4)); + select.value = 'contact.created'; + await fireEvent.change(select); + expect((await findByTestId('audit-log-row-action')).textContent).toBe('RÉCENTE'); + resolveFirst(page1([entry({ id: 1, actionLabel: 'PÉRIMÉE' })])); + await new Promise((r) => setTimeout(r, 20)); + expect(getByTestId('audit-log-row-action').textContent).toBe('RÉCENTE'); + }); + + it("⛔ une plage inversée est refusée DANS LA PAGE, traduite, sans appel à la route", async () => { + url.value = new URL('http://localhost/audit-log?dateFrom=2026-09-20&dateTo=2026-09-01'); + const { findByTestId } = render(Page); + expect((await findByTestId('audit-log-error')).textContent).toContain( + 'La date de début doit précéder', + ); + expect(listMock).not.toHaveBeenCalled(); + }); + + it('⛔ une page vide au-delà de la fin garde un « Précédent » (mutation : pagination absente de l’état vide)', async () => { + url.value = new URL('http://localhost/audit-log?offset=500'); + listMock.mockResolvedValue({ items: [], total: 3, offset: 500, limit: 50 }); + const { findByTestId } = render(Page); + await findByTestId('audit-log-empty'); + await fireEvent.click(await findByTestId('audit-log-prev')); + await waitFor(() => expect(listMock).toHaveBeenCalledTimes(2)); + expect((listMock.mock.calls[1][0] as Record).offset).toBe(450); + }); + + it("⛔ l'URL de départ n'est pas écrasée au montage (mutation : effet d'URL avant la lecture)", async () => { + url.value = new URL('http://localhost/audit-log?action=contact.created&entityType=contact&entityId=12'); + render(Page); + await waitFor(() => expect(listMock).toHaveBeenCalled()); + expect(listMock.mock.calls[0][0]).toMatchObject({ + action: 'contact.created', + entityType: 'contact', + entityId: 12, + }); + expect(url.value.searchParams.get('action')).toBe('contact.created'); + expect(url.value.searchParams.get('entityId')).toBe('12'); + }); +}); diff --git a/frontend/tests/e2e/audit-log.spec.ts b/frontend/tests/e2e/audit-log.spec.ts index c26b9be3e..138afb82a 100644 --- a/frontend/tests/e2e/audit-log.spec.ts +++ b/frontend/tests/e2e/audit-log.spec.ts @@ -121,25 +121,25 @@ test('un Comptable ouvre le journal par le menu, filtre, déplie et exporte', as await page.reload(); await expect(row).toHaveCount(1); - // 5. Le détail se déplie et contient le JSON. + // 3. Le détail se déplie et contient le JSON. await row.getByTestId('audit-log-details-toggle').click(); await expect(row.getByTestId('audit-log-details')).toContainText('{'); - // 6. L'export télécharge le fichier nommé par la route. + // 4. L'export télécharge le fichier nommé par la route. const [download] = await Promise.all([ page.waitForEvent('download'), page.getByTestId('audit-log-export').click(), ]); expect(download.suggestedFilename()).toMatch(/^kesh-journal-audit-/); - // 3. Une période PASSÉE l'écarte. + // 5. Une période PASSÉE l'écarte. await page.getByTestId('audit-log-filter-date-from').fill('2000-01-01'); await page.getByTestId('audit-log-filter-date-to').fill('2000-01-02'); await page.getByTestId('audit-log-filter-date-to').blur(); await expect(page.getByTestId('audit-log-empty')).toBeVisible(); await expect(row).toHaveCount(0); - // 4. ⛔ La période qui la GARDE se calcule en jour UTC, non en jour local : + // 6. ⛔ La période qui la GARDE se calcule en jour UTC, non en jour local : // entre 00:00 et 02:00 à Zurich, le jour local précède le jour UTC. await page.getByTestId('audit-log-filter-date-from').fill(todayUtc()); await page.getByTestId('audit-log-filter-date-to').fill(todayUtc()); From cbccafcfb27a876fef9c76cc188e1fd50d4650d6 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:03:13 +0200 Subject: [PATCH 07/19] docs(25-1c-b1): prompt de la passe 2 de revue (refs #378) Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b1-review-prompt-p2.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 _bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p2.md diff --git a/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p2.md b/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p2.md new file mode 100644 index 000000000..0e0cc613b --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b1-review-prompt-p2.md @@ -0,0 +1,74 @@ +# Prompt — passe 2 de `bmad-code-review`, Story 25-1c-b1 + +*Versionné le 2026-09-26. Trois lentilles en contexte frais (Haiku 4.5) — cycle Sonnet → Haiku → Opus. +Diffs APLATIS (règle Haiku du `CLAUDE.md`).* + +⛔ **Premier suspect : la remédiation de la passe 1** (entrée « review P1 » du Change Log et section +« Review Findings » de la fiche) — jeton de requête dans `loadList`, identifiant vidé à tout +changement de type, plage de dates contrôlée dans la page, « Précédent » dans l'état vide, `FakeURL`, +`goto` bouclé dans le test de page. Diff de la remédiation seule : +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/ca5ce2e2-67a3-4eeb-817f-c2de35620a1c/scratchpad/25-1c-b1-remediation-p1.diff`. +Reporté, **à ne pas re-signaler** : #469 (refus de validation de la route non traduits). + +Dépôt `/home/gcorbaz/devel/kesh`, branche `story/25-1c-b1-journal-audit-ecran`, tête `576464df`. +**Diff revu** : `git diff main..HEAD` sur le code (PDF et `_bmad-output/` exclus), enregistré dans +`/tmp/claude-1000/-home-gcorbaz-devel-kesh/ca5ce2e2-67a3-4eeb-817f-c2de35620a1c/scratchpad/25-1c-b1-code-p2.diff`. +Fiche : `_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md` — AC 1 à 14, arbitrages +du Project Lead (**ne pas les contester**, en contester la mise en œuvre), Dev Agent Record. Contrat de la +route consommée : `crates/kesh-api/src/routes/audit_log.rs` (Story 25-1c-a, mergée). + +Objet : l'**écran de consultation du journal d'audit** (#378) — feature `frontend/src/lib/features/audit-log/`, +page `frontend/src/routes/(app)/audit-log/`, module partagé `frontend/src/lib/shared/utils/download.ts`, +`i18nLocale()` dans le store i18n, entrée de menu, 26 clés ×4, gardes i18n, spec E2E. ⚠️ Les manuels et +le vocabulaire des catalogues sont la **25-1c-b2**, hors de cette revue. + +## Lentille 1 — Blind Hunter (diff SEUL) + +Revue adversariale générale : logique, réactivité Svelte 5 (`$state`, `$derived`, `$effect` — boucle +possible entre l'effet qui écrit l'URL et `onMount` qui la lit ?), gestion d'erreurs, tests qui ne +prouvent rien ou passent à vide, incohérences code / commentaires / textes, clés i18n (quatre locales, +replis **mot pour mot** le FTL fr-CH, vocabulaire arbitré), sélecteurs E2E par libellé. + +## Lentille 2 — Edge Case Hunter (diff + dépôt) + +Chaque branche du code neuf, en lisant l'appelé et l'appelant. Priorités : +1. **La page** : ordre des effets au montage (l'effet d'URL s'exécute-t-il avant que `onMount` ait lu + l'URL, et l'écrase-t-il ?), double chargement, courses entre deux `loadList` (un filtre changé pendant + un chargement — la réponse la plus ancienne peut-elle écraser la plus récente ?), `entityIdValue` lié à + un `` (nombre ou chaîne ? `null` quand on efface ?), pagination aux bornes, + `details` à `null` / chaîne / tableau, `createdAt` invalide. +2. **Le contrat** : chaque paramètre envoyé à `/api/v1/audit-log`, `/vocabulary`, `/export.csv` contre + `ListAuditLogQuery` (noms, valeurs par défaut, `offset`/`limit` obligatoires ou non) ; chaque champ lu + contre `AuditLogEntryResponse`. Une réponse 400 de la route (plage inversée, date hors bornes, action + > 64 caractères, `entityId` sans type) est-elle atteignable depuis l'écran, et que voit l'utilisateur ? +3. **`i18nLocale()`** : qui d'autre charge les messages (`loadI18nMessages`) — la locale peut-elle rester + `'fr-CH'` sur une installation allemande (écran ouvert avant le chargement) ? +4. **`download.ts`** : strictement identique à l'original d'`exports.api.ts` ? Les commentaires + modifiés disent-ils vrai ? +5. **Les gardes i18n** : les compteurs modifiés, recomptés depuis la source ; `FAMILLES_RESOLUES`. +6. **L'E2E** : chaque scénario peut-il passer à vide (isolation par type et identifiant, attente de la + liste avant l'assertion, fuseau `Europe/Zurich`, jour UTC) ? + +## Lentille 3 — Acceptance Auditor (diff + fiche) + +Chaque AC (1 à 14) contre le diff, sous-point par sous-point. Les décomptes du Dev Agent Record +**recomptés** depuis la source : 27 clés ×4, total 165 par catalogue, `sitesTotal` 1709 → 1740 (+31), +vitest +40 (et sa ventilation), E2E +2, 17 mutations (juge, en lisant le code et les tests, que chacune +aurait bien rougi). Les `data-testid` de l'AC 8 tous présents ? Aucun sélecteur par libellé ? Les trois +commentaires de l'AC 9 réécrits et `reports.api.ts` intact ? Les termes du glossaire +(`docs/i18n-glossaire.md`) respectés dans les quatre langues ? + +## Ce que tu rends + +- Findings : sévérité (CRITICAL / HIGH / MEDIUM / LOW), `fichier:ligne`, **preuve** (code lu, commande + et résultat), correction. Pour tout CRITICAL ou HIGH affirmant qu'une chose est absente ou présente : + la commande `grep -nF` exécutée et son résultat. +- ⛔ **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 vitest`, `npx playwright`. Lecture, `grep`, `git log`/`show`/`diff` et `cargo check` sont autorisés. From 6cdc662ffa47f1e89bb7591be029e86d446d6ccf Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:09:25 +0200 Subject: [PATCH 08/19] docs(25-1c-b1): revue P2 close (0 au-dessus de LOW) (refs #378) Co-Authored-By: Claude Opus 5.5 --- .../implementation-artifacts/25-1c-b1-journal-audit-ecran.md | 1 + _bmad-output/implementation-artifacts/sprint-status.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 index 0ab17d9db..db526c903 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md +++ b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md @@ -780,6 +780,7 @@ de texte de spec, sans assertion ajoutée ni ligne de code. | passe | modèle | rendu (après reclassement) | |---|---|---| +| 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`. | diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index 7230e37a2..e4174ff4f 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -330,7 +330,7 @@ development_status: 25-1b-trous-alimentation: done # 2026-09-13 ✅ **GATE E2E DE CLOTURE — 216 passed / 19 skipped / 7 failed (8m42)**, montage COMPLET, base `kesh_e2e` DETRUITE ET RECONSTRUITE, `/health` controle avant lancement. ✅ **LES 7 ECHECS SONT EXACTEMENT LA BASELINE KF-029** — aucune pollution, aucune KF-045 (run d'apres-midi), aucune KF-046. Attendu 8 a 10, observe **7** : la baseline PURE, le meilleur resultat possible. Recoupement : 216+19+7 = 242 comme au run du 11 (214+19+9), les 2 tests de plus qui passent sont les 2 pollutions non declenchees ⇒ **ZERO REGRESSION**. ⚠️ **LE BUILD FRONTEND ETAIT EN RETARD ET A ETE REFAIT** : date du 11/09 11:40 alors que le dernier commit touchant `frontend/` (celui de la 25-1a, merge depuis dans `main`) est de 19:18. *Cette story ne touche pourtant PAS le frontend* — c'est le piege de la 24-5 : **un gate declare vert sur un perimetre qui a bouge n'est plus un gate**. **LES TROIS GATES SONT VERTS** : backend **2322/2322**, E2E **216/7-attendus**, frontend et i18n NON TOUCHES (verifie par `git diff`). **Reste : push + PR avec `closes #379` DANS LE TITRE** (squash-merge : le titre devient le message du commit). # 2026-09-13 2026-09-12 **IMPLEMENTEE** — **14 routes tracees**, gate backend COMPLET **2318/2318** (4 skipped, 90 s), fmt et clippy propres, frontend et i18n **NON TOUCHES (verifie)**. ⛔ **CINQ DEFAUTS QUE SEULE L'EXECUTION A REVELES, ET CE SONT TOUS DES DETECTEURS MAL FORMES — LES MIENS** : (1) `creditor_iban` n'est PAS `Option` — le compilateur a rejete le `is_some()` que la spec prescrivait ; (2) ⛔ **les 4 helpers d'audit CASSAIENT LE GATE** — `tests/common/mod.rs` est compile SEPAREMENT dans chaque binaire de test, donc un helper employe par 10 fichiers sur 40 est « jamais utilise » dans les 30 autres et `clippy -D warnings` refusait ; (3) ⛔ **mon extracteur du registre rendait un ensemble VIDE** sur `test_endpoints.rs` (forme `.route("/x", post(h))` et non `post(routes::m::h)`) et **comparait donc deux vides AVEC SUCCES** ⇒ garde contre l'ensemble vide ; (4) ⚠️ **mon controle de PDF a rendu un FAUX NEGATIF** sur une apostrophe TYPOGRAPHIQUE cherchee en apostrophe droite — le piege que le `CLAUDE.md` documente, pris sur le fait ; (5) ⛔ **ma remise a zero de la base de gate etait INCOMPLETE puis OMISE** : seed saute (29 tests tombes sur « need at least one Admin user in DB »), puis base non reconstruite entre deux runs alors que la regle est **INCONDITIONNELLE**. ⚠️ La 1re erreur a ete MASQUEE par une **verification mal formee** : elle comptait migrations et tables — ce qui etait commode — au lieu de ce dont les tests ont besoin. *Verifier n'est pas executer ; mais verifier la MAUVAISE CHOSE ne vaut pas mieux que ne rien verifier.* ✅ **Un test a rougi A RAISON, et c'etait LE TEST** : il comptait les traces globalement alors que le chemin d'amorçage ecrit desormais la sienne. Les 3 compteurs globaux du depot inspectes ensuite : aucun casse. ✅ **DEUX gardes eprouvees par MUTATION** : la garde no-op de `users` et celle du registre. ✅ **Trou de couverture ferme** : `contact_persons_e2e.rs` cree — ces 3 routes n'avaient AUCUN test. ✅ **Registre de 108 routes** (105 `lib.rs` + 3 `test_endpoints.rs`), diff ENSEMBLISTE par identite (105 couples verbe+handler, 0 doublon), ventilation recomptee : **87 tracees / 15 exemptees avec numero d'issue / 3 sans matiere**. ✅ **8 sites de manuel corriges**, 3 PDF regeneres, verifies sur les PDF **APLATIS**. **Decompte recoupe : 2307 + 11 tests neufs = 2318.** ⚠️ **E2E AU PUSH.** Prochaine : `bmad-code-review` passe 1, trois lentilles. # 2026-09-11 **VALIDATE 2026-09-11 **VALIDATE PASSE 1** (Sonnet 4.6 + Haiku 4.5, contextes frais, orthogonales a l'auteur Opus 5, prompt versionne) : **1 CRITICAL, 1 HIGH, 3 MEDIUM, 2 LOW** — tous verifies au sol, tous patches. ⇒ **12 AC (3 volets), 11 taches** (recomptes depuis la source : `grep -cE '^\*\*[0-9]+\. '`). ⛔ **LE CRITICAL EST UN INVARIANT INTENABLE SUR UNE SEULE ROUTE** : `imported_supplier_invoices::create` ecrit **sur le POOL** (`:61`) et **aucun `create_in_tx` n'existe** — a la difference de `mark_completed` et `mark_discarded`, tous deux transactionnels dans le MEME fichier. La spec ne prevoyait que le threading de l'acteur ⇒ la trace se serait ecrite dans une transaction DISTINCTE, apres l'autocommit : *exactement la fenetre non atomique que l'AC 9 ferme sur les douze autres routes*. Un developpeur ne l'aurait decouvert qu'en codant. ⛔ **LE HIGH VIENT DE L'ORCHESTRATEUR, EN VERIFIANT UN MEDIUM DE HAIKU** : la lentille signalait UN champ faux (`metadata_json`) au manuel ; il y en a **TROIS sur sept** — `company_id` **QUI N'EXISTE PAS** (et que l'arbitrage du jour vient de decider d'ajouter), `timestamp` pour `created_at`, `metadata_json` pour `details_json` — et **quatre manquent**. Surtout, la lentille n'a PAS releve l'affirmation qui OUVRE la section : `admin-manual.tex:1782` « **TOUTE** action metier mutante est enregistree » — faux pour 28 routes, et **TOUJOURS faux apres cette story** pour 15. **Le manuel se contredit lui-meme a 170 lignes d'ecart** (`:1956` avoue la couverture partielle) : motif exact paye par la 25-1a sur la section OLICo. ⇒ **AC 12 ajoute**. ⚠️ **MEDIUM de Sonnet sur l'AC 11 — le detecteur etait MAL FORME** : comparer un NOMBRE ne detecte pas une route retiree pendant qu'une autre est ajoutee ; `admin_pat_denied_e2e` peut compter parce qu'il opere sur un BLOC CLOS entre marqueurs, pas les 105 routes eparpillees ⇒ **diff ENSEMBLISTE par identite**. ⚠️ 2 LOW : deux references de ligne derivees (`companies.rs` le `if` est a `:181` pas dans 175-178 ; `audit.rs` le trait est a `:18`, son impl a `:33`). ✅ **1 finding de Haiku ECARTE** : `contact_persons.rs:5` etait **deja nomme par l'AC 2**. ✅ **Grep de propagation fait AVANT la passe 2** : symptome **circonscrit au manuel admin FR** (DE/EN/IT n'ont qu'un README). ⛔ **TROUVE EN CHEMIN, HORS PERIMETRE** : `user-manual.tex:503` affirme « aucun verrou de periode plus fin n'existe encore » alors que `:444-453` **le documente** — le manuel se contredit a 50 lignes d'ecart depuis la 24-4c. A TRACER. # 2026-09-11 **VALIDATE PASSE 2** (Opus 5, lentille unique, contexte frais, prompt versionne, braquee sur les 7 patches de la P1) : **0 CRIT, 3 HIGH, 5 MED, 6 LOW** — severite en RECUL (CRIT → HIGH) ⇒ **pas de split**. ⚠️ **13 findings sur 15 sont D'ORIGINE** — 1 HIGH et 1 LOW seulement nes d'un patch : *l'INVERSE du motif habituel du depot, la remediation n'a presque rien casse, c'est la conception qui etait incomplete*. ⛔ **P2-1 HIGH — UN TROISIEME CHEMIN D'ENTREE** : la REACTIVATION d'une piece ecartee (`inbox_import.rs:418-431`) rend `Accepted` **sans rien creer** ⇒ la piste garderait `discarded` pour derniere trace d'une piece `to_complete`. *Un journal qui affirme le contraire de la base est pire qu'un journal muet.* Et `reactivate_to_complete` porte le MEME defaut que `create` (`.execute(pool)`), a 60 lignes de la : **le patch de la P1 avait ferme la moitie du trou**. ⛔ **P2-2 HIGH — UNE JUSTIFICATION FONDEE SUR UN FAIT INEXISTANT** : la spec disait « la creation du premier admin n'est pas tracee ALORS QUE SON CHEMIN JUMEAU L'EST » ; `auth/bootstrap.rs:123` cree le meme admin et **n'ecrit rien** (le seul `insert_in_tx` du fichier, `:252`, est le break-glass). Et ce chemin **n'est pas une route** ⇒ hors des 105, hors [#434]/[#435] ⇒ **ecrit NULLE PART**. ⛔ **P2-3 HIGH, NE DU PATCH P1 — UN BALAYAGE DECLARE COMPLET NE L'ETAIT PAS** : la P1 avait ecrit « symptome circonscrit au manuel admin » ; il restait **`admin:2184`** (glossaire, meme promesse 400 lignes plus loin — la corriger seule REINSTALLE la contradiction) et **`user-manual:1591-1594`** (un AUTRE manuel). Le grep portait sur `metadata_json`, **UN des TROIS symptomes** de l'AC 12. *Greper un symptome n'est pas greper le defaut* — **sixieme fois** que ce geste est pris en defaut sur ces deux stories. ⇒ AC 12 passe a **CINQ sites, deux manuels**. ⚠️ P2-4 : l'AC 8 disait « trois routes », il y en a **QUATRE** (2 `users` + 2 `companies` partagent leur repository). ⛔ **P2-5 : LE COMMENTAIRE PUBLIE SUR [#431] ETAIT FAUX** — 35 sites annonces, **38** reels, sa propre ventilation sommant a 41 (les 3 de `mod tests` soustraits DEUX FOIS). **Rectifie sur l'issue le meme jour.** *Recompter depuis la source vaut aussi pour ce qu'on vient d'ecrire ailleurs.* ⚠️ P2-6 : deux silences, deux mecanismes — les `410`/`500` rollbackent, les quatre `400` sont AVANT le `begin`. ⚠️ P2-7 : **l'ensemble clos est celui de `lib.rs`, PAS du backend** — 3 routes mutantes vivent dans `test_endpoints.rs` via le `nest()` de `:995` ⇒ a exempter NOMMEMENT. ⚠️ **P2-8, ne du patch P1 : « transaction portee par `process_one_file` » ne disait pas OU** — pool a **5 connexions** (`main.rs:73`), `GET_LOCK` en tient deja une ⇒ la poser autour de la SEULE insertion (`:488-516`), sinon une connexion *idle-in-transaction* par fichier, jusqu'a 200 par run. ✅ Verifie en P2 et a ne pas re-debattre : le `GET_LOCK` n'interfere pas, la transaction PAR FICHIER est le bon grain, l'identite verbe+handler est discriminante (105 couples, 0 doublon). ⚠️ **Axe NON exerce declare par la lentille** : rien n'a ete COMPILE — la faisabilite des `_in_tx` reste raisonnee, pas prouvee par le compilateur ; et **la « 1 partielle » et les « 3 sans matiere » de l'inventaire ne sont nommees nulle part** (faiblesse reelle de l'inventaire). # 2026-09-12 **VALIDATE PASSE 3** (Sonnet 4.6, contexte frais, prompt versionne, DEUX FRONTS : les patches de la P2 **et les axes que la P2 declarait NON EXERCES**) : **0 CRIT, 1 HIGH, 2 MED, 2 LOW** — **TROISIEME RECUL CONSECUTIF** (1C/1H → 3H/5M → 1H/2M), convergence monotone, **les 5 sont D'ORIGINE**. ⛔ **P3-1 HIGH — T6 CONTREDISAIT L'AC 9 EN SILENCE** : elle ne disait ni d'extraire `update_step_in_tx`, ni qui mene la transaction. Pire : `onboarding::update_step` a **d'autres appelants** (le seed + 8 routes d'onboarding) ⇒ poser la trace DANS le repository ferait ecrire `installation.ui_mode_changed` **a chaque etape de l'onboarding**. *Une trace au mauvais etage ne manque pas : elle ment.* ⛔ **P3-2 — UN SIXIEME SITE DE MANUEL, ET IL PARLE D'ATTRIBUTION, PAS DE COUVERTURE** : `admin:1762` promet que les mutations par cle API sont tracees `actor_type='api_key'` — **faux pour les 38 sites de [#431]**, que cette story ne corrige pas. *Un inventaire construit sur une seule question ne trouve que les reponses a cette question* : cinq relectures cherchaient « ce qui est trace », celui-ci dit « comment c'est attribue ». **SEPTIEME defaut de manuel sur ces deux stories.** ⚠️ P3-3 : « 1 partielle » n'avait pas de PORTEE declaree — [#434] en documente une seconde (`onboarding/finalize`) ⇒ le chiffre a ete relu contre le mauvais perimetre. ⚠️ P3-5 : les 3 routes de `test_endpoints.rs` doivent sortir du DIFF AUTOMATIQUE (elles n'ont jamais ete dans `lib.rs`) — *un garde qui rougit toujours est un garde qu'on desactive*. ✅ **REPRENDRE LES AXES NON EXERCES PAIE** : l'axe « contenu de [#434] » a rendu P3-3, l'axe « faisabilite » a rendu P3-1. *Une passe qui declare honnetement ses manques rend la suivante plus efficace que si elle avait bluffe.* ✅ Verifie et a ne pas re-debattre : 105 routes **recomptees independamment** par script depuis `lib.rs` ; frontend et i18n **exerces, 0 finding** ; aucun changement de schema ; `cargo check --workspace --all-targets` propre ; les 5 sites de manuel exacts a la ligne. ⚠️ **Axes NON exerces declares** : les **73 routes reputees tracees** ne sont toujours pas verifiees UNE PAR UNE (coherence par fichier seulement) ; `cargo clippy` non lance ; la faisabilite du code a ecrire reste **raisonnee, non prototypee**. # 2026-09-12 **VALIDATE PASSE 4** (Haiku 4.5, contexte frais, prompt versionne) : **1 MEDIUM** apres reprise d'axe par l'orchestrateur. ⛔ **LA LENTILLE A RENDU 0 FINDING EN DECLARANT N'AVOIR TRAITE QUE 4 ROUTES SUR 73** — ni le 7e site du manuel, ni les recomptes ⇒ **NE COMPTE PAS COMME PASSE** (regle « une passe qui ne declare pas ses axes exerces »). ✅ **Mais sa FRANCHISE a fait son travail** : *un « 0 finding » adosse a la liste de ce qui n'a PAS ete fait vaut infiniment mieux qu'un « 0 finding » qui se tait* — contraire exact du precedent de la 24-5. Les 3 axes repris par l'orchestrateur. ✅ **AXE PRINCIPAL EXERCE AUTREMENT** : non route par route, mais par les **94 appels d'audit** du backend et leur gouvernance conditionnelle ⇒ **8 sous condition, les 8 justifies** (garde no-op, lot vide, verrou qui recule, best-effort, complementarite) ⇒ **aucun trou de couverture nouveau**. ⚠️ **Ce que la methode NE VOIT PAS, et c'est ecrit** : une route dont un chemin de succes CONTOURNERAIT le repository qui trace. ⛔ **P4-1 — UN SEPTIEME SITE DE MANUEL, ANGLE ENCORE INEDIT : L'EXPORTABILITE.** `admin:1935` gage la conformite **OLICo art. 9 al. 3** sur un ZIP d'export « garanti par `audit_log` » — or l'export porte **19 tables sur 38 et PAS `audit_log`** ([#386]). *Sept sites, SEPT QUESTIONS DIFFERENTES : couverture, champs, attribution, exportabilite — chacun trouve en posant une question que le precedent ne posait pas. Un inventaire documentaire ne se clot pas par l'epuisement des FICHIERS mais par celui des QUESTIONS.* ⛔ **HORS PERIMETRE, PORTE A [#431]** : `reconciliation_rule.deleted` ecrit son attribution **DIFFEREMMENT SELON L'EFFET** — `::user` quand la suppression aboutit (repo `:402`), `from_current_user` quand elle est no-op (route `:473`). La couverture est entiere, l'attribution non : *un reviseur qui filtre sur `actor_type='api_key'` verra les suppressions SANS effet et manquera celles qui en ont eu un.* **L'incoherence vaut pire que l'absence.** ⛔ **HORS PERIMETRE, PORTE A [#386]** : le site `:1935`, avec la correction INVERSE a prevoir quand l'export integrera la piste. **TREND : 1C/1H/3M/2L → 0C/3H/5M/6L → 0C/1H/2M/2L → 1M.** Convergence monotone depuis la P2. # 2026-09-12 **VALIDATE PASSE 5 CIBLEE** (Opus 5, contexte frais, prompt versionne, perimetre = LA REMEDIATION des P3/P4) : **0C, 0H, 4 MED, 5 LOW** ⇒ **LA BOUCLE NE PEUT PAS SE CLORE** (critere : plus rien au-dessus de LOW). ⛔ **P5-1, NE DU PATCH P3 — UN PATCH QUI ANNULE LE BENEFICE QU'IL INVOQUE** : sortir les 3 routes de `test_endpoints.rs` du diff ENTIER rend une QUATRIEME route de ce fichier aussi invisible qu'avant, alors que la phrase « sans quoi une 4e route ne ferait rougir aucun garde » a SURVECU au patch qui la rendait fausse ⇒ exclusion du **SECOND VOLET seulement**, l'extracteur lit les DEUX fichiers. *Corriger un mecanisme sans relire ce qui le motivait produit un texte qui se refute lui-meme.* ⛔ **P5-2 — « PROPAGE PARTOUT » EST UNE AFFIRMATION, DONC ELLE SE VERIFIE** : mon commit annonçait « propage aux SIX enonces », il y en avait **HUIT** — l'AC 7 n'assignait de constructeur qu'a TREIZE routes (la 14e, `complete`, absente du tableau) et T4 s'intitulait « deux routes » pour trois. *Celui qui corrige une derive de compteur n'est pas immunise contre elle* — c'est le defaut meme que la P2 avait reproche a la spec. ⛔ **P5-3 — UN HUITIEME SITE DE MANUEL, ET IL AMENDE LA LEÇON DE LA P4** : `user-manual:1733` (glossaire) porte la meme promesse que `:1591-1594` **plus un renvoi vers une section qui n'existe pas**. Les 7 premiers avaient chacun demande une QUESTION NEUVE ; le 8e repond a une question **DEJA POSEE, dans un fichier qu'elle n'avait pas visite**. ⇒ *Un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT**.* La leçon de la P4 etait vraie ET INCOMPLETE : elle orientait vers des themes neufs et detournait du balayage exhaustif. ⚠️ P5-4 : l'AC 12 s'intitulait « le manuel administrateur… TROIS affirmations » quand sa table en listait SEPT dans DEUX manuels — les P3 et P4 avaient incremente la ligne JUSTE EN DESSOUS sans toucher celle-la. ⚠️ P5-5 : « sept des quatorze chemins » → **SIX** (5 autres n'ouvrent AUCUNE transaction, 3 en ont deja une au handler : 6+5+3=14). ✅ **CONFIRME AU SOL par la P5** : les 94 appels / 8 conditionnels, les 19 tables d'export sans `audit_log`, la 2e partielle de [#434], la faisabilite de T6 (10 appelants, ZERO changement de signature), les 7 sites precedents, et **105 routes re-derivees independamment**. ⛔ **ARBITRAGE DEMANDE AU PROJECT LEAD** : la severite MAXIMALE recule encore (CRIT → HIGH → HIGH → MED) mais le NOMBRE de MED remonte (2 → 4). La § *Regle de splitting preventif* vise « une passe N+1 qui remonte une severite EGALE OU SUPERIEURE (e.g. MED → MED) ». **Recommandation de l'orchestrateur ET de la lentille : PAS de split** — aucun des 4 MED ne touche la conception (deux totaux non propages, un site documentaire, un garde dont le patch a vide la justification) ; c'est le motif Epic 16 *« le compte rendu est devenu le lieu du defaut, pas le fond »*. # 2026-09-12 **VALIDATE PASSE 6 CIBLEE** (Sonnet 4.6, contexte frais, prompt versionne, perimetre = LE SEUL COMMIT `2f87b1aa`) : **0C, 0H, 2 MED, 0 LOW** — **LES DEUX NES DU CORRECTIF DE LA P5**. ⛔ **P6-2 — UN PATCH MET A JOUR LE CRITERE ET OUBLIE LA TACHE QUI L'EXECUTE** : l'AC 12 comptait HUIT sites, **T10 en listait encore SEPT**. *Un developpeur suivant la tache a la lettre aurait corrige 7 sites sur 8 — et le gate serait reste VERT.* ⚠️ P6-1 : « quatre autres n'ouvrent aucune transaction » alors que le tableau JUSTE EN DESSOUS en liste CINQ (6+5+3=14). ✅ **Les deux verifications demandees sont FAVORABLES** : le constructeur de `complete` est EXACT (route dans `comptable_routes` ⇒ atteignable par jeton ⇒ `from_current_user`), et **AUCUN neuvieme site** — le manuel utilisateur ne promet rien sur les champs, l'attribution ni l'exportabilite. ⛔ **LE MOTIF ETAIT DEVENU CLAIR — la P5 corrige, la P6 trouve deux defauts DU PATCH P5 — et une passe de plus l'aurait RECONDUIT.** ⇒ § *Propagation post-patch* appliquee par l'orchestrateur sur **TOUS** les enonces numeriques du document. Elle a rendu : (a) l'ambiguite **105 vs 108** entrees au registre, que la P6 signalait sans la trancher — l'inventaire porte sur `lib.rs` (105), **le registre est plus large** (108), et c'est voulu ; (b) ⛔ **UN DERNIER PIEGE QUE SIX PASSES ONT LAISSE PASSER** : « les conventions, verifiees sur les **73 SITES** en place » — ce 73 designait les **LIBELLES D'ACTION**, pas les 73 routes tracees. **Deux grandeurs sans rapport portant le MEME NOMBRE sont indetectables a la relecture** ; seul un balayage qui interroge **ce que chaque nombre COMPTE**, et non sa valeur, pouvait le voir. *C'est la § « greper la VALEUR, pas la formulation » RETOURNEE : ici la valeur etait juste et l'OBJET faux.* L'enonce nomme desormais son objet. # 2026-09-12 ✅ **BOUCLE DE VALIDATE CLOSE — 7 PASSES** : **1C/1H/3M/2L → 3H/5M/6L → 1H/2M/2L → 1M → 4M/5L → 2M → 0**. **Severite maximale : CRIT → HIGH → HIGH → MED → MED → MED → RIEN**, decroissance STRICTE. Rotation complete (Sonnet+Haiku → Opus → Sonnet → Haiku → Opus → Sonnet → Haiku), **7 prompts versionnes**, 3 passes CIBLEES. **PASSE 7** (Haiku 4.5, ciblee sur `1f02097f`) : **0 finding**, 4 axes exerces, axes non exerces declares et tous LOGIQUEMENT IMPOSSIBLES a ce stade (verifier des PDF regeneres pour une spec non implementee). **Les 2 affirmations principales REPRISES par l'orchestrateur** — *un « 0 finding » se verifie comme un finding* : plus aucun « sept sites » hors Change Log, somme du tableau **6+5+3=14**. ⚠️ **Le controle a produit un FAUX POSITIF INSTRUCTIF** : un test cherchant « audit » sur `admin:1786` l'a signalee vide — elle enumere des NOMS DE COLONNES et ne contient pas le mot. *Un detecteur mal forme coute le meme diagnostic qu'un defaut reel* — jusque dans la verification d'une cloture. **CE QUE LA BOUCLE LAISSE AU-DELA DE LA STORY** : (1) ⛔ *un inventaire ne se clot ni par l'epuisement des FICHIERS ni par celui des QUESTIONS, mais par leur **PRODUIT*** — 8 sites de manuel = 4 questions x 2 fichiers ; (2) ⛔ **deux grandeurs differentes portant le MEME NOMBRE sont indetectables a la relecture** (« 73 » = routes tracees OU libelles d'action) ⇒ interroger ce que chaque nombre COMPTE, pas sa valeur ; (3) ⛔ **un patch met a jour le CRITERE et oublie la TACHE qui l'execute** — 7 sites corriges sur 8, **gate vert** ; (4) ⛔ **quand une passe ne trouve plus que les defauts de son propre patch, ajouter une passe RECONDUIT le motif** — ajouter le GESTE, et le faire soi-meme ; (5) ✅ **une passe qui declare honnetement ce qu'elle n'a pas fait vaut mieux qu'une passe qui bluffe** (la P4 a designe ou chercher, 2 findings de la P3 en sont nes). **Statut `ready-for-dev`. Prochaine : `bmad-dev-story`.** ⛔ **L'INVENTAIRE DEPLACE LE PERIMETRE** : balayage de l'ENSEMBLE CLOS des **105 routes mutantes** du backend ⇒ **73 tracees, 28 NON tracees, 1 partielle, 3 sans matiere**. Le releve des six trous est EXACT mais INCOMPLET : deux familles entieres manquaient — **`onboarding` (11 routes)**, dont `accounts::bulk_create_from_chart` (non audite alors que les 4 autres mutations d'`accounts` le sont), la creation du compte bancaire, le seed et le reset — le code le sait, `routes/onboarding.rs:523-527` porte un TODO — et **`auth`/session (4 routes)**, ou `change_password` (`auth.rs:511`) n'ecrit RIEN alors que `reset_password`, EFFET MATERIEL IDENTIQUE, audite (`auth.rs:948`) ; y compris la revocation en masse pour VOL DE JETON (`auth.rs:382`). ⇒ **8 familles > 5 modules : la § Regle de splitting prefentif s'applique** ⇒ story tenue au perimetre de [#379] (6 familles, **13 routes**), les deux decouvertes sortent en issues propres. ✅ **ISSUES OUVERTES LE 2026-09-11 : [#434]** (onboarding, 11 routes) et **[#435]** (`auth`/session, 4 routes) — l'AC 11 les cite au registre des exemptions. ⚠️ **[#435] porte une question de conception** : tracer chaque `login`/`refresh` ferait de la piste un JOURNAL DE SESSIONS ; les 4 routes ne se valent pas (`change_password` et `theft_detected` sont rares et lourds, `login`/`refresh` sont du trafic). ⛔ **LE FAIT STRUCTURANT N'EST PAS DANS L'ISSUE** : `insert_in_tx` ne prend qu'une `&mut Transaction`, or **7 des 13 chemins** passent par un repository qui OUVRE ET COMMITE SEUL ⇒ le handler n'a rien ou greffer. Decision: extraire des variants `_in_tx` plutot que descendre `user_id` — parce que (1) `update_role_and_active` sert DEUX routes qui doivent ecrire DEUX libelles, (2) `companies::update` et `onboarding::update_step` ont d'autres appelants, (3) **descendre `user_id` NU est le mecanisme meme de [#431]**. ⚠️ **4 routes sont quasi gratuites** (`discard` et `setup/admin` : tx deja ouverte ; `POST /users` et `/reset-password` : variant `_in_tx` deja ecrit POUR CELA) — commencer par elles. ⛔ **Le critere du constructeur n'est PAS le role mais « un PAT peut-il atteindre ce chemin »** : `require_not_pat` est pose **UNE SEULE FOIS** (`lib.rs:330`, bloc `admin_routes`). ⇒ `::user` sur les 4 `users` + 2 `companies` ; `from_current_user` sur les 3 `contact_persons`, `discard`, `profile` ; `for_actor` threade sur `inbox-import` ; `::user(user.id)` sur `setup` (PRE-AUTH, acteur = la cible). ⚠️ **`user.role_changed` porte son propre libelle** — precedent `books.unlocked` vs `books.restored` : « confondre les deux rendrait le filtre inutilisable pour le reviseur ». ⚠️ **`profile/mode` trace l'INSTALLATION, pas l'utilisateur** : `onboarding_state` est MONO-LIGNE ET GLOBALE, sans `company_id` — un utilisateur bascule l'installation entiere en mode Expert. ⛔ **TROU DE COUVERTURE DECOUVERT** : les 3 routes `contact_persons` n'ont **AUCUN test**, ni integration ni E2E ⇒ fichier a creer (on n'ajoute pas une trace a du code que rien n'exerce). ⚠️ **AC 11 — registre des routes mutantes + garde** : la story ferme 13 des 28 sites ; les 15 restants deviennent des angles morts ASSUMES, et un angle mort ecrit nulle part redevient un oubli au premier ajout de route. Le test ne peut PAS verifier qu'une route est tracee (le tracage est au repository, a 3 appels de distance) — il verifie que toute route mutante a ete EXAMINEE, seule propriete decidable. ✅ **[#431] CORRIGEE le 2026-09-11** (commentaire) : **15 repositories de production et 35 sites**, l'issue en annoncait 10 ; +`audit_log.rs` dont les 3 occurrences sont dans `mod tests`. Les 6 ajoutes : `companies`, `contacts`, `invoices`, `invoice_settlements_write`, `reconciliation_rules`, `supplier_invoices`. ⚠️ `invoices.rs` porte **LES DEUX FORMES** (`::user` x5 ET `for_actor` `:2112`) — *aucun decompte par fichier ne montre une migration a moitie faite*. Les references de ligne de l'issue avaient derive (`require_not_pat` est a `lib.rs:330`, pas `:319`). Aucun changement de schema ⇒ ni P2-bis/P3/P5/P6/P7/P8. **Prochaine : `bmad-create-story validate` passe 1.** # [#379] + decouvertes de la passe 1 — **SIX trous, pas deux** : `users` (4 operations mutantes, 0 trace), `contact_persons` (3), `imported_supplier_invoices` (2), `profile`, `setup`, `companies::update`. ⚠️ **Le traçage se fait au REPOSITORY, pas a la route** — un `grep` au niveau du fichier de route conclut a un trou la ou il n'y en a pas, et l'inverse : `companies.rs` compte 2 insertions, mais dans `lock_books`/`unlock_books`, pas dans `update`. ⛔ Employer `NewAuditLogEntry::from_current_user` et non `::user` des que la route n'est pas admin-only. Convention des libelles relevee au sol : `.`. 25-1c-zero-audit-company-id: done # 2026-09-15 **REVUE DE CODE CLOSE EN 2 PASSES** : `1M → 1L`, 3 lentilles (Sonnet, Haiku, Sonnet) puis passe CIBLEE (Opus). Les deux findings retenus portent sur le MEME test de caracterisation (AC 8), aucune ligne de production prise en defaut ; le MEDIUM venait du montage prescrit par la spec. Gate de cloture **2330/2330**, base remise a zero. ✅ **GATE E2E DE CLOTURE — 214 passed / 19 skipped / 9 failed (8,3 min)**, montage COMPLET, `kesh_e2e` reconstruite (68 migrations), `/health` smtpConfigured:true ; **le binaire a demarre contre la migration sans erreur de checksum (P8)**. Les 9 : 7 KF-029 + 2 pollutions (`product-revenue-account:133`, `sidebar-navigation:75`) **vertes rejouees seules** ; branche sans aucun fichier frontend. **ZERO REGRESSION.** Prete pour le push. Aucune issue fermee (#378 appartient a la 25-1c). # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5). Gate backend **2330/2330** (4 skipped), base remise a zero et verifiee (68 migrations, 1 Admin) ; fmt + clippy verts. **+8 tests** (3 backfill, 3 alimentation, 2 import) + 1 assertion d'export ; **6 mutations sur 6 tuees par assertion**. Manuel admin corrige, PDF regeneres et verifies aplatis. `backup.rs`/`export.rs`/`import.rs` INTACTS. ⚠️ **E2E NON LANCEE — prerequis du push** (seule a voir un defaut P8). Prochaine etape : `bmad-code-review`. # 2026-09-15 **SPECIFIEE** — `25-1c-zero-audit-company-id.md`. Story-zero de la 25-1c, nee de l'arbitrage du 2026-09-11 (« la colonne et son backfill d'abord »). 18 AC en six volets, 10 taches. Colonne `BIGINT NULL` sans FK + index `(company_id, created_at)`, remplie par SOUS-SELECT dans `insert_in_tx` (aucun des 106 sites ne bouge — recompte de la passe 1 de validation ; l'epic disait 89). ⛔ **P7 : EXEMPTION PERISSABLE `PerishableSince(20260827000001)`**, precedent 24-5 — pas de rejeu, conformement a l'arbitrage ; fondement verifie par `git tag` (dernier tag v0.11.1, 2026-08-24). ⚠️ **Ecart releve avec l'epic** : apres restore, le point ouvert a DEUX sous-cas — identifiants differents (entrees montrees a personne) OU identiques (entrees d'une autre instance montrees a la societe restauree) ; l'epic ne nommait que le premier. Traite en CARACTERISATION ici, tranche en 25-1c. Aucune issue fermee. ✅ **ARBITRAGES RENDUS LE 2026-09-15 (Guy)** : (1) **AUCUNE RELEASE AVANT LE MERGE** de cette story — condition du fondement de l'exemption perissable ; (2) le partage mesure ici / tranche en 25-1c est valide. ✅ **BOUCLE DE VALIDATION CLOSE EN 4 PASSES** (2026-09-15) : max `MEDIUM → HIGH → HIGH → LOW`, soit `0C/0H/2M/1L → 0C/1H/4M/4L → 0C/1H/1M → 0C/0H/0M/1L` ; Sonnet+Haiku, Opus, Sonnet, Opus CIBLEE. ⚠️ Signal HIGH → HIGH de la regle de splitting SIGNALE au Project Lead : **pas de decoupage** (Guy, « continue »). ⛔ Le HIGH de la passe 2 : `admin-manual.tex:1786` affirme « il n'y a pas de colonne company_id » — l'AC 18 disait « aucun site », sur un grep aveugle au `\_` de LaTeX ; le manuel est desormais dans le perimetre (PDF a regenerer). Montage de l'AC 8 EXECUTE sur base jetable en passe 4. Prochaine etape : `bmad-dev-story`. 25-1c-a-journal-audit-route: done # 2026-09-19 **MERGEE — PR #439, squash `c7de37c4`**, trois checks verts ; #378 reste OUVERTE pour l'ecran (25-1c-b). 2026-09-16 **REVUE DE CODE : 7 PASSES · GATE DE CLOTURE EN COURS** — gate RELANCE EN ENTIER (la garde a ete reecrite 5 fois depuis T11, la cellule d'export a change, 4 documents et un PDF avec) : base remise a zero ET VERIFIEE (40 tables, 1 Admin), **backend 2372/2372** (fmt + clippy compris, 100,8 s), **frontend 0 erreur / 740 tests / build ✔**. ✅ **E2E 215 passed / 8 failed / 19 skipped (8,7 min)**, `kesh_e2e` reconstruite, montage COMPLET (avec `KESH_INBOX_DIR` et `KESH_DOCUMENTS_DIR` cette fois). Les 8 QUALIFIES UN PAR UN : **7 KF-029** + **`sidebar-navigation:75` = KF-046**, confirmee par REJEU ISOLE (elle echoue seule, donc pas de la pollution). KF-045 absente (run a 16:53 UTC) et `inbox-import:106` absent — la preuve que l'echec de l'apres-midi venait de MON montage. ⇒ **ZERO REGRESSION, zero pollution.** ⚠️ **REVUE CLOSE en 7 passes** (P8 non consommee, sur arbitrage du Project Lead : « clore la boucle »). Doc mise a jour : `user-manual.tex` (la consultation « ne peut pas encore etre produite » etait devenue faux — l'export CSV la produit), README (ligne v0.12.1), CHANGELOG (section `[Unreleased]` creee), `api-external.md`, manuel admin, glossaire. PDF regeneres et controles aplatis. **PRETE POUR LE PUSH** — rien n'est pousse, #378 reste OUVERTE (le `closes` appartient a la PR de l'ecran). — P7 CIBLEE (Sonnet) : **1 MEDIUM, zero CRITICAL ni HIGH** — une premiere depuis P1. Le MEDIUM : la distinction lifetime/litteral introduite a P6 n'etait gardee par RIEN (prouve par mutation EXECUTEE, invisible aux 10 tests comme aux 218 fichiers reels) ; nuance relevee par la lentille elle-meme : mon commit l'annoncait comme lacune assumee. ⛔ Ma PREMIERE tentative de correctif ne gardait rien non plus — l'epreuve l'a montre : sur `impl<'a> Chose<'a> {` le saut « large » va d'une apostrophe a l'autre SANS enjamber d'accolade, donc meme compte. ⇒ la propriete se teste DIRECTEMENT sur `accolades_hors_chaines`, avec le cas discriminant `impl<'a> Foo { fn g<'b>() {} }` = (2,2). EPROUVE : rouge. ⚠️ Lecon : tester une fonction par le detour de son appelant laisse passer ce que l'appelant ne distingue pas. **CLOTURE PROPOSEE au Project Lead** : P8 disponible mais rendement nul — le MEDIUM etait annonce par moi-meme, il est remedie et eprouve, la remediation ne touche AUCUNE ligne de production depuis P3, et le code de production n'est plus en cause depuis P2. # Avant : **6 PASSES** — P6 CIBLEE (Opus) : 3 HIGH, 2M, 1L, **tous sur ma garde, aucun sur le code de production**. ⛔ Ma garde symetrique etait VERTE PAR VACUITE : sur 99 fichiers portant `#[cfg(test)]`, **95** n'ont aucun item reconnu en colonne 0 apres l'attribut, et retirer le correctif qu'elle surveillait ne changeait RIEN sur le depot (0 fichier sur 218). ⛔ La VISIBILITE decidait de la protection : `pub(crate) fn` (28), `pub mod` (202), `pub use` (85) non reconnus. ⛔ Mon doc-comment affirmait « verifie » et disait FAUX : `loader.rs:386` porte `contains('{')` DANS son bloc de test, masque par ACCIDENT. ⇒ **SIMPLIFICATION** (recommandation de la lentille, annoncee au Project Lead a P5) : le balayage du depot est remplace par **4 entrees SYNTHETIQUES en dur**, decidables et independantes du depot. EPROUVEES : 2 mutations, 2 rouges, chacune nommant le cas. ⚠️ Verdict : « elargir l'enumeration ne ferait que deplacer la frontiere ; cout reel de deux passes, rendement mesure nul ». **P7 de confirmation, puis CLOTURE** — la remediation ne touche aucune ligne de production depuis P3. # Avant : **5 PASSES** — P5 CIBLEE (Sonnet, prompt VERSIONNE) : **1 CRITICAL** (l'angle que je lui avais donne, confirme par mutation compilee), 1M, 2L. ⛔ Mon appariement d'accolades ne sortait que si une accolade AVAIT ETE OUVERTE : un item garde sans bloc (`use …;`, `mod fixtures;`, `const …;`) faisait courir le saut jusqu'a la prochaine accolade du fichier — ou jusqu'a sa fin. Plus profond que la troncature que P4 avait remplacee ; dormant, mais `#[cfg(test)] mod fixtures;` est un idiome courant. ⛔ Et mon garde-fou restait VERT PAR EXCES : il ne verifiait que l'absence d'attribut residuel, or si le masquage avale tout il n'en reste aucun. ⇒ seconde sortie de boucle + garde SYMETRIQUE (le masquage ne retire aucun item de production). EPROUVE DANS LES DEUX SENS, une premiere dans cette boucle : correctif en place ⇒ l'action neuve est vue ; correctif retire ⇒ la garde nomme l'item perdu. ⛔ MEDIUM : SIXIEME decompte faux, dans le test cense fermer cette classe. ⚠️ **ARBITRAGE DE CONDUITE PORTE AU PROJECT LEAD** : severite non decroissante (HIGH en P4, CRITICAL en P5), production plus prise en defaut depuis P2, cinq passes sur UN SEUL fichier de test — ce detecteur vaut-il ce qu'il coute ? # Avant : **4 PASSES** — P4 CIBLEE (Opus, prompt VERSIONNE) : **1 HIGH**, 2M, 3L. ⛔ Ma coupe `#[cfg(test)]` fermait UNE variante du defaut et laissait l'autre : elle s'arretait au premier attribut QUEL QUE SOIT l'item garde. Or un attribut sur une METHODE (`invoice_email.rs:901`) ou un `mod tests` AU MILIEU (`version.rs:97`, `entities/user.rs:178`) sont ordinaires en Rust ⇒ **686, 256 et 75 lignes de PRODUCTION** hors balayage, dont `send_reminder_batch`, `check_downgrade_protection` (le garde-fou P2-bis) et `UserUpdate`. ⚠️ Et `invoice_email.rs` ECRIT DEJA de l'audit : la zone perdue appelait cette ecriture. ⛔ Le defaut n'etait pas la coupe seule mais MON DOC-COMMENT, qui declarait la classe fermee. ⇒ blocs desormais MASQUES par appariement d'accolades HORS CHAINES (ce que l'AC 18 demandait des l'origine). EPROUVE dans les DEUX variantes (2 mutations, 2 rouges) + controle que l'accolade d'un litteral ne casse rien. ⛔ MEDIUM : « six » corrige a UN site et laisse a DEUX AUTRES du meme fichier — regle de propagation enfreinte dans le commit qui corrigeait ce defaut ⇒ nombres doubles d'une ASSERTION. Garde 9/9. ⚠️ **P5 requise** (severite > LOW) ; la remediation ne touche toujours AUCUNE ligne de production. # Avant : **3 PASSES** — P3 CIBLEE (Sonnet, prompt VERSIONNE `25-1c-a-review-prompt-p3-ciblee.md`, sur le seul commit `9b47e3b8`) : **2 CRITICAL demontres par REPRODUCTION**, 1M, 2L — et les deux viennent de MON patch de P2. (1) le tamis appariait les guillemets sans suivre les ECHAPPEMENTS : un `\"` impair desynchronisait tout le reste du fichier, l'ensemble devenait VIDE, l'assertion ne s'executait plus et le trou se rouvrait, garde VERTE (reproduit : 7 litteraux → 0) ; (2) la coupe `#[cfg(test)]` portait sur la source BRUTE : `config.rs:406` porte cette chaine dans un DOC-COMMENT, 83% du fichier sortait du balayage — defaut DORMANT active par mon elargissement de P2. ⇒ PROPAGATION faite dans le meme patch (`relever()` portait le meme defaut sur TOUTES les crates) : une seule fonction `source_assainie`, coupe sur LIGNE ENTIERE. Les deux correctifs EPROUVES (2 mutations, 2 rouges). ⚠️ La 2e epreuve a d'abord rendu un FAUX NEGATIF : sonde posee en fin de fichier, donc dans les 93 lignes coupees — precaution prise pour config.rs et oubliee ici. ⚠️ **P4 requise** (severite > LOW) ; mais la remediation de P3 ne touche AUCUNE ligne de production, ce qui est le critere de cloture. # Avant : **2 PASSES** — P1 (Sonnet/Haiku/Opus, 3 lentilles) : 2H/5M/6L ; P2 (Opus/Sonnet/Haiku, braquee sur la REMEDIATION `8599035c..HEAD`) : 2H/4M/2L. ⛔ **Le HIGH de P2 porte sur MON correctif de P1** : il fermait le RENOMMAGE d'un code et laissait ouvert son AJOUT — une branche neuve passait les 7 tests, et la production aurait ecrit un code SANS LIBELLE. Le defaut de P1 deplace d'un cran, motif mesure du depot. ⇒ controle rendu BILATERAL, **eprouve par 3 mutations** (branche neuve, derivation dans un AUTRE fichier de route, code laisse en commentaire) : 3 rouges sur assertion. ⚠️ **Signal `HIGH → HIGH` SIGNALE au Project Lead** (regle de splitting) — mais les deux HIGH portent sur la remediation, non sur la conception. ⚠️ **P3 requise** (severite > LOW). ⚠️ **Deux arbitrages en attente** : `user-manual.tex:498-503` (« pas encore PRODUIRE » — l'export CSV le fait) et « on termine » = 25-1c-a seule ou #378 entiere. # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5) — les ONZE taches cochees, RIEN DE POUSSE. ⛔ Perimetre FIGE `main..8599035c` (cloture de l'implementation, avant la revue) : **25 commits, 29 fichiers, +5217/-41**. Decomptes recomptes depuis la source, perimetre `main..HEAD` : **+42 tests** (7 depot, 4 `util`, 5 module, **11 garde**, 15 E2E — recompte apres la passe 1 de revue, qui ajoute 3 tests a la garde ; la fiche a annonce « +35 » tant qu'elle en portait 4), **133 cles** par locale x 4, etat **DATE a la cloture de P5** (une mesure « actuelle » se perime a chaque commit — celle-ci l'avait deja fait deux fois) : 37 commits, 34 fichiers, +6218/-44. ⚠️ « 24 commits » et « +5206 » etaient FAUX des l'ecriture (25 et +5217) — releve par la passe 2. Gates : backend **2365/2365** (fmt + clippy compris), frontend **740/740** + build, base remise a zero ET verifiee (40 tables, 1 Admin). ✅ **E2E 214 passed / 9 failed / 19 skipped (8,3 min)** — les neuf qualifies UN PAR UN : 7 KF-029, `sidebar-navigation:75` = pollution (PASSE rejoue seul, donc pas la KF-046), et `inbox-import:106` = **defaut de MON montage** (KESH_INBOX_DIR/KESH_DOCUMENTS_DIR manquants ; diagnostic VERIFIE au log, spec rejouee ⇒ passe en 1,5 s). KF-045 absente, et c'est correct : run a 14:34 UTC. ⇒ **ZERO REGRESSION.** Epreuve par mutation : **9 mutations, 9 rouges SUR ASSERTION** (aucune sur compilation), fichiers restaures et identite verifiee. ⚠️ **Trois points laisses au Project Lead** : ecart garde/spec sur le decoupage de l'inventaire des sites indirects (six de part et d'autre, mais pas les memes six) ; libelle de `reconciliation.split_applied` (« Ventilation appliquee » arbitre vs « Eclater » au catalogue) ; emojis du manuel qui ne s'impriment pas (anterieur a la story). ⚠️ **Question en suspens : « on termine » = 25-1c-a seule, ou #378 entiere avec l'ecran ?** Prochaine etape : `bmad-code-review`. # Avant : **EN COURS** — PR #437 fusionnee (squash 842eaac0), pile rebasee, base de dev remise a zero et verifiee (40 tables, 1 Admin), revalidation close en 4 passes (1H/3M/3L -> 2M -> 1M/3L -> 1L), 122 libelles francais arbitres. Avant : **VALIDEE — boucle close en 5 passes** : `1C/1M/2L → 5M/8L → 1M/1L → 2M/2L (ciblee) → 1L (ciblee)` ; Sonnet+Haiku, Opus, Sonnet, Opus, Sonnet. ⛔ Stagnation MEDIUM sur 3 passes, toujours sur les bornes de date : la forme « created_at < date_to + 1 jour » a ete ABANDONNEE pour des bornes inclusives 23:59:59.999 (eprouvees sur base jetable). **Ne pas implementer avant le merge de #437** (rebase par SHA `42b6aac0`). **SPECIFIEE** — `25-1c-a-journal-audit-route.md`, 22 AC en six volets, 11 taches. `GET /api/v1/audit-log` + `/export.csv` dans `comptable_routes`, `ensure_not_pat` en handler, DTO dedie (`actorType` via `as_str`, `createdAt` en UTC explicite), `csv_sanitize` EXTRAITE d'`invoices.rs`, export audite `audit_log.exported`. ⚠️ **Trois** choix de conception laisses a confirmer en revue : inclusion des entrees sans societe, audit de l'export, codes bruts (non traduits) dans les cellules du CSV. Branche empilee sur la 25-1c-zero, a rebaser apres le merge de #437. [#378] — **issue du SPLIT de la 25-1c** (arbitrage de Guy, regle de splitting : 7 modules). La ROUTE de consultation du journal d'audit et son export CSV, cote backend. Acces **Comptable + Admin**, ni Consultation ni cle API ; filtre **strict par `company_id`** (l'import restaure la MEME installation) ; entrees sans societe montrees « societe indeterminee » (choix par defaut, non arbitre explicitement). Cf. `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15*. - 25-1c-b1-journal-audit-ecran: review # 2026-09-26 **IMPLEMENTEE** — 17/17 mutations tuees ; gates b1 seule 812/812, 2463/2463, E2E 223/8/19 sans regression ; prochaine etape : code review, puis la b2. # 2026-09-26 **R5 DERIVE : 1 MEDIUM / 3 LOW corriges** (97 actions, 138 cles ; lignes des gardes i18n) ; contrat de la route mergee confirme. Prochaine etape : dev. # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en natifs sur le vocabulaire, plus de carte de libelles ni de colonne Societe. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : MED en passes 3 à 6, tous introduits par la remédiation précédente, puis 0 après le retrait d'une assertion E2E trois fois corrigée. **issue du SPLIT de la 25-1c-b** (arbitrage de Guy, signal MEDIUM → MEDIUM en passe 2 de validation). L'ECRAN : feature `audit-log`, page, garde Comptable+, menu, carte des **29** types d'entite (dont `audit_log`, cree par la 25-1c-a), i18n et gardes, tests unitaires et E2E. Module partage `download.ts` neuf ; le regroupement des 7 copies existantes part en issue de dette. Livree dans la MEME PR que la b2 ; porte `closes #378`. [#378] 25-1c-b2-journal-audit-textes: ready-for-dev # 2026-09-26 ⛔ **R5 DERIVE : 1 CRITICAL / 4 HIGH / 4 MEDIUM** — fiche a REECRIRE en T0 contre le texte actuel, APRES la b1, puis revalider (la facture validee se devalide, elle ne se supprime plus ; plusieurs textes deja reecrits par 25-1c-a, 25-2-b, 25-5-a). # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] 25-1c-b-journal-audit-ecran: split # 2026-09-15 **DECOUPEE en 25-1c-b1 (ecran) + 25-1c-b2 (textes)** — arbitrage de Guy apres la passe 2 de validation. Fiche conservee comme reference (passes 1-2). **SPECIFIEE** — `25-1c-b-journal-audit-ecran.md`, 21 AC en six volets, 11 taches, PAS ENCORE VALIDEE. Feature `audit-log`, carte des **28 types d'entite** recomptes depuis la source (29 litteraux dont l'action `admin_break_glass_reset`), garde Comptable+, menu `comptableOnly`, `triggerDownload` extrait (4 copies), glossaire « journal d'audit » en partie A avec alignement de 2 cles divergentes. ⚠️ Defaut PREEXISTANT constate : `user-manual.tex:502-503` nie le verrou de periode livre par la 24-4c. Ne pas implementer avant le merge de la 25-1c-a. [#378] — **issue du SPLIT de la 25-1c**. L'ECRAN : page, entree de menu, libelles des 28 types d'entite dans les 4 langues, E2E, manuels (qui promettent tous que la consultation « manque encore »). Affichage sobre : code d'action brut, detail en JSON indente. Vocabulaire fige : « journal d'audit » / Audit-Protokoll / registro di audit / audit log. **Portera le `closes #378`.** 25-1c-piste-consultable: split # 2026-09-15 **DECOUPEE en 25-1c-a (route) + 25-1c-b (ecran)** — arbitrage de Guy. [#378] — route + ecran de consultation. ✅ **ARBITRAGE RENDU LE 2026-09-11 (Guy) : AJOUTER LA COLONNE `company_id`** — ni restreindre la route, ni documenter la limite. ⚠️ **C'est une migration AVEC BACKFILL** ⇒ les garde-fous **P2-bis, P3, P5, P6, P7 et P8** s'arment tous, alors que la 25-1b n'en arme aucun. ✅ **MECANISME ARRETE LE 2026-09-11 apres lecture ciblee : LE SOUS-SELECT**, patron exact d'`actor_label`. Colonne **`BIGINT NULL`, SANS FK**. ⇒ **Aucun des 106 sites d'ecriture ne bouge** ⇒ **L'ORDRE REDEVIENT INDIFFERENT** : la 25-1b n'attend pas la colonne. **Verifie et non suppose** : `users.company_id` est NOT NULL, ecrit une fois, jamais modifie, aucune table de jonction ; **AUCUNE route n'ecrit un audit sur une entite d'une AUTRE societe** (aucun handler ne reçoit de `company_id` de la requete, tout ecart devient 404) ; et le seul site ou l'acteur n'est pas `current_user` — `admin.full_import`, qui prend le plus petit Admin du JEU RESTAURE — est celui ou le sous-SELECT est **PLUS JUSTE** qu'un champ, lequel propagerait l'id d'une societe que le restore vient de detruire. `routes/exports.rs:137-139` l'assumait deja par ecrit. ⚠️ **L'ESTIMATION DE COUT QUI FONDAIT L'HESITATION ETAIT FAUSSE D'UN FACTEUR 2,5** : « ~30 sites » annonces, **106** reels sur 38 fichiers *(recompte le 2026-09-15, passe 1 de validation de la 25-1c-zero : le « 89 sur 32 » d'abord ecrit ici etait lui-meme faux, et le facteur est donc de 3,5 et non de 2,5)*, dont ~40 dans des repositories qui ne reçoivent meme pas de `company_id`. ⛔ **TROIS CONTRAINTES, chacune adossee a un mode d'echec du depot** : (1) **NULL et jamais NOT NULL sans defaut** — `check_schema_compat` (`import.rs:195-236`) rendrait INIMPORTABLES TOUTES les sauvegardes existantes, c'est pourquoi `actor_label` est `NOT NULL DEFAULT ''` ; (2) **aucune FK vers `companies`** — `companies` EST remplacee au restore (`backup.rs:76`) alors que les entrees locales sont CONSERVEES depuis la 25-1a : c'est litteralement le scenario qui a fait retirer `fk_audit_log_user` ; (3) **ni COALESCE ni rejeu post-restore** — `NULL` est un etat LEGITIME ET PERMANENT (« societe indeterminable »), pas un trou a combler ; la garde d'`actor_label` reposait sur une sentinelle TEXTUELLE, `NULL` n'en est pas une. ⛔ **POINT OUVERT, A TRAITER DANS CETTE STORY-CI** : apres un restore, les entrees locales CONSERVEES portent le `company_id` d'une societe DETRUITE — une consultation scopee ne les montrerait A PERSONNE. *La 25-1a s'est battue pour que ces entrees survivent a l'import ; l'ecran qui les rend lisibles ne doit pas etre celui qui les cache.* ⇒ **A DECOUPER** : la colonne et son backfill d'un cote, la route et l'ecran de l'autre. *(Motif de l'arbitrage : `audit_log` n'a **pas de `company_id`** — verifie sur `20260413000001_audit_log.sql` —, la table est GLOBALE alors que Kesh est multi-societe, et une route de consultation exposerait les traces de TOUTES les societes a l'admin d'une seule.)* ⚠️ Le repository n'a que `insert_in_tx` et `find_by_entity` — une methode filtrable/paginee est a ecrire (S5). ⚠️ Le manuel annonce cette page « prevue pour une version ulterieure » (`user-manual.tex:1597-1603`) : c'est ICI qu'elle se livre, pas en 25-1a. ⛔ **NE PAS LA LIVRER AVANT LA 25-1a** : un ecran qui donne a voir une trace qu'un `reset` dement serait une promesse de plus. From ecaeadb86300b6a3ba0279d8153db509b73de4d5 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:11:51 +0200 Subject: [PATCH 09/19] =?UTF-8?q?docs(25-1c-b2):=20fiche=20r=C3=A9=C3=A9cr?= =?UTF-8?q?ite=20contre=20le=20texte=20actuel=20(T0)=20=E2=80=94=20la=20d?= =?UTF-8?q?=C3=A9validation,=20un=20seul=20alignement=20de=20catalogue,=20?= =?UTF-8?q?l'inventaire=20refait=20(refs=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b2-journal-audit-textes.md | 383 +++++++----------- 1 file changed, 149 insertions(+), 234 deletions(-) diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md index 6cf076412..2b3bad99d 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md +++ b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md @@ -1,6 +1,6 @@ # Story 25.1c-b2 : Ce que l'écran du journal d'audit rend faux — manuels, README, vocabulaire -Status: ready-for-dev — ⛔ **À REPRENDRE avant son développement** (revalidation R5 « dérive », 2026-09-26) +Status: ready-for-dev — ✅ **RÉÉCRITE le 2026-09-26 (T0)** contre le texte actuel ; revalidation R6 à passer ⛔ **REVALIDATION R5 « DÉRIVE » — 1 CRITICAL, 4 HIGH, 4 MEDIUM** (Change Log). La fiche a dormi dix jours sur une branche locale ; entre-temps, la 25-1c-a (PR #439), les 25-2-b-1/b-2 (#440) et la 25-5-a (#386) ont @@ -59,250 +59,165 @@ d'audit **consultable** à l'écran, nommé d'un seul mot partout, ## Acceptance Criteria -### Volet A — le vocabulaire +⛔ **RÉÉCRITS LE 2026-09-26 (T0), contre le texte de `main` à `0e4c2682` + la 25-1c-b1** — la version +validée en 2026-09-16 décrivait des textes que d'autres stories ont réécrits depuis, et un mécanisme +(« la suppression d'une facture validée emporte son écriture ») qui n'existe plus : une facture validée +se **dévalide** (`invoices.rs:1293`, `InvoiceMustBeUnvalidatedFirst` ; action `invoice.unvalidated`). +**Les numéros de ligne sont ceux du 2026-09-26 ; chaque site se retrouve par sa PHRASE citée.** + +### Volet A — le vocabulaire « journal d'audit » **1. Le glossaire** — `docs/i18n-glossaire.md`, **partie A**, une ligne : « journal d'audit » | `Audit-Protokoll` | `registro di audit` | `audit log` | précédents **déjà livrés** : -`export-global-content-excludes` (`fr-CH:1241`, « journal d'audit interne » ; `en-CH:1184`, « internal -audit log ») et `fiscal-year-reopen-confirmation-body` (`de-CH:729`, « Audit-Protokoll ») ; **aucun en -italien** — la forme `registro di audit` vient de l'arbitrage du 2026-09-15. +`export-global-content-excludes` (réécrite par la 25-5-a, **conforme dans les quatre locales**) et +`fiscal-year-reopen-confirmation-body` en allemand (« Audit-Protokoll »). ⚠️ **Ne pas citer** `nav-audit-log` +ni les clés d'écran de la 25-1c-b1 : elles naissent dans la même PR (le glossaire pose qu'une clé qu'on +écrit n'est pas un précédent). -⚠️ **Ne pas citer `nav-audit-log` comme précédent** : la clé naît dans la même PR, et le glossaire pose -lui-même qu'une clé qu'on écrit n'est pas un précédent (ligne « modèle (d'e-mail) »). +**2. Les catalogues** — **une seule clé** s'écarte encore du terme, sur **trois** cellules (la 25-5-a a +aligné l'autre, `export-global-content-excludes`) : -**Ce qui impose quoi — les trois sources ne se confondent pas** : +| clé | fr | de | it | en | +|---|---|---|---|---| +| `fiscal-year-reopen-confirmation-body` | « piste d’audit » ✗ | « Audit-Protokoll » ✓ | « pista di audit » ✗ | « audit trail » ✗ | -| périmètre | source | -|---|---| -| les catalogues (AC 2) | **règle 3 du glossaire** (§ *Comment s'en servir*) : *« Un terme de la partie A ne se change pas dans une story de rollout. Il se change dans une story qui met AUSSI à jour toutes ses occurrences déjà livrées — sinon le produit dit deux mots pour une chose. »* **La ligne du glossaire ne s'écrit pas sans l'AC 2.** | -| les manuels (AC 3) | **l'arbitrage du soir** (« dans les catalogues et le manuel »). La règle 3, à la lettre, ne vise que les clés, et le glossaire distingue même le vocabulaire du manuel de celui de l'interface (`i18n-glossaire.md:180`) | -| la brochure et le README (AC 3) | **une extension décidée par cette story** — motif : le manuel et le README nomment l'écran « Journal d'audit », et ne peuvent pas désigner la même chose par un second mot | +⇒ fr « journal d’audit » (apostrophe **typographique** `’`, comme le reste de la valeur), it « registro di +audit » (« sarà conservato nel registro di audit »), en « audit log ». La règle 3 du glossaire l'impose : +la ligne de l'AC 1 ne s'écrit pas sans cet alignement. -**2. Les catalogues** — deux clés s'écartent du terme, **sur cinq cellules** — trois sur huit sont déjà conformes : +**Contrôle**, exécutable, **sur la VALEUR seule** (Python) : pour chaque `.ftl`, chaque clé dont la valeur +— lignes de continuation rattachées — contient `audit|protokoll|registro|journal|piste|trail` (sans casse) +est triée à la main. **Triés, à laisser** : `bank-accounts-errors-has-transactions` (« audit comptable » : +la **révision**, autre sens, quatre langues) ; les clés `audit-log-*` du vocabulaire de la 25-1c-a (codes +et libellés d'actions : « écriture », etc.) ; les clés d'écran de la b1, **déjà conformes** (vérifié : +« Journal d'audit » / « Audit-Protokoll » / « Registro di audit » / « Audit log »). -| 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 » ✓ | - -⇒ les cellules ✗ **alignées**, accords et articles ajustés à chaque langue. Lignes : `fr-CH:766` et -`:1241`, `de/it/en-CH:729` et `:1184` — **à retrouver par la clé**. - -⚠️ **Greper le mot `audit` seul, sans casse, jamais la locution** : -`grep -rniE "audit" crates/kesh-i18n/locales/*/messages.ftl | grep -vE ':[0-9]+:(nav-)?audit-log'` — ⚠️ **en excluant les clés -`^(nav-)?audit-log`**, dont le NOM contient le mot et qui, après la 25-1c-a, ajouteraient plus de 500 lignes -(vérifiées à part, ci-dessous). ⚠️ **Le filtre n'ôte que la LIGNE de clé** : les lignes de continuation -d'une valeur `audit-log-*`, ses attributs ou variantes, et les **en-têtes de blocs** neufs -(`# --- Journal d'audit — …`) sortent encore, et se trient comme « vérifiés à part ». 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 ; l'allemand écrit `Audit` avec majuscule. **Triés, à laisser** : - -- `bank-accounts-errors-has-transactions` (`fr-CH:986`, `de/it/en-CH:936`) — « audit comptable », - `Buchhaltungsprüfung`, « audit contabile », « accounting audit » : la **révision** des comptes, sans lien - avec `audit_log` (`errors.rs:1933`). Autre sens, dans les quatre langues ; -- les commentaires `# Story 14-2 — … audit …` (`fr-CH:763`, `de/it/en-CH:726`) : commentaires, pas - valeurs. - -⛔ **Les clés neuves ne sont PAS exemptées : elles sont VÉRIFIÉES.** Après la b1, chaque clé -`^(nav-)?audit-log` dont la valeur **nomme le journal** (`nav-audit-log`, et les clés d'écran de la b1 — -titre, sous-titre, état vide, bouton d'export — ; parmi les 133 clés de la 25-1c-a **à la spécification**, -tout libellé qui nommerait le journal) porte **exactement** la forme de sa langue — -« journal d'audit » / `Audit-Protokoll` / `registro di audit` / `audit log`. Motif : la 25-1c-a renvoie au -« vocabulaire de l'arbitrage » sans en écrire les valeurs, et le modèle allemand le plus proche à recopier, -`de-CH:1184`, dit « Audit-Log ». - -**Le procédé, exécutable** — en Python, **sur la VALEUR seule** : pour chaque `.ftl`, les lignes -`^((nav-)?audit-log[\w-]*)\s*=\s*(.*)`, et dans la valeur `audit|protokoll|registro|journal|piste|trail`, -sans casse ; puis tri à la main. ⚠️ **Une valeur Fluent peut continuer sur les lignes suivantes**, -indentées (patron `fr-CH:1330-1331`, `email-password-reset-body`) : la ligne de la clé rend alors une valeur -vide. Les lignes de continuation sont rattachées à leur clé avant la recherche. ⚠️ **Un grep sur « audit » ne suffit pas** : une variante qui ne contient -pas ce mot (« Protokoll » seul, « journal » seul) lui échapperait — un inventaire lexical se clôt sur ses -propres mots (passes 3 à 5). - -**3. Les manuels, la brochure et le README** (sources : tableau de l'AC 1). L'inventaire a été établi -par **deux procédés**, et c'est leur réunion qui fait foi : - -- **lexical** — `grep -rniE "piste d.audit|audit-trail|audit trail|audit log|piste de contr" docs/manual/fr/*.tex README.md` - rend **exactement** les douze premières lignes du tableau, rien à écarter ; -- **par le concept** (passe 4, PDF aplatis) — les phrases qui **désignent le journal** sans employer ces - mots : les cinq dernières lignes. - -| fichier | ligne | texte actuel | procédé | +**3. Les manuels, la brochure et le README** — inventaire du 2026-09-26, réunion de deux procédés : +**lexical** (`grep -niE "piste d.audit|audit-trail|audit trail|audit log|piste de contr|trace d.audit|dans l.audit\b"` +sur `docs/manual/fr/*.tex` et `README.md`) et **par le concept** (une phrase qui désigne le journal sans +ces mots) : + +| fichier | ligne | texte actuel | devient | |---|---|---|---| -| `user-manual.tex` | `:301` | « pour préserver l'audit-trail » | lexical | -| `user-manual.tex` | `:995` | « journalisée dans l'audit-trail » | lexical | -| `user-manual.tex` | `:1589` | `\section{Traçabilité (audit-trail)}` | lexical | -| `user-manual.tex` | `:1591` | « une \textbf{piste d'audit} (\texttt{audit\_log}) » | lexical | -| `user-manual.tex` | `:1603` | « Cette piste d'audit est \textbf{insert-only} » | lexical | -| `user-manual.tex` | `:1739` | `\item[Audit-trail (piste d'audit)]` — entrée du glossaire | lexical | -| `admin-manual.tex` | `:1141` | « l'audit-trail (\texttt{invoice.emailed}, …) » | lexical | -| `admin-manual.tex` | `:1780` | `\subsection{Audit-trail (audit\_log)}` | lexical | -| `admin-manual.tex` | `:2184` | `\item[Audit-trail]` — entrée du glossaire | lexical | -| `marketing-brochure.tex` | `:319` | « Audit-trail centralisé pour la responsabilité fiduciaire. » | lexical | -| `README.md` | `:29` | « audit log » (liste des fonctionnalités, en français) | lexical | -| `README.md` | `:219` | « la **piste de contrôle survit désormais à l'import d'une sauvegarde** » | lexical | -| `user-manual.tex` | `:858` | « nouvelle trace d'audit » → « nouvelle entrée au journal d'audit » | concept | -| `user-manual.tex` | `:1684` | « L'\texttt{audit\_log} garde toujours la trace » — le nom de table y sert de nom au concept | concept | -| `admin-manual.tex` | `:1088` | « La demande reste tracée dans l'audit » | concept | -| `admin-manual.tex` | `:1143` | « laisse sa propre trace d'audit » → « sa propre entrée au journal d'audit » | concept | -| `admin-manual.tex` | `:1935` | « La piste existe et est conservée, mais elle ne voyage pas avec l'export » — « piste » seul, abrégé de « piste d'audit » | concept | - -⇒ **« journal d'audit »** partout, soit **dix-sept sites**. **Triés autre sens, à laisser** (passes 4 et 5) : -`user-manual.tex:597` (« piste de correction »), `:1373`, `README.md:45`, `:213`, `:218` (« audit par -trois experts »), `marketing-brochure.tex:106`, `:209` (« Une piste pour les fiduciaires » — une piste commerciale), `:296`. -⚠️ En passe 4, le mot « piste » seul n'avait été cherché que dans les manuels : -`marketing-brochure.tex:209` a été trouvé en passe 5, par le rejeu du motif élargi de l'AC 8. - -Précisions : - -- **Le nom technique `audit\_log` ne change pas** là où il désigne la table (`:1591`, `:1780`, les - champs) ; il change là où il sert de nom au concept (`:1684`). -- **Les deux entrées de glossaire deviennent « Journal d'audit (angl. audit log) »** — l'anglais - **arbitré**, pas « audit-trail », que l'AC 2 retire du catalogue anglais — et **se déplacent à la - lettre J** (les glossaires des manuels sont alphabétiques), **à côté d'une entrée voisine qui ne désigne - pas la même chose** : dans `user-manual.tex`, **après** `\item[Journal]` (`:1754`, le journal des - écritures) ; dans `admin-manual.tex`, **avant** `\item[Journal entry]` (`:2189`). La définition doit - rendre la différence lisible. -- **Les renvois se suivent** : `user-manual.tex:1739` renvoie à la section « Traçabilité », dont le - titre change (`:1589`) ; toute mention textuelle de ces titres se met à jour. - -⛔ **L'inventaire se REJOUE après le rebase, sur le PDF aplati, avec un motif ÉLARGI** (AC 8) : un site -ajouté par la 25-1c-a ou la b1 n'y figure pas. +| `user-manual.tex` | `:888` | « nouvelle trace d'audit » | « nouvelle entrée au journal d'audit » | +| `user-manual.tex` | `:1111` | « journalisées dans l'audit-trail » | « inscrites au journal d'audit » | +| `user-manual.tex` | `:1789` | `\section{Traçabilité (audit-trail)}` | `\section{Traçabilité (journal d'audit)}` | +| `user-manual.tex` | `:1791` | « une \textbf{piste d'audit} (\texttt{audit\_log}) » | « un \textbf{journal d'audit} (\texttt{audit\_log}) » — accords suivants ajustés | +| `user-manual.tex` | `:1803` | « Cette piste d'audit est » | « Ce journal d'audit est » — accords (« enregistré ») | +| `user-manual.tex` | `:1889` | « L'\texttt{audit\_log} garde toujours la trace » *(concept)* | « Le journal d'audit garde toujours la trace » | +| `user-manual.tex` | `:1944` | `\item[Audit-trail (piste d'audit)]` — glossaire | `\item[Journal d'audit (angl. audit log)]`, **déplacé à la lettre J**, après `\item[Journal]` (le journal des écritures) — la définition rend la différence lisible ; l'avertissement de couverture qui suit est **conservé** | +| `admin-manual.tex` | `:1088` | « La demande reste tracée dans l'audit » | « … dans le journal d'audit » | +| `admin-manual.tex` | `:1141` | « l'audit-trail (\texttt{invoice.emailed}, …) » | « le journal d'audit (…) » | +| `admin-manual.tex` | `:1143` | « laisse sa propre trace d'audit » | « laisse sa propre entrée au journal d'audit » | +| `admin-manual.tex` | `:1589` | « les conséquences sur la piste de contrôle » | « … sur le journal d'audit » | +| `admin-manual.tex` | `:1784` | `\subsection{Audit-trail (audit\_log)}` | `\subsection{Journal d'audit (audit\_log)}` | +| `admin-manual.tex` | `:1817` | « la piste de contrôle mêlerait deux comptabilités » | « le journal d'audit mêlerait … » | +| `admin-manual.tex` | `:2197` | `\item[Audit-trail]` — glossaire | `\item[Journal d'audit (angl. audit log)]`, **déplacé à la lettre J**, avant `\item[Journal entry]` ; avertissement conservé | +| `marketing-brochure.tex` | `:319` | « Audit-trail centralisé pour la responsabilité fiduciaire. » | « Journal d'audit centralisé … » | +| `README.md` | `:29` | « audit log » (liste des fonctionnalités, en français) | « journal d'audit consultable à l'écran » | + +**Déjà conformes, à laisser** : `user-manual.tex:474`, `:514`, `:543`, `:593`, `:1039`, `:1085`, `:1159`, +`:1427`, `:1712`, `:1721` ; `admin-manual.tex:1066`, `:1613`, `:1760`, `:1810` ; `marketing-brochure.tex:139`. +**Autre sens, à laisser** : `admin-manual.tex:159`, `:1242`, `:1946`, `:2082` (`audit\_log` y nomme la +**table**, non le concept) ; `README.md:219` (« audit par trois experts ») ; la section *« Numérotation »* +(`user-manual.tex:543`) parle de **trou**, non du journal. ⚠️ **L'historique publié ne se réécrit pas** : +les lignes v0.12.0 du README et la section `[0.12.0]` du CHANGELOG gardent « piste de contrôle ». +`website/` (anglais, « audit log » déjà) ne se touche pas. + +⛔ **Le nom technique `audit\_log`** ne change pas là où il désigne la table ou le champ. ### Volet B — ce que l'écran rend faux -**4. Le manuel utilisateur, l'encadré « Ce que le logiciel ne fait pas encore à votre place »** -(`user-manual.tex:497-511`, du `\begin{keshwarning}` au `\end`) porte **cinq** énoncés : **quatre sont faux ou périmés, le premier est vrai mais incomplet** — il se -**nuance**, il ne se supprime pas : +**4. Le manuel utilisateur, l'encadré** `\begin{keshwarning}[title=… Ce que le logiciel ne fait pas +encore à votre place]` (`user-manual.tex:511-528`, juste avant `\subsection{Numérotation}`). Il porte +aujourd'hui **quatre** énoncés, dont **trois sont périmés** : -| lignes | énoncé | état | -|---|---|---| -| `:498-499` | « la modification et la suppression d'une écriture sont refusées » | **vrai pour la route des écritures, à nuancer** : une écriture disparaît encore avec la facture validée qu'on supprime (ci-dessous) | -| `:499-502` | « Ce qui manque encore, c'est la consultation du journal d'audit depuis l'application » | **faux** : l'écran existe (25-1c-b1) | -| `:502-503` | « Le verrou reste par ailleurs annuel : aucun verrou de période plus fin […] n'existe encore » | **déjà faux** : la Story 24-4c a livré le verrou de période (PR #425) | -| `:505-506` | « Une seule exception, déjà en vigueur : une écriture déjà contre-passée ne peut plus être supprimée » | **périmé** : la route des écritures refuse désormais **toute** suppression. La seule disparition d'une écriture passe par la suppression d'une facture validée, et ce n'est pas l'exception que l'énoncé décrit | -| `:508-510` | « préférez la contre-passation […], tant que le logiciel vous laisse le choix » | **périmé** : la contre-passation est **imposée**, le logiciel ne laisse plus le choix | - -⚠️ **Pas de formule absolue dans le texte de remplacement** : la suppression d'une **facture** validée -emporte son écriture — `crates/kesh-db/src/repositories/invoices.rs:1350` — ⚠️ le **repository**, et non -`crates/kesh-api/src/routes/invoices.rs`, homonyme — passe `enforce_immutability = false`, « seul site du dépôt » (commentaire -`crates/kesh-db/src/repositories/invoices.rs:1339-1340`, repris par `crates/kesh-db/src/repositories/journal_entries.rs:973-975`), résidu assumé que le commentaire du code rattache à #380 — **fermée le 2026-09-09** — et à #381, -ouverte. Le manuel utilisateur le décrit -déjà dans sa section sur la suppression d'une facture ; le nouveau texte ne doit pas écrire qu'une -écriture ne peut disparaître « en aucun cas ». - -⇒ L'encadré se **réécrit**, ou se **remplace par une note**. Dans les deux cas, le texte **conserve le -premier énoncé, nuancé** : la route des écritures refuse modification et suppression, mais la suppression -d'une facture validée emporte son écriture. Il dit aussi la contre-passation imposée. Ce n'est plus un -avertissement « ne fait pas encore » : l'encadré n'annonce plus de fonction manquante. - -**5. Le manuel utilisateur, section « Traçabilité »** (`user-manual.tex:1611-1616`) : +| énoncé actuel | état | +|---|---| +| « La contre-passation est désormais imposée […] : la modification et la suppression d'une écriture sont refusées » | **vrai**, à conserver — avec la seule voie qui fait disparaître une écriture : la **dévalidation** d'une facture (renvoi à « Supprimer ou dévalider une facture ») | +| « Ce qui manque encore, c'est l'écran de consultation du journal d'audit […] par l'interface de programmation seulement […] La page […] reste à venir » | **faux** : l'écran existe (25-1c-b1) | +| « Une seule exception, déjà en vigueur : une écriture déjà contre-passée ne peut plus être supprimée » | **périmé** : **aucune** écriture ne se supprime plus | +| « préférez la contre-passation […], tant que le logiciel vous laisse le choix » | **périmé** : il ne laisse plus le choix | + +⇒ L'encadré devient une **note** (`keshnote`), qui n'annonce plus de fonction manquante : contre-passation +imposée ; la dévalidation comme seule disparition d'une écriture, **journalisée sous son propre nom** ; +l'historique des corrections **consultable à l'écran** — *Administration → Journal d'audit*, renvoi à la +section « Traçabilité » — et par l'API ; le verrou de période conservé tel quel. ⛔ **Aucune formule +absolue** (« en aucun cas ») sur la disparition d'une écriture. + +**5. Le manuel utilisateur, section « Traçabilité »** — la fin de son `keshnote` (`:1812-1816`) : *« 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, **tel que la 25-1c-b1 le construit**. Critère : **chaque cas où -l'écran rend une liste vide ou un refus sans explication se dit dans le manuel**. - -- menu **Administration → Journal d'audit**, réservé au **Comptable** et à l'**Admin** ; -- le détail déplié, l'export CSV ; -- ⛔ **les dates** : le tableau affiche l'heure **locale** ; le filtre de période porte sur des **jours - UTC** — une opération faite peu après minuit, heure suisse, appartient au jour UTC **précédent** ; le - CSV écrit les heures en **UTC** ; -- **la langue** : les actions et les types d'entité s'affichent **traduits**, dans la langue de - l'interface de l'installation — et le CSV les écrit dans la même langue (25-1c-a AC 12) ; -- **les filtres « type d'entité » et « action » sont des listes** de libellés (25-1c-b1 AC 5) : on - choisit une action, on ne la tape pas ; -- **l'identifiant d'entité exige un type** : le champ reste inactif tant qu'aucun type n'est choisi ; -- **l'export est plafonné à 10 000 lignes** (25-1c-a AC 11) : au-delà, un message demande d'affiner - les filtres ; -- **une action historique**, écrite par une version antérieure et absente de la liste, s'affiche sous la - forme de son **code** (25-1c-a AC 16), et **n'est pas proposée dans la liste « Action »** (25-1c-b1 - AC 3 : seul un code déjà présent dans l'adresse de la page y est ajouté) — on la retrouve par le type et - l'identifiant d'entité. - -La phrase voisine sur le **centre de notifications** (`:1615-1616`) reste vraie et se conserve. - -**6. Le manuel d'administration, `admin-manual.tex:1786`** — l'item « Champs » dit aujourd'hui, sur la -branche, que le journal *« ne se \textbf{consulte} pas encore~: aucune route ni aucun écran ne permet de le -lire depuis l'application --- c'est le sujet de l'issue \#378 »* (graphie LaTeX exacte : un grep de « ne se -consulte » sur le `.tex` ne trouve rien), et la 25-1c-a le réécrit en « une route existe, l'écran reste à -venir ». → **l'écran existe** : accès Comptable et Admin, **refusé aux clés API** (la route rejette un -jeton d'accès personnel), et **ni la consultation ni l'export ne s'inscrivent au journal** (25-1c-a AC 9 -et 14) — un réviseur qui chercherait « qui a exporté le journal » doit savoir que le journal ne le dit pas. - -✅ **Trois défauts voisins du manuel d'administration, CORRIGÉS DANS CETTE STORY — à deux titres** : - -- `:1796` et `:1778`, trouvées en passe 4 : **par l'arbitrage du 2026-09-15 au soir** — la question portait - sur ces deux lignes, la réponse fut *« corrige »* ; -- `:1797`, trouvée en revalidation R1 : **par décision de la story**, que l'arbitrage ne nomme pas. Motif : la - nuance apportée à `:1796` rend `:1797` trompeuse, et l'une ne se corrige pas sans l'autre. - -Les consignes : - -- `:1796` — la formule absolue se **nuance** comme l'AC 4 le prescrit au manuel utilisateur : la route - des écritures refuse modification et suppression quel que soit le rôle ; la suppression d'une facture - validée emporte son écriture ; -- `:1778` — « Les 5 rôles standards » devient **trois**, en accord avec `:1345` ; -- `:1797`, **voisine de `:1796`** — « Il conserve aussi les modifications et suppressions **antérieures au - gel** » laisse entendre, une fois `:1796` nuancée, qu'aucune suppression postérieure n'est journalisée ; - or la suppression d'une facture validée inscrit `journal_entry.deleted` (`crates/kesh-db/src/repositories/invoices.rs:1350`, - `crates/kesh-db/src/repositories/journal_entries.rs:1061`) → à reformuler dans le même geste. - -| ligne | texte | ce que dit le code | -|---|---|---| -| `admin-manual.tex:1796` | « l'application refuse les deux, **sans exception et quel que soit le rôle** » (conformité OLICo) | faux : la suppression d'une facture validée emporte son écriture (`crates/kesh-db/src/repositories/invoices.rs:1350`) — la formule absolue que l'AC 4 interdit | -| `admin-manual.tex:1778` | « Les **5** rôles standards » | faux : trois rôles (`entities/user.rs:20-27`), ce que dit déjà `admin-manual.tex:1345` — et c'est la ligne qui précède la sous-section que l'AC 3 renomme | -| `admin-manual.tex:1797` | « Il conserve aussi les modifications et suppressions **antérieures au gel** » | trompeur une fois `:1796` nuancée : la suppression d'une facture validée journalise encore `journal_entry.deleted` après le gel (`crates/kesh-db/src/repositories/invoices.rs:1350`, `crates/kesh-db/src/repositories/journal_entries.rs:1061`) | - -**7. Le README**, feuille de route : - -- `README.md:218` — « le journal d'audit n'est consultable par aucun écran » ; -- `README.md:219` — « la **consultation depuis l'application** ([#378]), qui n'existe toujours pas ». - -→ mis à jour **dans le même commit** que l'écran (§ *Synchroniser le planning du README*), ainsi que les -deux sites de vocabulaire de l'AC 3. +version ultérieure) »* → une **sous-section « Consulter le journal d'audit »** qui décrit l'écran **tel que la +25-1c-b1 le construit** — critère : **chaque cas où l'écran rend une liste vide, un refus ou une valeur +inattendue se dit** : + +- menu **Administration → Journal d'audit**, réservé au **Comptable** et à l'**Administrateur** ; refusé + aux clés API ; +- les filtres : période, type d'entité, numéro d'entité, action ; ⛔ **les dates filtrent sur des jours + UTC**, le tableau affiche l'heure **locale** — une opération faite peu après minuit, heure suisse, + appartient au jour UTC **précédent** ; une date de début postérieure à la date de fin est refusée par + l'écran ; +- **le numéro d'entité exige un type** : le champ reste inactif sans type, et se vide quand on change de + type ; +- **la langue** : actions et types d'entité traduits dans la langue de l'installation, à l'écran comme dans + le CSV ; +- **une action historique**, absente du vocabulaire, s'affiche sous son **code** et n'est pas proposée + dans la liste « Action » — on la retrouve par le type et le numéro d'entité ; +- le détail déplié (JSON), la pagination ; +- **l'export CSV** porte les filtres affichés, en UTC, **plafonné à 10 000 lignes** — au-delà, un message + demande d'affiner ; +- ⚠️ ni la consultation ni l'export ne s'inscrivent eux-mêmes au journal. + +La phrase sur le **centre de notifications** (« prévu mais pas encore disponible ») reste vraie et se +conserve. Les **manques connus** de la section (séquence d'installation #434, gestes de session #435, +**toutes deux ouvertes**) restent. + +**6. Le manuel d'administration** : + +- **item « Champs »** de la sous-section du journal (`admin-manual.tex:1790`) : *« L'\textbf{écran}, lui, + reste à venir (issue \#378). »* → l'écran existe (*Administration → Journal d'audit*, Comptable et + Administrateur, refusé aux clés API) ; la suite de la phrase (« ne filtre que votre société, et ne + s'inscrit pas elle-même au journal ») se conserve ; +- `:1782` — « Les **5** rôles standards » → **trois**, en accord avec `:1345` (`entities/user.rs`) ; +- `:1803` — « Il conserve aussi les modifications et suppressions **antérieures au gel** » : à compléter + par les **dévalidations** de factures, postérieures au gel et journalisées (`invoice.unvalidated`) — sans + quoi la phrase laisse croire qu'aucune disparition postérieure n'est tracée. ⚠️ Le paragraphe qui précède + (`:1802`) nomme déjà la dévalidation : **ne pas le réécrire**. + +**7. Le README et le CHANGELOG** : + +- `README.md:220` (ligne v0.12.1 de la feuille de route) : « **Restent ouverts** : l'**écran** de + consultation ([#378]) — la piste de contrôle se lit désormais par l'API, […] mais aucune page ne la rend + lisible depuis l'application — » → l'écran est **livré** ; #434 et #435 restent ouverts ; +- `CHANGELOG.md`, section `[0.12.1] — Non publié`, *Added* : **« Consulter le journal d'audit à + l'écran »** — l'entrée de la 25-1c-a (`[0.12.0]`, « La piste de contrôle se lit enfin ») disait la route + seule ; ⛔ ne pas la réécrire. ### Volet C — le contrôle **8. Régénération et vérification** : - `make fr` dans `docs/manual/`, PDF commités ; -- **PDF aplati vérifié** (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`) — les anciennes phrases - absentes, les nouvelles présentes ; -- **inventaire rejoué sur les PDF aplatis et le README**, motif élargi puis tri à la main — **en - Python** : `grep` est ici `ugrep`, qui refuse ce motif avec son contexte `{0,60}`. Motif, sans casse : - `piste|trace d.audit|dans l.audit\b|audit-?trail|audit log`. - - **Recoller d'abord les césures** du texte aplati (`re.sub(r"(\w)- (\w)", r"\1\2", t)`) : le PDF du - manuel d'administration coupe des mots en fin de ligne, et un « jour- nal » ferait passer à tort le - contrôle « nouvelles phrases présentes ». - - Une occurrence peut apparaître **plusieurs fois** dans un PDF (sommaire, en-tête de page) : on compare - des **sites**, pas des nombres. - - **Reste attendu, et seulement lui, écrit en entier** : - - les deux « (angl. audit log) » des glossaires ; - - `user-manual.tex:597` (« piste de correction ») ; - - `marketing-brochure.tex:209` (« Une piste pour les fiduciaires »). - - Les six autres sites triés « autre sens » ne contiennent que « audit » ou « auditable » : le motif ne - les rend pas ; -- ⛔ en LaTeX le souligné s'écrit `\_` : un grep de `audit_log` sur le `.tex` ne voit rien. - -`website/index.html:106` et `roadmap.html:105` (« an audit log that a backup import no longer -replaces ») **restent vrais**, emploient déjà le terme `en-CH`, et ne se touchent pas. -`marketing-brochure.tex:139` dit déjà « journal d'audit ». - -**9. Les catalogues passent leurs gardes** : `cargo test -p kesh-i18n` (parité des quatre locales, -`loader.rs:693`) ; `npm run test:unit` — les deux clés de l'AC 2 changent de **valeur**, pas de nom, et -aucun compteur ne doit bouger ; s'il bouge, l'écrire. +- **PDF aplatis vérifiés** (`pdftotext f.pdf - | tr '\n' ' ' | tr -s ' '`, **césures recollées**, + `re.sub(r"(\w)- (\w)", r"\1\2", t)`) : les anciennes phrases absentes, les nouvelles présentes ; +- **inventaire REJOUÉ** sur les PDF aplatis et le README, motif, sans casse : + `piste|trace d.audit|dans l.audit\b|audit-?trail|audit log` — **reste attendu, et seulement lui** : les + deux « (angl. audit log) » des glossaires, et les sites « autre sens » de l'AC 3 ; on compare des + **sites**, pas des nombres (un site peut apparaître au sommaire). + +**9. Les gardes** : `cargo test -p kesh-i18n` (parité) ; `npm run test:unit` — la clé de l'AC 2 change de +**valeur**, pas de nom : **aucun compteur ne doit bouger** ; s'il bouge, l'écrire. ## Tasks / Subtasks -- [ ] **T0 — Après la 25-1c-b1**, sur la même branche : relire chaque site **par sa phrase** ; rejouer - l'inventaire de l'AC 3 (motif élargi de l'AC 8) et le grep de l'AC 2. -- [ ] **T1 — Glossaire et catalogues** (AC 1, 2), dont la vérification des clés neuves. -- [ ] **T2 — Vocabulaire des manuels, de la brochure et du README** (AC 3), dix-sept sites. -- [ ] **T3 — Encadré et section « Traçabilité » du manuel utilisateur** (AC 4, 5) — contrôler chaque - affirmation neuve **contre le code de la b1**, pas contre cette fiche. -- [ ] **T4 — Manuel d'administration et README** (AC 6, 7), dont les trois défauts voisins - (`:1796`, `:1797`, `:1778`). -- [ ] **T5 — Régénération et contrôle aplati** (AC 8) ; gardes (AC 9). +- [ ] **T0 — Réécriture de la fiche** contre le texte actuel, **après la 25-1c-b1** *(fait le 2026-09-26, + cette version)* ; puis revalidation. +- [ ] **T1 — Glossaire et catalogue** (AC 1, 2). +- [ ] **T2 — Vocabulaire des manuels, de la brochure et du README** (AC 3). +- [ ] **T3 — Encadré et section « Traçabilité » du manuel utilisateur** (AC 4, 5) — chaque affirmation + neuve contrôlée **contre le code de la b1**. +- [ ] **T4 — Manuel d'administration, README, CHANGELOG** (AC 6, 7). +- [ ] **T5 — Régénération, contrôle aplati, inventaire rejoué** (AC 8) ; gardes (AC 9). ## Dev Notes @@ -310,23 +225,22 @@ aucun compteur ne doit bouger ; s'il bouge, l'écrire. | zone | fichiers | |---|---| -| vocabulaire | `docs/i18n-glossaire.md` ; `crates/kesh-i18n/locales/{fr,de,it,en}-CH/messages.ftl` (deux clés, cinq cellules) | +| vocabulaire | `docs/i18n-glossaire.md` ; `crates/kesh-i18n/locales/{fr,it,en}-CH/messages.ftl` (une clé, trois cellules) | | manuels | `docs/manual/fr/user-manual.tex`, `admin-manual.tex`, `marketing-brochure.tex` + les trois PDF | -| README | `README.md` | +| README, CHANGELOG | `README.md`, `CHANGELOG.md` | Aucun code applicatif. -### Faits établis à la spécification, et non supposés +### Faits établis à la réécriture (2026-09-26), et non supposés -- **L'inventaire de l'AC 3** : un grep exécuté sur la branche le 2026-09-15 (douze sites — la première - rédaction en annonçait onze, avec un motif qui ne pouvait pas trouver `README.md:29`), puis une - recherche par le concept sur les PDF aplatis en passe 4 (cinq de plus). La parente en citait trois. -- **Les sources de l'alignement** : la règle 3 pour les catalogues, l'arbitrage du soir pour le manuel, - une décision de la story pour la brochure et le README (AC 1). -- **L'encadré de l'AC 4** a été relu en entier (`user-manual.tex:497-511`) : la parente n'en corrigeait - que deux énoncés. -- **La suppression d'une facture validée emporte son écriture** : appel `crates/kesh-db/src/repositories/invoices.rs:1350`, commentaire `:1339-1340`, - `crates/kesh-db/src/repositories/journal_entries.rs:973-975`. +- **Une facture validée ne se supprime plus**, elle se dévalide (`crates/kesh-db/src/repositories/invoices.rs:1293`, + `DbError::InvoiceMustBeUnvalidatedFirst`) ; #381 et #440 sont **fermées**. Le manuel d'administration + (`:1802`) le dit déjà. +- **L'inventaire de l'AC 3** : grep lexical et lecture par le concept exécutés sur `main` le 2026-09-26. +- **`export-global-content-excludes`** a été réécrite par la 25-5-a dans les quatre locales, et y est + conforme (vérifié : « le journal d'audit » / « das Audit-Protokoll » / « il registro di audit » / « the + audit log »). +- **#434 et #435 sont ouvertes** (vérifié `gh issue view`). ### Ce qui ne bouge PAS @@ -565,6 +479,7 @@ aucune ligne de code, pas plus que la story, qui n'est que texte. | passe | modèle | rendu (après reclassement) | |---|---|---| +| 2026-09-26 | réécriture T0 | Après la 25-1c-b1 (revue close), la fiche est **réécrite contre le texte de `main`** : AC 2 réduite à **une clé, trois cellules** (la 25-5-a a aligné l'autre) ; AC 3 : **inventaire refait** (16 sites à changer, dont les « piste de contrôle » ajoutés depuis, et la liste des sites déjà conformes ou d'autre sens) ; AC 4 : l'encadré a **quatre** énoncés, trois périmés, et la seule disparition d'une écriture est la **dévalidation** ; AC 5 : l'écran décrit tel que la b1 le livre (dont le refus de la plage inversée et le vidage du numéro au changement de type, ajoutés par sa revue) ; AC 6 : ce qui reste à corriger au manuel d'administration (« reste à venir », « 5 rôles », `:1803`) ; AC 7 : la ligne v0.12.1 du README et une entrée CHANGELOG *Added* — ⚠️ l'historique publié ne se réécrit pas. Revalidation à passer. | | 2026-09-26 | revalidation R5 « dérive » | Reprise sur `main` à `0e4c2682`. **Une lentille Sonnet**, prompt `25-1c-b-validate-prompt-r5-derive.md`, axes déclarés (non exercés : rejeu complet de la procédure Python de l'AC 3/8, manuels DE/IT/EN, PDF de la brochure, CHANGELOG). Constats **vérifiés par l'orchestrateur** (`grep -nF`) avant d'être retenus. ⛔ **CRITICAL** — le mécanisme des AC 4 et 6 n'existe plus : une facture validée ne se supprime plus (`invoices.rs:1293`, `DbError::InvoiceMustBeUnvalidatedFirst`), elle se **dévalide** (`invoice.unvalidated`) ; le code cité (`invoices.rs:1339-1350`) a disparu ; #381 est **fermée**. **HIGH** — (1) `admin-manual.tex` « sans exception » : déjà réécrit ailleurs, la formule n'existe plus à ce site ; (2) item « Champs » (`:1790`) déjà corrigé par la 25-1c-a — reste « L'écran, lui, reste à venir (issue #378) » ; (3) encadré du manuel utilisateur (`:511-528`) déjà réécrit par la 25-1c-a — l'énoncé du verrou de période a disparu ; (4) AC 2 : `export-global-content-excludes` réécrite par la 25-5-a dans les quatre locales, **déjà conforme** — reste une seule clé à aligner (`fiscal-year-reopen-confirmation-body`, fr/it/en). **MEDIUM** — 97 actions et 138 clés `audit-log-*` (non 92 / 133) ; inventaire lexical plus large (nouveaux « piste de contrôle » `admin-manual.tex:1589`, `:1760`, `:1817`, `:1946`, et `user-manual.tex:1111` reformulée) ; toutes les lignes des manuels décalées ; la phrase du README citée par l'AC 7 n'existe plus (la ligne v0.12.1 porte « Restent ouverts : l'écran de consultation ([#378]) »). **LOW** — les entrées de glossaire portent un avertissement ajouté. ⇒ **Décision de l'orchestrateur** : pas de réécriture partielle maintenant — la fiche se réécrit en T0, contre le texte réel, **après** la 25-1c-b1, puis se revalide. | | 1 | Sonnet + Haiku | 2 M, 2 L | | 2 | Opus | 6 M, 7 L → **split** | From ef575755e85a45e82520d2c0ba752c7c88fe9d62 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:12:26 +0200 Subject: [PATCH 10/19] docs(25-1c-b2): prompt de la revalidation R6 (refs #378) Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b2-validate-prompt-r6.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r6.md diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r6.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r6.md new file mode 100644 index 000000000..c89660df9 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r6.md @@ -0,0 +1,49 @@ +# Prompt — revalidation R6, Story 25-1c-b2 (fiche réécrite) + +*Versionné le 2026-09-26. **Une lentille** (Opus), contexte frais, passe **complète** : la fiche a été +réécrite en T0 contre le texte actuel (commit `ecaeadb8`), après la revalidation R5 « dérive » qui avait +remonté 1 CRITICAL et 4 HIGH.* + +Dépôt `/home/gcorbaz/devel/kesh`, branche `story/25-1c-b1-journal-audit-ecran` — elle porte la 25-1c-b1 +**implémentée** (l'écran, revue close) et cette fiche. Ta fiche : +`_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md` — lis ses AC 1 à 9 (réécrits), +ses Dev Notes et l'entrée « réécriture T0 » de son Change Log. Contexte : la fiche de la b1 +`25-1c-b1-journal-audit-ecran.md` (ce que l'écran fait réellement), le code de l'écran +(`frontend/src/routes/(app)/audit-log/+page.svelte`, `+page.ts`, `+layout.svelte`), la route +(`crates/kesh-api/src/routes/audit_log.rs`), les arbitrages de l'epic +(`_bmad-output/planning-artifacts/epic-25-vague1-suite.md` — **ne pas les contester**). + +## Ce que tu vérifies + +1. **Chaque ligne de l'inventaire de l'AC 3** : le texte cité est-il bien à cette ligne (± quelques + lignes), et le remplacement est-il juste (accords, sens) ? **Rejoue toi-même le grep lexical et une + lecture par le concept** sur `docs/manual/fr/*.tex`, `README.md` **et les PDF aplatis** + (`pdftotext - | tr '\n' ' ' | tr -s ' '`, vers le scratchpad) : un site oublié ? un site classé + « déjà conforme » ou « autre sens » à tort ? +2. **L'AC 4** : l'encadré actuel porte-t-il bien ces quatre énoncés ? La prescription de remplacement + est-elle **vraie contre le code** — la dévalidation est-elle bien la seule voie qui fait disparaître une + écriture (cherche les appelants de la suppression d'écriture : `delete_in_tx`, `enforce_immutability`, + `journal_entry.deleted`) ? +3. **L'AC 5** : chaque comportement décrit de l'écran est-il **vrai contre le code de la b1** (menu, rôles, + jours UTC, plage inversée, numéro vidé au changement de type, action historique, export plafonné, rien + n'est journalisé) ? Un comportement de l'écran que le texte ne dit pas et qui laisserait l'utilisateur + devant une liste vide ou un refus inexpliqué ? +4. **L'AC 6 et 7** : les phrases citées existent-elles ? `:1782` « 5 rôles », `:1803`, `README.md:220`, + le CHANGELOG `[0.12.1]`. Une autre phrase des manuels, du README, du CHANGELOG, de `website/` ou de + `docs/` dit-elle encore que l'écran du journal d'audit n'existe pas ou « reste à venir » (#378) ? +5. **L'AC 2** : la clé et ses trois cellules ; une autre valeur de catalogue qui nomme le journal + autrement ? +6. **Faisabilité** : le développeur peut-il implémenter sans deviner ? + +## Ce que tu rends + +- Findings : sévérité (CRITICAL / HIGH / MEDIUM / LOW), endroit exact de la fiche, **preuve** (commande et + résultat, texte lu), correction. +- ⛔ **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 une base — +`scripts/*.sh`, `make`, tout `git commit`/`add`/`checkout`/`reset`/`stash`/`switch`, `sqlx migrate`, +`cargo test`, `npm run`, `npx`. Lecture, `grep`, `git log`/`show`/`diff`, `gh issue view`, `pdftotext` +(vers `/tmp/claude-1000/-home-gcorbaz-devel-kesh/ca5ce2e2-67a3-4eeb-817f-c2de35620a1c/scratchpad/`) seulement. From 5291c693174a8b46f9b54ae0af305062da0b3217 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:19:55 +0200 Subject: [PATCH 11/19] =?UTF-8?q?docs(25-1c-b2):=20revalidation=20R6=20?= =?UTF-8?q?=E2=80=94=20inventaire=20referm=C3=A9=20sur=20son=20grep,=20r?= =?UTF-8?q?=C3=A9serve=20de=20la=20restauration,=20repli=20Svelte=20(refs?= =?UTF-8?q?=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b2-journal-audit-textes.md | 119 ++++++++++-------- 1 file changed, 68 insertions(+), 51 deletions(-) diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md index 2b3bad99d..ca81213b7 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md +++ b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md @@ -2,38 +2,22 @@ Status: ready-for-dev — ✅ **RÉÉCRITE le 2026-09-26 (T0)** contre le texte actuel ; revalidation R6 à passer -⛔ **REVALIDATION R5 « DÉRIVE » — 1 CRITICAL, 4 HIGH, 4 MEDIUM** (Change Log). La fiche a dormi dix jours sur -une branche locale ; entre-temps, la 25-1c-a (PR #439), les 25-2-b-1/b-2 (#440) et la 25-5-a (#386) ont -**réécrit** une partie des textes qu'elle prescrit de corriger, et **changé le mécanisme** qu'elle décrit : -une facture validée ne se **supprime** plus, elle se **dévalide** (`invoices.rs:1293`, -`InvoiceMustBeUnvalidatedFirst`) ; #381 est fermée. **Les AC 2, 4, 6 et 7 et l'inventaire de l'AC 3 sont à -réécrire contre le texte ACTUEL**, dans la tâche T0, **une fois la 25-1c-b1 implémentée** — la fiche le -prévoyait déjà (« relire chaque site par sa phrase ») ; la réécrire deux fois ne servirait à rien. Puis -revalidation, puis développement. - -⚠️ **RÉOUVERTE le 2026-09-15 au soir**, après sa validation en 7 passes : les arbitrages qui ont rouvert la -25-1c-a changent la section « Traçabilité » et le manuel d'administration (cf. « Réouverture » au Change -Log). **Revalidation ciblée en cours.** - -⚠️ **Issue du SPLIT de la 25-1c-b** (arbitrage du Project Lead du 2026-09-15, après la passe 2 de -validation) : - -| | objet | état | +⛔ **Historique de la fiche** : validée en 7 passes le 2026-09-15, rouverte, revalidée le 2026-09-16 ; restée +dix jours sur une branche locale jamais poussée ; la revalidation R5 « dérive » (2026-09-26) l'a trouvée +périmée — d'autres stories avaient réécrit une partie des textes visés, et une facture validée se +**dévalide** désormais au lieu de se supprimer. **Réécrite en T0 le 2026-09-26** contre le texte de `main`, +après l'implémentation de la 25-1c-b1, puis revalidée (R6, R7 — Change Log). Les lignes citées sont celles +du 2026-09-26 ; **chaque site se retrouve par sa phrase**. + +| | objet | état au 2026-09-26 | |---|---|---| -| 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 | l'écran : feature, page, garde, menu, gardes i18n, tests | ready-for-dev, réouverte | -| **25-1c-b2** *(celle-ci)* | **les textes : manuels, README, vocabulaire « journal d'audit »** | ready-for-dev, réouverte | +| 25-1c-zero | la colonne `audit_log.company_id` | mergée (PR #437) | +| 25-1c-a | la route de consultation, son vocabulaire traduit et son export CSV | mergée (PR #439) | +| 25-1c-b1 | l'écran | implémentée, revue close, même branche | +| **25-1c-b2** *(celle-ci)* | **les textes : manuels, README, CHANGELOG, vocabulaire « journal d'audit »** | fiche réécrite | -⛔ **Livrée dans la MÊME PR que la 25-1c-b1, et implémentée APRÈS elle** sur la même branche : les -manuels décrivent l'écran **tel qu'il est construit** (menu, rôles, filtres), et ne se relisent contre -le code qu'une fois ce code écrit. C'est cette PR qui porte `closes #378`. - -⛔ **Dépendance à la 25-1c-a** : elle réécrit elle-même trois passages du manuel d'administration -(`admin-manual.tex:1585`, `:1590`, `:1803-1810`) et la phrase de `:1786`. **Les numéros de ligne de cette -fiche sont ceux de la branche `story/25-1c-b-journal-audit-ecran` au 2026-09-15** — empilée sur la -25-1c-zero, **avant** la 25-1c-a et la b1, et donc déjà différente de `main` (la 25-1c-zero a réécrit -`admin-manual.tex:1786`). Chaque site se retrouve **par sa phrase citée**, et se relit après le rebase. +⛔ **Livrée dans la MÊME PR que la 25-1c-b1**, qui porte `closes #378` : les textes décrivent l'écran **tel +qu'il est construit**. ## Story @@ -85,12 +69,20 @@ aligné l'autre, `export-global-content-excludes`) : audit » (« sarà conservato nel registro di audit »), en « audit log ». La règle 3 du glossaire l'impose : la ligne de l'AC 1 ne s'écrit pas sans cet alignement. +⛔ **Et son repli Svelte** (revalidation R6) : `frontend/src/routes/(app)/settings/fiscal-years/+page.svelte:554` +porte « … sera conservé dans la piste d'audit. » → « … dans le journal d'audit. », **mot pour mot** le FTL +fr-CH (à l'apostrophe près : le repli emploie l'apostrophe droite `'` là où le FTL a `’` — la garde +`i18n-un-repli-par-cle` dit si l'écart compte). Le catalogue gagne sur le repli, le défaut est latent — mais +la règle 3 vise « toutes ses occurrences déjà livrées ». + **Contrôle**, exécutable, **sur la VALEUR seule** (Python) : pour chaque `.ftl`, chaque clé dont la valeur — lignes de continuation rattachées — contient `audit|protokoll|registro|journal|piste|trail` (sans casse) est triée à la main. **Triés, à laisser** : `bank-accounts-errors-has-transactions` (« audit comptable » : la **révision**, autre sens, quatre langues) ; les clés `audit-log-*` du vocabulaire de la 25-1c-a (codes et libellés d'actions : « écriture », etc.) ; les clés d'écran de la b1, **déjà conformes** (vérifié : -« Journal d'audit » / « Audit-Protokoll » / « Registro di audit » / « Audit log »). +« Journal d'audit » / « Audit-Protokoll » / « Registro di audit » / « Audit log »). ⚠️ Le motif `journal` +rend une quarantaine de clés « journal comptable », et `protokoll` des mots allemands d'un autre sens +(`protokolliertem Grund`, `Serverprotokolle`) : **tri à la main**, attendu. **3. Les manuels, la brochure et le README** — inventaire du 2026-09-26, réunion de deux procédés : **lexical** (`grep -niE "piste d.audit|audit-trail|audit trail|audit log|piste de contr|trace d.audit|dans l.audit\b"` @@ -99,29 +91,39 @@ ces mots) : | fichier | ligne | texte actuel | devient | |---|---|---|---| +| `user-manual.tex` | `:301` | « (pour préserver l'audit-trail) » | « (pour préserver le journal d'audit) » | | `user-manual.tex` | `:888` | « nouvelle trace d'audit » | « nouvelle entrée au journal d'audit » | | `user-manual.tex` | `:1111` | « journalisées dans l'audit-trail » | « inscrites au journal d'audit » | | `user-manual.tex` | `:1789` | `\section{Traçabilité (audit-trail)}` | `\section{Traçabilité (journal d'audit)}` | | `user-manual.tex` | `:1791` | « une \textbf{piste d'audit} (\texttt{audit\_log}) » | « un \textbf{journal d'audit} (\texttt{audit\_log}) » — accords suivants ajustés | -| `user-manual.tex` | `:1803` | « Cette piste d'audit est » | « Ce journal d'audit est » — accords (« enregistré ») | +| `user-manual.tex` | `:1803-1812` | « Cette piste d'audit est » … « Elle \textbf{contribue} » … « elle est \textbf{enregistrée} » | « Ce journal d'audit est » … « Il contribue » … « il est enregistré » — **tous** les accords du paragraphe | | `user-manual.tex` | `:1889` | « L'\texttt{audit\_log} garde toujours la trace » *(concept)* | « Le journal d'audit garde toujours la trace » | | `user-manual.tex` | `:1944` | `\item[Audit-trail (piste d'audit)]` — glossaire | `\item[Journal d'audit (angl. audit log)]`, **déplacé à la lettre J**, après `\item[Journal]` (le journal des écritures) — la définition rend la différence lisible ; l'avertissement de couverture qui suit est **conservé** | | `admin-manual.tex` | `:1088` | « La demande reste tracée dans l'audit » | « … dans le journal d'audit » | | `admin-manual.tex` | `:1141` | « l'audit-trail (\texttt{invoice.emailed}, …) » | « le journal d'audit (…) » | | `admin-manual.tex` | `:1143` | « laisse sa propre trace d'audit » | « laisse sa propre entrée au journal d'audit » | | `admin-manual.tex` | `:1589` | « les conséquences sur la piste de contrôle » | « … sur le journal d'audit » | +| `admin-manual.tex` | `:1760` | « La piste de contrôle se lit dans l'interface web » | « Le journal d'audit se lit dans l'interface web » | +| `admin-manual.tex` | `:1766` | `\item \textbf{Audit} : les mutations […] tracées dans \texttt{audit\_log}` | `\item \textbf{Journal d'audit} : …` (le nom de table reste) | | `admin-manual.tex` | `:1784` | `\subsection{Audit-trail (audit\_log)}` | `\subsection{Journal d'audit (audit\_log)}` | +| `admin-manual.tex` | `:1816` | « les entrées d'audit \textbf{déjà présentes chez vous} » | « les entrées du journal d'audit déjà présentes … » | | `admin-manual.tex` | `:1817` | « la piste de contrôle mêlerait deux comptabilités » | « le journal d'audit mêlerait … » | +| `admin-manual.tex` | `:1946` | « la piste de contrôle voyage avec l'export » *(la ligne cite aussi `audit\_log`, la table : lui reste)* | « le journal d'audit voyage avec l'export » | +| `admin-manual.tex` | `:1969` | « \texttt{audit\_log} en conserve la trace » *(concept)* | « le journal d'audit en conserve la trace » | | `admin-manual.tex` | `:2197` | `\item[Audit-trail]` — glossaire | `\item[Journal d'audit (angl. audit log)]`, **déplacé à la lettre J**, avant `\item[Journal entry]` ; avertissement conservé | | `marketing-brochure.tex` | `:319` | « Audit-trail centralisé pour la responsabilité fiduciaire. » | « Journal d'audit centralisé … » | | `README.md` | `:29` | « audit log » (liste des fonctionnalités, en français) | « journal d'audit consultable à l'écran » | +| `README.md` | `:220` | « la **piste de contrôle survit désormais à l'import d'une sauvegarde** » — ligne **v0.12.1, non publiée** | « le **journal d'audit survit désormais …** » | **Déjà conformes, à laisser** : `user-manual.tex:474`, `:514`, `:543`, `:593`, `:1039`, `:1085`, `:1159`, -`:1427`, `:1712`, `:1721` ; `admin-manual.tex:1066`, `:1613`, `:1760`, `:1810` ; `marketing-brochure.tex:139`. -**Autre sens, à laisser** : `admin-manual.tex:159`, `:1242`, `:1946`, `:2082` (`audit\_log` y nomme la -**table**, non le concept) ; `README.md:219` (« audit par trois experts ») ; la section *« Numérotation »* -(`user-manual.tex:543`) parle de **trou**, non du journal. ⚠️ **L'historique publié ne se réécrit pas** : -les lignes v0.12.0 du README et la section `[0.12.0]` du CHANGELOG gardent « piste de contrôle ». +`:1427`, `:1712`, `:1721` ; `admin-manual.tex:1066`, `:1613`, `:1810` ; `marketing-brochure.tex:139`. +**Autre sens, à laisser** : `admin-manual.tex:159`, `:1242`, `:2082` (`audit\_log` y nomme la +**table**, non le concept) ; `README.md:219` (« audit par trois experts ») ; `user-manual.tex:624` (« une +piste de correction lisible ») ; `marketing-brochure.tex:209` (« Une piste pour les fiduciaires », piste +commerciale). ⚠️ **L'historique publié ne se réécrit pas** : +la ligne **v0.12.0** du README (`:219`, dont « n'est consultable par aucun écran ») et la section +`[0.12.0]` du CHANGELOG ne se touchent pas. La ligne **v0.12.1**, elle, n'est pas publiée : elle se met à +jour (AC 3, AC 7). `website/` (anglais, « audit log » déjà) ne se touche pas. ⛔ **Le nom technique `audit\_log`** ne change pas là où il désigne la table ou le champ. @@ -130,20 +132,30 @@ les lignes v0.12.0 du README et la section `[0.12.0]` du CHANGELOG gardent « pi **4. Le manuel utilisateur, l'encadré** `\begin{keshwarning}[title=… Ce que le logiciel ne fait pas encore à votre place]` (`user-manual.tex:511-528`, juste avant `\subsection{Numérotation}`). Il porte -aujourd'hui **quatre** énoncés, dont **trois sont périmés** : +aujourd'hui **cinq** énoncés, dont **trois sont périmés** : | énoncé actuel | état | |---|---| | « La contre-passation est désormais imposée […] : la modification et la suppression d'une écriture sont refusées » | **vrai**, à conserver — avec la seule voie qui fait disparaître une écriture : la **dévalidation** d'une facture (renvoi à « Supprimer ou dévalider une facture ») | | « Ce qui manque encore, c'est l'écran de consultation du journal d'audit […] par l'interface de programmation seulement […] La page […] reste à venir » | **faux** : l'écran existe (25-1c-b1) | +| « Un verrou plus fin que l'exercice existe, lui : le verrou de période […] » | **vrai**, à conserver | | « Une seule exception, déjà en vigueur : une écriture déjà contre-passée ne peut plus être supprimée » | **périmé** : **aucune** écriture ne se supprime plus | | « préférez la contre-passation […], tant que le logiciel vous laisse le choix » | **périmé** : il ne laisse plus le choix | ⇒ L'encadré devient une **note** (`keshnote`), qui n'annonce plus de fonction manquante : contre-passation -imposée ; la dévalidation comme seule disparition d'une écriture, **journalisée sous son propre nom** ; +imposée ; **en usage courant**, la dévalidation d'une facture est la seule voie qui fait disparaître une +écriture, **journalisée sous son propre nom** — ⚠️ **hors restauration d'une sauvegarde**, qui remet les +livres dans l'état de l'archive (`backup.rs`, import d'installation), et hors réinitialisation des données de +démonstration, refusée après la finalisation (revalidation R6 : `delete_in_tx` n'a qu'un appelant qui +supprime, `invoices.rs` — la dévalidation ; mais `backup.rs:457` et `kesh-seed` effacent des tables) ; l'historique des corrections **consultable à l'écran** — *Administration → Journal d'audit*, renvoi à la -section « Traçabilité » — et par l'API ; le verrou de période conservé tel quel. ⛔ **Aucune formule -absolue** (« en aucun cas ») sur la disparition d'une écriture. +section « Traçabilité », qui reçoit pour cela un `\label{sec:tracabilite}` — et par l'API ; le verrou de période conservé tel quel. ⛔ **Aucune formule +absolue** (« en aucun cas », « seule » sans réserve) sur la disparition d'une écriture. + +⚠️ **Voisin, dans la même section** (`user-manual.tex:536`, sous-section « Numérotation ») : « Si vous +supprimez la dernière écriture d'un exercice, la suivante prendra le numéro d'après » — une écriture ne se +supprime plus → « Si la dernière écriture d'un exercice disparaît (par la dévalidation de sa facture), la +suivante… ». **5. Le manuel utilisateur, section « Traçabilité »** — la fin de son `keshnote` (`:1812-1816`) : *« n'est pas encore consultable via un écran dédié dans l'interface (page de consultation prévue pour une @@ -158,14 +170,15 @@ inattendue se dit** : appartient au jour UTC **précédent** ; une date de début postérieure à la date de fin est refusée par l'écran ; - **le numéro d'entité exige un type** : le champ reste inactif sans type, et se vide quand on change de - type ; + type ; un numéro nul, négatif ou non entier est ignoré ; une entrée sans entité précise affiche « — » ; - **la langue** : actions et types d'entité traduits dans la langue de l'installation, à l'écran comme dans le CSV ; - **une action historique**, absente du vocabulaire, s'affiche sous son **code** et n'est pas proposée dans la liste « Action » — on la retrouve par le type et le numéro d'entité ; - le détail déplié (JSON), la pagination ; - **l'export CSV** porte les filtres affichés, en UTC, **plafonné à 10 000 lignes** — au-delà, un message - demande d'affiner ; + demande d'affiner ; ⚠️ une plage inversée, à l'export, est refusée par le serveur, avec un message qui + n'est pas encore traduit (#469) ; - ⚠️ ni la consultation ni l'export ne s'inscrivent eux-mêmes au journal. La phrase sur le **centre de notifications** (« prévu mais pas encore disponible ») reste vraie et se @@ -181,8 +194,10 @@ conserve. Les **manques connus** de la section (séquence d'installation #434, g - `:1782` — « Les **5** rôles standards » → **trois**, en accord avec `:1345` (`entities/user.rs`) ; - `:1803` — « Il conserve aussi les modifications et suppressions **antérieures au gel** » : à compléter par les **dévalidations** de factures, postérieures au gel et journalisées (`invoice.unvalidated`) — sans - quoi la phrase laisse croire qu'aucune disparition postérieure n'est tracée. ⚠️ Le paragraphe qui précède - (`:1802`) nomme déjà la dévalidation : **ne pas le réécrire**. + quoi la phrase laisse croire qu'aucune disparition postérieure n'est tracée ; +- `:1802` — « ⚠️ \textbf{Une seule voie fait encore disparaître une écriture} » : même réserve que l'AC 4 — + **en usage courant**, hors restauration d'une sauvegarde (la sous-section « Restaurer » et l'encadré des + réserves le disent déjà) ; le reste du paragraphe (dévalidation, huit motifs) se conserve. **7. Le README et le CHANGELOG** : @@ -202,8 +217,9 @@ conserve. Les **manques connus** de la section (séquence d'installation #434, g `re.sub(r"(\w)- (\w)", r"\1\2", t)`) : les anciennes phrases absentes, les nouvelles présentes ; - **inventaire REJOUÉ** sur les PDF aplatis et le README, motif, sans casse : `piste|trace d.audit|dans l.audit\b|audit-?trail|audit log` — **reste attendu, et seulement lui** : les - deux « (angl. audit log) » des glossaires, et les sites « autre sens » de l'AC 3 ; on compare des - **sites**, pas des nombres (un site peut apparaître au sommaire). + deux « (angl. audit log) » des glossaires ; `user-manual.tex:624` (« piste de correction ») ; + `marketing-brochure.tex:209` (« Une piste pour les fiduciaires ») ; dans le README, la ligne **v0.12.0** + (historique publié). On compare des **sites**, pas des nombres (un site peut apparaître au sommaire). **9. Les gardes** : `cargo test -p kesh-i18n` (parité) ; `npm run test:unit` — la clé de l'AC 2 change de **valeur**, pas de nom : **aucun compteur ne doit bouger** ; s'il bouge, l'écrire. @@ -225,11 +241,11 @@ conserve. Les **manques connus** de la section (séquence d'installation #434, g | zone | fichiers | |---|---| -| vocabulaire | `docs/i18n-glossaire.md` ; `crates/kesh-i18n/locales/{fr,it,en}-CH/messages.ftl` (une clé, trois cellules) | +| vocabulaire | `docs/i18n-glossaire.md` ; `crates/kesh-i18n/locales/{fr,it,en}-CH/messages.ftl` (une clé, trois cellules) ; le repli `frontend/src/routes/(app)/settings/fiscal-years/+page.svelte:554` | | manuels | `docs/manual/fr/user-manual.tex`, `admin-manual.tex`, `marketing-brochure.tex` + les trois PDF | | README, CHANGELOG | `README.md`, `CHANGELOG.md` | -Aucun code applicatif. +Aucun code applicatif, hors **un repli Svelte** (AC 2). ### Faits établis à la réécriture (2026-09-26), et non supposés @@ -280,6 +296,9 @@ Aucun code applicatif. ## Change Log +- **2026-09-26** — **revalidation R6** — **Une lentille Opus**, passe complète, prompt `25-1c-b2-validate-prompt-r6.md`, axes déclarés (non exercés : fiche de la b1 lue par son code seulement, manuels DE/IT/EN, lecture exhaustive de « trace » / « tracé »). **1 HIGH, 4 MEDIUM, 8 LOW**, tous retenus : ① HIGH — `user-manual.tex:301` (« pour préserver l'audit-trail »), rendu par le grep même de la fiche, **absent de l'inventaire** : c'est le HIGH de la passe 3 qui revient, perdu à la réécriture. ② MEDIUM — `admin-manual.tex:1760` et `:1946` mal classés (« piste de contrôle » y désigne le journal) ; ③ MEDIUM — trois sites non triés au rejeu (`user-manual.tex:624`, `marketing-brochure.tex:209` — tri de la passe 5 perdu —, `README.md:220`, ligne v0.12.1 **non publiée**) ; ④ MEDIUM — « la dévalidation, seule disparition d'une écriture » est **faux contre le code** : la restauration d'une sauvegarde (`backup.rs:457`) et la réinitialisation de démonstration effacent aussi des écritures ⇒ « en usage courant, hors restauration » — et `admin-manual.tex:1802`, même défaut, **réécrit aussi** (l'AC 6 disait de ne pas y toucher) ; ⑤ MEDIUM — le repli Svelte de `fiscal-year-reopen-confirmation-body` (`fiscal-years/+page.svelte:554`) non propagé ⇒ inscrit à l'AC 2 et aux zones touchées. LOW : l'encadré a **cinq** énoncés (le verrou de période reste) ; `:543` classé deux fois ; `admin-manual.tex:1766`, `:1816`, `:1969` et les accords de `user-manual.tex:1809-1812` ajoutés ; préambule périmé réécrit ; AC 5 complétée (numéro invalide ignoré, « — », plage inversée à l'export → #469) ; `user-manual.tex:536` (« Si vous supprimez la dernière écriture ») ajouté à l'AC 4 ; `\label{sec:tracabilite}` prescrit ; bruit du motif `journal` / `protokoll` dit. Ces deux entrées-ci et les deux précédentes, d'abord insérées par erreur dans le tableau de tendance de la passe 7, sont **remontées** en tête du Change Log. +- **2026-09-26** — **réécriture T0** — Après la 25-1c-b1 (revue close), la fiche est **réécrite contre le texte de `main`** : AC 2 réduite à **une clé, trois cellules** (la 25-5-a a aligné l'autre) ; AC 3 : **inventaire refait** (16 sites à changer, dont les « piste de contrôle » ajoutés depuis, et la liste des sites déjà conformes ou d'autre sens) ; AC 4 : l'encadré a **quatre** énoncés, trois périmés, et la seule disparition d'une écriture est la **dévalidation** ; AC 5 : l'écran décrit tel que la b1 le livre (dont le refus de la plage inversée et le vidage du numéro au changement de type, ajoutés par sa revue) ; AC 6 : ce qui reste à corriger au manuel d'administration (« reste à venir », « 5 rôles », `:1803`) ; AC 7 : la ligne v0.12.1 du README et une entrée CHANGELOG *Added* — ⚠️ l'historique publié ne se réécrit pas. Revalidation à passer. +- **2026-09-26** — **revalidation R5 « dérive »** — Reprise sur `main` à `0e4c2682`. **Une lentille Sonnet**, prompt `25-1c-b-validate-prompt-r5-derive.md`, axes déclarés (non exercés : rejeu complet de la procédure Python de l'AC 3/8, manuels DE/IT/EN, PDF de la brochure, CHANGELOG). Constats **vérifiés par l'orchestrateur** (`grep -nF`) avant d'être retenus. ⛔ **CRITICAL** — le mécanisme des AC 4 et 6 n'existe plus : une facture validée ne se supprime plus (`invoices.rs:1293`, `DbError::InvoiceMustBeUnvalidatedFirst`), elle se **dévalide** (`invoice.unvalidated`) ; le code cité (`invoices.rs:1339-1350`) a disparu ; #381 est **fermée**. **HIGH** — (1) `admin-manual.tex` « sans exception » : déjà réécrit ailleurs, la formule n'existe plus à ce site ; (2) item « Champs » (`:1790`) déjà corrigé par la 25-1c-a — reste « L'écran, lui, reste à venir (issue #378) » ; (3) encadré du manuel utilisateur (`:511-528`) déjà réécrit par la 25-1c-a — l'énoncé du verrou de période a disparu ; (4) AC 2 : `export-global-content-excludes` réécrite par la 25-5-a dans les quatre locales, **déjà conforme** — reste une seule clé à aligner (`fiscal-year-reopen-confirmation-body`, fr/it/en). **MEDIUM** — 97 actions et 138 clés `audit-log-*` (non 92 / 133) ; inventaire lexical plus large (nouveaux « piste de contrôle » `admin-manual.tex:1589`, `:1760`, `:1817`, `:1946`, et `user-manual.tex:1111` reformulée) ; toutes les lignes des manuels décalées ; la phrase du README citée par l'AC 7 n'existe plus (la ligne v0.12.1 porte « Restent ouverts : l'écran de consultation ([#378]) »). **LOW** — les entrées de glossaire portent un avertissement ajouté. ⇒ **Décision de l'orchestrateur** : pas de réécriture partielle maintenant — la fiche se réécrit en T0, contre le texte réel, **après** la 25-1c-b1, puis se revalide. - **2026-09-15** — **Fiche créée par le split de la 25-1c-b** (arbitrage du Project Lead). Les volets « vocabulaire » et « ce que l'écran rend faux » viennent de la parente (AC 13-18) ; deux findings de sa passe 2 sont appliqués **ici** : @@ -479,8 +498,6 @@ aucune ligne de code, pas plus que la story, qui n'est que texte. | passe | modèle | rendu (après reclassement) | |---|---|---| -| 2026-09-26 | réécriture T0 | Après la 25-1c-b1 (revue close), la fiche est **réécrite contre le texte de `main`** : AC 2 réduite à **une clé, trois cellules** (la 25-5-a a aligné l'autre) ; AC 3 : **inventaire refait** (16 sites à changer, dont les « piste de contrôle » ajoutés depuis, et la liste des sites déjà conformes ou d'autre sens) ; AC 4 : l'encadré a **quatre** énoncés, trois périmés, et la seule disparition d'une écriture est la **dévalidation** ; AC 5 : l'écran décrit tel que la b1 le livre (dont le refus de la plage inversée et le vidage du numéro au changement de type, ajoutés par sa revue) ; AC 6 : ce qui reste à corriger au manuel d'administration (« reste à venir », « 5 rôles », `:1803`) ; AC 7 : la ligne v0.12.1 du README et une entrée CHANGELOG *Added* — ⚠️ l'historique publié ne se réécrit pas. Revalidation à passer. | -| 2026-09-26 | revalidation R5 « dérive » | Reprise sur `main` à `0e4c2682`. **Une lentille Sonnet**, prompt `25-1c-b-validate-prompt-r5-derive.md`, axes déclarés (non exercés : rejeu complet de la procédure Python de l'AC 3/8, manuels DE/IT/EN, PDF de la brochure, CHANGELOG). Constats **vérifiés par l'orchestrateur** (`grep -nF`) avant d'être retenus. ⛔ **CRITICAL** — le mécanisme des AC 4 et 6 n'existe plus : une facture validée ne se supprime plus (`invoices.rs:1293`, `DbError::InvoiceMustBeUnvalidatedFirst`), elle se **dévalide** (`invoice.unvalidated`) ; le code cité (`invoices.rs:1339-1350`) a disparu ; #381 est **fermée**. **HIGH** — (1) `admin-manual.tex` « sans exception » : déjà réécrit ailleurs, la formule n'existe plus à ce site ; (2) item « Champs » (`:1790`) déjà corrigé par la 25-1c-a — reste « L'écran, lui, reste à venir (issue #378) » ; (3) encadré du manuel utilisateur (`:511-528`) déjà réécrit par la 25-1c-a — l'énoncé du verrou de période a disparu ; (4) AC 2 : `export-global-content-excludes` réécrite par la 25-5-a dans les quatre locales, **déjà conforme** — reste une seule clé à aligner (`fiscal-year-reopen-confirmation-body`, fr/it/en). **MEDIUM** — 97 actions et 138 clés `audit-log-*` (non 92 / 133) ; inventaire lexical plus large (nouveaux « piste de contrôle » `admin-manual.tex:1589`, `:1760`, `:1817`, `:1946`, et `user-manual.tex:1111` reformulée) ; toutes les lignes des manuels décalées ; la phrase du README citée par l'AC 7 n'existe plus (la ligne v0.12.1 porte « Restent ouverts : l'écran de consultation ([#378]) »). **LOW** — les entrées de glossaire portent un avertissement ajouté. ⇒ **Décision de l'orchestrateur** : pas de réécriture partielle maintenant — la fiche se réécrit en T0, contre le texte réel, **après** la 25-1c-b1, puis se revalide. | | 1 | Sonnet + Haiku | 2 M, 2 L | | 2 | Opus | 6 M, 7 L → **split** | | 3 | Sonnet | 1 H, 2 M, 4 L | From b97612d3ebc30484c788068b6a4ae6a601831254 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:20:09 +0200 Subject: [PATCH 12/19] =?UTF-8?q?docs(25-1c-b1):=20entr=C3=A9es=20du=20Cha?= =?UTF-8?q?nge=20Log=20remont=C3=A9es=20hors=20d'un=20tableau=20(refs=20#3?= =?UTF-8?q?78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b1-journal-audit-ecran.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 index db526c903..56c1a34b8 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md +++ b/_bmad-output/implementation-artifacts/25-1c-b1-journal-audit-ecran.md @@ -620,6 +620,12 @@ Claude Opus 5.5 — implémentation. ## Change Log +- **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 @@ -780,11 +786,6 @@ de texte de spec, sans assertion ajoutée ni ligne de code. | passe | modèle | rendu (après reclassement) | |---|---|---| -| 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. | | 1 | Sonnet + Haiku | 2 M, 2 L | | 2 | Opus | 6 M, 7 L → **split** | | 3 | Sonnet | 1 M, 3 L | From a548241293597a42848d1cfd12ca6fd7b5258298 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:20:27 +0200 Subject: [PATCH 13/19] =?UTF-8?q?docs(25-1c-b2):=20prompt=20de=20la=20reva?= =?UTF-8?q?lidation=20R7=20cibl=C3=A9e=20(refs=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b2-validate-prompt-r7-ciblee.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 _bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r7-ciblee.md diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r7-ciblee.md b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r7-ciblee.md new file mode 100644 index 000000000..ab4f058e3 --- /dev/null +++ b/_bmad-output/implementation-artifacts/25-1c-b2-validate-prompt-r7-ciblee.md @@ -0,0 +1,36 @@ +# Prompt — revalidation R7 CIBLÉE, Story 25-1c-b2 + +*Versionné le 2026-09-26. Une lentille (Haiku 4.5), contexte frais, braquée sur la seule remédiation de +la R6 — CLAUDE.md, § « La passe ciblée ». Diff APLATI.* + +Dépôt `/home/gcorbaz/devel/kesh`, branche `story/25-1c-b1-journal-audit-ecran`. Diff à relire : +`git diff ef575755 5291c693 -- _bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md`. +Fiche : `_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md` (l'entrée « revalidation +R6 » en tête du Change Log dit ce qui a été corrigé). + +## Ce que tu vérifies + +1. **Chaque ligne AJOUTÉE à l'inventaire de l'AC 3** (`user-manual.tex:301`, `:1803-1812`, + `admin-manual.tex:1760`, `:1766`, `:1816`, `:1946`, `:1969`, `README.md:220`) : le texte cité est-il à + cette ligne (`grep -nF` d'une portion de la phrase), et le remplacement juste ? +2. **Le repli Svelte** de l'AC 2 : `frontend/src/routes/(app)/settings/fiscal-years/+page.svelte` porte-t-il + bien « piste d'audit » à la ligne dite ? +3. **La réserve de l'AC 4 et de l'AC 6** (« en usage courant, hors restauration d'une sauvegarde ») : vraie + contre `crates/kesh-db/src/backup.rs` (effacement des tables à l'import) et `crates/kesh-seed` ? +4. **Le symptôme ailleurs dans la fiche** : reste-t-il une phrase qui dit « quatre énoncés », « la seule » + sans réserve, ou qui classe `:1760` / `:1946` autrement ? Le rejeu de l'AC 8 et l'inventaire de l'AC 3 + sont-ils d'accord entre eux (un site trié « autre sens » dans l'un et « à changer » dans l'autre) ? +5. **Rejoue le grep lexical** de l'AC 3 sur `docs/manual/fr/*.tex` et `README.md` : chaque ligne rendue + est-elle, dans la fiche, soit à changer, soit déjà conforme, soit autre sens ? + +## Ce que tu rends + +- Findings : sévérité, endroit, **preuve** (commande et résultat) ; pour tout CRITICAL ou HIGH, la commande + `grep -nF` et son résultat. +- ⛔ **La liste des axes réellement exercés ET de ceux qui ne l'ont pas été.** + +## Interdits + +⛔ N'écris aucun fichier ; aucune commande qui écrit dans le dépôt ou une base (`scripts/*.sh`, `make`, +`git commit`/`add`/`checkout`/`reset`/`stash`, `cargo test`, `npm run`, `npx`). Lecture, `grep`, +`git diff`/`show` seulement. From ac0a2fc9e359cc414fab8c18058ac32a9fc1d5a0 Mon Sep 17 00:00:00 2001 From: Guy Corbaz Date: Sat, 26 Sep 2026 15:23:53 +0200 Subject: [PATCH 14/19] =?UTF-8?q?docs(25-1c-b2):=20revalidation=20R7=20cib?= =?UTF-8?q?l=C3=A9e=20=C3=A0=20z=C3=A9ro=20=E2=80=94=20fiche=20valid=C3=A9?= =?UTF-8?q?e,=20dev=20en=20cours=20(refs=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../25-1c-b2-journal-audit-textes.md | 5 +++-- _bmad-output/implementation-artifacts/sprint-status.yaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md index ca81213b7..ebfa96426 100644 --- a/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md +++ b/_bmad-output/implementation-artifacts/25-1c-b2-journal-audit-textes.md @@ -1,6 +1,6 @@ # Story 25.1c-b2 : Ce que l'écran du journal d'audit rend faux — manuels, README, vocabulaire -Status: ready-for-dev — ✅ **RÉÉCRITE le 2026-09-26 (T0)** contre le texte actuel ; revalidation R6 à passer +Status: in-progress ⛔ **Historique de la fiche** : validée en 7 passes le 2026-09-15, rouverte, revalidée le 2026-09-16 ; restée dix jours sur une branche locale jamais poussée ; la revalidation R5 « dérive » (2026-09-26) l'a trouvée @@ -226,7 +226,7 @@ conserve. Les **manques connus** de la section (séquence d'installation #434, g ## Tasks / Subtasks -- [ ] **T0 — Réécriture de la fiche** contre le texte actuel, **après la 25-1c-b1** *(fait le 2026-09-26, +- [x] **T0 — Réécriture de la fiche** contre le texte actuel, **après la 25-1c-b1** *(fait le 2026-09-26, cette version)* ; puis revalidation. - [ ] **T1 — Glossaire et catalogue** (AC 1, 2). - [ ] **T2 — Vocabulaire des manuels, de la brochure et du README** (AC 3). @@ -296,6 +296,7 @@ Aucun code applicatif, hors **un repli Svelte** (AC 2). ## Change Log +- **2026-09-26** — **revalidation R7 ciblée** — **Une lentille Haiku 4.5**, contexte frais, braquée sur la seule remédiation R6 (`git diff ef575755 5291c693`), prompt `25-1c-b2-validate-prompt-r7-ciblee.md`. **0 finding** : chaque ligne ajoutée vérifiée par `grep -nF` à sa source ; repli Svelte présent ; réserve « hors restauration » fondée contre `backup.rs` et `kesh-seed` ; aucun « seule » sans réserve ; **grep lexical rejoué — 20 lignes, ensemble égal à celui de l'inventaire**. La remédiation ne touchait que la fiche ⇒ **boucle close**, fiche validée. *(Trend : R5 Sonnet 1 CRIT / 4 HIGH → réécriture T0 → R6 Opus 1 HIGH / 4 MED → R7 Haiku 0.)* - **2026-09-26** — **revalidation R6** — **Une lentille Opus**, passe complète, prompt `25-1c-b2-validate-prompt-r6.md`, axes déclarés (non exercés : fiche de la b1 lue par son code seulement, manuels DE/IT/EN, lecture exhaustive de « trace » / « tracé »). **1 HIGH, 4 MEDIUM, 8 LOW**, tous retenus : ① HIGH — `user-manual.tex:301` (« pour préserver l'audit-trail »), rendu par le grep même de la fiche, **absent de l'inventaire** : c'est le HIGH de la passe 3 qui revient, perdu à la réécriture. ② MEDIUM — `admin-manual.tex:1760` et `:1946` mal classés (« piste de contrôle » y désigne le journal) ; ③ MEDIUM — trois sites non triés au rejeu (`user-manual.tex:624`, `marketing-brochure.tex:209` — tri de la passe 5 perdu —, `README.md:220`, ligne v0.12.1 **non publiée**) ; ④ MEDIUM — « la dévalidation, seule disparition d'une écriture » est **faux contre le code** : la restauration d'une sauvegarde (`backup.rs:457`) et la réinitialisation de démonstration effacent aussi des écritures ⇒ « en usage courant, hors restauration » — et `admin-manual.tex:1802`, même défaut, **réécrit aussi** (l'AC 6 disait de ne pas y toucher) ; ⑤ MEDIUM — le repli Svelte de `fiscal-year-reopen-confirmation-body` (`fiscal-years/+page.svelte:554`) non propagé ⇒ inscrit à l'AC 2 et aux zones touchées. LOW : l'encadré a **cinq** énoncés (le verrou de période reste) ; `:543` classé deux fois ; `admin-manual.tex:1766`, `:1816`, `:1969` et les accords de `user-manual.tex:1809-1812` ajoutés ; préambule périmé réécrit ; AC 5 complétée (numéro invalide ignoré, « — », plage inversée à l'export → #469) ; `user-manual.tex:536` (« Si vous supprimez la dernière écriture ») ajouté à l'AC 4 ; `\label{sec:tracabilite}` prescrit ; bruit du motif `journal` / `protokoll` dit. Ces deux entrées-ci et les deux précédentes, d'abord insérées par erreur dans le tableau de tendance de la passe 7, sont **remontées** en tête du Change Log. - **2026-09-26** — **réécriture T0** — Après la 25-1c-b1 (revue close), la fiche est **réécrite contre le texte de `main`** : AC 2 réduite à **une clé, trois cellules** (la 25-5-a a aligné l'autre) ; AC 3 : **inventaire refait** (16 sites à changer, dont les « piste de contrôle » ajoutés depuis, et la liste des sites déjà conformes ou d'autre sens) ; AC 4 : l'encadré a **quatre** énoncés, trois périmés, et la seule disparition d'une écriture est la **dévalidation** ; AC 5 : l'écran décrit tel que la b1 le livre (dont le refus de la plage inversée et le vidage du numéro au changement de type, ajoutés par sa revue) ; AC 6 : ce qui reste à corriger au manuel d'administration (« reste à venir », « 5 rôles », `:1803`) ; AC 7 : la ligne v0.12.1 du README et une entrée CHANGELOG *Added* — ⚠️ l'historique publié ne se réécrit pas. Revalidation à passer. - **2026-09-26** — **revalidation R5 « dérive »** — Reprise sur `main` à `0e4c2682`. **Une lentille Sonnet**, prompt `25-1c-b-validate-prompt-r5-derive.md`, axes déclarés (non exercés : rejeu complet de la procédure Python de l'AC 3/8, manuels DE/IT/EN, PDF de la brochure, CHANGELOG). Constats **vérifiés par l'orchestrateur** (`grep -nF`) avant d'être retenus. ⛔ **CRITICAL** — le mécanisme des AC 4 et 6 n'existe plus : une facture validée ne se supprime plus (`invoices.rs:1293`, `DbError::InvoiceMustBeUnvalidatedFirst`), elle se **dévalide** (`invoice.unvalidated`) ; le code cité (`invoices.rs:1339-1350`) a disparu ; #381 est **fermée**. **HIGH** — (1) `admin-manual.tex` « sans exception » : déjà réécrit ailleurs, la formule n'existe plus à ce site ; (2) item « Champs » (`:1790`) déjà corrigé par la 25-1c-a — reste « L'écran, lui, reste à venir (issue #378) » ; (3) encadré du manuel utilisateur (`:511-528`) déjà réécrit par la 25-1c-a — l'énoncé du verrou de période a disparu ; (4) AC 2 : `export-global-content-excludes` réécrite par la 25-5-a dans les quatre locales, **déjà conforme** — reste une seule clé à aligner (`fiscal-year-reopen-confirmation-body`, fr/it/en). **MEDIUM** — 97 actions et 138 clés `audit-log-*` (non 92 / 133) ; inventaire lexical plus large (nouveaux « piste de contrôle » `admin-manual.tex:1589`, `:1760`, `:1817`, `:1946`, et `user-manual.tex:1111` reformulée) ; toutes les lignes des manuels décalées ; la phrase du README citée par l'AC 7 n'existe plus (la ligne v0.12.1 porte « Restent ouverts : l'écran de consultation ([#378]) »). **LOW** — les entrées de glossaire portent un avertissement ajouté. ⇒ **Décision de l'orchestrateur** : pas de réécriture partielle maintenant — la fiche se réécrit en T0, contre le texte réel, **après** la 25-1c-b1, puis se revalide. diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index e4174ff4f..b22ab7aaa 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -331,7 +331,7 @@ development_status: 25-1c-zero-audit-company-id: done # 2026-09-15 **REVUE DE CODE CLOSE EN 2 PASSES** : `1M → 1L`, 3 lentilles (Sonnet, Haiku, Sonnet) puis passe CIBLEE (Opus). Les deux findings retenus portent sur le MEME test de caracterisation (AC 8), aucune ligne de production prise en defaut ; le MEDIUM venait du montage prescrit par la spec. Gate de cloture **2330/2330**, base remise a zero. ✅ **GATE E2E DE CLOTURE — 214 passed / 19 skipped / 9 failed (8,3 min)**, montage COMPLET, `kesh_e2e` reconstruite (68 migrations), `/health` smtpConfigured:true ; **le binaire a demarre contre la migration sans erreur de checksum (P8)**. Les 9 : 7 KF-029 + 2 pollutions (`product-revenue-account:133`, `sidebar-navigation:75`) **vertes rejouees seules** ; branche sans aucun fichier frontend. **ZERO REGRESSION.** Prete pour le push. Aucune issue fermee (#378 appartient a la 25-1c). # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5). Gate backend **2330/2330** (4 skipped), base remise a zero et verifiee (68 migrations, 1 Admin) ; fmt + clippy verts. **+8 tests** (3 backfill, 3 alimentation, 2 import) + 1 assertion d'export ; **6 mutations sur 6 tuees par assertion**. Manuel admin corrige, PDF regeneres et verifies aplatis. `backup.rs`/`export.rs`/`import.rs` INTACTS. ⚠️ **E2E NON LANCEE — prerequis du push** (seule a voir un defaut P8). Prochaine etape : `bmad-code-review`. # 2026-09-15 **SPECIFIEE** — `25-1c-zero-audit-company-id.md`. Story-zero de la 25-1c, nee de l'arbitrage du 2026-09-11 (« la colonne et son backfill d'abord »). 18 AC en six volets, 10 taches. Colonne `BIGINT NULL` sans FK + index `(company_id, created_at)`, remplie par SOUS-SELECT dans `insert_in_tx` (aucun des 106 sites ne bouge — recompte de la passe 1 de validation ; l'epic disait 89). ⛔ **P7 : EXEMPTION PERISSABLE `PerishableSince(20260827000001)`**, precedent 24-5 — pas de rejeu, conformement a l'arbitrage ; fondement verifie par `git tag` (dernier tag v0.11.1, 2026-08-24). ⚠️ **Ecart releve avec l'epic** : apres restore, le point ouvert a DEUX sous-cas — identifiants differents (entrees montrees a personne) OU identiques (entrees d'une autre instance montrees a la societe restauree) ; l'epic ne nommait que le premier. Traite en CARACTERISATION ici, tranche en 25-1c. Aucune issue fermee. ✅ **ARBITRAGES RENDUS LE 2026-09-15 (Guy)** : (1) **AUCUNE RELEASE AVANT LE MERGE** de cette story — condition du fondement de l'exemption perissable ; (2) le partage mesure ici / tranche en 25-1c est valide. ✅ **BOUCLE DE VALIDATION CLOSE EN 4 PASSES** (2026-09-15) : max `MEDIUM → HIGH → HIGH → LOW`, soit `0C/0H/2M/1L → 0C/1H/4M/4L → 0C/1H/1M → 0C/0H/0M/1L` ; Sonnet+Haiku, Opus, Sonnet, Opus CIBLEE. ⚠️ Signal HIGH → HIGH de la regle de splitting SIGNALE au Project Lead : **pas de decoupage** (Guy, « continue »). ⛔ Le HIGH de la passe 2 : `admin-manual.tex:1786` affirme « il n'y a pas de colonne company_id » — l'AC 18 disait « aucun site », sur un grep aveugle au `\_` de LaTeX ; le manuel est desormais dans le perimetre (PDF a regenerer). Montage de l'AC 8 EXECUTE sur base jetable en passe 4. Prochaine etape : `bmad-dev-story`. 25-1c-a-journal-audit-route: done # 2026-09-19 **MERGEE — PR #439, squash `c7de37c4`**, trois checks verts ; #378 reste OUVERTE pour l'ecran (25-1c-b). 2026-09-16 **REVUE DE CODE : 7 PASSES · GATE DE CLOTURE EN COURS** — gate RELANCE EN ENTIER (la garde a ete reecrite 5 fois depuis T11, la cellule d'export a change, 4 documents et un PDF avec) : base remise a zero ET VERIFIEE (40 tables, 1 Admin), **backend 2372/2372** (fmt + clippy compris, 100,8 s), **frontend 0 erreur / 740 tests / build ✔**. ✅ **E2E 215 passed / 8 failed / 19 skipped (8,7 min)**, `kesh_e2e` reconstruite, montage COMPLET (avec `KESH_INBOX_DIR` et `KESH_DOCUMENTS_DIR` cette fois). Les 8 QUALIFIES UN PAR UN : **7 KF-029** + **`sidebar-navigation:75` = KF-046**, confirmee par REJEU ISOLE (elle echoue seule, donc pas de la pollution). KF-045 absente (run a 16:53 UTC) et `inbox-import:106` absent — la preuve que l'echec de l'apres-midi venait de MON montage. ⇒ **ZERO REGRESSION, zero pollution.** ⚠️ **REVUE CLOSE en 7 passes** (P8 non consommee, sur arbitrage du Project Lead : « clore la boucle »). Doc mise a jour : `user-manual.tex` (la consultation « ne peut pas encore etre produite » etait devenue faux — l'export CSV la produit), README (ligne v0.12.1), CHANGELOG (section `[Unreleased]` creee), `api-external.md`, manuel admin, glossaire. PDF regeneres et controles aplatis. **PRETE POUR LE PUSH** — rien n'est pousse, #378 reste OUVERTE (le `closes` appartient a la PR de l'ecran). — P7 CIBLEE (Sonnet) : **1 MEDIUM, zero CRITICAL ni HIGH** — une premiere depuis P1. Le MEDIUM : la distinction lifetime/litteral introduite a P6 n'etait gardee par RIEN (prouve par mutation EXECUTEE, invisible aux 10 tests comme aux 218 fichiers reels) ; nuance relevee par la lentille elle-meme : mon commit l'annoncait comme lacune assumee. ⛔ Ma PREMIERE tentative de correctif ne gardait rien non plus — l'epreuve l'a montre : sur `impl<'a> Chose<'a> {` le saut « large » va d'une apostrophe a l'autre SANS enjamber d'accolade, donc meme compte. ⇒ la propriete se teste DIRECTEMENT sur `accolades_hors_chaines`, avec le cas discriminant `impl<'a> Foo { fn g<'b>() {} }` = (2,2). EPROUVE : rouge. ⚠️ Lecon : tester une fonction par le detour de son appelant laisse passer ce que l'appelant ne distingue pas. **CLOTURE PROPOSEE au Project Lead** : P8 disponible mais rendement nul — le MEDIUM etait annonce par moi-meme, il est remedie et eprouve, la remediation ne touche AUCUNE ligne de production depuis P3, et le code de production n'est plus en cause depuis P2. # Avant : **6 PASSES** — P6 CIBLEE (Opus) : 3 HIGH, 2M, 1L, **tous sur ma garde, aucun sur le code de production**. ⛔ Ma garde symetrique etait VERTE PAR VACUITE : sur 99 fichiers portant `#[cfg(test)]`, **95** n'ont aucun item reconnu en colonne 0 apres l'attribut, et retirer le correctif qu'elle surveillait ne changeait RIEN sur le depot (0 fichier sur 218). ⛔ La VISIBILITE decidait de la protection : `pub(crate) fn` (28), `pub mod` (202), `pub use` (85) non reconnus. ⛔ Mon doc-comment affirmait « verifie » et disait FAUX : `loader.rs:386` porte `contains('{')` DANS son bloc de test, masque par ACCIDENT. ⇒ **SIMPLIFICATION** (recommandation de la lentille, annoncee au Project Lead a P5) : le balayage du depot est remplace par **4 entrees SYNTHETIQUES en dur**, decidables et independantes du depot. EPROUVEES : 2 mutations, 2 rouges, chacune nommant le cas. ⚠️ Verdict : « elargir l'enumeration ne ferait que deplacer la frontiere ; cout reel de deux passes, rendement mesure nul ». **P7 de confirmation, puis CLOTURE** — la remediation ne touche aucune ligne de production depuis P3. # Avant : **5 PASSES** — P5 CIBLEE (Sonnet, prompt VERSIONNE) : **1 CRITICAL** (l'angle que je lui avais donne, confirme par mutation compilee), 1M, 2L. ⛔ Mon appariement d'accolades ne sortait que si une accolade AVAIT ETE OUVERTE : un item garde sans bloc (`use …;`, `mod fixtures;`, `const …;`) faisait courir le saut jusqu'a la prochaine accolade du fichier — ou jusqu'a sa fin. Plus profond que la troncature que P4 avait remplacee ; dormant, mais `#[cfg(test)] mod fixtures;` est un idiome courant. ⛔ Et mon garde-fou restait VERT PAR EXCES : il ne verifiait que l'absence d'attribut residuel, or si le masquage avale tout il n'en reste aucun. ⇒ seconde sortie de boucle + garde SYMETRIQUE (le masquage ne retire aucun item de production). EPROUVE DANS LES DEUX SENS, une premiere dans cette boucle : correctif en place ⇒ l'action neuve est vue ; correctif retire ⇒ la garde nomme l'item perdu. ⛔ MEDIUM : SIXIEME decompte faux, dans le test cense fermer cette classe. ⚠️ **ARBITRAGE DE CONDUITE PORTE AU PROJECT LEAD** : severite non decroissante (HIGH en P4, CRITICAL en P5), production plus prise en defaut depuis P2, cinq passes sur UN SEUL fichier de test — ce detecteur vaut-il ce qu'il coute ? # Avant : **4 PASSES** — P4 CIBLEE (Opus, prompt VERSIONNE) : **1 HIGH**, 2M, 3L. ⛔ Ma coupe `#[cfg(test)]` fermait UNE variante du defaut et laissait l'autre : elle s'arretait au premier attribut QUEL QUE SOIT l'item garde. Or un attribut sur une METHODE (`invoice_email.rs:901`) ou un `mod tests` AU MILIEU (`version.rs:97`, `entities/user.rs:178`) sont ordinaires en Rust ⇒ **686, 256 et 75 lignes de PRODUCTION** hors balayage, dont `send_reminder_batch`, `check_downgrade_protection` (le garde-fou P2-bis) et `UserUpdate`. ⚠️ Et `invoice_email.rs` ECRIT DEJA de l'audit : la zone perdue appelait cette ecriture. ⛔ Le defaut n'etait pas la coupe seule mais MON DOC-COMMENT, qui declarait la classe fermee. ⇒ blocs desormais MASQUES par appariement d'accolades HORS CHAINES (ce que l'AC 18 demandait des l'origine). EPROUVE dans les DEUX variantes (2 mutations, 2 rouges) + controle que l'accolade d'un litteral ne casse rien. ⛔ MEDIUM : « six » corrige a UN site et laisse a DEUX AUTRES du meme fichier — regle de propagation enfreinte dans le commit qui corrigeait ce defaut ⇒ nombres doubles d'une ASSERTION. Garde 9/9. ⚠️ **P5 requise** (severite > LOW) ; la remediation ne touche toujours AUCUNE ligne de production. # Avant : **3 PASSES** — P3 CIBLEE (Sonnet, prompt VERSIONNE `25-1c-a-review-prompt-p3-ciblee.md`, sur le seul commit `9b47e3b8`) : **2 CRITICAL demontres par REPRODUCTION**, 1M, 2L — et les deux viennent de MON patch de P2. (1) le tamis appariait les guillemets sans suivre les ECHAPPEMENTS : un `\"` impair desynchronisait tout le reste du fichier, l'ensemble devenait VIDE, l'assertion ne s'executait plus et le trou se rouvrait, garde VERTE (reproduit : 7 litteraux → 0) ; (2) la coupe `#[cfg(test)]` portait sur la source BRUTE : `config.rs:406` porte cette chaine dans un DOC-COMMENT, 83% du fichier sortait du balayage — defaut DORMANT active par mon elargissement de P2. ⇒ PROPAGATION faite dans le meme patch (`relever()` portait le meme defaut sur TOUTES les crates) : une seule fonction `source_assainie`, coupe sur LIGNE ENTIERE. Les deux correctifs EPROUVES (2 mutations, 2 rouges). ⚠️ La 2e epreuve a d'abord rendu un FAUX NEGATIF : sonde posee en fin de fichier, donc dans les 93 lignes coupees — precaution prise pour config.rs et oubliee ici. ⚠️ **P4 requise** (severite > LOW) ; mais la remediation de P3 ne touche AUCUNE ligne de production, ce qui est le critere de cloture. # Avant : **2 PASSES** — P1 (Sonnet/Haiku/Opus, 3 lentilles) : 2H/5M/6L ; P2 (Opus/Sonnet/Haiku, braquee sur la REMEDIATION `8599035c..HEAD`) : 2H/4M/2L. ⛔ **Le HIGH de P2 porte sur MON correctif de P1** : il fermait le RENOMMAGE d'un code et laissait ouvert son AJOUT — une branche neuve passait les 7 tests, et la production aurait ecrit un code SANS LIBELLE. Le defaut de P1 deplace d'un cran, motif mesure du depot. ⇒ controle rendu BILATERAL, **eprouve par 3 mutations** (branche neuve, derivation dans un AUTRE fichier de route, code laisse en commentaire) : 3 rouges sur assertion. ⚠️ **Signal `HIGH → HIGH` SIGNALE au Project Lead** (regle de splitting) — mais les deux HIGH portent sur la remediation, non sur la conception. ⚠️ **P3 requise** (severite > LOW). ⚠️ **Deux arbitrages en attente** : `user-manual.tex:498-503` (« pas encore PRODUIRE » — l'export CSV le fait) et « on termine » = 25-1c-a seule ou #378 entiere. # **IMPLEMENTEE** (`bmad-dev-story`, Opus 5) — les ONZE taches cochees, RIEN DE POUSSE. ⛔ Perimetre FIGE `main..8599035c` (cloture de l'implementation, avant la revue) : **25 commits, 29 fichiers, +5217/-41**. Decomptes recomptes depuis la source, perimetre `main..HEAD` : **+42 tests** (7 depot, 4 `util`, 5 module, **11 garde**, 15 E2E — recompte apres la passe 1 de revue, qui ajoute 3 tests a la garde ; la fiche a annonce « +35 » tant qu'elle en portait 4), **133 cles** par locale x 4, etat **DATE a la cloture de P5** (une mesure « actuelle » se perime a chaque commit — celle-ci l'avait deja fait deux fois) : 37 commits, 34 fichiers, +6218/-44. ⚠️ « 24 commits » et « +5206 » etaient FAUX des l'ecriture (25 et +5217) — releve par la passe 2. Gates : backend **2365/2365** (fmt + clippy compris), frontend **740/740** + build, base remise a zero ET verifiee (40 tables, 1 Admin). ✅ **E2E 214 passed / 9 failed / 19 skipped (8,3 min)** — les neuf qualifies UN PAR UN : 7 KF-029, `sidebar-navigation:75` = pollution (PASSE rejoue seul, donc pas la KF-046), et `inbox-import:106` = **defaut de MON montage** (KESH_INBOX_DIR/KESH_DOCUMENTS_DIR manquants ; diagnostic VERIFIE au log, spec rejouee ⇒ passe en 1,5 s). KF-045 absente, et c'est correct : run a 14:34 UTC. ⇒ **ZERO REGRESSION.** Epreuve par mutation : **9 mutations, 9 rouges SUR ASSERTION** (aucune sur compilation), fichiers restaures et identite verifiee. ⚠️ **Trois points laisses au Project Lead** : ecart garde/spec sur le decoupage de l'inventaire des sites indirects (six de part et d'autre, mais pas les memes six) ; libelle de `reconciliation.split_applied` (« Ventilation appliquee » arbitre vs « Eclater » au catalogue) ; emojis du manuel qui ne s'impriment pas (anterieur a la story). ⚠️ **Question en suspens : « on termine » = 25-1c-a seule, ou #378 entiere avec l'ecran ?** Prochaine etape : `bmad-code-review`. # Avant : **EN COURS** — PR #437 fusionnee (squash 842eaac0), pile rebasee, base de dev remise a zero et verifiee (40 tables, 1 Admin), revalidation close en 4 passes (1H/3M/3L -> 2M -> 1M/3L -> 1L), 122 libelles francais arbitres. Avant : **VALIDEE — boucle close en 5 passes** : `1C/1M/2L → 5M/8L → 1M/1L → 2M/2L (ciblee) → 1L (ciblee)` ; Sonnet+Haiku, Opus, Sonnet, Opus, Sonnet. ⛔ Stagnation MEDIUM sur 3 passes, toujours sur les bornes de date : la forme « created_at < date_to + 1 jour » a ete ABANDONNEE pour des bornes inclusives 23:59:59.999 (eprouvees sur base jetable). **Ne pas implementer avant le merge de #437** (rebase par SHA `42b6aac0`). **SPECIFIEE** — `25-1c-a-journal-audit-route.md`, 22 AC en six volets, 11 taches. `GET /api/v1/audit-log` + `/export.csv` dans `comptable_routes`, `ensure_not_pat` en handler, DTO dedie (`actorType` via `as_str`, `createdAt` en UTC explicite), `csv_sanitize` EXTRAITE d'`invoices.rs`, export audite `audit_log.exported`. ⚠️ **Trois** choix de conception laisses a confirmer en revue : inclusion des entrees sans societe, audit de l'export, codes bruts (non traduits) dans les cellules du CSV. Branche empilee sur la 25-1c-zero, a rebaser apres le merge de #437. [#378] — **issue du SPLIT de la 25-1c** (arbitrage de Guy, regle de splitting : 7 modules). La ROUTE de consultation du journal d'audit et son export CSV, cote backend. Acces **Comptable + Admin**, ni Consultation ni cle API ; filtre **strict par `company_id`** (l'import restaure la MEME installation) ; entrees sans societe montrees « societe indeterminee » (choix par defaut, non arbitre explicitement). Cf. `epic-25-vague1-suite.md` § *Arbitrages du 2026-09-15*. 25-1c-b1-journal-audit-ecran: review # 2026-09-26 **REVUE CLOSE EN 2 PASSES** (2 HIGH / 4 MED -> 0 > LOW), #469 ouverte ; reste `review` jusqu'aux gates rejoues avec la b2. **IMPLEMENTEE** — 17/17 mutations tuees ; gates b1 seule 812/812, 2463/2463, E2E 223/8/19 sans regression ; prochaine etape : code review, puis la b2. # 2026-09-26 **R5 DERIVE : 1 MEDIUM / 3 LOW corriges** (97 actions, 138 cles ; lignes des gardes i18n) ; contrat de la route mergee confirme. Prochaine etape : dev. # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en natifs sur le vocabulaire, plus de carte de libelles ni de colonne Societe. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : MED en passes 3 à 6, tous introduits par la remédiation précédente, puis 0 après le retrait d'une assertion E2E trois fois corrigée. **issue du SPLIT de la 25-1c-b** (arbitrage de Guy, signal MEDIUM → MEDIUM en passe 2 de validation). L'ECRAN : feature `audit-log`, page, garde Comptable+, menu, carte des **29** types d'entite (dont `audit_log`, cree par la 25-1c-a), i18n et gardes, tests unitaires et E2E. Module partage `download.ts` neuf ; le regroupement des 7 copies existantes part en issue de dette. Livree dans la MEME PR que la b2 ; porte `closes #378`. [#378] - 25-1c-b2-journal-audit-textes: in-progress # 2026-09-26 **FICHE REECRITE ET VALIDEE** (R5 -> T0 -> R6 1 HIGH/4 MED -> R7 0) ; dev en cours. # 2026-09-26 ⛔ **R5 DERIVE : 1 CRITICAL / 4 HIGH / 4 MEDIUM** — fiche a REECRIRE en T0 contre le texte actuel, APRES la b1, puis revalider (la facture validee se devalide, elle ne se supprime plus ; plusieurs textes deja reecrits par 25-1c-a, 25-2-b, 25-5-a). # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] + 25-1c-b1-journal-audit-ecran: done # 2026-09-26 gates b1+b2 verts (817, 2463, E2E 224/7/19). # 2026-09-26 **REVUE CLOSE EN 2 PASSES** (2 HIGH / 4 MED -> 0 > LOW), #469 ouverte ; reste `review` jusqu'aux gates rejoues avec la b2. **IMPLEMENTEE** — 17/17 mutations tuees ; gates b1 seule 812/812, 2463/2463, E2E 223/8/19 sans regression ; prochaine etape : code review, puis la b2. # 2026-09-26 **R5 DERIVE : 1 MEDIUM / 3 LOW corriges** (97 actions, 138 cles ; lignes des gardes i18n) ; contrat de la route mergee confirme. Prochaine etape : dev. # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes** (R1 2M/8L -> R2 3M -> R3 2M/5L -> R4 0), preuves executees sur mutations ; i18nLocale() a ajouter au store i18n. Avant : **REOUVERTE, revalidation requise** : l'ecran affiche les libelles de la route, filtres type/action en natifs sur le vocabulaire, plus de carte de libelles ni de colonne Societe. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : MED en passes 3 à 6, tous introduits par la remédiation précédente, puis 0 après le retrait d'une assertion E2E trois fois corrigée. **issue du SPLIT de la 25-1c-b** (arbitrage de Guy, signal MEDIUM → MEDIUM en passe 2 de validation). L'ECRAN : feature `audit-log`, page, garde Comptable+, menu, carte des **29** types d'entite (dont `audit_log`, cree par la 25-1c-a), i18n et gardes, tests unitaires et E2E. Module partage `download.ts` neuf ; le regroupement des 7 copies existantes part en issue de dette. Livree dans la MEME PR que la b2 ; porte `closes #378`. [#378] - 25-1c-b2-journal-audit-textes: review # 2026-09-26 **IMPLEMENTEE** — gates b1+b2 verts ; prochaine etape : code review. # 2026-09-26 **FICHE REECRITE ET VALIDEE** (R5 -> T0 -> R6 1 HIGH/4 MED -> R7 0) ; dev en cours. # 2026-09-26 ⛔ **R5 DERIVE : 1 CRITICAL / 4 HIGH / 4 MEDIUM** — fiche a REECRIRE en T0 contre le texte actuel, APRES la b1, puis revalider (la facture validee se devalide, elle ne se supprime plus ; plusieurs textes deja reecrits par 25-1c-a, 25-2-b, 25-5-a). # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] + 25-1c-b2-journal-audit-textes: done # 2026-09-26 **REVUE CLOSE EN 2 PASSES** (1 HIGH / 1 MED -> 0 > LOW) ; PR commune b1+b2 a ouvrir (closes #378). # 2026-09-26 **IMPLEMENTEE** — gates b1+b2 verts ; prochaine etape : code review. # 2026-09-26 **FICHE REECRITE ET VALIDEE** (R5 -> T0 -> R6 1 HIGH/4 MED -> R7 0) ; dev en cours. # 2026-09-26 ⛔ **R5 DERIVE : 1 CRITICAL / 4 HIGH / 4 MEDIUM** — fiche a REECRIRE en T0 contre le texte actuel, APRES la b1, puis revalider (la facture validee se devalide, elle ne se supprime plus ; plusieurs textes deja reecrits par 25-1c-a, 25-2-b, 25-5-a). # 2026-09-15 nuit **REVALIDATION CLOSE en 4 passes ciblees** (R1 2M/6L -> R2 2M/3L -> R3 1M/3L -> R4 2L), tous les MEDIUM venant de la remediation precedente. Avant : **REOUVERTE, revalidation ciblee requise** : section Tracabilite (filtres en listes, langue, plus de societe indeterminee), export non trace, :1796 et :1778 corriges dans la story. Avant : **VALIDATION CLOSE en 7 passes** (dont 2 sur la parente) : 1 HIGH en passe 3 (inventaire faux à total juste), puis MED décroissants, puis 0 ; 17 sites de vocabulaire. ⏸ Deux défauts voisins du manuel d'administration (:1796 « sans exception », :1778 « 5 rôles ») en attente d'arbitrage. **issue du SPLIT de la 25-1c-b**. LES TEXTES : manuel utilisateur (encadre 497-510, section tracabilite, glossaire 1739), manuel d'administration, README, glossaire i18n « journal d'audit » en partie A et alignement des cles et du manuel qui disent « piste d'audit ». Livree avec la b1. [#378] 25-1c-b-journal-audit-ecran: split # 2026-09-15 **DECOUPEE en 25-1c-b1 (ecran) + 25-1c-b2 (textes)** — arbitrage de Guy apres la passe 2 de validation. Fiche conservee comme reference (passes 1-2). **SPECIFIEE** — `25-1c-b-journal-audit-ecran.md`, 21 AC en six volets, 11 taches, PAS ENCORE VALIDEE. Feature `audit-log`, carte des **28 types d'entite** recomptes depuis la source (29 litteraux dont l'action `admin_break_glass_reset`), garde Comptable+, menu `comptableOnly`, `triggerDownload` extrait (4 copies), glossaire « journal d'audit » en partie A avec alignement de 2 cles divergentes. ⚠️ Defaut PREEXISTANT constate : `user-manual.tex:502-503` nie le verrou de periode livre par la 24-4c. Ne pas implementer avant le merge de la 25-1c-a. [#378] — **issue du SPLIT de la 25-1c**. L'ECRAN : page, entree de menu, libelles des 28 types d'entite dans les 4 langues, E2E, manuels (qui promettent tous que la consultation « manque encore »). Affichage sobre : code d'action brut, detail en JSON indente. Vocabulaire fige : « journal d'audit » / Audit-Protokoll / registro di audit / audit log. **Portera le `closes #378`.** 25-1c-piste-consultable: split # 2026-09-15 **DECOUPEE en 25-1c-a (route) + 25-1c-b (ecran)** — arbitrage de Guy. [#378] — route + ecran de consultation. ✅ **ARBITRAGE RENDU LE 2026-09-11 (Guy) : AJOUTER LA COLONNE `company_id`** — ni restreindre la route, ni documenter la limite. ⚠️ **C'est une migration AVEC BACKFILL** ⇒ les garde-fous **P2-bis, P3, P5, P6, P7 et P8** s'arment tous, alors que la 25-1b n'en arme aucun. ✅ **MECANISME ARRETE LE 2026-09-11 apres lecture ciblee : LE SOUS-SELECT**, patron exact d'`actor_label`. Colonne **`BIGINT NULL`, SANS FK**. ⇒ **Aucun des 106 sites d'ecriture ne bouge** ⇒ **L'ORDRE REDEVIENT INDIFFERENT** : la 25-1b n'attend pas la colonne. **Verifie et non suppose** : `users.company_id` est NOT NULL, ecrit une fois, jamais modifie, aucune table de jonction ; **AUCUNE route n'ecrit un audit sur une entite d'une AUTRE societe** (aucun handler ne reçoit de `company_id` de la requete, tout ecart devient 404) ; et le seul site ou l'acteur n'est pas `current_user` — `admin.full_import`, qui prend le plus petit Admin du JEU RESTAURE — est celui ou le sous-SELECT est **PLUS JUSTE** qu'un champ, lequel propagerait l'id d'une societe que le restore vient de detruire. `routes/exports.rs:137-139` l'assumait deja par ecrit. ⚠️ **L'ESTIMATION DE COUT QUI FONDAIT L'HESITATION ETAIT FAUSSE D'UN FACTEUR 2,5** : « ~30 sites » annonces, **106** reels sur 38 fichiers *(recompte le 2026-09-15, passe 1 de validation de la 25-1c-zero : le « 89 sur 32 » d'abord ecrit ici etait lui-meme faux, et le facteur est donc de 3,5 et non de 2,5)*, dont ~40 dans des repositories qui ne reçoivent meme pas de `company_id`. ⛔ **TROIS CONTRAINTES, chacune adossee a un mode d'echec du depot** : (1) **NULL et jamais NOT NULL sans defaut** — `check_schema_compat` (`import.rs:195-236`) rendrait INIMPORTABLES TOUTES les sauvegardes existantes, c'est pourquoi `actor_label` est `NOT NULL DEFAULT ''` ; (2) **aucune FK vers `companies`** — `companies` EST remplacee au restore (`backup.rs:76`) alors que les entrees locales sont CONSERVEES depuis la 25-1a : c'est litteralement le scenario qui a fait retirer `fk_audit_log_user` ; (3) **ni COALESCE ni rejeu post-restore** — `NULL` est un etat LEGITIME ET PERMANENT (« societe indeterminable »), pas un trou a combler ; la garde d'`actor_label` reposait sur une sentinelle TEXTUELLE, `NULL` n'en est pas une. ⛔ **POINT OUVERT, A TRAITER DANS CETTE STORY-CI** : apres un restore, les entrees locales CONSERVEES portent le `company_id` d'une societe DETRUITE — une consultation scopee ne les montrerait A PERSONNE. *La 25-1a s'est battue pour que ces entrees survivent a l'import ; l'ecran qui les rend lisibles ne doit pas etre celui qui les cache.* ⇒ **A DECOUPER** : la colonne et son backfill d'un cote, la route et l'ecran de l'autre. *(Motif de l'arbitrage : `audit_log` n'a **pas de `company_id`** — verifie sur `20260413000001_audit_log.sql` —, la table est GLOBALE alors que Kesh est multi-societe, et une route de consultation exposerait les traces de TOUTES les societes a l'admin d'une seule.)* ⚠️ Le repository n'a que `insert_in_tx` et `find_by_entity` — une methode filtrable/paginee est a ecrire (S5). ⚠️ Le manuel annonce cette page « prevue pour une version ulterieure » (`user-manual.tex:1597-1603`) : c'est ICI qu'elle se livre, pas en 25-1a. ⛔ **NE PAS LA LIVRER AVANT LA 25-1a** : un ecran qui donne a voir une trace qu'un `reset` dement serait une promesse de plus. 25-2-a-retypage-compte-mouvemente: done # 2026-09-19 **PR #441** — revue close en 2 passes, gates backend 2348/2348, frontend 745/745, E2E 216/7 (KF-029) ; rebasee par merge sur `main` apres #439. 2026-09-16 **SPECIFIEE** — [#382] + [#274], fusionnees : MEME site (`accounts.rs:415-417`, l'`UPDATE` qui ecrit `account_type` sans condition), MEME correctif ; #382 en donne la lecture comptable, #274 le mecanisme et les trois options. ✅ **ARBITRAGE RENDU LE 2026-09-16 (Guy) : AVERTISSEMENT BLOQUANT** — ni refus sec (un compte mal type le resterait a vie, rien ne permet de deplacer ses ecritures), ni reclassement silencieux. 409 `ACCOUNT_HAS_ENTRIES` qui NOMME l'ampleur (ecritures distinctes + exercices clos touches), leve par `confirmAccountRetype: true` sur le motif `confirm_*` de `bank_imports.rs:279-292` — le SEUL motif de confirmation du depot, a ne pas doubler. Code d'audit DISTINCT `account.retyped`, donc inscription au registre `audit_labels.rs` + 4 locales, faute de quoi le journal affiche le code brut. ⛔ **AUCUNE MIGRATION** : la garde est en lecture, P5/P6/P7/P8 sans objet — une migration au diff signalerait une derive. 4 modules touches, sous le seuil de split.