7 Commits

Author SHA1 Message Date
Oussama Knouz
7fb76e89f6 feat(anonymizer): strict anti-leak mode (default on), prefilter decoupling, boundary-aware substitution
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>
2026-07-02 18:00:06 +01:00
Mobiletic
5ed8001101 feat: multi-turn conversation support (0.4.0)
anonymizeTurn(text, session) threads a serializable AnonymizerSession
({mapping, legend, history}) so one entity keeps one id across a whole chat
(applyKnown reuse + usedIds + de-collision merge). Adds conversation()
in-memory wrapper and an optional LlmProvider.anonymizeInConversation(text, ctx)
for rich cross-turn context; providers without it fall back to the batch path.

openAICompatibleProvider gains includeMappingInContext (default false — only
send real values to a trusted anonymizer endpoint) + historyMaxTurns (default
10). Verified live vs Gemma 4: Nora stays PER_1 across 4 turns (name/email/AVS/
IBAN), Yanis = PER_2; 41 tests, 98% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:26:41 +01:00
Mobiletic
2584bd8c95 feat: prompt rules for lossless round-trips (0.3.1)
Two coherence rules added to DEFAULT_SYSTEM_PROMPT:
- different values of the same type get distinct placeholders (context),
  e.g. old/new IBAN -> [PER_1.IBAN:Ancien] / [PER_1.IBAN:Nouveau]
- never absorb adjacent punctuation/separators into a placeholder

Took the live Gemma-4 (Infomaniak) e-learning demo from 4/5 to 5/5 exact
round-trips. 36 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:58:56 +01:00
Mobiletic
f5c9f0fbd4 feat: make openAICompatibleProvider work with Infomaniak out of the box (0.3.0)
- response_format is omitted by default (Infomaniak rejects the legacy
  json_object → HTTP 422); opt in via the new `responseFormat` option
  ({type:'json_object'} or a json_schema object). BREAKING for endpoints
  that relied on the forced json_object.
- parse LLM JSON leniently (tolerate markdown fences / surrounding prose)
- fold strict-coherence rules into DEFAULT_SYSTEM_PROMPT (exact
  placeholder<->mapping-key identity, values are originals, strict format,
  mask value not adjacent label) → reliable output across models

Verified live against Gemma 4 (google/gemma-4-31B-it, Infomaniak v2): all
demo phrases anonymize with clean round-trips, no custom provider needed.
36 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:43:19 +01:00
Mobiletic
669794522e feat!: optional fail-closed fallback, legend output, richer prompt & docs
BREAKING CHANGE: regex fallback is now opt-in (patterns no longer defaults
to presets.swiss). With no fallback an LLM failure throws AnonymizationError
(fail-closed) and the pre-filter is bypassed; at least one of llm/patterns is
required. AnonymizationResult gains a required `legend`; anonymizeChunks seed
is now { mapping, legend? } and returns legend.

- prompt: model may coin new UPPERCASE abbreviations and returns a 'legende'
  explaining every abbreviation used (French); backfilled by DEFAULT_LEGEND
- PatternDef.meaning surfaces in the legend; swiss/generic presets get meanings
- AnonymizationError (exported) wraps the cause on fail-closed
- README: drop the chatbot provenance line; add 'How it works' + nLPD sections
- 34 tests / 99% coverage; bump to 0.2.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:28:04 +01:00
Mobiletic
81b6b03239 feat: harden generic preset, add LLM retry, and finish tooling/docs
- PatternDef.validate + Luhn-gated credit-card detection; de-overlap the
  generic phone/date/IP patterns; presets.swiss unchanged (production behavior)
- strip g/y flags from nameHint so .test() is stateless (latent footgun)
- openAICompatibleProvider: bounded retry on transient failures (network /
  timeout / 429 / 5xx), configurable via retries + retryDelayMs
- eslint + prettier + vitest coverage (97%); CI runs lint/format/coverage
- docs: README badges + new-option docs, SECURITY.md, issue/PR templates

26 tests passing; build emits ESM+CJS+types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:03:20 +01:00
Mobiletic
f09b917f1a feat: initial release of @mobiletic/anonymizer
Framework-agnostic PII anonymization extracted from Mobiletic's chatbot.
Pluggable LLM detection + configurable regex fallback, deterministic
coreference, and streaming-safe de-anonymization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:58:38 +01:00