@piecemaker-legal/piecemaker 2.0.9 → 2.0.10

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 (165) hide show
  1. package/dist/assets/FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js +1 -0
  2. package/dist/assets/FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js +1 -0
  3. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-qIPBOssF.css +1 -0
  4. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js +144 -0
  5. package/dist/assets/ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js +1 -0
  6. package/dist/assets/ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js +1 -0
  7. package/dist/assets/PageSetupDialog-AIGIXO3E-_PYIO8eb.js +1 -0
  8. package/dist/assets/SplitCellDialog-IFBKYCD6-Cb9uQOB8.js +1 -0
  9. package/dist/assets/TablePropertiesDialog-TMLKGOOW-12vmjb6B.js +1 -0
  10. package/dist/assets/WatermarkDialog-KRL7WMIS-C-d8rzt2.js +1 -0
  11. package/dist/assets/{abnfDiagram-VCTEODGH-WrsSAHOe.js → abnfDiagram-VCTEODGH-B4OsX7eF.js} +1 -1
  12. package/dist/assets/arc-BK1oUIk7.js +1 -0
  13. package/dist/assets/architectureDiagram-5GKGNRK7-CfaFI6L7.js +36 -0
  14. package/dist/assets/{blockDiagram-I7D4REHJ-zGMnF7Ox.js → blockDiagram-I7D4REHJ-DQEI2azY.js} +1 -1
  15. package/dist/assets/{c4Diagram-7LVT6UL2-ClWw3bqt.js → c4Diagram-7LVT6UL2-DbravpVi.js} +1 -1
  16. package/dist/assets/channel-DVyNr2Yg.js +1 -0
  17. package/dist/assets/{chunk-2Q5K7J3B-C-9ICPHv.js → chunk-2Q5K7J3B-BLpZ9huC.js} +1 -1
  18. package/dist/assets/{chunk-5VM5RSS4-Bawy3teT.js → chunk-5VM5RSS4-BLlEdmX9.js} +1 -1
  19. package/dist/assets/{chunk-F27PBJKO-DQ375RQO.js → chunk-F27PBJKO-D-ZNhI6q.js} +1 -1
  20. package/dist/assets/{chunk-IMKFNOWR-CflNF2f6.js → chunk-IMKFNOWR-vzSiKnhh.js} +1 -1
  21. package/dist/assets/{chunk-JWPE2WC7-CIXQv0gc.js → chunk-JWPE2WC7-DfF0k4nb.js} +1 -1
  22. package/dist/assets/{chunk-POPQ4Y6H-Cza5j6Qw.js → chunk-POPQ4Y6H-BnwCYsPP.js} +1 -1
  23. package/dist/assets/{chunk-SVP7TREG-GQ_VvWZR.js → chunk-SVP7TREG-DRxc8fhq.js} +1 -1
  24. package/dist/assets/{chunk-TICWLB2K-BEmU16W5.js → chunk-TICWLB2K-Dd2Q-md-.js} +1 -1
  25. package/dist/assets/{chunk-XXDRQBXY-C1KpkLYH.js → chunk-XXDRQBXY-DBjkEUp_.js} +1 -1
  26. package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +1 -0
  27. package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +1 -0
  28. package/dist/assets/{cose-bilkent-JH36ORCC-CZE48TRG.js → cose-bilkent-JH36ORCC-By6PS4oe.js} +1 -1
  29. package/dist/assets/{cynefin-OW5HDTMX-L-tx1eC-.js → cynefin-OW5HDTMX-YxWpjc5W.js} +1 -1
  30. package/dist/assets/{cynefinDiagram-5FMLGOSQ-Coa8oJwR.js → cynefinDiagram-5FMLGOSQ-DrtXzz3p.js} +1 -1
  31. package/dist/assets/{dagre-GXQ25YYZ-Bu_3Y75K.js → dagre-GXQ25YYZ-BDpTLKqZ.js} +1 -1
  32. package/dist/assets/{diagram-S7CK7UJ4-CFiiV4es.js → diagram-S7CK7UJ4-C4LvqeCs.js} +1 -1
  33. package/dist/assets/{diagram-UQ7AKVKN-CCXheGAA.js → diagram-UQ7AKVKN-BTpnMeHp.js} +1 -1
  34. package/dist/assets/{diagram-VSXAHHWV-B70m4lr_.js → diagram-VSXAHHWV-DucHHWev.js} +1 -1
  35. package/dist/assets/{diagram-VX7I27RA-CHkiMOVA.js → diagram-VX7I27RA-DYHZNmmH.js} +1 -1
  36. package/dist/assets/{diagram-Z3DM3KII-_awpc4Bs.js → diagram-Z3DM3KII-BHIH2BGN.js} +1 -1
  37. package/dist/assets/{ebnfDiagram-PWID7BFC-CZzjH86H.js → ebnfDiagram-PWID7BFC-CGiEkwj0.js} +1 -1
  38. package/dist/assets/{erDiagram-RLTQ6QDP-9DgiHZg9.js → erDiagram-RLTQ6QDP-AJMHi2Us.js} +1 -1
  39. package/dist/assets/{flowDiagram-HODETNUW-DHY_503b.js → flowDiagram-HODETNUW-BqzANBh2.js} +1 -1
  40. package/dist/assets/{ganttDiagram-EL5Y4UJY-DPiiYZ_l.js → ganttDiagram-EL5Y4UJY-B0xPDY2_.js} +1 -1
  41. package/dist/assets/{gitGraphDiagram-WWUBYQGX-Ddd_evm7.js → gitGraphDiagram-WWUBYQGX-CzPX5CNO.js} +1 -1
  42. package/dist/assets/{index-tibHq7Qp.js → index-Bk2iXKnJ.js} +140 -140
  43. package/dist/assets/index-Dc9wNuS4.css +1 -0
  44. package/dist/assets/{infoDiagram-27XIBGKW-BPRNhfwb.js → infoDiagram-27XIBGKW-Cjc44H0O.js} +1 -1
  45. package/dist/assets/{ishikawaDiagram-5VMMS53U-BXx6aqkq.js → ishikawaDiagram-5VMMS53U-BWhr2v7r.js} +1 -1
  46. package/dist/assets/{journeyDiagram-3NMN7TZE-BK8TZM29.js → journeyDiagram-3NMN7TZE-Bl4r6RrP.js} +1 -1
  47. package/dist/assets/{kanban-definition-UXKFOSKX-CHioRmxz.js → kanban-definition-UXKFOSKX-FV-T0Vy1.js} +6 -6
  48. package/dist/assets/layout-CTfb1sAF.js +1 -0
  49. package/dist/assets/{linear-CTEhUcKG.js → linear-Dfgi6vlo.js} +1 -1
  50. package/dist/assets/{mermaid.core-DYUb_UUB.js → mermaid.core-DOCGsdlo.js} +7 -7
  51. package/dist/assets/{mindmap-definition-YA3MSWOX-D7ODZ4oS.js → mindmap-definition-YA3MSWOX-CymQ86ir.js} +1 -1
  52. package/dist/assets/{pegDiagram-XKGWAZYB-8J-_2_kw.js → pegDiagram-XKGWAZYB-1VMq_vi0.js} +1 -1
  53. package/dist/assets/{pieDiagram-E7YTZNPT-ebg0yfMi.js → pieDiagram-E7YTZNPT-C5GgXtJU.js} +1 -1
  54. package/dist/assets/processTemplate-GPFDLQMX-B1sVV2O0.js +22 -0
  55. package/dist/assets/{quadrantDiagram-AXDQQJYC-BoWkDiFb.js → quadrantDiagram-AXDQQJYC-C6zpryYf.js} +1 -1
  56. package/dist/assets/{railroadDiagram-O6MQD6OU-BRJ77ldz.js → railroadDiagram-O6MQD6OU-D-sv4SWi.js} +1 -1
  57. package/dist/assets/{requirementDiagram-BXWQKSXE-Z0JJgktA.js → requirementDiagram-BXWQKSXE-CkjY-z4B.js} +1 -1
  58. package/dist/assets/sankeyDiagram-P5KCCOFB-De-d8Q6L.js +40 -0
  59. package/dist/assets/{sequenceDiagram-WJ2MYXX4-BPAjtXwp.js → sequenceDiagram-WJ2MYXX4-DCsGSJa9.js} +1 -1
  60. package/dist/assets/{server.browser-BzI1jrZw.js → server.browser-BY3bL-IX.js} +1 -1
  61. package/dist/assets/{sizeCapture-INFHLROL-BGZPCzM4.js → sizeCapture-INFHLROL-xINZhgER.js} +1 -1
  62. package/dist/assets/stateDiagram-D77RDMKH-CuLUTWSL.js +1 -0
  63. package/dist/assets/stateDiagram-v2-MP3YSRHH-Cnbs-M92.js +1 -0
  64. package/dist/assets/{swimlanes-42K2YHIH-DvuB15qF.js → swimlanes-42K2YHIH-BwxARD7N.js} +1 -1
  65. package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +8 -0
  66. package/dist/assets/{timeline-definition-24CTP7MA-ZIziYCIF.js → timeline-definition-24CTP7MA-BIOvnn-l.js} +3 -3
  67. package/dist/assets/{vendor-codemirror-CjW0lHR5.js → vendor-codemirror-CN-sJePf.js} +2 -2
  68. package/dist/assets/{vendor-react-D809iwXR.js → vendor-react-B22wqN3J.js} +5 -5
  69. package/dist/assets/{vennDiagram-4TSXK5OY-C5_B9gWa.js → vennDiagram-4TSXK5OY-BSD6Pfmt.js} +4 -4
  70. package/dist/assets/{wardleyDiagram-VM6X3IG4-DMlH9Afw.js → wardleyDiagram-VM6X3IG4-7Zz2iRkF.js} +1 -1
  71. package/dist/assets/{xychartDiagram-S5SC5T6Z-BldhJt7i.js → xychartDiagram-S5SC5T6Z-Ctx17Y1D.js} +1 -1
  72. package/dist/index.html +4 -4
  73. package/dist/sw.js +1 -1
  74. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js +4 -53
  75. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js +2 -3
  77. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js +30 -0
  79. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js.map +1 -0
  80. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js +24 -0
  81. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js.map +1 -0
  82. package/dist-server/server/piecemaker/anonymizer/lifecycle.js +2 -3
  83. package/dist-server/server/piecemaker/anonymizer/lifecycle.js.map +1 -1
  84. package/dist-server/server/piecemaker/docx-document.js +50 -0
  85. package/dist-server/server/piecemaker/docx-document.js.map +1 -0
  86. package/dist-server/server/piecemaker/index.js +9 -2
  87. package/dist-server/server/piecemaker/index.js.map +1 -1
  88. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js +46 -0
  89. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js.map +1 -0
  90. package/dist-server/server/piecemaker/model-discovery/index.js +42 -0
  91. package/dist-server/server/piecemaker/model-discovery/index.js.map +1 -0
  92. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js +19 -0
  93. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js.map +1 -0
  94. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js +61 -0
  95. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js.map +1 -0
  96. package/dist-server/server/piecemaker/project-registry.js +29 -0
  97. package/dist-server/server/piecemaker/project-registry.js.map +1 -0
  98. package/package.json +12 -1
  99. package/scripts/piecemaker/cli/disable-anonymizer.mjs +19 -0
  100. package/scripts/piecemaker/cli/lib/config.mjs +0 -1
  101. package/scripts/piecemaker/cli/lib/services.mjs +1 -10
  102. package/scripts/piecemaker/cli/piecemaker.mjs +0 -3
  103. package/server/modules/providers/list/mistral/mistral-models.provider.ts +4 -53
  104. package/server/modules/providers/list/mistral/mistral-runtime.provider.ts +2 -3
  105. package/server/modules/providers/list/mistral/mistral-vibe-models.ts +43 -0
  106. package/server/modules/providers/tests/mistral-vibe-models.test.ts +27 -0
  107. package/server/piecemaker/anonymizer/client-config.cjs +92 -92
  108. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +9 -16
  109. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +1 -1
  110. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +251 -268
  111. package/server/piecemaker/anonymizer/lifecycle.ts +2 -3
  112. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +6 -0
  113. package/server/piecemaker/anonymizer/service.cjs +103 -174
  114. package/server/piecemaker/docx-document.ts +54 -0
  115. package/server/piecemaker/harness/decisions.cjs +1 -1
  116. package/server/piecemaker/harness/index.cjs +2 -3
  117. package/server/piecemaker/harness/verification.cjs +1 -1
  118. package/server/piecemaker/index.ts +11 -3
  119. package/server/piecemaker/model-discovery/codex-catalog.ts +67 -0
  120. package/server/piecemaker/model-discovery/index.ts +69 -0
  121. package/server/piecemaker/model-discovery/mistral-catalog.ts +36 -0
  122. package/server/piecemaker/model-discovery/stdio-rpc.ts +79 -0
  123. package/server/piecemaker/project-registry.ts +34 -0
  124. package/server/piecemaker/router.cjs +6 -7
  125. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +31 -77
  126. package/server/piecemaker/vendor/installer/lib/codex-skills.mjs +2 -84
  127. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -24
  128. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +10 -21
  129. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -11
  130. package/server/piecemaker/vendor/installer/steps/09-codex-plugin.mjs +6 -36
  131. package/server/piecemaker/vendor/installer/steps/13-garde-secrets.mjs +30 -12
  132. package/server/piecemaker/vendor/piecemaker-plugin/README.md +2 -9
  133. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +5 -5
  134. package/server/piecemaker/vendor/piecemaker-plugin/scripts/classify-ai-documents.mjs +8 -8
  135. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +2 -2
  136. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +4 -6
  137. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +71 -38
  138. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +8 -38
  139. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +82 -61
  140. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +4 -55
  141. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +24 -117
  142. package/server/piecemaker/vendor/piecemaker-plugin/skills/tamponnage/SKILL.md +2 -2
  143. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +15 -121
  144. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +16 -32
  145. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +0 -73
  146. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +0 -7
  147. package/server/piecemaker/vendor/websocket-server/stamping-routes.cjs +3 -12
  148. package/server/piecemaker/vendor/websocket-server/workspace-paths.cjs +6 -6
  149. package/dist/assets/arc-DAA6wH-C.js +0 -1
  150. package/dist/assets/architectureDiagram-5GKGNRK7-BGNHr7ef.js +0 -36
  151. package/dist/assets/channel-BAQyfMll.js +0 -1
  152. package/dist/assets/classDiagram-ZZMXUADV-BNGCuvrR.js +0 -1
  153. package/dist/assets/classDiagram-v2-VYDZK3BY-BNGCuvrR.js +0 -1
  154. package/dist/assets/index-WwknP0l4.css +0 -1
  155. package/dist/assets/layout-DEnkyfTY.js +0 -1
  156. package/dist/assets/sankeyDiagram-P5KCCOFB-4PW5HTOP.js +0 -40
  157. package/dist/assets/stateDiagram-D77RDMKH-o-vEcKwz.js +0 -1
  158. package/dist/assets/stateDiagram-v2-MP3YSRHH-BsuDaKkN.js +0 -1
  159. package/dist/assets/swimlanesDiagram-VR7AAH4N-DcCAyKlT.js +0 -8
  160. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +0 -78
  161. package/scripts/piecemaker/hooks/proxy-guard.mjs +0 -251
  162. package/server/piecemaker/anonymizer/providers.cjs +0 -120
  163. package/server/piecemaker/vendor/installer/steps/08-telegram.mjs +0 -482
  164. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/central-mapping.cjs +0 -0
  165. package/server/piecemaker/vendor/websocket-server/telegram-admin.cjs +0 -602
@@ -1,7 +1,18 @@
1
- /** Registered PieceMaker legal-case folders, including folders outside a common root. */
1
+ /** Legal-case folders: every CloudCLI project, as published by the PieceMaker server. */
2
2
  const fs = require('node:fs');
3
+ const os = require('node:os');
3
4
  const path = require('node:path');
4
5
 
6
+ const PROJECTS_FILE = 'projects.json';
7
+
8
+ function piecemakerHome() {
9
+ return process.env.PIECEMAKER_HOME || path.join(os.homedir(), '.piecemaker');
10
+ }
11
+
12
+ function projectsFile() {
13
+ return path.join(piecemakerHome(), PROJECTS_FILE);
14
+ }
15
+
5
16
  function realDirectory(value) {
6
17
  const requested = String(value || '').trim();
7
18
  if (!requested || !path.isAbsolute(requested)) return null;
@@ -13,23 +24,57 @@ function realDirectory(value) {
13
24
  }
14
25
  }
15
26
 
16
- /** Absolute, existing and de-duplicated folders explicitly registered by the admin. */
17
- function registeredCaseFolders(config) {
18
- const folders = Array.isArray(config?.caseFolders) ? config.caseFolders : [];
19
- return [...new Set(folders.map(realDirectory).filter(Boolean))];
20
- }
21
-
22
- function isInsideOrEqual(root, candidate) {
27
+ function isInsideOrEqualPath(root, candidate) {
23
28
  const relative = path.relative(root, candidate);
24
29
  return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
25
30
  }
26
31
 
27
- /** Locate a target inside one of the explicitly registered legal-case folders. */
28
- function locateRegisteredCase(folders, target) {
29
- if (!target) return null;
30
- let absolute;
32
+ function excludedRoots() {
33
+ return [os.tmpdir(), '/tmp', '/private/tmp', '/var/folders', '/private/var/folders']
34
+ .map(realDirectory)
35
+ .filter(Boolean);
36
+ }
37
+
38
+ function isEligibleProjectFolder(folder) {
39
+ const home = realDirectory(os.homedir());
40
+ if (home && isInsideOrEqualPath(folder, home)) return false;
41
+ return !excludedRoots().some((root) => isInsideOrEqualPath(root, folder));
42
+ }
43
+
44
+ function readProjectSources() {
45
+ try {
46
+ const parsed = JSON.parse(fs.readFileSync(projectsFile(), 'utf8'));
47
+ return parsed?.sources && typeof parsed.sources === 'object' && !Array.isArray(parsed.sources) ? parsed.sources : {};
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ /** Absolute, existing and de-duplicated project folders, across every publishing server. */
54
+ function registeredProjectFolders() {
55
+ const folders = Object.values(readProjectSources()).flatMap((list) => (Array.isArray(list) ? list : []));
56
+ return [...new Set(folders.map(realDirectory).filter(Boolean))].filter(isEligibleProjectFolder);
57
+ }
58
+
59
+ function publishProjectSource(sourceId, folders) {
60
+ const file = projectsFile();
61
+ const sources = readProjectSources();
62
+ if (folders === null) delete sources[sourceId];
63
+ else sources[sourceId] = [...new Set(folders.map((folder) => path.resolve(String(folder))))].sort();
64
+ fs.mkdirSync(path.dirname(file), { recursive: true });
65
+ const temporaryFile = `${file}.${process.pid}.${Date.now()}.tmp`;
66
+ try {
67
+ fs.writeFileSync(temporaryFile, `${JSON.stringify({ version: 1, sources }, null, 2)}\n`, 'utf8');
68
+ fs.renameSync(temporaryFile, file);
69
+ } catch (error) {
70
+ fs.rmSync(temporaryFile, { force: true });
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ function resolveTarget(target) {
31
76
  try {
32
- absolute = fs.realpathSync(path.resolve(String(target)));
77
+ return fs.realpathSync(path.resolve(String(target)));
33
78
  } catch {
34
79
  // Write/Edit may target a file that does not exist yet. Resolve its nearest
35
80
  // existing parent, then append the missing tail without following links.
@@ -37,22 +82,24 @@ function locateRegisteredCase(folders, target) {
37
82
  const tail = [];
38
83
  while (path.dirname(current) !== current) {
39
84
  try {
40
- absolute = path.join(fs.realpathSync(current), ...tail);
41
- break;
85
+ return path.join(fs.realpathSync(current), ...tail);
42
86
  } catch {
43
87
  tail.unshift(path.basename(current));
44
88
  current = path.dirname(current);
45
89
  }
46
90
  }
47
- if (!absolute) return null;
91
+ return null;
48
92
  }
93
+ }
49
94
 
50
- // A nested registered matter wins over its parent. It prevents a folder
51
- // intentionally registered as its own case from sharing the parent's map.
52
- const roots = [...new Set((folders || []).map(realDirectory).filter(Boolean))]
53
- .sort((a, b) => b.length - a.length);
95
+ /** Locate a target inside one of the project folders; a nested project wins over its parent. */
96
+ function locateProjectCase(target) {
97
+ if (!target) return null;
98
+ const absolute = resolveTarget(target);
99
+ if (!absolute) return null;
100
+ const roots = registeredProjectFolders().sort((a, b) => b.length - a.length);
54
101
  for (const caseRoot of roots) {
55
- if (!isInsideOrEqual(caseRoot, absolute)) continue;
102
+ if (!isInsideOrEqualPath(caseRoot, absolute)) continue;
56
103
  return {
57
104
  casesRoot: path.dirname(caseRoot),
58
105
  caseName: path.basename(caseRoot),
@@ -65,22 +112,8 @@ function locateRegisteredCase(folders, target) {
65
112
  return null;
66
113
  }
67
114
 
68
- /**
69
- * Locate a target inside one of the explicitly registered legal-case folders.
70
- * There is no longer a workspace-root fallback: a matter is protected only once
71
- * it is explicitly registered, never by mere location under a common root.
72
- */
73
- function locateConfiguredCase(config, target) {
74
- return locateRegisteredCase(registeredCaseFolders(config), target);
75
- }
76
-
77
- function configuredWatchPaths(config) {
78
- return registeredCaseFolders(config);
79
- }
80
-
81
115
  module.exports = {
82
- configuredWatchPaths,
83
- locateConfiguredCase,
84
- locateRegisteredCase,
85
- registeredCaseFolders,
116
+ locateProjectCase,
117
+ publishProjectSource,
118
+ registeredProjectFolders,
86
119
  };
@@ -2,8 +2,7 @@
2
2
  * Mapping d'anonymisation d'un dossier juridique — implémentation unique.
3
3
  *
4
4
  * Le pipeline admin (`websocket-server/originals-pipeline.cjs`), le serveur
5
- * Word, l'historique et les vues juridiques la partagent. Le proxy PII lit le
6
- * mapping central produit à partir de ces mappings de dossier.
5
+ * Word, l'historique et les vues juridiques la partagent.
7
6
  *
8
7
  * Deux sens, jamais symétriques dans leur usage :
9
8
  * - `applyMapping` entité → code, sur tout ce que l'IA s'apprête à lire ;
@@ -16,11 +15,11 @@
16
15
  const fs = require('node:fs');
17
16
  const path = require('node:path');
18
17
 
19
- const { locateCase, WORKSPACE_SUBDIR } = require('./protection.cjs');
20
- const { locateConfiguredCase } = require('./case-folders.cjs');
18
+ const { WORKSPACE_SUBDIR } = require('./protection.cjs');
19
+ const { locateProjectCase } = require('./case-folders.cjs');
21
20
  const { isInstitutionalEntity } = require('./institutional-terms.cjs');
22
21
  // Le moteur de substitution est extrait dans un module autonome : il est aussi
23
- // requis par le hook central global, distribué hors du plugin. Une seule
22
+ // requis par le proxy PII (`anonymizer/dictionary.cjs`). Une seule
24
23
  // implémentation, ré-exportée ici pour les appelants historiques.
25
24
  const {
26
25
  applyMapping,
@@ -286,40 +285,13 @@ function readCaseMapping(caseRoot) {
286
285
  };
287
286
  }
288
287
 
289
- /**
290
- * Un mapping d'anonymisation exploitable existe-t-il pour ce dossier ? C'est la
291
- * condition de la garantie « anonymisé avant envoi » : sans lui, le proxy n'a
292
- * rien à coder et une surface lisible partirait en clair. `protect-originals`
293
- * s'en sert pour refuser la lecture d'un dossier PieceMaker pas encore anonymisé.
294
- * On s'appuie sur `readCaseMapping` (et non sur la seule présence du fichier) pour
295
- * qu'un `mapping_default.json` illisible compte comme absent — c'est exactement ce
296
- * que voit la reconstruction du mapping central.
297
- */
298
- function caseHasMapping(caseRoot) {
299
- return readCaseMapping(caseRoot).exists;
300
- }
301
-
302
288
  // La substitution (`applyMapping` / `revertMapping`) vit dans `substitution.cjs`
303
289
  // et est importée en tête de fichier. Elle est ré-exportée ci-dessous pour les
304
290
  // appelants historiques de `mapping.cjs`.
305
291
 
306
- /**
307
- * Retrouve le mapping du dossier juridique auquel `hint` appartient. `hint` est
308
- * un chemin de fichier (Read, Write, Edit) ou un répertoire de travail (Bash,
309
- * Telegram). Retourne `null` hors dossier juridique ou sans mapping utilisable :
310
- * les hooks n'ont alors rien à faire.
311
- */
312
- function resolveCaseMapping(casesRoot, hint) {
313
- const located = locateCase(casesRoot, hint);
314
- if (!located) return null;
315
- const mapping = readCaseMapping(located.caseRoot);
316
- if (!mapping.exists) return null;
317
- return { caseRoot: located.caseRoot, caseName: located.caseName, ...mapping };
318
- }
319
-
320
- /** Resolve a mapping from the explicit folder registry plus legacy workspace. */
321
- function resolveConfiguredCaseMapping(config, hint) {
322
- const located = locateConfiguredCase(config, hint);
292
+ /** Resolve the mapping of the project folder containing the hint. */
293
+ function resolveProjectCaseMapping(hint) {
294
+ const located = locateProjectCase(hint);
323
295
  if (!located) return null;
324
296
  const mapping = readCaseMapping(located.caseRoot);
325
297
  if (!mapping.exists) return null;
@@ -330,15 +302,13 @@ module.exports = {
330
302
  applyMapping,
331
303
  buildEntityRegex,
332
304
  byDescendingEntityLength,
333
- caseHasMapping,
334
305
  caseMappingFile,
335
306
  escapeWithVariants,
336
307
  normalizeMappingDocument,
337
308
  normalizeProcedureInfo,
338
309
  readCaseMapping,
339
310
  readJsonFile,
340
- resolveConfiguredCaseMapping,
341
- resolveCaseMapping,
311
+ resolveProjectCaseMapping,
342
312
  resolveMappedPath,
343
313
  revertMapping,
344
314
  sortedMapping,
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * L'ancienne règle protégeait le contenu d'un sous-dossier « Pièces
5
5
  * originales ». Elle ne protégeait donc rien dans un dossier organisé
6
- * autrement, ce qui est le cas courant : les PDF et DOCX y côtoient le Markdown
7
- * qu'on en a tiré. La protection est désormais une propriété du fichier,
6
+ * autrement, ce qui est le cas courant : les PDF y côtoient le Markdown qu'on
7
+ * en a tiré. La protection est désormais une propriété du fichier,
8
8
  * décidée dans l'administration.
9
9
  *
10
10
  * **Protégés par défaut : les PDF et les images** (`PROTECTED_EXTENSIONS`), qui
@@ -81,7 +81,7 @@ const PROTECTED_EXTENSIONS = new Set([
81
81
  * celles rangées sous `ignored` et les variantes d'`extracted_data` ressortiraient
82
82
  * telles quelles. Le seul traitement correct est le refus.
83
83
  */
84
- const FORBIDDEN_JSON_PATTERNS = [/^mapping.*\.json$/i, /_sensitive_map\.json$/i, /^central-mapping\.json$/i];
84
+ const FORBIDDEN_JSON_PATTERNS = [/^mapping.*\.json$/i, /_sensitive_map\.json$/i];
85
85
 
86
86
  /** Vrai pour un mapping de dossier ou un scan PII, où qu'il soit rangé. */
87
87
  function isMappingFile(filePath) {
@@ -172,37 +172,6 @@ function relativeKey(absolutePath, caseRoot) {
172
172
  return relative.split(path.sep).join('/');
173
173
  }
174
174
 
175
- /**
176
- * Situe un chemin dans la racine PieceMaker : chaque enfant direct de cette
177
- * racine est un dossier juridique indépendant. Retourne `null` hors racine, ce
178
- * qui est le cas de la grande majorité des appels d'outil — c'est le chemin
179
- * rapide des hooks.
180
- */
181
- function locateCase(casesRoot, target) {
182
- if (!casesRoot || !target) return null;
183
- let root;
184
- try {
185
- root = fs.realpathSync(path.resolve(String(casesRoot)));
186
- } catch {
187
- return null;
188
- }
189
- // Les deux côtés doivent être résolus : sur macOS `/var/…` est un lien vers
190
- // `/private/var/…`, si bien qu'une racine résolue et une cible qui ne l'est
191
- // pas ne se recouvrent jamais — le dossier passait alors pour hors racine et
192
- // rien n'était protégé.
193
- const absolute = realpathOrParent(path.resolve(String(target)));
194
- if (absolute === root || !absolute.startsWith(`${root}${path.sep}`)) return null;
195
- const [caseName, ...rest] = path.relative(root, absolute).split(path.sep);
196
- if (!caseName || caseName.startsWith('.')) return null;
197
- const caseRoot = path.join(root, caseName);
198
- try {
199
- if (!fs.statSync(caseRoot).isDirectory()) return null;
200
- } catch {
201
- return null;
202
- }
203
- return { casesRoot: root, caseName, caseRoot, absolute, relative: rest.join('/') };
204
- }
205
-
206
175
  /** Normalise une liste de chemins relatifs en Set de clés POSIX. */
207
176
  function normalizeKeySet(list) {
208
177
  return new Set(
@@ -244,26 +213,76 @@ function readProtection(caseRoot) {
244
213
  * que `unprotected` — l'initialisation d'un dossier — n'efface pas les
245
214
  * ressources déjà déclarées.
246
215
  */
247
- function writeProtection(caseRoot, { unprotected, resources } = {}) {
216
+ function writeProtection(caseRoot, lists = {}) {
217
+ return mutateProtection(caseRoot, () => lists);
218
+ }
219
+
220
+ const PROTECTION_LOCK_TIMEOUT_MS = 3000;
221
+ const PROTECTION_LOCK_STALE_MS = 10000;
222
+ const PROTECTION_LOCK_RETRY_MS = 25;
223
+
224
+ function sleepSync(milliseconds) {
225
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
226
+ }
227
+
228
+ function acquireProtectionLock(lockFile) {
229
+ const deadline = Date.now() + PROTECTION_LOCK_TIMEOUT_MS;
230
+ for (;;) {
231
+ try {
232
+ fs.closeSync(fs.openSync(lockFile, 'wx'));
233
+ return;
234
+ } catch (error) {
235
+ if (error.code !== 'EEXIST') throw error;
236
+ }
237
+ try {
238
+ if (Date.now() - fs.statSync(lockFile).mtimeMs > PROTECTION_LOCK_STALE_MS) {
239
+ fs.rmSync(lockFile, { force: true });
240
+ continue;
241
+ }
242
+ } catch {
243
+ continue;
244
+ }
245
+ if (Date.now() > deadline) throw new Error(`Le fichier de protection est verrouillé : ${lockFile}`);
246
+ sleepSync(PROTECTION_LOCK_RETRY_MS);
247
+ }
248
+ }
249
+
250
+ function mutateProtection(caseRoot, mutate) {
248
251
  const file = protectionFile(caseRoot);
249
- const existing = readProtection(caseRoot);
250
- const clean = (list, fallback) => [...new Set(
251
- (Array.isArray(list) ? list : [...fallback])
252
- .map((entry) => String(entry || '').replaceAll('\\', '/').trim())
253
- .filter((entry) => entry && !entry.startsWith('../') && !path.isAbsolute(entry))
254
- )].sort((a, b) => a.localeCompare(b, 'fr'));
255
- // Une pièce ne peut être à la fois « espace de travail » et « ressource » :
256
- // `resources` a priorité, on la retire donc de `unprotected`.
257
- const resourcesList = clean(resources, existing.resources);
258
- const resourceSet = new Set(resourcesList);
259
- const unprotectedList = clean(unprotected, existing.unprotected).filter((key) => !resourceSet.has(key));
252
+ const lockFile = `${file}.lock`;
260
253
  fs.mkdirSync(path.dirname(file), { recursive: true });
261
- fs.writeFileSync(
262
- file,
263
- `${JSON.stringify({ version: 1, unprotected: unprotectedList, resources: resourcesList }, null, 2)}\n`,
264
- 'utf8'
265
- );
266
- return { file, unprotected: new Set(unprotectedList), resources: resourceSet };
254
+ acquireProtectionLock(lockFile);
255
+ try {
256
+ const existing = readProtection(caseRoot);
257
+ const next = mutate(existing);
258
+ if (!next) return { file, unprotected: existing.unprotected, resources: existing.resources };
259
+ const { unprotected, resources } = next;
260
+ const clean = (list, fallback) => [...new Set(
261
+ (Array.isArray(list) ? list : [...fallback])
262
+ .map((entry) => String(entry || '').replaceAll('\\', '/').trim())
263
+ .filter((entry) => entry && !entry.startsWith('../') && !path.isAbsolute(entry))
264
+ )].sort((a, b) => a.localeCompare(b, 'fr'));
265
+ // Une pièce ne peut être à la fois « espace de travail » et « ressource » :
266
+ // `resources` a priorité, on la retire donc de `unprotected`.
267
+ const resourcesList = clean(resources, existing.resources);
268
+ const resourceSet = new Set(resourcesList);
269
+ const unprotectedList = clean(unprotected, existing.unprotected).filter((key) => !resourceSet.has(key));
270
+ const temporaryFile = `${file}.${process.pid}.${Date.now()}.tmp`;
271
+ try {
272
+ fs.writeFileSync(
273
+ temporaryFile,
274
+ `${JSON.stringify({ version: 1, unprotected: unprotectedList, resources: resourcesList }, null, 2)}\n`,
275
+ 'utf8'
276
+ );
277
+ fs.renameSync(temporaryFile, file);
278
+ } catch (error) {
279
+ fs.rmSync(temporaryFile, { force: true });
280
+ throw error;
281
+ }
282
+ return { file, unprotected: new Set(unprotectedList), resources: resourceSet };
283
+ } finally {
284
+ fs.rmSync(lockFile, { force: true });
285
+ }
267
286
  }
268
287
 
269
288
  /** Vrai pour un chemin recevable dans une des deux listes d'exceptions. */
@@ -367,9 +386,9 @@ function markdownCounterpart(absolutePath, caseRoot) {
367
386
  * de naître au coffre-fort, elle ne pourrait plus se relire.
368
387
  *
369
388
  * Ne fait rien, et retourne pourquoi, quand le chemin n'est pas une pièce
370
- * classable : hors dossier, `.md`/`.json` (déjà lisibles), dotfile, mapping ou
371
- * scan PII (jamais déclassés), espace de travail OOXML implicite, ou clé déjà
372
- * inscrite. `resources` est prioritaire : une ressource n'est jamais
389
+ * classable : hors dossier, extension jamais protégée (seuls PDF et images le
390
+ * sont), dotfile, mapping ou scan PII (jamais déclassés), espace de travail
391
+ * OOXML implicite, ou clé déjà inscrite. `resources` est prioritaire : une ressource n'est jamais
373
392
  * rétrogradée en espace de travail. Idempotent, ne lève jamais.
374
393
  */
375
394
  function classifyAsWorkspace(absolutePath, caseRoot) {
@@ -378,12 +397,15 @@ function classifyAsWorkspace(absolutePath, caseRoot) {
378
397
  if (isMappingFile(absolutePath)) return { classified: false, key: null, reason: 'mapping' };
379
398
  const key = exceptionKey(absolutePath, caseRoot);
380
399
  if (!key) return { classified: false, key: null, reason: 'non-classable' };
400
+ if (!PROTECTED_EXTENSIONS.has(path.extname(key).toLowerCase())) return { classified: false, key, reason: 'jamais protégée' };
381
401
  if (isOoxmlWorkspacePath(absolutePath, caseRoot)) return { classified: false, key, reason: 'ooxml' };
382
- const { unprotected, resources } = readProtection(caseRoot);
383
- if (resources.has(key)) return { classified: false, key, reason: 'ressource' };
384
- if (unprotected.has(key)) return { classified: false, key, reason: 'déjà classée' };
385
- writeProtection(caseRoot, { unprotected: [...unprotected, key] });
386
- return { classified: true, key, reason: null };
402
+ let reason = null;
403
+ mutateProtection(caseRoot, ({ unprotected, resources }) => {
404
+ if (resources.has(key)) reason = 'ressource';
405
+ else if (unprotected.has(key)) reason = 'déjà classée';
406
+ return reason ? null : { unprotected: [...unprotected, key] };
407
+ });
408
+ return { classified: !reason, key, reason };
387
409
  } catch (error) {
388
410
  return { classified: false, key: null, reason: error?.message || 'erreur' };
389
411
  }
@@ -395,7 +417,6 @@ module.exports = {
395
417
  exceptionKey,
396
418
  isMappingFile,
397
419
  isSecretFile,
398
- locateCase,
399
420
  isProtectedFile,
400
421
  isProtectionBypassed,
401
422
  isOoxmlWorkspacePath,
@@ -21,19 +21,16 @@
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import { createRequire } from 'node:module';
24
- import { loadPieceMakerConfig, readHookPayload, runHook, noop } from './lib/hook-io.mjs';
24
+ import { readHookPayload, runHook, noop } from './lib/hook-io.mjs';
25
25
 
26
26
  const require = createRequire(import.meta.url);
27
- const { locateConfiguredCase } = require('./lib/case-folders.cjs');
28
- const { caseHasMapping } = require('./lib/mapping.cjs');
27
+ const { locateProjectCase } = require('./lib/case-folders.cjs');
29
28
  const {
30
29
  isMappingFile,
31
30
  isSecretFile,
32
31
  isProtectedFile,
33
32
  markdownCounterpart,
34
33
  readProtection,
35
- relativeKey,
36
- READABLE_EXTENSIONS,
37
34
  } = require('./lib/protection.cjs');
38
35
 
39
36
  /** Au-delà, on ne cherche pas de chemin : une commande pareille n'en cite pas un. */
@@ -81,36 +78,6 @@ function secretReason(absolute) {
81
78
  return `[PieceMaker] « ${path.basename(absolute)} » est un fichier de secrets d’environnement : il n’est jamais accessible à l’IA.`;
82
79
  }
83
80
 
84
- /**
85
- * Refus « dossier non anonymisé ». Sans `mapping_default.json`, le proxy n'a
86
- * rien à coder : lire une pièce livrerait des données personnelles en clair.
87
- * On l'explique synthétiquement au modèle, avec l'action qui débloque la lecture.
88
- */
89
- function missingMappingReason(caseName) {
90
- return `[PieceMaker] « ${caseName} » est un dossier PieceMaker sans mapping d’anonymisation (mapping_default.json absent) : sa lecture est bloquée car son contenu n’est pas encore anonymisable et partirait en clair. Lancez l’anonymisation du dossier dans l’administration PieceMaker avant toute lecture.`;
91
- }
92
-
93
- /**
94
- * Surface lisible dont le contenu serait normalement codé à la lecture (`.md`,
95
- * `.json` de pièce) : c'est précisément ce que `missingMappingReason` doit
96
- * protéger quand aucun mapping n'existe. On écarte les originaux (déjà refusés
97
- * par `isProtectedFile`), le mapping et les scans (refusés à part), la
98
- * configuration du dossier (`.piecemaker/…`, dotfiles) et les consignes
99
- * structurelles `CLAUDE.md`/`AGENTS.md` — jamais des pièces, et l'assistant du
100
- * dossier doit pouvoir charger sa consigne même avant anonymisation.
101
- */
102
- function isReadableCaseSurface(absolute, caseRoot) {
103
- const stat = statSafe(absolute);
104
- if (!stat || stat.isDirectory()) return false;
105
- const key = relativeKey(absolute, caseRoot);
106
- if (!key) return false;
107
- if (key.split('/').some((segment) => segment.startsWith('.'))) return false;
108
- if (!READABLE_EXTENSIONS.has(path.extname(key).toLowerCase())) return false;
109
- if (isMappingFile(absolute)) return false;
110
- const base = path.basename(key);
111
- return base !== 'CLAUDE.md' && base !== 'AGENTS.md';
112
- }
113
-
114
81
  function statSafe(target) {
115
82
  try {
116
83
  return fs.statSync(target);
@@ -189,23 +156,19 @@ function caseRootHasMapping(located) {
189
156
  async function main() {
190
157
  const payload = await readHookPayload(2000);
191
158
  if (!payload) return null;
192
- const config = loadPieceMakerConfig();
193
159
 
194
160
  const toolName = payload.tool_name;
195
161
  const cwd = payload.cwd || process.cwd();
196
162
 
197
163
  if (toolName === 'Bash') {
198
164
  for (const candidate of commandPaths(payload.tool_input?.command, cwd)) {
199
- const located = locateConfiguredCase(config, candidate);
165
+ const located = locateProjectCase(candidate);
200
166
  if (!located) continue;
201
167
  if (isMappingFile(candidate)) return deny(mappingReason(candidate));
202
168
  if (isSecretFile(candidate)) return deny(secretReason(candidate));
203
169
  if (isProtectedFile(candidate, located.caseRoot)) {
204
170
  return deny(protectionReason(candidate, located.caseRoot));
205
171
  }
206
- if (isReadableCaseSurface(candidate, located.caseRoot) && !caseHasMapping(located.caseRoot)) {
207
- return deny(missingMappingReason(located.caseName));
208
- }
209
172
  }
210
173
  return null;
211
174
  }
@@ -214,7 +177,7 @@ async function main() {
214
177
 
215
178
  const target = absolutePath(payload.tool_input?.file_path || payload.tool_input?.path, cwd);
216
179
  if (!target) return null;
217
- const located = locateConfiguredCase(config, target);
180
+ const located = locateProjectCase(target);
218
181
  if (!located) return null;
219
182
 
220
183
  // Un répertoire n'est pas une pièce : `Grep`/`Glob` en visent un couramment,
@@ -227,20 +190,6 @@ async function main() {
227
190
  return deny(protectionReason(target, located.caseRoot));
228
191
  }
229
192
 
230
- // Dossier PieceMaker sans mapping : la garantie « anonymisé à la lecture »
231
- // n'existe pas encore. Lire une surface lisible (`.md`/`.json` de pièce) la
232
- // ferait sortir en clair, et un `Grep` récursif à la racine en ferait autant
233
- // sur tout le dossier (un `Glob`, qui ne ramène que des noms, reste permis).
234
- // On calcule l'absence de mapping au plus tard, une fois les refus francs
235
- // (mapping, original) écartés — c'est le seul chemin qui paie la lecture du
236
- // mapping.
237
- if (isReadableCaseSurface(target, located.caseRoot) && !caseHasMapping(located.caseRoot)) {
238
- return deny(missingMappingReason(located.caseName));
239
- }
240
- if (toolName === 'Grep' && located.absolute === located.caseRoot && !caseHasMapping(located.caseRoot)) {
241
- return deny(missingMappingReason(located.caseName));
242
- }
243
-
244
193
  // Un `Grep` récursif ramène le *contenu* du mapping ; un `Glob` n'en ramène
245
194
  // que le nom, qui ne trahit rien — d'où la restriction à Grep ici.
246
195
  if (toolName === 'Grep' && caseRootHasMapping(located)) {