From ec1f9a4e553d17d354c0d7797601b5b29513b8ad Mon Sep 17 00:00:00 2001 From: muena Date: Mon, 28 Sep 2026 09:17:53 +0200 Subject: [PATCH] fix: allow rerunning imported projects --- src/lib/project-review.ts | 9 +++++++-- src/pseudonymize-project-files.tsx | 10 +++++----- tests/project-review.test.ts | 4 ++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib/project-review.ts b/src/lib/project-review.ts index a3c7cdb..081caae 100644 --- a/src/lib/project-review.ts +++ b/src/lib/project-review.ts @@ -89,9 +89,14 @@ export function withoutWhitelistedHits(hits: T[], whitel } /** A later run must not turn an existing Velum placeholder plus adjacent text into a new entity. */ -export function withoutPlaceholderHits(hits: T[]): T[] { +export function withoutPlaceholderHits( + hits: T[], knownPlaceholders: Iterable = [], +): T[] { const placeholder = /(? hit.type === "PASSWORT" || !placeholder.test(hit.original)); + const known = [...knownPlaceholders].filter(Boolean); + return hits.filter((hit) => hit.type === "PASSWORT" || ( + !placeholder.test(hit.original) && !known.some((key) => hit.original.includes(key)) + )); } export function withoutWhitelistedRules( diff --git a/src/pseudonymize-project-files.tsx b/src/pseudonymize-project-files.tsx index 8648d03..8616df9 100644 --- a/src/pseudonymize-project-files.tsx +++ b/src/pseudonymize-project-files.tsx @@ -101,10 +101,8 @@ async function prepare(root: string, files: ProjectFile[], rules: ProjectRule[], const masks = forceMasks(withoutWhitelistedRules(rules, whitelist)); const latestEntry = (await listRuns(root))[0]; const latest = latestEntry?.run; - if (latest?.origin === "imported") { - throw new Error("Dieser Quellordner enthält bereits pseudonymisierte Dateien. Für einen neuen Lauf bitte einen Klartext-Quellordner wählen."); - } const previousMapping: PlaceholderMapping = latest?.mapping ?? {}; + const previousPlaceholders = Object.keys(previousMapping); let mapping: PlaceholderMapping = previousMapping; const drafts: Draft[] = []; const retained: Preview["retained"] = []; @@ -112,6 +110,8 @@ async function prepare(root: string, files: ProjectFile[], rules: ProjectRule[], const hits: Hit[] = []; const texts = new Map(); const sourceInPlace = Boolean(latest?.inPlace || latest?.sourceInPlace); + // A migrated snapshot may differ from the source folder. Recheck its files + // unless an earlier run actually replaced them in place. const previousOutputs = new Set(sourceInPlace ? latest?.files.map((file) => file.output) : []); const previousDirectory = latestEntry?.directory; for (const file of files) { @@ -131,7 +131,7 @@ async function prepare(root: string, files: ProjectFile[], rules: ProjectRule[], if (file.kind === "text") texts.set(file.relPath, textFromBytes(data)); } const addHits = (draft: Draft, found: Hit[]) => { - draft.hits = withoutWhitelistedHits(withoutPlaceholderHits(found), whitelist); + draft.hits = withoutWhitelistedHits(withoutPlaceholderHits(found, previousPlaceholders), whitelist); hits.push(...draft.hits); if (hits.length > MAX_REVIEW_HITS) throw new Error("Zu viele Treffer. Bitte weniger Kategorien auswählen."); }; @@ -175,7 +175,7 @@ async function prepare(root: string, files: ProjectFile[], rules: ProjectRule[], source: "Datei- oder Ordnername", kind: "path" as const, context: path.value, })); - path.hits = withoutWhitelistedHits(withoutPlaceholderHits(found), whitelist); + path.hits = withoutWhitelistedHits(withoutPlaceholderHits(found, previousPlaceholders), whitelist); hits.push(...path.hits); if (hits.length > MAX_REVIEW_HITS) throw new Error("Zu viele Treffer. Bitte weniger Kategorien auswählen."); } diff --git a/tests/project-review.test.ts b/tests/project-review.test.ts index 5a2803e..28b2de3 100644 --- a/tests/project-review.test.ts +++ b/tests/project-review.test.ts @@ -67,6 +67,10 @@ it("zeigt beim erneuten Lauf keine Treffer, die einen bestehenden Platzhalter ei { type: "PASSWORT", original: "ORT_08!" }, ]; expect(withoutPlaceholderHits(hits)).toEqual(hits.slice(2)); + expect(withoutPlaceholderHits([ + { type: "PERSON", original: "KUNDE_100005Service" }, + { type: "PERSON", original: "Roman" }, + ], ["KUNDE_100005"])).toEqual([{ type: "PERSON", original: "Roman" }]); }); it("fasst ausgewählte Personen zu einem kanonischen Platzhalter zusammen und bewahrt alte Zuordnungen", () => {