feat: garde-fous d'écriture des classeurs Excel #153 (P0)
L'édition d'un .xlsx pouvait détruire une partie du classeur, le concurrencer en silence, ou diffuser une injection de formule. - BUG-085 : inspect_workbook() détecte ce qu'un round-trip openpyxl perd (valeurs calculées en cache, slicers, contrôles, connexions, custom XML, signature, commentaires enrichis, macros) → xlsx_lossy_features exposé en lecture, bandeau FR/EN, et 409 xlsx_lossy_content sans `force` (confirmation explicite puis reprise). Périmètre réel revalidé : graphiques, images et TCD survivent au round-trip. - BUG-086 : écriture atomique (fichier .tmp + os.replace) : un plantage ne peut plus tronquer le classeur, le backup reste intact. - BUG-087 : verrou par fichier autour du read-modify-write (timeout 15 s, 409 conflict) ; endpoint xlsx/save devenu synchrone pour que l'attente s'exécute dans le threadpool. - BUG-088 : une saisie en '=' ou '@' est stockée en texte, sauf opt-in `allow_formula` ou le bouton f(x) de la visionneuse. Le handler ServiceError expose désormais code + details, que api() propage. - BUG-084 : la suppression d'une vault purge enfin l'index inversé (documents fantômes qui continuaient de matcher) et is_stale() devient is_ready(), le nom étant trompeur (la staleness n'existe plus). Tests : 1390 pytest, 10 JSDOM (xlsx-viewer.test.mjs, branché au CI), 3 E2E Playwright, suite E2E complète verte, ruff/mypy 0. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <[email protected]>
This commit is contained in:
+113
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||
> [Unreleased](#unreleased). La dernière version livrée est **2.28.16**.
|
||||
> [Unreleased](#unreleased). La dernière version livrée est **2.29.0**.
|
||||
|
||||
---
|
||||
|
||||
@@ -14,6 +14,118 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [2.29.0] — 2026-09-27
|
||||
|
||||
### Correction
|
||||
|
||||
- **BUG-084 — l'index inversé conservait des documents fantômes après la
|
||||
suppression d'une vault.** `remove_vault_from_index()`
|
||||
(`backend/indexer.py`) ne notifiait pas le hook incrémental : après
|
||||
suppression d'une vault, ses documents restaient dans l'index inversé
|
||||
(`postings`, `doc_info`, `doc_vault`, `vault_docs`) et continuaient de
|
||||
correspondre aux recherches pour une vault inexistante — seul un reindex
|
||||
manuel les effaçait. Le correctif déclenche
|
||||
`_on_index_change('remove', …)` pour chaque fichier de la vault, et
|
||||
`_remove_doc_internals()` supprime désormais la clé `vault_docs` dont le set
|
||||
devient vide (c'est un `defaultdict` : une lecture la recréait).
|
||||
Test : `TestVaultRemovalPurgesInvertedIndex` (contre-preuve : échoue sans le
|
||||
correctif).
|
||||
|
||||
### Maintenance
|
||||
|
||||
- **Index inversé — `is_stale()` renommé `is_ready()`.** La relecture de
|
||||
`plan.md` a établi que les étapes 6 et 7 (suppression du cooldown et du hack
|
||||
de coalescence) étaient **déjà livrées** : ni `_last_rebuild`, ni
|
||||
`_rebuild_cooldown`, ni `_source_generation`, ni `_on_vault_change` ne
|
||||
subsistent. `is_stale()` ne mesurait donc plus aucune staleness — il
|
||||
indiquait seulement si l'index initial était construit, sous un nom
|
||||
trompeur. Renommé `is_ready()`, cohérent avec le `is_ready()` de
|
||||
`SemanticIndex` ; l'alias `is_stale()` de `SemanticIndex`, sans appelant, est
|
||||
supprimé. `/api/diagnostics` expose désormais `is_ready` (libellé « Index
|
||||
prêt » côté `frontend/js/config.js`). Tests :
|
||||
`test_is_ready_tracks_initial_build`, `test_is_ready_survives_incremental_updates`.
|
||||
|
||||
- **`plan.md` recalibré.** Le fichier est désormais marqué « livré » et
|
||||
suivi d'une section « État réel » : le code a divergé du plan sur quatre
|
||||
points (pas de repli `_needs_rebuild`, `_ready` au lieu de `doc_count == 0`,
|
||||
`rebuild()` conservé au démarrage, `is_stale()` repurposé). Les extraits de
|
||||
code du plan sont explicitement signalés comme ne décrivant pas le code
|
||||
actuel.
|
||||
|
||||
- **Fixtures de test locales exclues du suivi Git.** `test_dir/music/`,
|
||||
`test_dir/video/`, `test_vault/images/`, `test_vault/markdown/` et
|
||||
`test_vault/budget.xlsx` (~200 Mo) sont ajoutés au `.gitignore` : aucun test
|
||||
ni job CI ne les référence — les tests unitaires génèrent leurs fixtures dans
|
||||
`tmp_path` et l'E2E n'utilise que les fixtures committées
|
||||
(`test_vault/sample-*.{mp3,png,svg,webm,pdf}`, `test_dir/*.md`). Ils
|
||||
restaient non suivis et polluaient `git status`.
|
||||
|
||||
### Sécurité
|
||||
|
||||
- **BUG-088 — plus d'injection de formule via la visionneuse Excel.** Une
|
||||
saisie `=cmd|'/c calc'!A1` (ou `@…`) était stockée comme **formule** par
|
||||
openpyxl, donc exécutée par Excel à la réouverture du fichier (DDE).
|
||||
`edit_xlsx_cells` force maintenant le type texte (`cell.data_type = "s"`)
|
||||
pour toute valeur commençant par `=` ou `@` ; l'API accepte
|
||||
`allow_formula: true` et la visionneuse expose un bouton `f(x)`
|
||||
(opt-in, état de session, jamais persisté). `+`/`-` restent des nombres.
|
||||
- **BUG-087 — écriture concurrente d'un classeur.** `load_workbook()` →
|
||||
`save()` n'était pas sérialisé : deux sauvegardes simultanées (deux
|
||||
onglets, l'agent IA et la visionneuse) faisaient gagner la dernière, en
|
||||
silence. Verrou par chemin (`backend/services/mutations.py::_xlsx_write_lock`,
|
||||
timeout 15 s) autour du cycle lecture → édition → remplacement ; attente
|
||||
dépassée → **409** `conflict`. L'endpoint `PUT …/xlsx/save` est devenu
|
||||
synchrone pour que l'attente s'exécute dans le threadpool.
|
||||
|
||||
### Corrigé
|
||||
|
||||
- **BUG-085 — la perte de données à l'enregistrement d'un `.xlsx` est
|
||||
annoncée, plus silencieuse.** `GET /api/file/{vault}` renvoie
|
||||
`xlsx_lossy_features` (éléments qu'un round-trip openpyxl perd) ; la
|
||||
visionneuse affiche un bandeau listant ces éléments et la première
|
||||
sauvegarde demande confirmation avant de renvoyer `force: true`. Sans
|
||||
`force`, l'API répond **409** `xlsx_lossy_content` avec
|
||||
`details.features`. Périmètre **remesuré** sur openpyxl 3.1.5 : graphiques,
|
||||
images, dessins et tableaux croisés sont bien préservés ; sont perdus les
|
||||
valeurs calculées en cache, slicers/chronologies, contrôles de formulaire,
|
||||
connexions/requêtes, custom XML, signature numérique, commentaires
|
||||
enrichis et macros.
|
||||
- **BUG-086 — écriture atomique des classeurs.** `wb.save()` écrivait en
|
||||
place sur le fichier du vault : un plantage laissait un `.xlsx` tronqué.
|
||||
L'écriture passe désormais par un `.tmp` puis `os.replace()` (le backup
|
||||
`.bak` est inchangé, le `.tmp` est ignoré par le watcher).
|
||||
- Le handler global `ServiceError` expose maintenant `code` et `details` dans
|
||||
la réponse JSON, et `api()` (frontend) les propage sur l'Error — nécessaire
|
||||
pour que le client distingue un 409 de confirmation d'une autre erreur.
|
||||
|
||||
### Ajouté
|
||||
|
||||
- **#153 (P0) — tests de la visionneuse Excel.**
|
||||
`tests/frontend/xlsx-viewer.test.mjs` (10 tests JSDOM : bannière,
|
||||
confirmation + reprise `force`, refus, toggle `f(x)`, payload de
|
||||
sauvegarde) et `tests/e2e/xlsx-viewer.spec.js` (3 tests Playwright sur la
|
||||
fixture `test_vault/sample-xlsx-lossy.xlsx`) ; la suite JSDOM est branchée
|
||||
dans le CI.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **#153 — Visionneuse & édition XLSX : audit complet et backlog de
|
||||
complétude.** La visionneuse `.xlsx` livrée par #152 a été auditée couche
|
||||
par couche (lecture `backend/xlsx_reader.py`, écriture
|
||||
`backend/services.mutations.edit_xlsx_cells`, UI `renderXlsxViewer`,
|
||||
indexation, outils IA, tests). Bilan : la grille de valeurs est éditée
|
||||
correctement (sécurité, backup, audit, échappement HTML), mais l'ensemble
|
||||
supporté est étroit, une partie du classeur est perdue à l'enregistrement,
|
||||
les tableurs sont **invisibles pour la recherche** et l'IA ne sait que les
|
||||
**créer**. Ouverture de l'item **#153** dans `docs/ROADMAP.md` (17
|
||||
sous-tâches suivies **A1 → A17** ; **P0 livré**, reste P1 recherche/IA/UX
|
||||
puis P2 étendu) et création de la fiche
|
||||
[docs/features/xlsx-viewer.md](docs/features/xlsx-viewer.md) : cartographie
|
||||
du code, limites par couche, tableau des risques R1-R5 et critères
|
||||
d'acceptation par sous-tâche.
|
||||
|
||||
---
|
||||
|
||||
## [2.28.16] — 2026-09-27
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user