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>
This commit is contained in:
Mobiletic
2026-06-25 15:28:04 +01:00
parent 08f84ae14a
commit 669794522e
13 changed files with 408 additions and 83 deletions

View File

@@ -25,7 +25,7 @@ describe('openAICompatibleProvider', () => {
vi.stubGlobal('fetch', fetchMock);
const r = await openAICompatibleProvider(opts).anonymize('a@b.ch');
expect(r).toEqual({ anon: '[EMAIL_1]', mapping: { '[EMAIL_1]': 'a@b.ch' } });
expect(r).toEqual({ anon: '[EMAIL_1]', mapping: { '[EMAIL_1]': 'a@b.ch' }, legend: {} });
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe('https://api.example.com/v1/chat/completions');
@@ -35,6 +35,18 @@ describe('openAICompatibleProvider', () => {
expect(init.headers.Authorization).toBe('Bearer k');
});
it('parses the model legende into result.legend, keeping only string values', async () => {
const fetchMock = mockFetchJson({
texte_anonymise: '[PER_1.NOM:M]',
mapping: { '[PER_1.NOM:M]': 'Alain Jaccard' },
legende: { PER: 'Personne', NOM: 'Nom de famille', M: 'Masculin', BAD: 42 },
});
vi.stubGlobal('fetch', fetchMock);
const r = await openAICompatibleProvider(opts).anonymize('Alain Jaccard');
expect(r.legend).toEqual({ PER: 'Personne', NOM: 'Nom de famille', M: 'Masculin' });
});
it('anonymizeBatch() returns {segments, mapping} and includes used ids in the prompt', async () => {
const fetchMock = mockFetchJson({ segments: ['[PER_2.NOM:M]'], mapping: { '[PER_2.NOM:M]': 'Bob' } });
vi.stubGlobal('fetch', fetchMock);