Harden the core privacy guarantee: - Add `strict` mode (default true): after detection, verify no mapped value survives as a whole token in the output (ignoring placeholders, whose context may legitimately echo a value like `B+`). Catches a model that redacts one mention of a value but leaves another in clear — which the placeholder/mapping bijection check missed. Fail-closed: throws AnonymizationError naming only the non-secret placeholder key, and runs on the final result so it is not swallowed into the regex fallback (which can't fix a name leak). Set strict:false to opt out. - Add `prefilter` option (default true): decouple the PII pre-filter from the presence of a regex fallback. Set false to always consult the LLM while keeping the fallback for LLM failures (max recall + graceful degradation). - Boundary-aware value substitution: applyKnown and the leak check now match values only as whole tokens (Unicode letter/digit boundaries, regex-escaped), so "Ann" no longer replaces inside "Anna" and "jean@x.ch" no longer matches inside "jean@x.church"; accented/non-Latin names preserved. - deanonymize restores longest placeholder keys first (prefix-overlap defense). Restructures anonymize/anonymizeChunks/anonymizeTurn to a single exit so the leak check runs once on the final result. Behavior is unchanged for callers that were already leak-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.1 KiB
7.1 KiB
Changelog
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.5.0] - Unreleased
Added
- Strict anti-leak mode (on by default). New
strictoption onAnonymizer. When enabled, the anonymized output is checked after detection to ensure no mapped value still appears as a whole token (ignoring placeholders — a placeholder's context field may legitimately echo a value, e.g.B+in[PER_1.SANG:B+]). This catches a model that redacts one mention of a value but leaves another in clear — a case the previous bidirectional validation (placeholder ⇄ mapping-key) did not detect. On a suspected leak it throwsAnonymizationErrornaming only the non-secret placeholder key. Fail-closed: it runs on the final result and is not swallowed into the regex fallback (which can't fix a name leak). Defaults totrue— setstrict: falseto restore the previous behaviour (or if a false positive rejects an otherwise-fine result). prefilteroption — decouples the cheap PII pre-filter (skip the LLM when no PII is heuristically detected) from the presence of a regex fallback. Defaulttrue; setfalseto always consult the LLM while still keeping the fallback for LLM failures (maximum recall with graceful degradation).
Changed
- Boundary-aware value substitution. Known-value reuse (
applyKnown, used byanonymizeChunksandanonymizeTurn) now matches values only as whole tokens (Unicode letter/digit boundaries) instead of raw substrings, so a short value like"Ann"is no longer replaced inside"Anna", and"jean@exemple.ch"no longer matches inside"jean@exemple.church". Accented and non-Latin names are preserved. The strict leak check uses the same boundary logic, so detection and substitution agree. deanonymizenow restores longest placeholder keys first (defensive against prefix overlaps).
[0.4.0] - Unreleased
Added
- Multi-turn conversation support. New
Anonymizer.anonymizeTurn(text, session?) → { anon, mapping, legend, session }keeps one stable id per entity across a whole chat: it seeds each turn with a running, serializableAnonymizerSession({ mapping, legend, history }), reuses known values viaapplyKnown, tells the model which ids are taken, and de-collides new ones. Persist the returnedsessionand pass it back next turn. Anonymizer.conversation(initial?)— a stateful in-memory wrapper (anonymize,deanonymize,session()) overanonymizeTurn.- Optional
LlmProvider.anonymizeInConversation(text, ctx)— providers can use prior context (anonymizedhistory,legend,usedIds, and optionallymapping) for better cross-turn coreference/attribution.openAICompatibleProviderimplements it; providers that don't fall back to the batch path automatically. openAICompatibleProvideroptions:includeMappingInContext(default false — only send real values to a trusted anonymizer endpoint) andhistoryMaxTurnsviaAnonymizerConfig(default 10).- Exported the
AnonymizerSessiontype.
[0.3.1] - Unreleased
Changed
- Default prompt hardening for lossless round-trips: (1) two different values of the same type never
share a placeholder — they must be distinguished by context (e.g.
[PER_1.IBAN:Ancien]vs[PER_1.IBAN:Nouveau]), preventing a within-message collision (old/new IBAN); (2) the model must not absorb adjacent punctuation/separators (commas, spaces, parentheses) into a placeholder. Together these took the live Gemma-4 e-learning demo from 4/5 to 5/5 exact round-trips.
[0.3.0] - Unreleased
Changed
openAICompatibleProvidernow works with Infomaniak (and other open-model endpoints) out of the box.response_formatis omitted by default instead of hard-coding{ type: 'json_object' }, which current Infomaniak rejects (HTTP 422). Pass the newresponseFormatoption (e.g.{ type: 'json_object' }or ajson_schemaobject) for endpoints that support/require it. Breaking for endpoints that relied on the previous forcedjson_object.- JSON responses are now parsed leniently — a fenced JSON code block or surrounding prose is tolerated
(the outermost
{ … }is extracted), so models without an enforcedresponse_formatdon't cause spurious parse failures. - The default prompt gained a COHÉRENCE block (exact placeholder↔mapping-key identity, values are the
original data never another placeholder, strict
[TYPE_N…]format, mask the value not the adjacent label) to improve reliability across models.
[0.2.0] - Unreleased
Added
legendon every result — abbreviation → French meaning (PER→Personne,M→Masculin), safe to forward to a downstream LLM so it understands the placeholder tokens. Backed by a built-inDEFAULT_LEGENDso coverage is guaranteed even if the model omits entries.- The default prompt now lets the model coin new uppercase abbreviations for entities/attributes/
context it discovers and return their meanings in
legende. PatternDef.meaning— optional human label for a tag, surfaced in thelegend.presets.swiss/presets.genericship French meanings.AnonymizationError(exported) — thrown when anonymization can't complete and no fallback exists; carries the originating error in.cause.
Changed (breaking)
- Regex fallback is now opt-in.
patternsno longer defaults topresets.swiss. With no fallback, an LLM failure throwsAnonymizationError(fail-closed) and the pre-filter is bypassed. At least one ofllmorpatternsis required, or the constructor throws. AnonymizationResultgained a requiredlegendfield;LlmProvider.anonymizeBatchreturnslegend.anonymizeChunks(chunks, seed)—seedis now{ mapping, legend? }(was the bare mapping) and the return includeslegend.
[0.1.0] - Unreleased
Added
- Initial public release.
Anonymizer— pre-filter → LLM → regex fallback, bidirectional validation, deterministic coreference, and de-collision across question and retrieved chunks (anonymize,anonymizeChunks,deanonymize).makeStreamDeanonymizer— streaming-safe de-anonymization that never leaks a split placeholder.openAICompatibleProvider— pluggable LLM detection over any OpenAI-compatible Chat Completions API.presets.swissandpresets.genericregex pattern sets; fully configurable custom patterns.- Regex-only mode (no LLM provider required).
PatternDef.validate— optional second-stage predicate to cut false positives;presets.genericuses it for a Luhn check on credit-card candidates, and de-overlaps its phone/date/IP patterns.openAICompatibleProviderretries transient failures (network/timeout/429/5xx) viaretriesandretryDelayMsoptions; non-transient 4xx and malformed responses are not retried.- Hardened the
nameHintheuristic:g/yflags are stripped internally so.test()is stateless.