@piecemaker-legal/piecemaker 2.0.10 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BBxf1he7.js} +1 -1
  2. package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-BYqp6U4x.js} +1 -1
  3. package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-BVC9zOMs.js} +4 -4
  4. package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-Xn2mC64y.js} +1 -1
  5. package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-CFcdqEif.js} +1 -1
  6. package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DECaeRS9.js} +1 -1
  7. package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-ByYa04H_.js} +1 -1
  8. package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-BuyqZB5A.js} +1 -1
  9. package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-ByZ1Acr2.js} +1 -1
  10. package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-BtttzdZA.js} +1 -1
  11. package/dist/assets/{arc-BK1oUIk7.js → arc-DN4m1tLs.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-Bt-zFumG.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-D14V8v4j.js} +1 -1
  14. package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-BynDo1-W.js} +1 -1
  15. package/dist/assets/channel-C4gJXzYa.js +1 -0
  16. package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-B8gKhSj9.js} +1 -1
  17. package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-GvOpglRR.js} +1 -1
  18. package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-CAgZy0qc.js} +1 -1
  19. package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-gf8Ux8Zd.js} +1 -1
  20. package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-D4zFFRMp.js} +1 -1
  21. package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-Ct8X_LaT.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-g5iH1bs5.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-89-VFKTn.js} +1 -1
  24. package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-CaQbdObb.js} +1 -1
  25. package/dist/assets/classDiagram-ZZMXUADV-Cm08FeYk.js +1 -0
  26. package/dist/assets/classDiagram-v2-VYDZK3BY-Cm08FeYk.js +1 -0
  27. package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-BiYOGJAU.js} +1 -1
  28. package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-F6mJ-4tj.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-q-Rggn6r.js} +1 -1
  30. package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-COqImSUH.js} +1 -1
  31. package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-BAg1dyEc.js} +1 -1
  32. package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-DFJS8qCV.js} +1 -1
  33. package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-BfldL35r.js} +1 -1
  34. package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-kLjovpYe.js} +1 -1
  35. package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-Dhwvi3Iu.js} +1 -1
  36. package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-Bp4YE1i7.js} +1 -1
  37. package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-BKDdaSWz.js} +1 -1
  38. package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-zHProVxL.js} +1 -1
  39. package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-B-B-BI6j.js} +1 -1
  40. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BTHMg5CH.js} +1 -1
  41. package/dist/assets/index-C5slQNvE.css +1 -0
  42. package/dist/assets/{index-Bk2iXKnJ.js → index-CJP4dZ-c.js} +146 -146
  43. package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-mabp4H9I.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-CLNhcCrM.js} +1 -1
  45. package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-CGEi2RB6.js} +1 -1
  46. package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-B2fLX1fK.js} +1 -1
  47. package/dist/assets/{layout-CTfb1sAF.js → layout-CloKWsFq.js} +1 -1
  48. package/dist/assets/{linear-Dfgi6vlo.js → linear-BFyydI0c.js} +1 -1
  49. package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-Dxl3LVAh.js} +6 -6
  50. package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-FT4pm55s.js} +1 -1
  51. package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-okyIfJWM.js} +1 -1
  52. package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-NF61CQz8.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-uVfvZzzt.js} +1 -1
  54. package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dp8IpeoH.js} +1 -1
  55. package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-D8VV0oQe.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-D-xhxjob.js} +1 -1
  57. package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-BAQBK95h.js} +1 -1
  58. package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-C6rk9SVx.js} +1 -1
  59. package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-DshqaEOj.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-BohZIrk5.js} +1 -1
  61. package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-DgcPDy4l.js} +1 -1
  62. package/dist/assets/swimlanesDiagram-VR7AAH4N-CuM3Su0K.js +8 -0
  63. package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-OgpIe-gQ.js} +1 -1
  64. package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-BZzGd6jt.js} +1 -1
  65. package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-1tg-H7l_.js} +1 -1
  66. package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-ByTsX8g0.js} +1 -1
  67. package/dist/index.html +2 -2
  68. package/dist/sw.js +1 -1
  69. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
  70. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
  71. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
  72. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
  73. package/dist-server/server/piecemaker/agent-instructions.js +148 -0
  74. package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
  75. package/dist-server/server/piecemaker/index.js +4 -0
  76. package/dist-server/server/piecemaker/index.js.map +1 -1
  77. package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
  78. package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
  79. package/dist-server/server/piecemaker/knowledge/service.js +1 -16
  80. package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
  81. package/dist-server/server/piecemaker/project-registry.js +5 -5
  82. package/dist-server/server/piecemaker/project-registry.js.map +1 -1
  83. package/package.json +1 -1
  84. package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
  85. package/server/piecemaker/agent-instructions.ts +148 -0
  86. package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
  87. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
  88. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
  89. package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
  90. package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
  91. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
  92. package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
  93. package/server/piecemaker/anonymizer/service.cjs +10 -8
  94. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +15 -32
  95. package/server/piecemaker/index.ts +5 -1
  96. package/server/piecemaker/knowledge/pipeline.ts +24 -31
  97. package/server/piecemaker/knowledge/service.ts +1 -14
  98. package/server/piecemaker/project-registry.ts +6 -6
  99. package/server/piecemaker/router.cjs +0 -1
  100. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
  101. package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
  102. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
  103. package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
  104. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
  105. package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
  106. package/server/piecemaker/vendor/mcp/README.md +1 -1
  107. package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
  108. package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
  109. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
  110. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
  111. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
  112. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
  113. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
  114. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
  115. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
  116. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
  117. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
  118. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
  119. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
  120. package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
  121. package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
  122. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +3 -179
  123. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
  124. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
  125. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
  126. package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
  127. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
  128. package/dist/assets/channel-DVyNr2Yg.js +0 -1
  129. package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
  130. package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
  131. package/dist/assets/index-Dc9wNuS4.css +0 -1
  132. package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +0 -8
  133. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
  134. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
  135. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
  136. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +0 -83
@@ -6,14 +6,12 @@
6
6
  * les pièces du dossier, sans restriction de zone. Le Markdown d'une pièce de
7
7
  * `01_CORRESPONDANCE` ou `02_DATA_ROOM` est rangé dans le sous-dossier de
8
8
  * conversion métier correspondant ; celui d'une pièce hors de ces deux zones
9
- * rejoint le sous-dossier de travail générique. Le mapping canonique reste
10
- * dans `Fichiers convertis PieceMaker/` et l'état technique dans
11
- * `.piecemaker/anonymization-state.json`. Seules les lignes `PROGRESS:` et un
9
+ * rejoint le sous-dossier de travail générique. Le mapping vit en base et
10
+ * l'état technique dans `.piecemaker/anonymization-state.json`. Seules les lignes `PROGRESS:` et un
12
11
  * extrait d'erreur sont conservés dans le journal d'un travail : la sortie
13
12
  * brute des scripts peut contenir du texte de pièce, qui ne doit jamais
14
13
  * remonter dans l'interface.
15
14
  */
16
- const crypto = require('node:crypto');
17
15
  const fs = require('node:fs');
18
16
  const os = require('node:os');
19
17
  const path = require('node:path');
@@ -23,28 +21,12 @@ const {
23
21
  terminateProcessTree,
24
22
  } = require('./process-group.cjs');
25
23
 
26
- const {
27
- originalFilesOverview,
28
- safeCaseFiles,
29
- } = require('../piecemaker-plugin/scripts/lib/commits.cjs');
30
- const { documentKey } = require('../piecemaker-plugin/scripts/lib/protection.cjs');
24
+ const { originalFilesOverview } = require('../piecemaker-plugin/scripts/lib/commits.cjs');
31
25
  const {
32
26
  classifyRelativeCaseFolderPath,
33
27
  readCaseFolderStructure,
34
28
  } = require('./case-folder-structure.cjs');
35
- const { markFilesAnonymized } = require('../piecemaker-plugin/scripts/lib/anonymization-state.cjs');
36
- // Le mapping vit dans le plugin : c'est le seul des trois consommateurs (hooks,
37
- // pipeline, routeur du task pane) qui soit distribué seul.
38
- const {
39
- caseMappingFile,
40
- normalizeMappingDocument,
41
- readCaseMapping,
42
- readJsonFile,
43
- sortedMapping,
44
- } = require('../piecemaker-plugin/scripts/lib/mapping.cjs');
45
- // Vocabulaire des sigles de sociétés (SA_1, SARL_1, PERS_MORALE_1…), miroir de
46
- // `_LEGAL_FORMS` (scan_utils.py) — sert à classer un code déjà attribué.
47
- const { isSocieteCode, societeCounterKey, detectCompanySigle, LEGAL_FORM_TOKENS } = require('./legal-forms.cjs');
29
+ const { reconcileRenamedOriginals } = require('./renamed-originals.cjs');
48
30
 
49
31
  const SCRIPTS_DIR = path.join(__dirname, 'scripts');
50
32
  const PIECEMAKER_HOME = path.join(os.homedir(), '.piecemaker');
@@ -95,7 +77,10 @@ const PROCESS_TERMINATION_GRACE_MS = () => {
95
77
 
96
78
  /** Les pièces d'un dossier listées dans l'administration : tout sauf le Markdown. */
97
79
  async function listOriginals(caseRoot) {
98
- const originals = await originalFilesOverview(caseRoot);
80
+ let originals = await originalFilesOverview(caseRoot);
81
+ if (!runningManaged.size && !waiting.length && (await reconcileRenamedOriginals(caseRoot, originals)).length) {
82
+ originals = await originalFilesOverview(caseRoot);
83
+ }
99
84
  const snapshot = readCaseFolderStructure(caseRoot);
100
85
  return originals
101
86
  .map((file) => ({
@@ -111,344 +96,6 @@ async function listOriginals(caseRoot) {
111
96
  }));
112
97
  }
113
98
 
114
- function caseMappingPayload(document) {
115
- const normalized = normalizeMappingDocument(document);
116
- return {
117
- mapping: sortedMapping(normalized.mapping),
118
- reverse_mapping: normalized.reverse_mapping,
119
- ...(Object.keys(normalized.extracted_data).length ? { extracted_data: normalized.extracted_data } : {}),
120
- ...(normalized.ignored.length ? { ignored: normalized.ignored } : {}),
121
- informations_dossier: normalized.informations_dossier,
122
- };
123
- }
124
-
125
- function writeCaseMapping(caseRoot, document) {
126
- const file = caseMappingFile(caseRoot);
127
- const payload = caseMappingPayload(document);
128
- fs.mkdirSync(path.dirname(file), { recursive: true });
129
- const temporary = `${file}.piecemaker-${process.pid}.tmp`;
130
- fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
131
- fs.renameSync(temporary, file);
132
- // Une fois le fichier canonique écrit avec succès, tous les autres mappings
133
- // sont supprimés : les legacy (`mapping_<id>.json`) comme un `mapping_default.json`
134
- // resté à la racine par une version antérieure — c'est la migration. La
135
- // comparaison porte sur le chemin complet, sinon un `mapping_default.json`
136
- // racine passerait pour le fichier canonique (même basename) et survivrait.
137
- // `readCaseMapping` les a tous fusionnés au préalable ; aucune entité n'est perdue.
138
- for (const dir of new Set([caseRoot, path.dirname(file)])) {
139
- let entries;
140
- try {
141
- entries = fs.readdirSync(dir, { withFileTypes: true });
142
- } catch {
143
- continue;
144
- }
145
- for (const entry of entries) {
146
- if (!entry.isFile() || !/^mapping.*\.json$/i.test(entry.name)) continue;
147
- const full = path.join(dir, entry.name);
148
- if (full === file) continue;
149
- fs.unlinkSync(full);
150
- }
151
- }
152
- return { file, exists: true, ignored: payload.ignored || [], ...payload };
153
- }
154
-
155
- /**
156
- * Enregistre un mapping édité à la main. Une entrée supprimée rejoint
157
- * `ignored` : c'est ce qui empêche un faux positif écarté par le juriste
158
- * d'être réintroduit par le scan suivant, que `rebuildCaseMapping` relit.
159
- */
160
- function saveCaseMapping(caseRoot, document) {
161
- const current = readCaseMapping(caseRoot);
162
- // L'éditeur n'envoie que `mapping` et `reverse_mapping` : `extracted_data`
163
- // est repris du fichier, sinon un simple enregistrement perdrait les variants.
164
- const next = normalizeMappingDocument({
165
- extracted_data: current.extracted_data,
166
- informations_dossier: current.informations_dossier,
167
- ...document,
168
- });
169
- const removed = Object.keys(current.mapping).filter((entity) => !next.mapping[entity]);
170
- const ignored = [...new Set([...current.ignored, ...removed])].filter((entity) => !next.mapping[entity]);
171
- return writeCaseMapping(caseRoot, { ...next, ignored });
172
- }
173
-
174
- function codePrefix(entityType) {
175
- return String(entityType || 'ENTITE')
176
- .normalize('NFKD')
177
- .replace(/[\u0300-\u036f]/g, '')
178
- .toUpperCase()
179
- .replace(/[^A-Z0-9]+/g, '_')
180
- .replace(/^_+|_+$/g, '')
181
- .slice(0, 32) || 'ENTITE';
182
- }
183
-
184
- // Vocabulaire de codes et regroupement des variantes : mêmes règles que
185
- // `convert_to_anonymization_format` et `consolidate_duplicate_entities` dans
186
- // `scripts/convert_and_scan_pipeline.py`. Les deux chemins écrivent le même
187
- // fichier de mapping — s'ils codaient différemment, une reconstruction depuis
188
- // l'administration dédoublerait les entités déjà codées par le CLI.
189
-
190
- const ENTITY_CATEGORIES = {
191
- PERSON: 'personnes_physiques',
192
- ORGANIZATION: 'societes',
193
- LOCATION: 'adresses',
194
- EMAIL: 'autres',
195
- PHONE: 'autres',
196
- CREDIT_CARD: 'autres',
197
- IBAN: 'autres',
198
- IP_ADDRESS: 'autres',
199
- URL: 'autres',
200
- };
201
-
202
- function entityCategory(entityType) {
203
- const type = String(entityType || '').toUpperCase();
204
- return ENTITY_CATEGORIES[type] || (type.startsWith('ORGANIZATION_') ? 'societes' : 'autres');
205
- }
206
-
207
- /** Catégorie d'un code déjà attribué — sert à repartir des bons compteurs. */
208
- function codeCategory(code) {
209
- if (code.startsWith('SIREN_')) return 'siren';
210
- if (code.startsWith('ADRESSE_') || code.startsWith('LIEU_NAISSANCE_')) return 'adresses';
211
- if (code.includes('PERSONNE_PHYSIQUE_') || code.startsWith('DIRIGEANT_') || code.startsWith('AVOCAT_')) return 'personnes_physiques';
212
- // Sociétés : repli/legacy (…MORALE…, SOCIETE_…) et codes à sigle (SA_1, GMBH_2).
213
- // Testé après les familles distinctives, qui ne portent aucun sigle.
214
- if (isSocieteCode(code)) return 'societes';
215
- return 'autres';
216
- }
217
-
218
- /** Clé de compteur société (le sigle) déduite du type d'entité scanné. */
219
- function societeCodeKey(entityType) {
220
- const type = String(entityType || '').toUpperCase();
221
- return type.startsWith('ORGANIZATION_')
222
- ? (codePrefix(type.slice('ORGANIZATION_'.length)) || 'PERS_MORALE')
223
- : 'PERS_MORALE';
224
- }
225
-
226
- function entityCode(entityType, category, index) {
227
- const type = String(entityType || 'AUTRE').toUpperCase();
228
- // Sociétés : sigle en préfixe, sans zéro (SA_1, SARL_1, PERS_MORALE_1), compteur
229
- // par sigle. Les autres familles gardent leur padding _01.
230
- if (category === 'societes') return `${societeCodeKey(entityType)}_${index}`;
231
- const number = String(index).padStart(2, '0');
232
- if (category === 'personnes_physiques') return `PERSONNE_PHYSIQUE_${number}`;
233
- if (category === 'adresses') return `ADRESSE_${number}`;
234
- return `${codePrefix(type)}_${number}`;
235
- }
236
-
237
- /** Forme comparable d'un nom : sans accents, sans civilité, en minuscules. */
238
- function normalizeEntityName(text) {
239
- return String(text || '')
240
- .normalize('NFKD')
241
- .replace(/[\u0300-\u036f]/g, '')
242
- .toLowerCase()
243
- .replace(/\b(mr\.?|mrs\.?|ms\.?|dr\.?|prof\.?|m\.|mme\.?|mlle\.?|maitre)\s*/g, '')
244
- .split(/\s+/)
245
- .filter(Boolean)
246
- .join(' ')
247
- .trim();
248
- }
249
-
250
- /** Deux écritures d'une même personne : « M. Dupont » et « Jean Dupont ». */
251
- function areNamesSimilar(first, second) {
252
- if (!first || !second) return false;
253
- if (first === second) return true;
254
- if (Math.min(first.length, second.length) >= 3 && (first.includes(second) || second.includes(first))) return true;
255
- const tokens = [new Set(first.split(' ')), new Set(second.split(' '))];
256
- const [shorter, longer] = tokens[0].size <= tokens[1].size ? tokens : [tokens[1], tokens[0]];
257
- return shorter.size > 0 && [...shorter].every((token) => longer.has(token));
258
- }
259
-
260
- const CIVILITY_PREFIX = /^(?:(?:m|mr|mrs|ms|mme|mlle|dr|pr|prof|ma[iî]tre)\.\s*|(?:mr|mrs|ms|mme|mlle|dr|pr|prof|ma[iî]tre)\s+)/i;
261
-
262
- function withoutCivility(text) {
263
- return String(text || '').replace(CIVILITY_PREFIX, '').trim();
264
- }
265
-
266
- function expandCivilityVariants(texts) {
267
- const expanded = [];
268
- for (const text of texts) {
269
- expanded.push(text);
270
- const bare = withoutCivility(text);
271
- if (bare && bare !== text) expanded.push(bare);
272
- }
273
- return [...new Set(expanded)];
274
- }
275
-
276
- function companyIdentity(text) {
277
- const tokens = normalizeEntityName(text)
278
- .split(/\s+/)
279
- .map((token) => token.replace(/[.,'’&"()]/g, ''))
280
- .filter(Boolean);
281
- const base = tokens.filter((token) => !LEGAL_FORM_TOKENS.has(token.toUpperCase())).join(' ');
282
- return { base, sigle: detectCompanySigle(text) };
283
- }
284
-
285
- function areCompaniesSame(first, second) {
286
- if (!first.base || !second.base) return false;
287
- if (first.base !== second.base) return false;
288
- return !first.sigle || !second.sigle || first.sigle === second.sigle;
289
- }
290
-
291
- function groupSimilarNames(texts) {
292
- const groups = [];
293
- for (const text of texts) {
294
- const normalized = normalizeEntityName(text);
295
- const group = groups.find((candidate) => candidate.normalized.some((member) => areNamesSimilar(normalized, member)));
296
- if (group) {
297
- group.texts.push(text);
298
- group.normalized.push(normalized);
299
- } else {
300
- groups.push({ texts: [text], normalized: [normalized] });
301
- }
302
- }
303
- return groups.map((group) => group.texts);
304
- }
305
-
306
- function groupSameCompanies(texts) {
307
- const groups = [];
308
- for (const text of texts) {
309
- const identity = companyIdentity(text);
310
- const group = groups.find((candidate) => candidate.identities.some((member) => areCompaniesSame(identity, member)));
311
- if (group) {
312
- group.texts.push(text);
313
- group.identities.push(identity);
314
- } else {
315
- groups.push({ texts: [text], identities: [identity] });
316
- }
317
- }
318
- return groups.map((group) => group.texts);
319
- }
320
-
321
- function groupEntityHits(hits, category) {
322
- const texts = [...new Set(hits.map((hit) => String(hit?.text || '').trim()).filter(Boolean))];
323
- if (category === 'personnes_physiques') return groupSimilarNames(expandCivilityVariants(texts));
324
- if (category === 'societes') return groupSameCompanies(texts);
325
- return texts.map((text) => [text]);
326
- }
327
-
328
- /**
329
- * Migre les anciens `*_sensitive_map.json` du dossier dans son mapping.
330
- * Une entrée déjà présente n'est jamais réécrite : un faux positif retiré à la
331
- * main ne doit pas revenir au scan suivant, et un code ne doit jamais servir
332
- * deux fois. Les écritures multiples d'une même personne rejoignent le code
333
- * déjà attribué au lieu d'en obtenir un second.
334
- */
335
- async function rebuildCaseMapping(caseRoot) {
336
- const current = readCaseMapping(caseRoot);
337
- const mapping = { ...current.mapping };
338
- const ignored = new Set(current.ignored);
339
- const reverse = Object.fromEntries(Object.entries(current.reverse_mapping).map(([code, list]) => [code, [...list]]));
340
- const extracted = Object.fromEntries(
341
- Object.entries(current.extracted_data).map(([category, codes]) => [category, { ...codes }])
342
- );
343
-
344
- // Compteurs amorcés sur les codes déjà attribués. Les sociétés comptent par
345
- // sigle : la clé `societes:<sigle>` (SA, SARL, PERS_MORALE…) sépare les suites,
346
- // pour que la 1re SA soit SA_1 et la 1re SARL SARL_1 indépendamment.
347
- const counters = new Map();
348
- for (const code of new Set(Object.values(mapping))) {
349
- const match = /_(\d+)$/.exec(code);
350
- if (!match) continue;
351
- const category = codeCategory(code);
352
- const key = category === 'societes' ? `societes:${societeCounterKey(code)}` : category;
353
- counters.set(key, Math.max(counters.get(key) || 0, Number(match[1])));
354
- }
355
-
356
- // Index des noms déjà codés : une variante détectée plus tard rejoint son
357
- // code d'origine plutôt que d'en créer un nouveau.
358
- const coded = Object.entries(mapping).map(([entity, code]) => ({
359
- normalized: normalizeEntityName(entity),
360
- identity: companyIdentity(entity),
361
- category: codeCategory(code),
362
- code,
363
- }));
364
-
365
- let added = 0;
366
- for (const relative of await safeCaseFiles(caseRoot)) {
367
- if (!relative.toLowerCase().endsWith('_sensitive_map.json')) continue;
368
- const payload = readJsonFile(path.join(caseRoot, ...relative.split('/')), null);
369
- const entities = payload && typeof payload.entities === 'object' ? payload.entities : {};
370
- for (const [entityType, hits] of Object.entries(entities)) {
371
- if (!Array.isArray(hits)) continue;
372
- const category = entityCategory(entityType);
373
- for (const group of groupEntityHits(hits, category)) {
374
- const texts = group.filter((text) => !ignored.has(text));
375
- if (!texts.length) continue;
376
-
377
- let code = texts.map((text) => mapping[text]).find(Boolean);
378
- if (!code && category === 'personnes_physiques') {
379
- const normalized = texts.map(normalizeEntityName);
380
- code = coded.find((entry) => entry.category === category
381
- && normalized.some((name) => areNamesSimilar(name, entry.normalized)))?.code;
382
- }
383
- if (!code && category === 'societes') {
384
- const identities = texts.map(companyIdentity);
385
- code = coded.find((entry) => entry.category === category
386
- && identities.some((identity) => areCompaniesSame(identity, entry.identity)))?.code;
387
- }
388
- const isNewCode = !code;
389
- if (!code) {
390
- const key = category === 'societes' ? `societes:${societeCodeKey(entityType)}` : category;
391
- const index = (counters.get(key) || 0) + 1;
392
- counters.set(key, index);
393
- code = entityCode(entityType, category, index);
394
- }
395
-
396
- // Valeur principale : la plus longue écriture, comme côté Python.
397
- const principal = [...texts].sort((a, b) => b.length - a.length)[0];
398
- for (const text of texts) {
399
- if (mapping[text]) continue;
400
- mapping[text] = code;
401
- coded.push({ normalized: normalizeEntityName(text), identity: companyIdentity(text), category, code });
402
- added += 1;
403
- }
404
- if (isNewCode) reverse[code] = [principal];
405
- for (const text of texts) {
406
- if (!reverse[code].includes(text)) reverse[code].push(text);
407
- }
408
-
409
- if (!extracted[category]) extracted[category] = {};
410
- const entry = extracted[category][code] || { original: principal, code, variants: [] };
411
- entry.variants = [...new Set([...(entry.variants || []), ...texts])];
412
- extracted[category][code] = entry;
413
- }
414
- }
415
- }
416
-
417
- const saved = writeCaseMapping(caseRoot, {
418
- mapping,
419
- reverse_mapping: reverse,
420
- extracted_data: extracted,
421
- ignored: [...ignored],
422
- informations_dossier: current.informations_dossier,
423
- });
424
- const migratedScans = await migrateLegacySensitiveMaps(caseRoot);
425
- return { ...saved, added, total: Object.keys(saved.mapping).length, migratedScans };
426
- }
427
-
428
- /**
429
- * Transfère l'état porté par les anciens sensitive maps vers le manifeste sans
430
- * PII, puis retire ces artefacts devenus inutiles. Le mapping doit avoir été
431
- * reconstruit avant cet appel.
432
- */
433
- async function migrateLegacySensitiveMaps(caseRoot) {
434
- const safeFiles = await safeCaseFiles(caseRoot);
435
- const legacy = safeFiles.filter((relative) => relative.toLowerCase().endsWith('_sensitive_map.json'));
436
- if (!legacy.length) return 0;
437
-
438
- const scannedKeys = new Set(legacy.map((relative) => {
439
- const basename = relative.split('/').at(-1) || '';
440
- return documentKey(basename).replace(/-sensitive-map$/, '');
441
- }));
442
- const originals = await listOriginals(caseRoot);
443
- const scannedOriginals = originals
444
- .filter((original) => scannedKeys.has(documentKey(original.name)))
445
- .map((original) => path.join(caseRoot, ...original.path.split('/')));
446
- if (scannedOriginals.length) markFilesAnonymized(caseRoot, scannedOriginals);
447
-
448
- for (const relative of legacy) fs.unlinkSync(path.join(caseRoot, ...relative.split('/')));
449
- return legacy.length;
450
- }
451
-
452
99
  // ── Travaux de conversion / anonymisation ──────────────────────────────────
453
100
 
454
101
  function appendLog(job, line) {
@@ -553,7 +200,7 @@ async function verifyJobProcessTreeStopped(job, processGroupId = job.processGrou
553
200
  });
554
201
  }
555
202
 
556
- function spawnTracked(job, script, args, progressScale = {}) {
203
+ function spawnTracked(job, script, args, progressScale = {}, io = {}) {
557
204
  const offset = progressScale.offset || 0;
558
205
  const weight = progressScale.weight ?? 100;
559
206
  const convertWeight = weight * CONVERT_SUBPHASE_SHARE;
@@ -600,6 +247,14 @@ function spawnTracked(job, script, args, progressScale = {}) {
600
247
  const lines = stdoutRest.split(/\r?\n/);
601
248
  stdoutRest = lines.pop() || '';
602
249
  for (const line of lines) {
250
+ if (line.startsWith('MAPPING:')) {
251
+ try {
252
+ io.onMapping?.(JSON.parse(line.slice('MAPPING:'.length)));
253
+ } catch (error) {
254
+ errorLines.push(`mapping illisible : ${error.message}`);
255
+ }
256
+ continue;
257
+ }
603
258
  const progress = /^PROGRESS:([A-Z]+):(\d+):(\d+):(\d+)/.exec(line.trim());
604
259
  if (!progress) continue;
605
260
  const [, marker, pct, current, total] = progress;
@@ -630,6 +285,10 @@ function spawnTracked(job, script, args, progressScale = {}) {
630
285
  }
631
286
  };
632
287
 
288
+ if (io.input !== undefined) {
289
+ child.stdin.on('error', () => {});
290
+ child.stdin.end(io.input);
291
+ }
633
292
  child.stdout.setEncoding('utf8');
634
293
  child.stderr.setEncoding('utf8');
635
294
  child.stdout.on('data', consumeStdout);
@@ -682,7 +341,7 @@ function spawnTracked(job, script, args, progressScale = {}) {
682
341
  * `onProgress`, si fourni, est appelé à chaque mise à jour de la progression
683
342
  * avec `{ phase, percent, processed, total }`.
684
343
  */
685
- function runManagedPythonJob({ action, script, args, onProgress, signal } = {}) {
344
+ function runManagedPythonJob({ action, script, args, onProgress, signal, input, onMapping } = {}) {
686
345
  if (!['convert', 'anonymize'].includes(action)) throw new Error('Action inconnue.');
687
346
  if (!acceptingJobs) throw new Error('Le serveur est en cours d’arrêt : aucun nouveau traitement ne peut démarrer.');
688
347
  const job = {
@@ -716,7 +375,7 @@ function runManagedPythonJob({ action, script, args, onProgress, signal } = {})
716
375
  if (signal.aborted) abort();
717
376
  else signal.addEventListener('abort', abort, { once: true });
718
377
  }
719
- const run = () => spawnTracked(job, script, args);
378
+ const run = () => spawnTracked(job, script, args, {}, { input, onMapping });
720
379
  const descriptor = { job, run };
721
380
  const completion = new Promise((resolve, reject) => {
722
381
  descriptor.resolve = resolve;
@@ -798,18 +457,10 @@ function stopOriginalsJobs() {
798
457
  }
799
458
 
800
459
  module.exports = {
801
- groupEntityHits,
802
460
  // Point d'entrée partagé : tout consommateur d'un pipeline GLiNER/markitdown
803
461
  // (même hors dossier juridique enregistré, ex. `knowledge/pipeline.ts`) doit
804
462
  // passer par ici pour rester sous l'exclusivité GLiNER et le budget RAM.
805
463
  runManagedPythonJob,
806
464
  stopOriginalsJobs,
807
- // Ré-exportés pour les routes de l'administration : l'implémentation vit
808
- // désormais dans `piecemaker-plugin/scripts/lib/mapping.cjs`.
809
- caseMappingFile,
810
465
  listOriginals,
811
- readCaseMapping,
812
- rebuildCaseMapping,
813
- saveCaseMapping,
814
- writeCaseMapping,
815
466
  };
@@ -0,0 +1,136 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const {
5
+ anonymizationStateFile,
6
+ readAnonymizationState,
7
+ stateKey,
8
+ statFingerprint,
9
+ } = require('../piecemaker-plugin/scripts/lib/anonymization-state.cjs');
10
+ const { safeCaseFiles } = require('../piecemaker-plugin/scripts/lib/commits.cjs');
11
+ const { markdownCounterpart } = require('../piecemaker-plugin/scripts/lib/protection.cjs');
12
+ const { documentIndexFile } = require('./document-index.cjs');
13
+
14
+ const HASH_KEY = /^[a-f0-9]{64}$/i;
15
+
16
+ const fingerprintId = ({ size, mtimeMs }) => `${size}:${mtimeMs}`;
17
+
18
+ const absoluteIn = (root, relative) => path.join(root, ...relative.split('/'));
19
+
20
+ const stemOf = (file) => path.basename(file, path.extname(file));
21
+
22
+ function readJsonObject(file) {
23
+ try {
24
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, ''));
25
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function writeJsonAtomically(file, value, spacing) {
32
+ const temporary = `${file}.piecemaker-${process.pid}.tmp`;
33
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, spacing)}\n`, { encoding: 'utf8', mode: 0o600 });
34
+ fs.renameSync(temporary, file);
35
+ }
36
+
37
+ function moveEntry(record, from, to) {
38
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return;
39
+ const source = Object.keys(record).find((key) => (HASH_KEY.test(key) ? key.toLowerCase() : stateKey(key)) === from);
40
+ if (!source) return;
41
+ const entry = record[source];
42
+ delete record[source];
43
+ record[to] = entry;
44
+ }
45
+
46
+ function orphanKeysByFingerprint(root, originals) {
47
+ const liveKeys = new Set(originals.map((file) => stateKey(file.path)));
48
+ const orphans = new Map();
49
+ for (const [key, entry] of Object.entries(readAnonymizationState(root).files)) {
50
+ if (liveKeys.has(key) || !entry.converted) continue;
51
+ const id = fingerprintId(entry.converted);
52
+ orphans.set(id, [...(orphans.get(id) || []), key]);
53
+ }
54
+ return orphans;
55
+ }
56
+
57
+ async function markdownStems(root) {
58
+ try {
59
+ const files = await safeCaseFiles(root);
60
+ return [...new Set(files.filter((file) => path.extname(file).toLowerCase() === '.md').map(stemOf))];
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ function previousPathCandidates(root, file, orphanKeys, stems) {
67
+ const directory = path.posix.dirname(file.path);
68
+ const prefix = directory === '.' ? '' : `${directory}/`;
69
+ const extension = path.posix.extname(file.path);
70
+ return stems
71
+ .map((stem) => `${prefix}${stem}${extension}`)
72
+ .filter((previous) => previous !== file.path
73
+ && orphanKeys.has(stateKey(previous))
74
+ && !fs.existsSync(absoluteIn(root, previous)));
75
+ }
76
+
77
+ function uniqueRenames(candidates) {
78
+ const claims = new Map();
79
+ for (const { previous } of candidates) claims.set(previous, (claims.get(previous) || 0) + 1);
80
+ return candidates.filter(({ previous }) => claims.get(previous) === 1);
81
+ }
82
+
83
+ function renameMarkdown(root, { previous, current }) {
84
+ const markdown = markdownCounterpart(absoluteIn(root, previous), root);
85
+ if (!markdown.exists) return false;
86
+ const target = path.join(path.dirname(markdown.path), `${stemOf(current)}.md`);
87
+ if (fs.existsSync(target)) return false;
88
+ fs.renameSync(markdown.path, target);
89
+ return true;
90
+ }
91
+
92
+ function migrateAnonymizationState(root, renames) {
93
+ const file = anonymizationStateFile(root);
94
+ const state = readJsonObject(file);
95
+ if (!state) return;
96
+ for (const { previous, current } of renames) moveEntry(state.files, stateKey(previous), stateKey(current));
97
+ writeJsonAtomically(file, state, 2);
98
+ }
99
+
100
+ function migrateDocumentIndex(root, renames) {
101
+ const file = documentIndexFile(root);
102
+ const index = readJsonObject(file);
103
+ if (!index) return;
104
+ for (const { previous, current } of renames) {
105
+ const from = stateKey(previous);
106
+ const to = stateKey(current);
107
+ for (const section of ['documents', 'overrides', 'entityDecisions']) moveEntry(index[section], from, to);
108
+ for (const revision of Array.isArray(index.revisions) ? index.revisions : []) {
109
+ if (revision && String(revision.documentKey || '').toLowerCase() === from) revision.documentKey = to;
110
+ }
111
+ }
112
+ writeJsonAtomically(file, index);
113
+ }
114
+
115
+ async function reconcileRenamedOriginals(caseRoot, originals) {
116
+ const root = fs.realpathSync(caseRoot);
117
+ const orphans = orphanKeysByFingerprint(root, originals);
118
+ if (!orphans.size) return [];
119
+ const suspects = originals
120
+ .filter((file) => !file.converted)
121
+ .map((file) => ({ file, keys: orphans.get(fingerprintId(statFingerprint(fs.statSync(absoluteIn(root, file.path))))) }))
122
+ .filter(({ keys }) => keys);
123
+ if (!suspects.length) return [];
124
+ const stems = await markdownStems(root);
125
+ const candidates = suspects.flatMap(({ file, keys }) => {
126
+ const matches = previousPathCandidates(root, file, new Set(keys), stems);
127
+ return matches.length === 1 ? [{ previous: matches[0], current: file.path }] : [];
128
+ });
129
+ const renames = uniqueRenames(candidates).filter((rename) => renameMarkdown(root, rename));
130
+ if (!renames.length) return [];
131
+ migrateAnonymizationState(root, renames);
132
+ migrateDocumentIndex(root, renames);
133
+ return renames;
134
+ }
135
+
136
+ module.exports = { reconcileRenamedOriginals };