docs: add examples/ (live demo + RESULTS) and slim the README examples

- examples/demo.mjs: live demo (5 single messages up to 5 people with rare
  auto-coined attributes + 5 escalating multi-turn conversations with
  long-range back-references); self-references the package via exports
- examples/RESULTS.md: its generated output (10 examples)
- examples/.env.example + examples/README.md; `npm run example` script
- README: replace the long examples section with two representative examples
  (dense 5-person message + 6-turn conversation) and a badge/link to
  examples/RESULTS.md for the full set
- eslint ignores examples/**, prettier ignores generated examples/RESULTS.md,
  .gitignore un-ignores .env.example

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mobiletic
2026-07-01 15:22:00 +01:00
parent d119c366b7
commit dcb0a3a448
9 changed files with 771 additions and 124 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ coverage/
.DS_Store
.env
.env.*
!.env.example

View File

@@ -2,3 +2,4 @@ dist
coverage
node_modules
package-lock.json
examples/RESULTS.md

145
README.md
View File

@@ -252,141 +252,40 @@ Three distinct outputs, with different sensitivities:
## Examples
Real learner ↔ platform chat messages, anonymized **live by Gemma 4** (via Infomaniak) with no regex
fallback. All personal data below is **fictional**. `mapping` is the secret re-identification key (kept by
the operator); `legend` is safe to send to the downstream model. Every case restores identically.
Two examples produced **live by Gemma 4** on fictional Swiss e-learning data. `mapping` is the secret
re-identification key (kept by the operator); `legend` is safe to send downstream. The model even **coins
its own abbreviations** for attributes not in the base prompt (passport, permit, blood type, …).
| # | Scenario | Sensitive data detected | Notable capability | Round-trip |
| --- | ----------------------- | --------------------------------------------------- | ----------------------------------------- | ---------- |
| 1 | Course signup | name, e-mail | baseline detection | ✅ |
| 2 | Login problem | + username, phone, signup date | coins `[PER_1.ID_USER:LOGIN]` on the fly | ✅ |
| 3 | Billing / IBAN change | + address, **two IBANs**, AVS | old vs new IBAN kept **distinct** | ✅ |
| 4 | HR enrolls staff | **3 people** + org, e-mails, phones, DOB, IBAN | coreference (same person → same id) + org | ✅ |
| 5 | Parent + minor + health | 3 people, DOB, address, contacts, IBAN, maiden name | minor/guardian/doctor kept distinct | ✅ |
[![See all examples](https://img.shields.io/badge/examples-see_all-6f42c1)](./examples/RESULTS.md) &nbsp;
5 single messages of rising complexity **+** 5 multi-turn conversations, with full mappings & legends.
### Case 1 — course signup (baseline)
### A dense single message — 5 people + rare attributes
**User message:**
> Bonjour, je suis Jean Dupont et mon adresse e-mail est jean.dupont@yopmail.com. Je viens de m'inscrire à la formation « Bureautique de base » et je voulais confirmer que tout est en ordre. Merci d'avance !
> Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) … Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent … Le Dr Farah Haddad (n° RCC V123456) a ouvert le dossier médical DM-2025-0417 … l'agent Kevin Zbinden de la caisse Helvetia … Noah utilise l'identifiant biométrique BIO-7729.
**Anonymized — what the model sees:**
**Anonymized — what the model sees** (5 distinct people `PER_1…PER_5` + `ORG_1`; `Noah` reused at the end):
> Bonjour, je suis [PER_1.PRENOM:M] [PER_1.NOM:M] et mon adresse e-mail est [PER_1.EMAIL:PERSO]. Je viens de m'inscrire à la formation « Bureautique de base » et je voulais confirmer que tout est en ordre. Merci d'avance !
> Note de dossier complète : le patient mineur [PER_1.PRENOM:U] [PER_1.NOM:U] (né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin B+, numéro d'assuré maladie [PER_1.ASSUR_ID:U], allergique aux arachides) … Ses parents, [PER_2.PRENOM:F] [PER_2.NOM:F] (mère, permis de séjour [PER_2.PERMIS:C], [PER_2.TELEPHONE:U]) et [PER_3.PRENOM:M] [PER_3.NOM:M] (père, passeport italien [PER_3.PASSPORT:U]), cosignent … Le Dr [PER_4.PRENOM:F] [PER_4.NOM:F] (n° RCC [PER_4.RCC:U]) a ouvert le dossier médical DM-2025-0417 … l'agent [PER_5.PRENOM:M] [PER_5.NOM:M] de la caisse [ORG_1.NOM:Assurance] … [PER_1.PRENOM:U] utilise l'identifiant biométrique [PER_1.BIO_ID:U].
| `mapping` 🔒 (secret) | value | | `legend` 🏷️ (shareable) | meaning |
| --------------------- | ----------------------- | --- | ----------------------- | ------------------ |
| `[PER_1.PRENOM:M]` | Jean | | `PER` | Personne |
| `[PER_1.NOM:M]` | Dupont | | `PRENOM` / `NOM` | Prénom / Nom |
| `[PER_1.EMAIL:PERSO]` | jean.dupont@yopmail.com | | `EMAIL` · `M` · `PERSO` | E-mail · M · Perso |
Attributes **coined by the model** (not in the prompt): `ASSUR_ID`, `PERMIS`, `PASSPORT`, `RCC`, `BIO_ID`.
Note the course name « Bureautique de base » is **left intact** — it isn't personal data.
### A multi-turn conversation — 4 people; blood group recalled turn 1 → turn 6
### Case 4 — HR enrolls staff (coreference + organization)
| Turn | User message | Anonymized (model sees) |
| ---- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 1 | …mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+. | …mon fils mineur, [PER_1.PRENOM:U] [PER_1.NOM:U], né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin [PER_1.SANG:B+]. |
| 3 | Moi, sa mère Delphine Rieder … permis de séjour de type C. | Moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F] … permis de séjour de type [PER_2.PERMIS:C]. |
| 4 | Son père, Marco Baumann, passeport italien YA9087654, … | Son père, [PER_3.PRENOM:M] [PER_1.NOM:U], passeport italien [PER_3.PASSPORT:ITA], … |
| 5 | Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah. | Le [PER_4.TITRE:MED] [PER_4.PRENOM:F] [PER_4.NOM:F] a ouvert le dossier médical [PER_1.DOSSIER:MED] pour [PER_1.PRENOM:U]. |
| 6 | Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge… | Rappel : le groupe sanguin [PER_1.SANG:B+] de [PER_1.PRENOM:U] doit figurer… |
**User message:**
Noah stays `PER_1` across all six turns, and **his blood group `[PER_1.SANG:B+]` from turn 1 is reused in
turn 6** — long-range coreference holds. (The father shares the surname, so `[PER_1.NOM:U]` is reused for
it — the same value maps to the same token.)
> Bonjour, je suis Sophie Meyer, responsable formation chez Nestlé Suisse (sophie.meyer@nestle.com, +41 21 924 11 11). Je souhaite inscrire deux collaborateurs à la formation « Sécurité au travail » qui débute le 03/03/2026 : Marc Rossi, né le 12.03.1990 (marc.rossi@nestle.com), et Amélie Girard, joignable au 078 321 65 43. La facture est à adresser à notre comptabilité, IBAN CH93 0076 2011 6238 5295 7. Marc Rossi avait déjà suivi une formation l'an dernier — pouvez-vous réactiver son ancien compte plutôt que d'en créer un nouveau ?
**Anonymized — what the model sees** (note **Marc Rossi → `PER_2` in both mentions**, and the org as `ORG_1`):
> Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], responsable formation chez [ORG_1.NOM:Entreprise] ([PER_1.EMAIL:F], [PER_1.TELEPHONE:F]). Je souhaite inscrire deux collaborateurs à la formation « Sécurité au travail » qui débute le 03/03/2026 : [PER_2.PRENOM:M] [PER_2.NOM:M], né le [PER_2.DATE_NAISSANCE:1990] ([PER_2.EMAIL:M]), et [PER_3.PRENOM:F] [PER_3.NOM:F], joignable au [PER_3.TELEPHONE:F]. La facture est à adresser à notre comptabilité, IBAN [ORG_1.IBAN:Comptabilite]. [PER_2.PRENOM:M] [PER_2.NOM:M] avait déjà suivi une formation l'an dernier — pouvez-vous réactiver son ancien compte plutôt que d'en créer un nouveau ?
**`mapping` 🔒 (secret — kept on your side):**
| Placeholder | Real value |
| ----------------------------- | -------------------------- |
| `[PER_1.PRENOM:F]` | Sophie |
| `[PER_1.NOM:F]` | Meyer |
| `[ORG_1.NOM:Entreprise]` | Nestlé Suisse |
| `[PER_1.EMAIL:F]` | sophie.meyer@nestle.com |
| `[PER_1.TELEPHONE:F]` | +41 21 924 11 11 |
| `[PER_2.PRENOM:M]` | Marc |
| `[PER_2.NOM:M]` | Rossi |
| `[PER_2.DATE_NAISSANCE:1990]` | 12.03.1990 |
| `[PER_2.EMAIL:M]` | marc.rossi@nestle.com |
| `[PER_3.PRENOM:F]` | Amélie |
| `[PER_3.NOM:F]` | Girard |
| `[PER_3.TELEPHONE:F]` | 078 321 65 43 |
| `[ORG_1.IBAN:Comptabilite]` | CH93 0076 2011 6238 5295 7 |
**`legend` 🏷️ (shareable — sent to the downstream model):** `PER`=Personne, `PRENOM`=Prénom, `NOM`=Nom de
famille, `F`=Féminin, `M`=Masculin, `ORG`=Organisation, `EMAIL`=Adresse e-mail, `TELEPHONE`=Numéro de
téléphone, `DATE_NAISSANCE`=Date de naissance, `IBAN`=Numéro de compte bancaire.
<details>
<summary><b>More examples — Case 2 (login), Case 3 (two IBANs), Case 5 (minor + health)</b></summary>
#### Case 2 — login problem (coins an abbreviation for the username)
**User message:**
> Salut, moi c'est Marie Favre. J'ai créé mon compte avec l'e-mail marie.favre@bluewin.ch le 15/02/2026 mais je n'arrive plus à me connecter. Mon identifiant est mfavre et vous pouvez me joindre au 079 456 78 90. Pouvez-vous réinitialiser mon accès à la formation « Machiniste » ?
**Anonymized:**
> Salut, moi c'est [PER_1.PRENOM:F] [PER_1.NOM:F]. J'ai créé mon compte avec l'e-mail [PER_1.EMAIL:PERSO] le [PER_1.DATE_CREATION:2026] mais je n'arrive plus à me connecter. Mon identifiant est [PER_1.ID_USER:LOGIN] et vous pouvez me joindre au [PER_1.TELEPHONE:MOBILE]. Pouvez-vous réinitialiser mon accès à la formation « Machiniste » ?
| Placeholder | Real value |
| ---------------------------- | ---------------------- |
| `[PER_1.PRENOM:F]` | Marie |
| `[PER_1.NOM:F]` | Favre |
| `[PER_1.EMAIL:PERSO]` | marie.favre@bluewin.ch |
| `[PER_1.DATE_CREATION:2026]` | 15/02/2026 |
| `[PER_1.ID_USER:LOGIN]` | mfavre |
| `[PER_1.TELEPHONE:MOBILE]` | 079 456 78 90 |
`ID_USER` and `DATE_CREATION` are **coined by the model** — not in the base vocabulary — and documented in
the legend.
#### Case 3 — billing / IBAN change (two different IBANs kept distinct)
**User message:**
> Bonjour, je m'appelle Luc Berset, domicilié au 14 avenue de la Gare, 1700 Fribourg. J'ai un souci avec le paiement de la formation « Comptabilité PME » (CHF 1'200.) : mon IBAN CH93 0076 2011 6238 5295 7 n'est plus valide, je souhaite le remplacer par CH56 0483 5012 3456 7800 9. Si besoin, mon numéro AVS est le 756.1234.5678.90. Merci de mettre à jour mon dossier.
**Anonymized** (old and new IBAN get **distinct** placeholders via context):
> Bonjour, je m'appelle [PER_1.PRENOM:M] [PER_1.NOM:M], domicilié au [PER_1.ADRESSE:Rue], [PER_1.ADRESSE:NPA] [PER_1.ADRESSE:Ville]. J'ai un souci avec le paiement de la formation « Comptabilité PME » (CHF 1'200.) : mon IBAN [PER_1.IBAN:Ancien] n'est plus valide, je souhaite le remplacer par [PER_1.IBAN:Nouveau]. Si besoin, mon numéro AVS est le [PER_1.AVS:Suisse]. Merci de mettre à jour mon dossier.
| Placeholder | Real value |
| ----------------------- | -------------------------- |
| `[PER_1.PRENOM:M]` | Luc |
| `[PER_1.NOM:M]` | Berset |
| `[PER_1.ADRESSE:Rue]` | 14 avenue de la Gare |
| `[PER_1.ADRESSE:NPA]` | 1700 |
| `[PER_1.ADRESSE:Ville]` | Fribourg |
| `[PER_1.IBAN:Ancien]` | CH93 0076 2011 6238 5295 7 |
| `[PER_1.IBAN:Nouveau]` | CH56 0483 5012 3456 7800 9 |
| `[PER_1.AVS:Suisse]` | 756.1234.5678.90 |
#### Case 5 — parent, minor child and health (dense coreference)
**User message:**
> Bonjour, je vous écris au sujet de mon fils, Lucas Favre, né le 04.07.2011, que j'aimerais inscrire à la formation junior « Robotique » à Lausanne. Étant mineur, c'est moi, sa mère Camille Favre, qui gère le dossier — vous pouvez me joindre au 021 555 12 34 ou à camille.favre@bluewin.ch, nous habitons au 8 chemin des Vignes, 1009 Pully. Lucas est asthmatique ; son médecin, le Dr Nadia Benali (cabinet à Renens), a établi un certificat le 15.05.2024. Par ailleurs, j'avais moi-même suivi la formation « Photographie » en 2023 sous mon nom de jeune fille, Camille Rochat — mes deux comptes peuvent-ils être fusionnés ? Le paiement se fera depuis mon IBAN CH56 0483 5012 3456 7800 9.
**Anonymized** (son = `PER_1`, mother = `PER_2` incl. her maiden name, doctor = `PER_3`):
> Bonjour, je vous écris au sujet de mon fils, [PER_1.PRENOM:M] [PER_1.NOM:M], né le [PER_1.DATE_NAISSANCE:2011], que j'aimerais inscrire à la formation junior « Robotique » à [LOC_1.VILLE:Lausanne]. Étant mineur, c'est moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F], qui gère le dossier — vous pouvez me joindre au [PER_2.TELEPHONE:Fixe] ou à [PER_2.EMAIL:Privé], nous habitons au [PER_2.ADRESSE:Rue], [PER_2.ADRESSE:NPA] [PER_2.ADRESSE:Ville]. [PER_1.PRENOM:M] est asthmatique ; son médecin, le Dr [PER_3.PRENOM:F] [PER_3.NOM:F] (cabinet à [LOC_2.VILLE:Renens]), a établi un certificat le 15.05.2024. Par ailleurs, j'avais moi-même suivi la formation « Photographie » en 2023 sous mon nom de jeune fille, [PER_2.PRENOM:F] [PER_2.NOM_JEUNE_FILLE:F] — mes deux comptes peuvent-ils être fusionnés ? Le paiement se fera depuis mon IBAN [PER_2.IBAN:Principal].
| Placeholder | Real value |
| ------------------------------------------------- | ---------------------------------- |
| `[PER_1.PRENOM:M]` / `[PER_1.NOM:M]` | Lucas / Favre |
| `[PER_1.DATE_NAISSANCE:2011]` | 04.07.2011 |
| `[PER_2.PRENOM:F]` / `[PER_2.NOM:F]` | Camille / Favre |
| `[PER_2.NOM_JEUNE_FILLE:F]` | Rochat |
| `[PER_2.TELEPHONE:Fixe]` | 021 555 12 34 |
| `[PER_2.EMAIL:Privé]` | camille.favre@bluewin.ch |
| `[PER_2.ADRESSE:Rue/NPA/Ville]` | 8 chemin des Vignes / 1009 / Pully |
| `[PER_3.PRENOM:F]` / `[PER_3.NOM:F]` | Nadia / Benali |
| `[LOC_1.VILLE:Lausanne]` / `[LOC_2.VILLE:Renens]` | Lausanne / Renens |
| `[PER_2.IBAN:Principal]` | CH56 0483 5012 3456 7800 9 |
The health detail ("asthmatique") is kept as non-identifying context, and the reference chain
("mon fils" / "sa mère" / "son médecin") is resolved into three distinct entities.
</details>
**[→ See all 10 examples, with full `mapping` and `legend` tables](./examples/RESULTS.md)**
## How this helps with the nLPD

View File

@@ -3,7 +3,7 @@ import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
{ ignores: ['dist/**', 'coverage/**', 'node_modules/**'] },
{ ignores: ['dist/**', 'coverage/**', 'node_modules/**', 'examples/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
prettier,

6
examples/.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Copy to examples/.env and fill in — .env is git-ignored, never commit your key.
# Any OpenAI-compatible endpoint works; the package appends "/chat/completions".
INFOMANIAK_AI_BASE_URL= # e.g. https://api.infomaniak.com/2/ai/<product_id>/openai/v1
INFOMANIAK_AI_API_KEY= # your API token
GEMMA_MODEL= # model id (e.g. google/gemma-4-31B-it)
PRIVACY_GEMMA_TIMEOUT_MS=60000

24
examples/README.md Normal file
View File

@@ -0,0 +1,24 @@
# Live example
[`demo.mjs`](./demo.mjs) runs `@mobiletic/anonymizer` against a real
OpenAI-compatible LLM (built with Gemma 4 via Infomaniak) on a set of fictional
Swiss e-learning chats: 5 single messages of rising complexity (up to 5 people,
with rare attributes the model must coin itself) and 5 multi-turn conversations
(with long-range back-references across turns). It writes the formatted output to
[`RESULTS.md`](./RESULTS.md).
> This is a **live example**, not a CI unit test — it needs an API key and calls a
> real endpoint. The deterministic test suite lives in [`../test`](../test).
## Run
```bash
cd ..
npm install
npm run build # examples/demo.mjs imports the built package
cp examples/.env.example examples/.env # then fill in your endpoint + key
npm run example # → regenerates examples/RESULTS.md
```
`examples/.env` is git-ignored. All personal data in the scripts and results is
**fictional**, generated for demonstration.

428
examples/RESULTS.md Normal file
View File

@@ -0,0 +1,428 @@
## Examples
> Real learner ↔ platform chat messages anonymized **live by Gemma 4** (via Infomaniak). All personal
> data is **fictional**. `mapping` is the secret re-identification key (kept by the operator); `legend`
> is safe to send downstream. **🆕 = an abbreviation the model coined on its own** — the attribute was
> not in the base prompt (e.g. passport, matricule, residence permit, plate, blood type, medical record).
### Single messages (complexity rising)
#### Case 1 — 1 personne — inscription simple
**User message:**
> Bonjour, je suis Camille Perret, mon e-mail est camille.perret@yopmail.com. Je souhaite m'inscrire à la formation « Bureautique de base ».
**Anonymized (what the model sees):**
> Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], mon e-mail est [PER_1.EMAIL:Professionnel]. Je souhaite m'inscrire à la formation « Bureautique de base ».
**`mapping` 🔒 (secret):**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:F]` | Camille |
| `[PER_1.NOM:F]` | Perret |
| `[PER_1.EMAIL:Professionnel]` | camille.perret@yopmail.com |
**`legend` 🏷️ (shareable):**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `F` | Féminin | |
| `NOM` | Nom de famille | |
| `EMAIL` | Adresse e-mail | |
**Round-trip:** ✅ restored identically · 4784 ms
#### Case 2 — 2 personnes — collègue + matricule
**User message:**
> Salut, c'est Diego Fontana (identifiant dfontana, 079 400 11 22). Je m'inscris avec mon collègue Rui Almeida à la formation « Machiniste » ; le matricule interne de Rui est EMP-88231.
**Anonymized (what the model sees):**
> Salut, c'est [PER_1.PRENOM:M] [PER_1.NOM:M] (identifiant [PER_1.ID:USER], [PER_1.TELEPHONE:MOBILE]). Je m'inscris avec mon collègue [PER_2.PRENOM:M] [PER_2.NOM:M] à la formation « Machiniste » ; le matricule interne de [PER_2.PRENOM:M] est [PER_2.ID:EMP].
**`mapping` 🔒 (secret):**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:M]` | Diego |
| `[PER_1.NOM:M]` | Fontana |
| `[PER_1.ID:USER]` | dfontana |
| `[PER_1.TELEPHONE:MOBILE]` | 079 400 11 22 |
| `[PER_2.PRENOM:M]` | Rui |
| `[PER_2.NOM:M]` | Almeida |
| `[PER_2.ID:EMP]` | EMP-88231 |
**`legend` 🏷️ (shareable):**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `M` | Masculin | |
| `NOM` | Nom de famille | |
| `ID` | Identifiant | 🆕 coined |
| `USER` | Identifiant utilisateur | 🆕 coined |
| `TELEPHONE` | Numéro de téléphone | |
| `MOBILE` | Téléphone mobile | 🆕 coined |
| `EMP` | Matricule employé | 🆕 coined |
**Round-trip:** ✅ restored identically · 8443 ms
#### Case 3 — 3 personnes + org — admission (passeport, permis, matricule)
**User message:**
> Dossier d'admission : Léna Hofmann, née le 03.11.1998, passeport suisse X1234567 et matricule étudiant 18-402-901, est parrainée par sa tutrice Sofia Marchetti et par son employeur, la société Béton Léman SA. Léna dispose aussi d'un permis de séjour de type B.
**Anonymized (what the model sees):**
> Dossier d'admission : [PER_1.PRENOM:F] [PER_1.NOM:F], née le [PER_1.DATE_NAISSANCE:1998], passeport suisse [PER_1.PASSPORT:CH] et matricule étudiant [PER_1.MATRICULE:ETUDIANT], est parrainée par sa tutrice [PER_2.PRENOM:F] [PER_2.NOM:F] et par son employeur, la société [ORG_1.NOM:Entreprise]. [PER_1.PRENOM:F] dispose aussi d'un permis de séjour de type B.
**`mapping` 🔒 (secret):**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:F]` | Léna |
| `[PER_1.NOM:F]` | Hofmann |
| `[PER_1.DATE_NAISSANCE:1998]` | 03.11.1998 |
| `[PER_1.PASSPORT:CH]` | X1234567 |
| `[PER_1.MATRICULE:ETUDIANT]` | 18-402-901 |
| `[PER_2.PRENOM:F]` | Sofia |
| `[PER_2.NOM:F]` | Marchetti |
| `[ORG_1.NOM:Entreprise]` | Béton Léman SA |
**`legend` 🏷️ (shareable):**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `F` | Féminin | |
| `NOM` | Nom de famille | |
| `DATE_NAISSANCE` | Date de naissance | |
| `PASSPORT` | Numéro de passeport | 🆕 coined |
| `CH` | Suisse | 🆕 coined |
| `MATRICULE` | Numéro de matricule | 🆕 coined |
| `ETUDIANT` | Étudiant | 🆕 coined |
| `ORG` | Organisation | |
**Round-trip:** ✅ restored identically · 9567 ms
#### Case 4 — 4 personnes + org — RH (permis conduire, plaque, LAMal, groupe sanguin)
**User message:**
> Bonjour, ici Patrick Nussbaum, responsable RH chez Migros Vaud (patrick.nussbaum@migros.ch, +41 21 924 11 11). J'inscris trois collaborateurs à « Sécurité au travail » : Chloé Aebischer (permis de conduire 5501234567, plaque VD 148 552), Deniz Yilmaz (numéro d'assuré LAMal 756.9911.2233.44, groupe sanguin A+) et Amir Haddad (permis de séjour C, matricule EMP-40127). Merci d'adresser la facture à l'IBAN CH70 0076 2011 6238 5295 7.
**Anonymized (what the model sees):**
> Bonjour, ici [PER_1.PRENOM:M] [PER_1.NOM:M], responsable RH chez [ORG_1.NOM:Entreprise] ([PER_1.EMAIL:Professionnel], [PER_1.TELEPHONE:Fixe]). J'inscris trois collaborateurs à « Sécurité au travail » : [PER_2.PRENOM:F] [PER_2.NOM:F] (permis de conduire [PER_2.PERMIS:Conduite], plaque [PER_2.PLAQUE:Vehicule]), [PER_3.PRENOM:M] [PER_3.NOM:M] (numéro d'assuré LAMal [PER_3.ASSURANCE:LAMal], groupe sanguin [PER_3.SANG:Type]) et [PER_4.PRENOM:M] [PER_4.NOM:M] (permis de séjour [PER_4.PERMIS:Residence], matricule [PER_4.MATRICULE:Employe]). Merci d'adresser la facture à l'IBAN [ORG_1.IBAN:Entreprise].
**`mapping` 🔒 (secret):**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:M]` | Patrick |
| `[PER_1.NOM:M]` | Nussbaum |
| `[ORG_1.NOM:Entreprise]` | Migros Vaud |
| `[PER_1.EMAIL:Professionnel]` | patrick.nussbaum@migros.ch |
| `[PER_1.TELEPHONE:Fixe]` | +41 21 924 11 11 |
| `[PER_2.PRENOM:F]` | Chloé |
| `[PER_2.NOM:F]` | Aebischer |
| `[PER_2.PERMIS:Conduite]` | 5501234567 |
| `[PER_2.PLAQUE:Vehicule]` | VD 148 552 |
| `[PER_3.PRENOM:M]` | Deniz |
| `[PER_3.NOM:M]` | Yilmaz |
| `[PER_3.ASSURANCE:LAMal]` | 756.9911.2233.44 |
| `[PER_3.SANG:Type]` | A+ |
| `[PER_4.PRENOM:M]` | Amir |
| `[PER_4.NOM:M]` | Haddad |
| `[PER_4.PERMIS:Residence]` | C |
| `[PER_4.MATRICULE:Employe]` | EMP-40127 |
| `[ORG_1.IBAN:Entreprise]` | CH70 0076 2011 6238 5295 7 |
**`legend` 🏷️ (shareable):**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `M` | Masculin | |
| `NOM` | Nom de famille | |
| `ORG` | Organisation | |
| `EMAIL` | Adresse électronique | |
| `TELEPHONE` | Numéro de téléphone | |
| `F` | Féminin | |
| `PERMIS` | Numéro de permis | 🆕 coined |
| `PLAQUE` | Plaque d'immatriculation | 🆕 coined |
| `ASSURANCE` | Numéro d'assurance | 🆕 coined |
| `SANG` | Groupe sanguin | 🆕 coined |
| `MATRICULE` | Numéro de matricule | 🆕 coined |
| `IBAN` | Numéro de compte bancaire | |
**Round-trip:** ✅ restored identically · 20162 ms
#### Case 5 — 5+ personnes — dossier médical complet (biométrie, RCC, dossier médical, allergie)
**User message:**
> Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent l'autorisation. Le suivi est assuré par le Dr Farah Haddad (n° RCC V123456), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent Kevin Zbinden de la caisse Helvetia. Pour accéder au laboratoire, Noah utilise l'identifiant biométrique BIO-7729.
**Anonymized (what the model sees):**
> Note de dossier complète : le patient mineur [PER_1.PRENOM:U] [PER_1.NOM:U] (né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin B+, numéro d'assuré maladie [PER_1.ASSUR_ID:U], allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, [PER_2.PRENOM:F] [PER_2.NOM:F] (mère, permis de séjour [PER_2.PERMIS:C], [PER_2.TELEPHONE:U]) et [PER_3.PRENOM:M] [PER_3.NOM:M] (père, passeport italien [PER_3.PASSPORT:U]), cosignent l'autorisation. Le suivi est assuré par le Dr [PER_4.PRENOM:F] [PER_4.NOM:F] (n° RCC [PER_4.RCC:U]), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent [PER_5.PRENOM:M] [PER_5.NOM:M] de la caisse [ORG_1.NOM:Assurance]. Pour accéder au laboratoire, [PER_1.PRENOM:U] utilise l'identifiant biométrique [PER_1.BIO_ID:U].
**`mapping` 🔒 (secret):**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:U]` | Noah |
| `[PER_1.NOM:U]` | Baumann |
| `[PER_1.DATE_NAISSANCE:2013]` | 12.06.2013 |
| `[PER_1.ASSUR_ID:U]` | 756.2211.9988.77 |
| `[PER_2.PRENOM:F]` | Delphine |
| `[PER_2.NOM:F]` | Rieder |
| `[PER_2.PERMIS:C]` | C |
| `[PER_2.TELEPHONE:U]` | 021 555 12 34 |
| `[PER_3.PRENOM:M]` | Marco |
| `[PER_3.NOM:M]` | Baumann |
| `[PER_3.PASSPORT:U]` | YA9087654 |
| `[PER_4.PRENOM:F]` | Farah |
| `[PER_4.NOM:F]` | Haddad |
| `[PER_4.RCC:U]` | V123456 |
| `[PER_5.PRENOM:M]` | Kevin |
| `[PER_5.NOM:M]` | Zbinden |
| `[ORG_1.NOM:Assurance]` | Helvetia |
| `[PER_1.BIO_ID:U]` | BIO-7729 |
**`legend` 🏷️ (shareable):**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `U` | Inconnu/Neutre | |
| `NOM` | Nom de famille | |
| `DATE_NAISSANCE` | Date de naissance | |
| `ASSUR_ID` | Numéro d'assuré | 🆕 coined |
| `F` | Féminin | |
| `PERMIS` | Type de permis de séjour | 🆕 coined |
| `C` | C | 🆕 coined |
| `TELEPHONE` | Numéro de téléphone | |
| `M` | Masculin | |
| `PASSPORT` | Numéro de passeport | 🆕 coined |
| `RCC` | Numéro RCC | 🆕 coined |
| `ORG` | Organisation | |
| `BIO_ID` | Identifiant biométrique | 🆕 coined |
**Round-trip:** ✅ restored identically · 19150 ms
### Multi-turn conversations (ids stay stable; new attributes appear as it escalates)
#### Conversation 1 — Inscription — rappel du matricule (T1 → T4)
| Turn | User message | Anonymized (model sees) |
| --- | --- | --- |
| 1 | Bonjour, je m'appelle Yasmine Cottier, matricule étudiant 21-305-118, je m'inscris à « Développement web ». | Bonjour, je m'appelle [PER_1.PRENOM:F] [PER_1.NOM:F], matricule étudiant [PER_1.MATRICULE:Etudiant], je m'inscris à « Développement web ». |
| 2 | Mon e-mail est yasmine.cottier@gmx.ch et mon téléphone 078 331 22 10. | Mon e-mail est [PER_1.EMAIL:Perso] et mon téléphone [PER_1.TELEPHONE:Mobile]. |
| 3 | Ma camarade Inès Lefort aimerait aussi rejoindre le cours. | Ma camarade [PER_2.PRENOM:F] [PER_2.NOM:F] aimerait aussi rejoindre le cours. |
| 4 | Pouvez-vous rattacher mon matricule 21-305-118 au dossier d'Inès pour le projet de groupe ? | Pouvez-vous rattacher mon matricule [PER_1.MATRICULE:Etudiant] au dossier d'[PER_2.PRENOM:F] pour le projet de groupe ? |
**Cumulative `mapping` 🔒:**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:F]` | Yasmine |
| `[PER_1.NOM:F]` | Cottier |
| `[PER_1.MATRICULE:Etudiant]` | 21-305-118 |
| `[PER_1.EMAIL:Perso]` | yasmine.cottier@gmx.ch |
| `[PER_1.TELEPHONE:Mobile]` | 078 331 22 10 |
| `[PER_2.PRENOM:F]` | Inès |
| `[PER_2.NOM:F]` | Lefort |
**`legend` 🏷️:**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `F` | Féminin | |
| `NOM` | Nom de famille | |
| `MATRICULE` | Numéro de matricule | 🆕 coined |
| `EMAIL` | Adresse e-mail | |
| `TELEPHONE` | Numéro de téléphone | |
**Auto-coined attributes:** `MATRICULE`
#### Conversation 2 — Identité — même nom de famille + rappel du passeport (T1 → T4)
| Turn | User message | Anonymized (model sees) |
| --- | --- | --- |
| 1 | Ici Tomás Varga, passeport P-CH 4451220, je confirme mon inscription. | Ici [PER_1.PRENOM:M] [PER_1.NOM:M], passeport [PER_1.PASSPORT:CH], je confirme mon inscription. |
| 2 | J'ai également un permis de séjour de type C. | J'ai également un permis de séjour de type C. |
| 3 | Mon épouse Ana Varga s'inscrit aussi ; son passeport est P-CH 4451221. | Mon épouse [PER_2.PRENOM:F] [PER_1.NOM:M] s'inscrit aussi ; son passeport est [PER_2.PASSPORT:CH]. |
| 4 | Utilisez le passeport P-CH 4451220 pour la facturation à mon nom. | Utilisez le passeport [PER_1.PASSPORT:CH] pour la facturation à mon nom. |
**Cumulative `mapping` 🔒:**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:M]` | Tomás |
| `[PER_1.NOM:M]` | Varga |
| `[PER_1.PASSPORT:CH]` | P-CH 4451220 |
| `[PER_2.PRENOM:F]` | Ana |
| `[PER_2.PASSPORT:CH]` | P-CH 4451221 |
**`legend` 🏷️:**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `M` | Masculin | |
| `NOM` | Nom de famille | |
| `PASSPORT` | Numéro de passeport | 🆕 coined |
| `CH` | Suisse | 🆕 coined |
| `F` | Féminin | |
**Auto-coined attributes:** `PASSPORT`, `CH`
#### Conversation 3 — Transport — 3 personnes + rappel de la plaque (T1 → T4)
| Turn | User message | Anonymized (model sees) |
| --- | --- | --- |
| 1 | Salut, c'est Bruno Steiner, plaque d'immatriculation GE 289 774, je viens en voiture. | Salut, c'est [PER_1.PRENOM:M] [PER_1.NOM:M], plaque d'immatriculation [VEH_1.PLAQUE:GE], je viens en voiture. |
| 2 | Mes collègues Farah Haddad et Kevin Zbinden covoiturent avec moi. | Mes collègues [PER_2.PRENOM:F] [PER_2.NOM:F] et [PER_3.PRENOM:M] [PER_3.NOM:M] covoiturent avec moi. |
| 3 | Le permis de conduire de Farah est 6602345678. | Le permis de conduire de [PER_2.PRENOM:F] est [PER_2.PERMIS:ID]. |
| 4 | Réservez la place de parking pour la plaque GE 289 774, merci. | Réservez la place de parking pour la plaque [VEH_1.PLAQUE:GE], merci. |
**Cumulative `mapping` 🔒:**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:M]` | Bruno |
| `[PER_1.NOM:M]` | Steiner |
| `[VEH_1.PLAQUE:GE]` | GE 289 774 |
| `[PER_2.PRENOM:F]` | Farah |
| `[PER_2.NOM:F]` | Haddad |
| `[PER_3.PRENOM:M]` | Kevin |
| `[PER_3.NOM:M]` | Zbinden |
| `[PER_2.PERMIS:ID]` | 6602345678 |
**`legend` 🏷️:**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `M` | Masculin | |
| `NOM` | Nom de famille | |
| `VEH` | Véhicule | 🆕 coined |
| `PLAQUE` | Plaque d'immatriculation | 🆕 coined |
| `GE` | Genève | 🆕 coined |
| `F` | Féminin | |
| `PERMIS` | Numéro de permis de conduire | 🆕 coined |
| `ID` | Identifiant | 🆕 coined |
**Auto-coined attributes:** `VEH`, `PLAQUE`, `GE`, `PERMIS`, `ID`
#### Conversation 4 — Santé — rappel du numéro LAMal (T1 → T5)
| Turn | User message | Anonymized (model sees) |
| --- | --- | --- |
| 1 | Bonjour, je suis Salomé Girard, numéro d'assurée LAMal 756.9911.2233.44, apprentie infirmière. | Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], numéro d'assurée LAMal [PER_1.LAMAL:ASSURANCE], apprentie infirmière. |
| 2 | Mon groupe sanguin est A+ et je suis allergique au latex. | Mon groupe sanguin est [PER_1.SANG:TYPE] et je suis allergique au latex. |
| 3 | Mon superviseur de stage est le Dr Killian Roux. | Mon superviseur de stage est le Dr [PER_2.PRENOM:M] [PER_2.NOM:M]. |
| 4 | Ma collègue Priya Nair, groupe sanguin O-, partage le même planning. | Ma collègue [PER_3.PRENOM:F] [PER_3.NOM:F], groupe sanguin [PER_3.SANG:TYPE], partage le même planning. |
| 5 | Facturez le stage sur mon assurance LAMal 756.9911.2233.44. | Facturez le stage sur mon assurance LAMal [PER_1.LAMAL:ASSURANCE]. |
**Cumulative `mapping` 🔒:**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:F]` | Salomé |
| `[PER_1.NOM:F]` | Girard |
| `[PER_1.LAMAL:ASSURANCE]` | 756.9911.2233.44 |
| `[PER_1.SANG:TYPE]` | A+ |
| `[PER_2.PRENOM:M]` | Killian |
| `[PER_2.NOM:M]` | Roux |
| `[PER_3.PRENOM:F]` | Priya |
| `[PER_3.NOM:F]` | Nair |
| `[PER_3.SANG:TYPE]` | O- |
**`legend` 🏷️:**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `F` | Féminin | |
| `NOM` | Nom de famille | |
| `LAMAL` | Numéro d'assurance maladie | 🆕 coined |
| `ASSURANCE` | Assurance | 🆕 coined |
| `SANG` | Groupe sanguin | 🆕 coined |
| `TYPE` | Type sanguin | 🆕 coined |
| `M` | Masculin | |
**Auto-coined attributes:** `LAMAL`, `ASSURANCE`, `SANG`, `TYPE`
#### Conversation 5 — Dossier complexe — 4 personnes, rappel du groupe sanguin (T1 → T6)
| Turn | User message | Anonymized (model sees) |
| --- | --- | --- |
| 1 | Je vous écris pour mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+. | Je vous écris pour mon fils mineur, [PER_1.PRENOM:U] [PER_1.NOM:U], né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin [PER_1.SANG:B+]. |
| 2 | Son numéro d'assuré maladie est 756.2211.9988.77. | Son numéro d'assuré maladie est [PER_1.AVS:SUI]. |
| 3 | Moi, sa mère Delphine Rieder, je gère le dossier ; mon permis de séjour est de type C. | Moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F], je gère le dossier ; mon permis de séjour est de type [PER_2.PERMIS:C]. |
| 4 | Son père, Marco Baumann, passeport italien YA9087654, cosigne l'autorisation. | Son père, [PER_3.PRENOM:M] [PER_1.NOM:U], passeport italien [PER_3.PASSPORT:ITA], cosigne l'autorisation. |
| 5 | Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah. | Le [PER_4.TITRE:MED] [PER_4.PRENOM:F] [PER_4.NOM:F] a ouvert le dossier médical [PER_1.DOSSIER:MED] pour [PER_1.PRENOM:U]. |
| 6 | Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge d'accès au laboratoire. | Rappel : le groupe sanguin [PER_1.SANG:B+] de [PER_1.PRENOM:U] doit figurer sur son badge d'accès au laboratoire. |
**Cumulative `mapping` 🔒:**
| Placeholder | Real value |
| --- | --- |
| `[PER_1.PRENOM:U]` | Noah |
| `[PER_1.NOM:U]` | Baumann |
| `[PER_1.DATE_NAISSANCE:2013]` | 12.06.2013 |
| `[PER_1.SANG:B+]` | B+ |
| `[PER_1.AVS:SUI]` | 756.2211.9988.77 |
| `[PER_2.PRENOM:F]` | Delphine |
| `[PER_2.NOM:F]` | Rieder |
| `[PER_2.PERMIS:C]` | C |
| `[PER_3.PRENOM:M]` | Marco |
| `[PER_3.PASSPORT:ITA]` | YA9087654 |
| `[PER_4.TITRE:MED]` | Dr |
| `[PER_4.PRENOM:F]` | Farah |
| `[PER_4.NOM:F]` | Haddad |
| `[PER_1.DOSSIER:MED]` | DM-2025-0417 |
**`legend` 🏷️:**
| Abbrev. | Meaning | |
| --- | --- | --- |
| `PER` | Personne | |
| `PRENOM` | Prénom | |
| `U` | Inconnu | |
| `NOM` | Nom de famille | |
| `DATE_NAISSANCE` | Date de naissance | |
| `SANG` | Groupe sanguin | 🆕 coined |
| `AVS` | Numéro d'assuré | |
| `SUI` | Suisse | 🆕 coined |
| `F` | Féminin | |
| `PERMIS` | Type de permis de séjour | 🆕 coined |
| `C` | Permis d'établissement | 🆕 coined |
| `M` | Masculin | |
| `PASSPORT` | Numéro de passeport | 🆕 coined |
| `ITA` | Italie | 🆕 coined |
| `TITRE` | Titre professionnel | 🆕 coined |
| `MED` | Médical | 🆕 coined |
| `DOSSIER` | Numéro de dossier | 🆕 coined |
**Auto-coined attributes:** `SANG`, `SUI`, `PERMIS`, `C`, `PASSPORT`, `ITA`, `TITRE`, `MED`, `DOSSIER`

287
examples/demo.mjs Normal file
View File

@@ -0,0 +1,287 @@
import { Anonymizer, openAICompatibleProvider, AnonymizationError } from '@mobiletic/anonymizer';
import { writeFileSync } from 'node:fs';
// ── LLM-only (no regex patterns) → everything is handled purely by the model.
// Infomaniak is a trusted processor (it already sees the cleartext being anonymized),
// so we feed the anonymizer the running mapping for the sharpest cross-turn coreference.
const anonymizer = new Anonymizer({
llm: openAICompatibleProvider({
baseUrl: process.env.INFOMANIAK_AI_BASE_URL,
apiKey: process.env.INFOMANIAK_AI_API_KEY,
model: process.env.GEMMA_MODEL,
timeoutMs: Number(process.env.PRIVACY_GEMMA_TIMEOUT_MS ?? 60000),
retries: 2,
includeMappingInContext: true,
}),
logger: { warn: (m) => console.warn(' ⚠ ', m) },
});
// Entities/attributes/context codes present in the default prompt. Anything in a
// result legend that is NOT here was COINED by the model on its own.
const BASE_VOCAB = new Set([
'PER',
'ORG',
'LOC',
'NOM',
'PRENOM',
'DATE_NAISSANCE',
'AGE',
'ADRESSE',
'EMAIL',
'TELEPHONE',
'AVS',
'IBAN',
'NSS',
'M',
'F',
'U',
'Mineur',
'Adulte',
'Ville',
'Rue',
'NPA',
'Pays',
'Entreprise',
'Ecole',
]);
const coined = (legend) => Object.keys(legend).filter((k) => !BASE_VOCAB.has(k));
// ── Single-message cases — complexity rises, and cases 35 carry RARE attributes
// NOT in the prompt (passport, matricule, residence permit, plate, health-insurance
// number, blood type, medical-record no., allergy) that the model must detect + name itself.
// All personal data is FICTIONAL.
const cases = [
{
title: '1 personne — inscription simple',
text: "Bonjour, je suis Camille Perret, mon e-mail est camille.perret@yopmail.com. Je souhaite m'inscrire à la formation « Bureautique de base ».",
},
{
title: '2 personnes — collègue + matricule',
text: "Salut, c'est Diego Fontana (identifiant dfontana, 079 400 11 22). Je m'inscris avec mon collègue Rui Almeida à la formation « Machiniste » ; le matricule interne de Rui est EMP-88231.",
},
{
title: '3 personnes + org — admission (passeport, permis, matricule)',
text: "Dossier d'admission : Léna Hofmann, née le 03.11.1998, passeport suisse X1234567 et matricule étudiant 18-402-901, est parrainée par sa tutrice Sofia Marchetti et par son employeur, la société Béton Léman SA. Léna dispose aussi d'un permis de séjour de type B.",
},
{
title: '4 personnes + org — RH (permis conduire, plaque, LAMal, groupe sanguin)',
text: "Bonjour, ici Patrick Nussbaum, responsable RH chez Migros Vaud (patrick.nussbaum@migros.ch, +41 21 924 11 11). J'inscris trois collaborateurs à « Sécurité au travail » : Chloé Aebischer (permis de conduire 5501234567, plaque VD 148 552), Deniz Yilmaz (numéro d'assuré LAMal 756.9911.2233.44, groupe sanguin A+) et Amir Haddad (permis de séjour C, matricule EMP-40127). Merci d'adresser la facture à l'IBAN CH70 0076 2011 6238 5295 7.",
},
{
title: '5+ personnes — dossier médical complet (biométrie, RCC, dossier médical, allergie)',
text: "Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent l'autorisation. Le suivi est assuré par le Dr Farah Haddad (n° RCC V123456), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent Kevin Zbinden de la caisse Helvetia. Pour accéder au laboratoire, Noah utilise l'identifiant biométrique BIO-7729.",
},
];
// ── Multi-turn conversations — escalating, with rarer attributes surfacing later.
// Harder conversations: 4+ turns, up to 4 people, and a LONG-RANGE back-reference
// (an attribute stated in turn 1 is recalled in a later turn) to stress session continuity.
const conversations = [
{
title: 'Inscription — rappel du matricule (T1 → T4)',
turns: [
"Bonjour, je m'appelle Yasmine Cottier, matricule étudiant 21-305-118, je m'inscris à « Développement web ».",
'Mon e-mail est yasmine.cottier@gmx.ch et mon téléphone 078 331 22 10.',
'Ma camarade Inès Lefort aimerait aussi rejoindre le cours.',
"Pouvez-vous rattacher mon matricule 21-305-118 au dossier d'Inès pour le projet de groupe ?",
],
},
{
title: 'Identité — même nom de famille + rappel du passeport (T1 → T4)',
turns: [
'Ici Tomás Varga, passeport P-CH 4451220, je confirme mon inscription.',
"J'ai également un permis de séjour de type C.",
"Mon épouse Ana Varga s'inscrit aussi ; son passeport est P-CH 4451221.",
'Utilisez le passeport P-CH 4451220 pour la facturation à mon nom.',
],
},
{
title: 'Transport — 3 personnes + rappel de la plaque (T1 → T4)',
turns: [
"Salut, c'est Bruno Steiner, plaque d'immatriculation GE 289 774, je viens en voiture.",
'Mes collègues Farah Haddad et Kevin Zbinden covoiturent avec moi.',
'Le permis de conduire de Farah est 6602345678.',
'Réservez la place de parking pour la plaque GE 289 774, merci.',
],
},
{
title: 'Santé — rappel du numéro LAMal (T1 → T5)',
turns: [
"Bonjour, je suis Salomé Girard, numéro d'assurée LAMal 756.9911.2233.44, apprentie infirmière.",
'Mon groupe sanguin est A+ et je suis allergique au latex.',
'Mon superviseur de stage est le Dr Killian Roux.',
'Ma collègue Priya Nair, groupe sanguin O-, partage le même planning.',
'Facturez le stage sur mon assurance LAMal 756.9911.2233.44.',
],
},
{
title: 'Dossier complexe — 4 personnes, rappel du groupe sanguin (T1 → T6)',
turns: [
'Je vous écris pour mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+.',
"Son numéro d'assuré maladie est 756.2211.9988.77.",
'Moi, sa mère Delphine Rieder, je gère le dossier ; mon permis de séjour est de type C.',
"Son père, Marco Baumann, passeport italien YA9087654, cosigne l'autorisation.",
'Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah.',
"Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge d'accès au laboratoire.",
],
},
];
const line = (c = '─') => console.log(c.repeat(78));
const fmt = (o) => JSON.stringify(o, null, 2).replace(/\n/g, '\n ');
const results = [];
const convResults = [];
async function runCase(i, { title, text }) {
line('═');
console.log(`CAS ${i}${title}`);
line();
console.log('MESSAGE :', text);
try {
const t0 = Date.now();
const { anon, mapping, legend } = await anonymizer.anonymize(text);
const ms = Date.now() - t0;
const restored = anonymizer.deanonymize(anon, mapping);
const ok = restored === text;
console.log(`ANONYMISÉ : ${anon} (${ms} ms)`);
console.log('MAPPING 🔒 :', fmt(mapping));
console.log('LEGEND 🏷️ :', fmt(legend), ' coined:', coined(legend).join(', ') || '—');
console.log(ok ? '✓ round-trip identique' : '✗ round-trip DIFFÈRE');
results.push({ i, title, text, anon, mapping, legend, ms, ok });
} catch (err) {
const msg = err instanceof AnonymizationError ? err.message : String(err);
console.error('✗', msg);
results.push({ i, title, text, error: msg });
}
console.log();
}
async function runConversation(i, { title, turns }) {
line('═');
console.log(`CONVERSATION ${i}${title}`);
line();
const conv = anonymizer.conversation();
const rows = [];
for (const text of turns) {
try {
const { anon } = await conv.anonymize(text);
const ok = conv.deanonymize(anon) === text;
console.log(` utilisateur : ${text}`);
console.log(` anonymisé : ${anon}`);
rows.push({ text, anon, ok });
} catch (err) {
console.error(' ✗', err?.message ?? String(err));
rows.push({ text, error: err?.message ?? String(err) });
}
}
const s = conv.session();
console.log('MAPPING 🔒 cumulé :', fmt(s.mapping));
console.log('coined :', coined(s.legend).join(', ') || '—');
convResults.push({ i, title, rows, mapping: s.mapping, legend: s.legend });
console.log();
}
// ── README-ready Markdown ───────────────────────────────────────────────────
const esc = (v) => String(v).replace(/\|/g, '\\|');
function kvTable(obj, [h1, h2], markCoined = false) {
const rows = Object.entries(obj);
if (!rows.length) return '_(vide)_';
return [
`| ${h1} | ${h2} |`,
'| --- | --- |',
...rows.map(([k, v]) => `| \`${esc(k)}\` | ${esc(v)} |`),
].join('\n');
}
function legendTable(legend) {
const rows = Object.entries(legend);
if (!rows.length) return '_(vide)_';
return [
'| Abbrev. | Meaning | |',
'| --- | --- | --- |',
...rows.map(([k, v]) => `| \`${esc(k)}\` | ${esc(v)} | ${BASE_VOCAB.has(k) ? '' : '🆕 coined'} |`),
].join('\n');
}
function toMarkdown() {
const parts = [
'## Examples',
'',
'> Real learner ↔ platform chat messages anonymized **live by Gemma 4** (via Infomaniak). All personal',
'> data is **fictional**. `mapping` is the secret re-identification key (kept by the operator); `legend`',
'> is safe to send downstream. **🆕 = an abbreviation the model coined on its own** — the attribute was',
'> not in the base prompt (e.g. passport, matricule, residence permit, plate, blood type, medical record).',
'',
'### Single messages (complexity rising)',
'',
];
for (const r of results) {
parts.push(`#### Case ${r.i}${r.title}`, '');
if (r.error) {
parts.push(`> ${r.text}`, '', `\`${r.error}\``, '');
continue;
}
parts.push(
'**User message:**',
'',
`> ${r.text}`,
'',
'**Anonymized (what the model sees):**',
'',
`> ${r.anon}`,
'',
'**`mapping` 🔒 (secret):**',
'',
kvTable(r.mapping, ['Placeholder', 'Real value']),
'',
'**`legend` 🏷️ (shareable):**',
'',
legendTable(r.legend),
'',
`**Round-trip:** ${r.ok ? '✅ restored identically' : '⚠️ differs'} · ${r.ms} ms`,
'',
);
}
parts.push('### Multi-turn conversations (ids stay stable; new attributes appear as it escalates)', '');
for (const c of convResults) {
parts.push(
`#### Conversation ${c.i}${c.title}`,
'',
'| Turn | User message | Anonymized (model sees) |',
'| --- | --- | --- |',
);
c.rows.forEach((row, idx) => {
const shown = row.error ? `${row.error}` : esc(row.anon);
parts.push(`| ${idx + 1} | ${esc(row.text)} | ${shown} |`);
});
parts.push(
'',
'**Cumulative `mapping` 🔒:**',
'',
kvTable(c.mapping, ['Placeholder', 'Real value']),
'',
'**`legend` 🏷️:**',
'',
legendTable(c.legend),
'',
);
const co = coined(c.legend);
if (co.length) parts.push(`**Auto-coined attributes:** ${co.map((k) => `\`${k}\``).join(', ')}`, '');
}
return parts.join('\n');
}
console.log('\n@mobiletic/anonymizer — démonstration live (Infomaniak)\n');
console.log(
`Modèle: ${process.env.GEMMA_MODEL || '(non défini)'} · Endpoint: ${process.env.INFOMANIAK_AI_BASE_URL || '(non défini)'}\n`,
);
for (let i = 0; i < cases.length; i++) await runCase(i + 1, cases[i]);
for (let i = 0; i < conversations.length; i++) await runConversation(i + 1, conversations[i]);
writeFileSync(new URL('./RESULTS.md', import.meta.url), toMarkdown());
line('═');
console.log(
`Terminé. Single: ${results.filter((r) => r.ok).length}/${results.length} round-trips OK · ` +
`${convResults.length} conversations. Résultats écrits dans RESULTS.md`,
);

View File

@@ -44,6 +44,7 @@
},
"scripts": {
"build": "tsup",
"example": "npm run build && node --env-file=examples/.env examples/demo.mjs",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",