From b6fdbbfe672c70de72f095ea9dca77be8719982d Mon Sep 17 00:00:00 2001 From: Axelle Abbadie Date: Sun, 17 May 2026 13:26:55 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20rename=20cascade=20=E2=80=94=20captu?= =?UTF-8?q?re=20TFile=20state=20before=20vault.rename,=20processFrontMatte?= =?UTF-8?q?r=20for=20audio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renameFileAndRelated: capture oldFilePath before vault.rename (TFile mutated in-place) - cascadeRelatedRename: capture oldAudioName before rename (same mutation issue) - cascadeRelatedRename: update both store-level and rule-level scope.path in mapping.json - moveFileToClass: same store-level scope.path fix - All frontmatter updates via app.fileManager.processFrontMatter (not regex) - Audio strategy b: rename pseudobs-audio referenced file even when basename differs --- main.js | 71 +++++++++++++++++++++++++++++++------------ src/main.ts | 87 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/main.js b/main.js index 6130a2d..0123f10 100644 --- a/main.js +++ b/main.js @@ -36542,6 +36542,10 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { const raw = await this.app.vault.read(found); const data = JSON.parse(raw); let changed = false; + if (data.scope?.type === "file" && data.scope.path === file.path) { + data.scope.path = newMdPath; + changed = true; + } for (const rule of data.mappings ?? []) { if (rule.scope?.type === "file" && rule.scope.path === file.path) { rule.scope.path = newMdPath; @@ -36619,6 +36623,7 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { */ async renameFileAndRelated(file, newBasename) { const oldBase = file.basename; + const oldFilePath = file.path; const folder = file.parent?.path ?? ""; const newPath = folder ? `${folder}/${newBasename}.${file.extension}` : `${newBasename}.${file.extension}`; this._renamingRelated = true; @@ -36626,7 +36631,7 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { await this.app.vault.rename(file, newPath); const newFile = this.app.vault.getAbstractFileByPath(newPath); if (newFile instanceof import_obsidian14.TFile) { - await this.cascadeRelatedRename(oldBase, newBasename, newFile, file.path); + await this.cascadeRelatedRename(oldBase, newBasename, newFile, oldFilePath); } } finally { this._renamingRelated = false; @@ -36667,13 +36672,11 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { const fileFolder = file.parent?.path ?? ""; if (file.extension === "md") { try { - const content = await this.app.vault.read(file); - const updated = content.replace( - /^pseudobs-source:\s*"[^"]*"/m, - `pseudobs-source: "${newBase}.${file.extension}"` - ); - if (updated !== content) - await this.app.vault.modify(file, updated); + await this.app.fileManager.processFrontMatter(file, (fm) => { + if (fm["pseudobs-source"] !== void 0) { + fm["pseudobs-source"] = `${newBase}.${file.extension}`; + } + }); } catch { } } @@ -36686,6 +36689,9 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { try { const raw = await this.app.vault.read(found); const data = JSON.parse(raw); + if (data.scope?.type === "file" && data.scope.path === oldFilePath) { + data.scope.path = file.path; + } for (const rule of data.mappings ?? []) { if (rule.scope?.type === "file" && rule.scope.path === oldFilePath) { rule.scope.path = file.path; @@ -36710,10 +36716,11 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { const ef = this.app.vault.getAbstractFileByPath(newExportPath); if (ef instanceof import_obsidian14.TFile) { try { - const c = await this.app.vault.read(ef); - const u = c.replace(/^pseudobs-source:\s*"[^"]*"/m, `pseudobs-source: "${newBase}.${file.extension}"`); - if (u !== c) - await this.app.vault.modify(ef, u); + await this.app.fileManager.processFrontMatter(ef, (fm) => { + if (fm["pseudobs-source"] !== void 0) { + fm["pseudobs-source"] = `${newBase}.${file.extension}`; + } + }); } catch { } } @@ -36721,22 +36728,48 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin { } } const AUDIO_EXTS = ["m4a", "mp3", "wav", "ogg", "flac", "mp4", "aac", "aiff"]; + const audioRenamed = /* @__PURE__ */ new Map(); for (const ext of AUDIO_EXTS) { const audioPath = fileFolder ? `${fileFolder}/${oldBase}.${ext}` : `${oldBase}.${ext}`; const audioFile = this.app.vault.getAbstractFileByPath(audioPath); if (!(audioFile instanceof import_obsidian14.TFile)) continue; + const oldAudioName = audioFile.name; const newAudioName = `${newBase}.${ext}`; const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName; await this.app.vault.rename(audioFile, newAudioPath); - if (file.extension === "md") { - try { - const c = await this.app.vault.read(file); - const u = c.replace(/^pseudobs-audio:\s*"[^"]*"/m, `pseudobs-audio: "${newAudioName}"`); - if (u !== c) - await this.app.vault.modify(file, u); - } catch { + audioRenamed.set(oldAudioName, newAudioName); + } + if (file.extension === "md") { + try { + const mdContent = await this.app.vault.read(file); + const audioMatch = /^pseudobs-audio:\s*"([^"]+)"/m.exec(mdContent); + if (audioMatch) { + const oldAudioName = audioMatch[1]; + if (!audioRenamed.has(oldAudioName)) { + const oldAudioPath = fileFolder ? `${fileFolder}/${oldAudioName}` : oldAudioName; + const audioFile = this.app.vault.getAbstractFileByPath(oldAudioPath); + if (audioFile instanceof import_obsidian14.TFile) { + const ext = oldAudioName.split(".").pop() ?? ""; + const newAudioName = ext ? `${newBase}.${ext}` : newBase; + const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName; + await this.app.vault.rename(audioFile, newAudioPath); + audioRenamed.set(oldAudioName, newAudioName); + } + } } + } catch { + } + } + if (file.extension === "md" && audioRenamed.size > 0) { + try { + await this.app.fileManager.processFrontMatter(file, (fm) => { + const current = fm["pseudobs-audio"]; + if (current && audioRenamed.has(current)) { + fm["pseudobs-audio"] = audioRenamed.get(current); + } + }); + } catch { } } const SOURCE_EXTS = ["srt", "vtt", "cha", "chat", "html", "txt", "yml", "yaml"]; diff --git a/src/main.ts b/src/main.ts index cf06d52..066d7c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -470,12 +470,16 @@ export default class PseudObsPlugin extends Plugin { if (found) { const newPath = `${mappingTargetDir}/${base}${suffix}`; if (found.path !== newPath) { - // Mettre à jour le scope.path des règles fichier après déplacement + // Mettre à jour le scope.path après déplacement (store + règles individuelles) if (suffix === '.mapping.json') { try { const raw = await this.app.vault.read(found); const data = JSON.parse(raw); let changed = false; + if (data.scope?.type === 'file' && data.scope.path === file.path) { + data.scope.path = newMdPath; + changed = true; + } for (const rule of data.mappings ?? []) { if (rule.scope?.type === 'file' && rule.scope.path === file.path) { rule.scope.path = newMdPath; @@ -567,6 +571,7 @@ export default class PseudObsPlugin extends Plugin { */ async renameFileAndRelated(file: TFile, newBasename: string): Promise { const oldBase = file.basename; + const oldFilePath = file.path; // capturer AVANT le rename (file.path change après) const folder = file.parent?.path ?? ''; const newPath = folder ? `${folder}/${newBasename}.${file.extension}` : `${newBasename}.${file.extension}`; @@ -575,7 +580,7 @@ export default class PseudObsPlugin extends Plugin { await this.app.vault.rename(file, newPath); const newFile = this.app.vault.getAbstractFileByPath(newPath); if (newFile instanceof TFile) { - await this.cascadeRelatedRename(oldBase, newBasename, newFile, file.path); + await this.cascadeRelatedRename(oldBase, newBasename, newFile, oldFilePath); } } finally { this._renamingRelated = false; @@ -623,15 +628,14 @@ export default class PseudObsPlugin extends Plugin { const s = this.settings; const fileFolder = file.parent?.path ?? ''; - // 1. Mettre à jour le frontmatter pseudobs-source dans le fichier renommé + // 1. Mettre à jour pseudobs-source dans le frontmatter via l'API Obsidian if (file.extension === 'md') { try { - const content = await this.app.vault.read(file); - const updated = content.replace( - /^pseudobs-source:\s*"[^"]*"/m, - `pseudobs-source: "${newBase}.${file.extension}"` - ); - if (updated !== content) await this.app.vault.modify(file, updated); + await this.app.fileManager.processFrontMatter(file, (fm) => { + if (fm['pseudobs-source'] !== undefined) { + fm['pseudobs-source'] = `${newBase}.${file.extension}`; + } + }); } catch { /* ignore */ } } @@ -644,6 +648,11 @@ export default class PseudObsPlugin extends Plugin { try { const raw = await this.app.vault.read(found); const data = JSON.parse(raw); + // Scope de niveau store (chemin du fichier de transcription) + if (data.scope?.type === 'file' && data.scope.path === oldFilePath) { + data.scope.path = file.path; + } + // Scopes individuels des règles for (const rule of data.mappings ?? []) { if (rule.scope?.type === 'file' && rule.scope.path === oldFilePath) { rule.scope.path = file.path; @@ -667,32 +676,68 @@ export default class PseudObsPlugin extends Plugin { const ef = this.app.vault.getAbstractFileByPath(newExportPath); if (ef instanceof TFile) { try { - const c = await this.app.vault.read(ef); - const u = c.replace(/^pseudobs-source:\s*"[^"]*"/m, `pseudobs-source: "${newBase}.${file.extension}"`); - if (u !== c) await this.app.vault.modify(ef, u); + await this.app.fileManager.processFrontMatter(ef, (fm) => { + if (fm['pseudobs-source'] !== undefined) { + fm['pseudobs-source'] = `${newBase}.${file.extension}`; + } + }); } catch { /* ignore */ } } } } } - // 4. Fichiers audio avec le même basename dans le même dossier + // 4. Fichiers audio — deux stratégies : + // a) Même basename (entretien_01.m4a avec entretien_01.md) + // b) Référencé dans pseudobs-audio du frontmatter (audio noScribe à nom différent) const AUDIO_EXTS = ['m4a', 'mp3', 'wav', 'ogg', 'flac', 'mp4', 'aac', 'aiff']; + const audioRenamed = new Map(); // oldName → newName + + // Stratégie a : même basename for (const ext of AUDIO_EXTS) { const audioPath = fileFolder ? `${fileFolder}/${oldBase}.${ext}` : `${oldBase}.${ext}`; const audioFile = this.app.vault.getAbstractFileByPath(audioPath); if (!(audioFile instanceof TFile)) continue; + const oldAudioName = audioFile.name; // capturer AVANT le rename (TFile muté in-place) const newAudioName = `${newBase}.${ext}`; const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName; await this.app.vault.rename(audioFile, newAudioPath); - // Mettre à jour pseudobs-audio dans le frontmatter du .md - if (file.extension === 'md') { - try { - const c = await this.app.vault.read(file); - const u = c.replace(/^pseudobs-audio:\s*"[^"]*"/m, `pseudobs-audio: "${newAudioName}"`); - if (u !== c) await this.app.vault.modify(file, u); - } catch { /* ignore */ } - } + audioRenamed.set(oldAudioName, newAudioName); + } + + // Stratégie b : pseudobs-audio dans le frontmatter (basename différent) + if (file.extension === 'md') { + try { + const mdContent = await this.app.vault.read(file); + const audioMatch = /^pseudobs-audio:\s*"([^"]+)"/m.exec(mdContent); + if (audioMatch) { + const oldAudioName = audioMatch[1]; + if (!audioRenamed.has(oldAudioName)) { + // Fichier audio à renommer avec le même basename que la nouvelle transcription + const oldAudioPath = fileFolder ? `${fileFolder}/${oldAudioName}` : oldAudioName; + const audioFile = this.app.vault.getAbstractFileByPath(oldAudioPath); + if (audioFile instanceof TFile) { + const ext = oldAudioName.split('.').pop() ?? ''; + const newAudioName = ext ? `${newBase}.${ext}` : newBase; + const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName; + await this.app.vault.rename(audioFile, newAudioPath); + audioRenamed.set(oldAudioName, newAudioName); + } + } + } + } catch { /* ignore */ } + } + + // Mettre à jour pseudobs-audio dans le frontmatter via l'API Obsidian + if (file.extension === 'md' && audioRenamed.size > 0) { + try { + await this.app.fileManager.processFrontMatter(file, (fm) => { + const current = fm['pseudobs-audio']; + if (current && audioRenamed.has(current)) { + fm['pseudobs-audio'] = audioRenamed.get(current); + } + }); + } catch { /* ignore */ } } // 5. Fichier source originale (même basename, extension différente — .srt, .vtt, .cha, .html, .yml…) From 330c3258cb53216ee9953601838f761fbce89422 Mon Sep 17 00:00:00 2001 From: Axelle Abbadie Date: Sun, 17 May 2026 13:59:35 +0200 Subject: [PATCH 2/2] chore: bump to 0.1.6 --- CHANGELOG.fr.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 9 ++++---- ROADMAP.md | 8 +++++-- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 7 files changed, 122 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index ccfabfc..3c1d7e7 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -1,5 +1,57 @@ # Changelog +## [prod] v0.1.6 — 17 mai 2026 + +### Nouvelles fonctionnalités + +**Onglet Corpus** +- Nouvel onglet **Corpus** (dernier dans le panneau) — transcriptions par classe, badges statut : règles, version pseudo, format d'export final +- **Gestion des classes** : créer, supprimer (déplace les fichiers à la racine) +- **Déplacer des fichiers entre classes** : dropdown par fichier — déplace `.md` + `.mapping.json` + `.words.json` + audio + fichiers source en miroir +- **Paramètres de destination des exports finaux** (inline dans l'onglet) : dossier vault (avec option miroir de classes), à côté du fichier source, ou dossier externe ; `FolderSuggest` autocomplete sur tous les champs de dossier +- Composant `FolderSuggest` — autocomplete dossiers vault dans les Paramètres et le wizard + +**Modale de scan — candidats par occurrence** +- `MappingScanReviewModal` : colonne occurrences cliquable (`N / total` si exceptions) +- `OccurrencesContextModal` : cartes par occurrence (✓ / ✗ / ⚠) avec contexte, callback "Confirmer la sélection", sans application immédiate +- **Bouton "Enregistrer les exceptions"** : persiste les décisions ignorées dans `mapping.json` sans pseudonymiser +- Pré-remplit les décisions depuis `ignoredOccurrences` existantes à la réouverture + +**Exceptions** +- `IgnoredOccurrence {text, contextBefore, contextAfter}` sur `MappingRule.ignoredOccurrences` +- Persistance dans `mapping.json`, chargée au démarrage — rouge sans re-scan +- Surlignage rouge : positionnel, par contexte (`findByContext`) — seulement l'occurrence précise est rouge +- `findSpansForRule()` exclut les occurrences ignorées du remplacement à l'export +- Clic droit "Déclarer une exception ici" sur les termes orange (contexte 30 caractères) +- Section Exceptions dans l'onglet Mappings avec cartes contextuelles et suppression + +**Exports** +- `markdownToSrt()` et `markdownToCha()` — re-export du Markdown pseudonymisé en SRT/CHAT +- `resolveExportPath()` — miroir de classes depuis le dossier mapping quand le fichier est dans exports +- `writeExport()` — vault, à côté du source, ou externe (Node.js fs) +- Onglet Exports : bouton adapté selon source ou pseudonymisé ; masque pseudonymiser pour les fichiers déjà exportés + +**Avertissement nom de fichier** +- Bannière au-dessus des onglets si le nom contient un terme pseudonymisable +- Deux boutons : ✏ renommage manuel · ✨ suggestion neutre (`transcript_N`) +- Statuts clarifiés : "Actif / Partiel / Ignoré / Suggéré" +- `getValidatedFor()` inclut `partial` comme état actif + +**Cascade de renommage** +- Écouteur `vault.on('rename')` — cascade automatique au renommage natif Obsidian +- `cascadeRelatedRename()` — met à jour : frontmatter (`processFrontMatter`), `.mapping.json` (scope store + règles), `.words.json`, exports pseudonymisés, audio (même basename + référence `pseudobs-audio`), fichiers source (.srt/.vtt/.cha/.html/.yml) +- `moveFileToClass()` déplace aussi l'audio et met à jour les deux niveaux de scope +- Guard `_renamingRelated` prévient les boucles + +### Corrections +- `renameFileAndRelated` : `oldFilePath` capturé avant `vault.rename()` (TFile muté in-place) +- Renommage audio : `oldAudioName` capturé avant rename (même problème de mutation) +- `processFrontMatter` pour toutes les modifications de frontmatter (regex peu fiable) +- Scope store ET scope des règles mis à jour lors du déplacement/renommage +- `esbuild` : `copyStylesPlugin` copie `styles.css` dans le vault de test à chaque rebuild dev + +--- + ## [prod] v0.1.5 — 17 mai 2026 ### Nouvelles fonctionnalités diff --git a/CHANGELOG.md b/CHANGELOG.md index 338176d..90ee8d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,61 @@ > Previous entries in French: [CHANGELOG.fr.md](CHANGELOG.fr.md) +## [prod] v0.1.6 — 17 May 2026 + +### New features + +**Corpus tab** +- New **Corpus** tab (last in the panel) — transcription files organized by class with status badges: rule count, pseudonymized version, final export format +- **Class management**: create, rename (coming), delete classes (moves files to root) +- **Move files between classes**: dropdown per file — moves `.md` + `.mapping.json` + `.words.json` + audio + source files in sync +- **Export destination settings** (inline in the tab): vault folder (with optional class-structure mirroring), next to source, or external folder; `FolderSuggest` autocomplete on all folder fields +- `FolderSuggest` component — folder autocomplete in Settings and Onboarding wizard + +**Scan modal — per-occurrence candidates** +- `MappingScanReviewModal`: occurrence count column is now a clickable button showing `N / total` when some are ignored +- `OccurrencesContextModal`: per-occurrence cards (✓ / ✗ / ⚠) with context, "Confirm selection" callback, no immediate application +- **"Save exceptions" button**: persists ignored decisions in `mapping.json` without pseudonymizing +- Pre-fills decisions from existing `ignoredOccurrences` on re-open +- `MappingRuleResult` now carries `occurrences: Occurrence[]` + +**Exceptions** +- `IgnoredOccurrence {text, contextBefore, contextAfter}` on `MappingRule.ignoredOccurrences` +- Persisted in `mapping.json`, loaded at startup — red highlighting without re-scanning +- Red highlighting: context-aware, position-based via `findByContext()` — only the specific ignored occurrence is red, not all occurrences of the word +- `findSpansForRule()` skips ignored occurrences — they are not pseudonymized on export +- Right-click "Declare as exception here" on orange terms (captures 30-char context) +- Exceptions section in Mappings tab with context cards and delete button + +**Exports** +- `markdownToSrt()` and `markdownToCha()` — re-export pseudonymized Markdown back to SRT/CHAT +- `resolveExportPath()` — mirroring class structure from mapping folder when file is in exports +- `writeExport()` — vault, next-to-source, or external (Node.js fs); ensures parent folder +- Exports tab: button adapts based on whether active file is source or pseudonymized export; hides pseudonymize button for files already in exports folder + +**Filename warning** +- Banner above the tab bar when active file's name contains a pseudonymizable term +- Two buttons: ✏ manual rename prompt · ✨ apply neutral suggestion (`transcript_N`) +- `suggestCorrectedFilename()` — neutral naming, auto-incremented, no replacement leak +- Status labels clarified: "Active / Partial / Ignored / Suggested" +- `getValidatedFor()` includes `partial` as active + +**Rename cascade** +- `vault.on('rename')` listener — automatic cascade when user renames via Obsidian explorer +- `cascadeRelatedRename()` — updates: frontmatter (`app.fileManager.processFrontMatter`), `.mapping.json` (store scope + rule scopes), `.words.json`, pseudonymized exports, audio (same basename + `pseudobs-audio` reference), source files (.srt/.vtt/.cha/.html/.yml) +- Audio strategy: renames audio with same basename AND audio referenced by `pseudobs-audio` (noScribe, different basename) +- Guard flag `_renamingRelated` prevents cascade loops +- `moveFileToClass()` also moves audio and updates both store-level and rule-level scope paths + +### Fixes +- `renameFileAndRelated`: capture `oldFilePath` before `vault.rename()` (TFile mutated in-place) +- Audio rename: capture `oldAudioName` before rename (same mutation issue) +- `processFrontMatter` for all frontmatter updates (regex was unreliable) +- Both store-level `scope.path` and individual rule `scope.path` updated on move/rename +- `esbuild`: `copyStylesPlugin` copies `styles.css` to test vault on each dev rebuild + +--- + ## [prod] v0.1.5 — 17 May 2026 ### New features diff --git a/README.md b/README.md index 30627ab..81a33af 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,11 @@ Accessible via the ribbon icon or `Ctrl+P → Pseudonymization: open panel`. | Tab | Content | |---|---| -| **Mappings** | Active rules · Edit · Delete · Add · Scan file · **Exceptions section** | +| **Mappings** | Active rules · Edit · Delete · Add · Scan file · Exceptions section | | **Dictionaries** | Mini cards · Dictionary scan · Local import | -| **Exports** | Pseudonymize and export · Export correspondence table · **Export as VTT** | +| **Exports** | Create pseudonymized version · Export correspondence table · Re-export as VTT/SRT/CHAT | | **NER** | Visible if NER enabled · Identify candidates · Confidence threshold · Function words | +| **Corpus** | Files by class · Class management · Move files · **Final export destination** | ### Highlighting and markers @@ -122,7 +123,7 @@ Highlighting is active in all open files, including `.pseudonymized.*` export fi | 🟠 Orange + outline | Source term still present — to be pseudonymized | | 🟢 Green + underline | Pseudonym applied directly in the file | | 🔵 Blue + outline | NER candidate — no rule yet | -| 🔴 Red + underline | **Exception** — occurrence explicitly ignored during scan (case-sensitive; persisted in mapping) | +| 🔴 Red + underline | **Exception** — specific occurrence explicitly ignored (context-aware; persisted in mapping) | In exported files, pseudonyms are wrapped in `{{Pierre}}` markers to distinguish them from raw data (enabled by default, configurable in settings). @@ -212,7 +213,7 @@ src/ | 7 — Coulmont | ✅ | Equivalent first name suggestions · JSON/CSV import | | 8 — Side panel | ✅ | 3 tabs · Embedded NER · Wizard · Cancellation · Export highlighting | | 9 — Structured dictionaries | ✅ | Format v1.1 · DictionaryLoader · Dictionary scan · Review modal · French communes | -| 10 — Refinement & noScribe | 🔄 | i18n · Corpus org · noScribe HTML/VTT import · per-occurrence scan · exceptions · VTT re-export | +| 10 — Refinement & noScribe | 🔄 | i18n · Corpus UI · noScribe import · per-occurrence scan · exceptions · rename cascade · export destination | | 11 — EMCA functions | ⏳ | Turn navigation · Jefferson/ICOR correction · ELAN export | See [ROADMAP.md](ROADMAP.md) for the full phase breakdown and planned features. diff --git a/ROADMAP.md b/ROADMAP.md index 1a59732..4c52ed7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,7 +16,7 @@ Architectural decision (May 2026): **identifying entity detection** is handled b ✅ Phases 0–8 Parsers · Engine · UI · Scopes · Highlighting · Validation · Coulmont · Panel · NER · Wizard ✅ Phase 9 Structured dictionaries · DictionaryLoader · Review modals · French communes 🔄 Phase 10 Refinement & noScribe integration (v0.1.x → v0.2.0) - ✅ i18n · Corpus org · noScribe HTML/VTT/audio · Scan candidates · Exceptions · VTT re-export + ✅ i18n · Corpus UI · noScribe import · scan candidats · exceptions · rename cascade · exports ⏳ Test coverage · Jefferson/ICOR checker · Audio redaction · Timestamp UI ⏳ Phase 11 Interactional analysis functions (v1.0.0) ``` @@ -223,7 +223,11 @@ Goal: consolidate all existing features and add the EMCA-specific functions that - [x] **Mappings tab grouped by scope** — File / Folder / Vault sections with active-file filter - [x] **Status labels clarified** — "Active / Partial / Ignored / Suggested" instead of ✓/◑/✗/?; `getValidatedFor()` includes `partial` - [x] **Scan modal — per-occurrence candidates** — `MappingScanReviewModal` count column opens `OccurrencesContextModal` (✓/✗/⚠ per occurrence, "Save exceptions" button) -- [x] **Exceptions** — `IgnoredOccurrence` on `MappingRule`, persisted in mapping.json, red highlighting (`pseudobs-exception`, case-sensitive, priority 0), Exceptions section in Mappings tab +- [x] **Exceptions** — `IgnoredOccurrence` on `MappingRule`, persisted in mapping.json, red highlighting (`pseudobs-exception`, context-aware position, priority 0), Exceptions section in Mappings tab +- [x] **Corpus tab** — file list by class, class management, move files (cascade: .md + mapping + audio + source), final export destination (vault/next-to-source/external, optional class mirroring), `FolderSuggest` autocomplete +- [x] **Rename cascade** — `vault.on('rename')` listener, `cascadeRelatedRename()` updates frontmatter (processFrontMatter), mapping scopes, words.json, exports, audio (same basename + pseudobs-audio ref), source files +- [x] **Filename warning** — banner above tabs when name contains pseudonymizable term; ✏ manual rename · ✨ neutral suggestion (transcript_N) +- [x] **Exports in original format** — `markdownToSrt()`, `markdownToCha()`, `resolveExportPath()` with class mirroring, Exports tab adapts based on file type - [ ] Unit test coverage ≥ 80% for parsers, engine, NER scanner, DictionaryLoader - [ ] Jefferson / ICOR convention checker: hover suggestions, editor highlighting - [ ] EMCA publication exports (PNG) diff --git a/manifest.json b/manifest.json index 6f2a04c..55b255e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "pseudonymizer-tool", "name": "Pseudonymizer Tool", - "version": "0.1.5", + "version": "0.1.6", "minAppVersion": "1.7.2", "description": "Pseudonymize and correct interactional transcripts (Jefferson, ICOR, SRT, CHAT/CHA). Designed for qualitative researchers in linguistics and conversation analysis.", "author": "Axelle Abbadie", diff --git a/package.json b/package.json index 37ee619..85cff5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pseudonymizer-tool", - "version": "0.1.5", + "version": "0.1.6", "description": "Obsidian plugin for pseudonymizing and correcting interactional transcripts (Jefferson, ICOR, SRT, CHAT/CHA).", "author": "Axelle Abbadie", "homepage": "https://cv.hal.science/axelle-abbadie/", diff --git a/versions.json b/versions.json index 9802bc5..a811878 100644 --- a/versions.json +++ b/versions.json @@ -10,5 +10,6 @@ "0.1.2": "1.7.2", "0.1.3": "1.7.2", "0.1.4": "1.7.2", - "0.1.5": "1.7.2" + "0.1.5": "1.7.2", + "0.1.6": "1.7.2" } \ No newline at end of file