@tricoteuses/occultatrice
v0.1.3
Published
Occultation par pseudonymes des personnes physiques dans les documents juridiques structurés
Readme
Tricoteuses Occultatrice
Cœur d'occultation par pseudonymes des personnes physiques dans les corpus à individus privés (nominations JORF, décisions de jurisprudence, actes RAA).
Reçu un document structuré (texte intégral et entités déjà typées), le module produit :
- la copie occultée : les noms des personnes sont remplacés par des pseudonymes en convention judiciaire (« M. X », « Mme Y »…), attribués par ordre d'apparition dans le document ; une même personne reçoit le même pseudonyme dans tout le document ;
- le manifeste de substitutions : positions exactes de chaque substitution, dans le texte original comme dans la copie. Il sert de piste d'audit privée et alimentera le caviardage PDF.
Le texte original n'est jamais modifié : l'occultation est une fonction pure, déterministe et sans dépendance d'exécution.
Usage
import { chercherResidus, occulterDocument } from "@tricoteuses/occultatrice"
const document = {
texte: "En qualité de secrétaire général : Mme Jeanne MARTIN.",
personnes: [
{
personneId: "jorf:personne:123",
civilite: "Mme",
formes: ["Jeanne MARTIN", "MARTIN Jeanne"],
},
],
}
const { texteOcculte, manifeste } = occulterDocument(document)
// texteOcculte === "En qualité de secrétaire général : Mme X."
// manifeste.substitutions[0].debut / .fin : positions dans le texte original
// manifeste.substitutions[0].debutOcculte / .finOcculte : positions dans la copie
// Contrôle qu'aucune chaîne occultée ne subsiste dans la copie :
const residus = chercherResidus(texteOcculte, manifeste, ["Jeanne", "MARTIN"])Règles de détection et de nommage
- Entités structurées exhaustives d'abord : les
personnesdéclarées (signataires, orateurs, nominés…) sont recherchées dans le texte, sans distinction de casse ni d'accents, aux frontières de mots ; la plus longue correspondance à chaque position l'emporte. Une mention NER qui chevauche une occurrence structurée d'une autre personne est écartée ; si elle couvre la même personne (après fusion d'identité), les intervalles sont fusionnés en une substitution unique (« Alain FAURE » détecté par NER étend « FAURE » déclaré en structuré, civilité comprise).- Mentions NER ensuite : lesmentions(intervalles déjà détectés) complètent les zones restantes. Les mentions sont regroupées parpersonneIdexplicite, ou à défaut par forme normalisée ; un groupe sans identité explicite est fusionné avec une personne structurée lorsque sa forme normalisée est égale — ou sous-suite ordonnée de mots, dans un sens ou dans l'autre — d'exactement une personne structurée (ainsi « FAURE », détecté par NER, reçoit le pseudonyme de « Alain FAURE », déclaré en structuré). - Pseudonymes :
M. X,Mme Y,M. Z, puisM. A… au-delà, une seconde passe ajoute un numéro (M. X2), garantissant l'unicité. La civilité du pseudonyme vient de l'entité déclarée, ou pour une mention NER du jeton de civilité qui précède le nom (« M. », « Mme », « Madame »…) ou deciviliteParDefaut(« M » par défaut). - Identifiants implicites : les personnes détectées uniquement par NER
reçoivent un identifiant dérivé (
forme:<clé normalisée>ouid:<personneId>), suffixé#2,#3… si cet identifiant est déjà déclaré dans le document — jamais de collision entre personnes. - Absorption des civilités : le jeton « M. » / « Mme » / « Madame »…
qui précède immédiatement un nom est englobé dans la substitution, pour
produire « Mme X » et non « Mme Mme X » (désactivable avec
absorberCivilites: false). - Déterminisme : deux exécutions sur le même document produisent exactement la même copie et le même manifeste.
- Tous les indices sont des offsets en unités UTF-16 (indices natifs
JavaScript, directement utilisables avec
String.prototype.slice).
Doctrine des signataires (ADR §2, révisé le 2026-10-18)
Le cœur occulte ce qu'on lui fournit ; la doctrine de sélection des cibles
est une fonction séparée, à appliquer avant occulterDocument :
import { occulterDocument, selectionnerCibles } from "@tricoteuses/occultatrice"
const cibles = selectionnerCibles(texte, personnes, mentions, {
blocsSignature: [{ debut, fin }], // bornes exactes des blocs de signature
visas: [{ debut, fin }],
})
const { texteOcculte, manifeste } = occulterDocument({
texte,
personnes: cibles.personnes,
mentions: cibles.mentions,
})Les signataires des actes officiels ne sont pas des cibles : une personne qui signe et dont aucune occurrence ne sort des blocs de signature, des visas ou d'une formule d'organe chargé de l'exécution sort de l'occultation ; un signataire aussi visé reste masqué, signature comprise. Une personne citée hors signature, visas compris, reste ciblée. Les mentions NER du bloc de signature ne sont gardées que si elles se rattachent à une personne visée. La doctrine vit une seule fois, dans ce module : la chaîne structurée fournit ses zones exactes par segments de projection, la chaîne de caviardage PDF d'Exploratrice consomme le même socle avec ses zones détectées sur le texte brut. Limites partagées par les deux chaînes : les organes coordonnés (« M. X et Mme Y sont chargés de l'exécution ») ne sont pas reconnus — sur-masquage, dans le sens sûr.
Détection NER auto-hébergée (dégradable)
La détection du texte libre s'appuie sur des services NER auto-hébergés, branchés en environnement de développement :
- la pseudonymisation-api de la Cour de cassation
(
Cour-de-cassation/pseudonymisation-api, code sous licence MIT) — point de terminaisonPOST /ner, contratDecisionde juritools (idLabel,idDecision,sourceId,sourceName,text,categories; identifiants transmissibles via l'optiondocument) ; étiquettespersonnePhysiquepar défaut (les personnes en fonction publique, commeprofessionnelMagistratGreffier, ne sont pas occultées) ; - Presidio (
microsoft/presidio, code sous licence MIT) avec le pipeline spaCyfr_core_news_lg(code spaCy sous MIT, modèle sous licence LGPL-LR).
Les positions renvoyées par ces services Python comptent des points de
code : elles sont converties en unités UTF-16 à la réception ; les
entités de la pseudonymisation-api sont en outre vérifiées contre le texte
envoyé (champ text de chaque entité) — toute entité incohérente est
écartée plutôt que mal découpée.
import {
creerDetecteurPseudonymisationApi,
occulterDocumentAvecDetection,
} from "@tricoteuses/occultatrice"
const detecteur = creerDetecteurPseudonymisationApi({
url: process.env.PSEUDONYMISATION_API_URL ?? "http://localhost:8081",
// jeton: process.env.PSEUDONYMISATION_API_TOKEN, // si l'instance en exige un
})
const { resultat, detection } = await occulterDocumentAvecDetection(
document,
detecteur,
)
if (detection.degrade) {
// Service injoignable ou réponse inutilisable : l'occultation a reposé
// sur les seules entités structurées (posture best-effort documentée).
console.warn(detection.detail)
}Dégradation : par défaut (strict: false), un service injoignable, lent
(delaiExpirationMs, 10 s par défaut), en erreur HTTP ou de réponse
inutilisable retourne { mentions: [], degrade: true, detail } au lieu de
lever — l'occultation repose alors sur les seules entités structurées. Avec
strict: true, l'erreur est levée. Aucune clé ni URL sensible n'est codée en
dur : la configuration passe par les options (typiquement des variables
d'environnement du paquet consommateur).
Posture best-effort
L'occultation ne vaut pas anonymisation au sens juridique : la détection est
best-effort, les corrections se font sur signalement par rejeu du pipeline,
sans altérer l'original. Voir l'ADR
docs/architecture/ARCHITECTURE_DECISION_MASQUAGE_PERSONNES_PHYSIQUES.md et
la spec (ticket Forgejo #276).
Commandes
npm run build # construction (vite, ESM + types)
npm run check # vérification des types (tsc --noEmit)
npm run lint # eslint
npm test # tests (vitest)
npm run format # prettier