@piecemaker-legal/piecemaker 2.0.10 → 2.0.11

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 (92) hide show
  1. package/dist/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BucOHLdm.js} +1 -1
  2. package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js} +1 -1
  3. package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js} +4 -4
  4. package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js} +1 -1
  5. package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-D86PckZ2.js} +1 -1
  6. package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DLETP-7v.js} +1 -1
  7. package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-CwdQBXeb.js} +1 -1
  8. package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js} +1 -1
  9. package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-XFJ9gTkC.js} +1 -1
  10. package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-DiCrND_Y.js} +1 -1
  11. package/dist/assets/{arc-BK1oUIk7.js → arc-DOjgbMYz.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-brzIFVhl.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-C8JhTjQQ.js} +1 -1
  14. package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-jfYxccyk.js} +1 -1
  15. package/dist/assets/channel-Dajzu2W1.js +1 -0
  16. package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-CrlEShp9.js} +1 -1
  17. package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-DzPlUHGq.js} +1 -1
  18. package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-Cp9cpeWU.js} +1 -1
  19. package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-BifUldEN.js} +1 -1
  20. package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-ggPEIkWu.js} +1 -1
  21. package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-3h_nayRX.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-CH29aG6J.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-PluJUM5k.js} +1 -1
  24. package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-DXU39Vwg.js} +1 -1
  25. package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +1 -0
  26. package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +1 -0
  27. package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-Dz6roYrN.js} +1 -1
  28. package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-BLzrpSit.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-kfxnERDZ.js} +1 -1
  30. package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-DP57bp-N.js} +1 -1
  31. package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-DAlsrQVW.js} +1 -1
  32. package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-By5yrMer.js} +1 -1
  33. package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-Dx89kryN.js} +1 -1
  34. package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-B9fARitZ.js} +1 -1
  35. package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-DC15CEH1.js} +1 -1
  36. package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-BKQzghGF.js} +1 -1
  37. package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-CPY6RM7G.js} +1 -1
  38. package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-4dL4GJcf.js} +1 -1
  39. package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-D4-cVcfb.js} +1 -1
  40. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BMeVIEiX.js} +1 -1
  41. package/dist/assets/{index-Bk2iXKnJ.js → index-DNtFDOni.js} +3 -3
  42. package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-D5qMSUEU.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-BDnyKRHm.js} +1 -1
  44. package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-dGz5ZGzq.js} +1 -1
  45. package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-D7UAz7o_.js} +1 -1
  46. package/dist/assets/{layout-CTfb1sAF.js → layout-DKrmEGrz.js} +1 -1
  47. package/dist/assets/{linear-Dfgi6vlo.js → linear-CrTdMY-p.js} +1 -1
  48. package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-CXQOT5G6.js} +6 -6
  49. package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-VgEiYfay.js} +1 -1
  50. package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-9IqT8gGx.js} +1 -1
  51. package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-D98pRE-t.js} +1 -1
  52. package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-b9PwkYzL.js} +1 -1
  53. package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dz87hsPE.js} +1 -1
  54. package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-CkcV73dK.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-UBN31Yrs.js} +1 -1
  56. package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-CpQxd5Za.js} +1 -1
  57. package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-mCsFmyzw.js} +1 -1
  58. package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-B0MTBN-3.js} +1 -1
  59. package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-DzBPN39W.js} +1 -1
  60. package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-Dzydjm0e.js} +1 -1
  61. package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +8 -0
  62. package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-Dz0toOLu.js} +1 -1
  63. package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-DOWp7dkf.js} +1 -1
  64. package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-CUxsztUB.js} +1 -1
  65. package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-DS96vNED.js} +1 -1
  66. package/dist/index.html +1 -1
  67. package/dist/sw.js +1 -1
  68. package/dist-server/server/piecemaker/agent-instructions.js +148 -0
  69. package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
  70. package/dist-server/server/piecemaker/index.js +4 -0
  71. package/dist-server/server/piecemaker/index.js.map +1 -1
  72. package/package.json +1 -1
  73. package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
  74. package/server/piecemaker/agent-instructions.ts +148 -0
  75. package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
  76. package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
  77. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
  78. package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
  79. package/server/piecemaker/anonymizer/service.cjs +7 -6
  80. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +12 -1
  81. package/server/piecemaker/index.ts +4 -0
  82. package/server/piecemaker/router.cjs +0 -1
  83. package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
  84. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
  85. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
  86. package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
  87. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -30
  88. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
  89. package/dist/assets/channel-DVyNr2Yg.js +0 -1
  90. package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
  91. package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
  92. package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +0 -8
@@ -5,9 +5,8 @@ const path = require('node:path');
5
5
  const tls = require('node:tls');
6
6
 
7
7
  const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
8
- const { startRewriterBridge } = require('./rewriter-bridge.cjs');
8
+ const { startRewriterWorker } = require('./rewriter-worker.cjs');
9
9
  const { removeLegacyProxyConfig } = require('./client-config.cjs');
10
- const { createHarnessJuridique } = require('../harness/index.cjs');
11
10
 
12
11
  const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
13
12
  const INTERCEPTED_HOSTS = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'];
@@ -52,7 +51,9 @@ function hudsuckerBinary() {
52
51
  path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name),
53
52
  path.join(__dirname, 'hudsucker-proxy', 'target', 'debug', name),
54
53
  ];
55
- return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
54
+ const present = candidates.filter((candidate) => fs.existsSync(candidate));
55
+ if (!present.length) return candidates[0];
56
+ return present.reduce((newest, candidate) => (fs.statSync(candidate).mtimeMs > fs.statSync(newest).mtimeMs ? candidate : newest));
56
57
  }
57
58
 
58
59
  function certificatePaths() {
@@ -136,8 +137,8 @@ function proxyEnvironment(proxyUrl, caFile, trustBundle) {
136
137
  }
137
138
 
138
139
  function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = console }) {
139
- const dictionary = createSqliteDictionaryLoader({ databasePath: process.env.DATABASE_PATH || path.join(homeDir, 'auth.db') });
140
- const harness = createHarnessJuridique({ homeDir, verifyResponses: false });
140
+ const databasePath = process.env.DATABASE_PATH || path.join(homeDir, 'auth.db');
141
+ const dictionary = createSqliteDictionaryLoader({ databasePath });
141
142
  const binDir = path.join(homeDir, 'bin');
142
143
  const state = { enabled: false, reason: 'not_started', origin: null, upstream: null, coverage: {} };
143
144
  let proxy = null;
@@ -165,7 +166,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
165
166
  const hosts = [...new Set([...INTERCEPTED_HOSTS, new URL(upstream).hostname])];
166
167
 
167
168
  try {
168
- bridge = await startRewriterBridge({ dictionary, harness });
169
+ bridge = await startRewriterWorker({ databasePath, homeDir });
169
170
  } catch (error) {
170
171
  state.reason = `listen_failed: ${error.message}`;
171
172
  logger.warn?.(`[piecemaker] réécriture PII non démarrée : ${error.message}`);
@@ -1,3 +1,4 @@
1
+ const fs = require('node:fs');
1
2
  const path = require('node:path');
2
3
 
3
4
  const Database = require('better-sqlite3');
@@ -62,6 +63,7 @@ function createSqliteDictionaryLoader({ databasePath }) {
62
63
  let mappingSignature = null;
63
64
  let generation = 0;
64
65
  let current = EMPTY_DICTIONARY;
66
+ let failure = null;
65
67
 
66
68
  function connection() {
67
69
  if (!database) {
@@ -97,15 +99,24 @@ function createSqliteDictionaryLoader({ databasePath }) {
97
99
  generation += 1;
98
100
  current = buildDictionary(buildMappingDocument(rows), generation);
99
101
  dataVersion = nextDataVersion;
102
+ failure = null;
100
103
  return current;
101
- } catch {
104
+ } catch (error) {
105
+ failure = fs.existsSync(file) ? error : null;
102
106
  return current;
103
107
  }
104
108
  }
105
109
 
110
+ function loadForOutbound() {
111
+ const dictionary = load(false);
112
+ if (failure) throw new Error(`dictionnaire illisible : ${failure.message}`);
113
+ return dictionary;
114
+ }
115
+
106
116
  return {
107
117
  file,
108
118
  get: () => load(false),
119
+ getForOutbound: loadForOutbound,
109
120
  refresh: () => load(true),
110
121
  exists: () => !load(false).empty,
111
122
  close: () => {
@@ -3,11 +3,13 @@ import path from 'path';
3
3
 
4
4
  import type { Router } from 'express';
5
5
 
6
+ import { projectsDb } from '@/modules/database/index.js';
6
7
  import { providerRuntimeService, sessionsService } from '@/modules/providers/index.js';
7
8
  import { providerRegistry } from '@/modules/providers/provider.registry.js';
8
9
  import { findApplicationRoot, getModuleDirectory } from '@/shared/utils.js';
9
10
 
10
11
 
12
+ import { createAgentInstructionsRouter, startAgentInstructionsMirror } from './agent-instructions.js';
11
13
  import { startRequiredAnonymizer } from './anonymizer/lifecycle.js';
12
14
  import { createCitationStore } from './harness/citation-store.js';
13
15
  import { installChatCitationHarness } from './harness/chat-harness.js';
@@ -62,6 +64,7 @@ installLibraryRuntime(providerRuntimeService, sessionsService, library.store);
62
64
  installModelDiscovery(providerRegistry);
63
65
  const timesheet = createTimesheetBackend(piecemakerHome());
64
66
  const knowledge = createKnowledgeBackend(applicationRoot);
67
+ startAgentInstructionsMirror(() => projectsDb.getProjectPaths().map((row) => row.project_path));
65
68
 
66
69
  export function createPieceMakerLocalRouter() {
67
70
  return knowledge.localRouter;
@@ -78,6 +81,7 @@ export function createPieceMakerRouter(options: { getRuntimeStatus?: () => Piece
78
81
  router.use(createCompanySearchRouter());
79
82
  router.use(createBodaccSearchRouter());
80
83
  router.use(createDocxDocumentRouter());
84
+ router.use(createAgentInstructionsRouter(path.join(piecemakerHome(), 'CLAUDE.md')));
81
85
  router.use(createShellEnvironmentRouter(shellEnvironment));
82
86
  return router;
83
87
  }
@@ -67,7 +67,6 @@ function createPieceMakerRouter({ getRuntimeStatus = defaultRuntimeStatus, anony
67
67
  publishProjects();
68
68
  const result = await registerLegalCase({
69
69
  folder: request.body?.folder,
70
- configFile: path.join(homeDir, 'config.json'),
71
70
  homeDir,
72
71
  });
73
72
  response.status(201).json({ ok: true, ...result });
@@ -9,7 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { log, spinner, columns } from '../lib/ui.mjs';
12
- import { run, npmBin, npmEnv, REPO_ROOT } from '../lib/platform.mjs';
12
+ import { run, npmBin, npmEnv, GIT_REPO_ROOT } from '../lib/platform.mjs';
13
13
 
14
14
  export const meta = {
15
15
  id: '02-dependances-node',
@@ -36,7 +36,7 @@ function readPackageJson(dir) {
36
36
  // Certaines distributions npm conservent spawn-helper de node-pty en 0644.
37
37
  // Le module se charge alors normalement mais tout pty.spawn échoue avec
38
38
  // « posix_spawnp failed ». Réparer le bit exécutable est idempotent.
39
- export function repairNodePtySpawnHelpers(repoRoot = REPO_ROOT, { repair = true } = {}) {
39
+ export function repairNodePtySpawnHelpers(repoRoot = GIT_REPO_ROOT, { repair = true } = {}) {
40
40
  if (process.platform === 'win32') return { found: 0, repaired: 0, ready: true };
41
41
  const prebuilds = path.join(repoRoot, 'node_modules', 'node-pty', 'prebuilds');
42
42
  if (!fs.existsSync(prebuilds)) return { found: 0, repaired: 0, ready: false };
@@ -88,7 +88,7 @@ async function npmInstall(dir, label, ctx) {
88
88
  export async function install(ctx) {
89
89
  const results = [];
90
90
 
91
- results.push(['racine du projet', await npmInstall(REPO_ROOT, 'Racine', ctx)]);
91
+ results.push(['racine du projet', await npmInstall(GIT_REPO_ROOT, 'Racine', ctx)]);
92
92
 
93
93
  if (ctx.dryRun) {
94
94
  return { status: 'skipped', note: 'Mode simulation — aucune installation effectuée.' };
@@ -116,8 +116,8 @@ export async function install(ctx) {
116
116
  }
117
117
 
118
118
  export async function check(ctx) {
119
- const rootInstalled = fs.existsSync(path.join(REPO_ROOT, 'node_modules'));
120
- const ptyReady = repairNodePtySpawnHelpers(REPO_ROOT, { repair: false }).ready;
119
+ const rootInstalled = fs.existsSync(path.join(GIT_REPO_ROOT, 'node_modules'));
120
+ const ptyReady = repairNodePtySpawnHelpers(GIT_REPO_ROOT, { repair: false }).ready;
121
121
 
122
122
  if (rootInstalled && ptyReady) return { status: 'done', note: '' };
123
123
  if (rootInstalled) {
@@ -7,7 +7,6 @@ const CASE_FOLDER_STRUCTURE_VERSION = 1;
7
7
  const CASE_FOLDER_STRUCTURE_RELATIVE = '.piecemaker/case-folder-structure.json';
8
8
 
9
9
  const DEFAULT_CASE_FOLDER_STRUCTURE = Object.freeze({
10
- administrative: '00_ADMINISTRATIF_ET_FACTURATION',
11
10
  correspondence: '01_CORRESPONDANCE',
12
11
  correspondenceClient: '01_Client',
13
12
  correspondenceOpposingCounsel: '02_Avocats_adverses',
@@ -65,7 +64,6 @@ function normalizeCaseFolderStructure(value = {}) {
65
64
  }
66
65
 
67
66
  ensureUniqueSiblingNames(structure, [
68
- 'administrative',
69
67
  'correspondence',
70
68
  'dataRoom',
71
69
  'drafts',
@@ -107,9 +105,8 @@ function readStructureManifest(caseRoot) {
107
105
  }
108
106
 
109
107
  /**
110
- * La structure est figée par dossier lors de son premier enregistrement. Ainsi,
111
- * personnaliser les prochains dossiers ne change pas le routage du Markdown
112
- * des dossiers déjà existants.
108
+ * Seuls les anciens dossiers portent encore un manifeste d'arborescence : il
109
+ * continue de router leur Markdown. Sans manifeste, le dossier n'est pas géré.
113
110
  */
114
111
  function readCaseFolderStructure(caseRoot, config = {}) {
115
112
  const manifest = readStructureManifest(caseRoot);
@@ -119,78 +116,6 @@ function readCaseFolderStructure(caseRoot, config = {}) {
119
116
  };
120
117
  }
121
118
 
122
- function caseFolderDirectoriesFromStructure(structure) {
123
- return [
124
- structure.administrative,
125
- structure.correspondence,
126
- path.join(structure.correspondence, structure.correspondenceMarkdown),
127
- path.join(structure.correspondence, structure.correspondenceClient),
128
- path.join(structure.correspondence, structure.correspondenceOpposingCounsel),
129
- path.join(structure.correspondence, structure.correspondenceThirdParties),
130
- structure.dataRoom,
131
- path.join(structure.dataRoom, structure.dataRoomMarkdown),
132
- structure.drafts,
133
- structure.notesAndResearch,
134
- structure.procedure,
135
- ];
136
- }
137
-
138
- /** Chemins relatifs, dans l'ordre lisible de l'arborescence documentée. */
139
- function caseFolderDirectories(config = {}) {
140
- return caseFolderDirectoriesFromStructure(configuredCaseFolderStructure(config));
141
- }
142
-
143
- function writeStructureManifest(file, structure) {
144
- fs.mkdirSync(path.dirname(file), { recursive: true });
145
- const temporary = `${file}.piecemaker-${process.pid}.tmp`;
146
- fs.writeFileSync(temporary, `${JSON.stringify({
147
- version: CASE_FOLDER_STRUCTURE_VERSION,
148
- structure,
149
- }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
150
- fs.renameSync(temporary, file);
151
- }
152
-
153
- /**
154
- * Crée uniquement les dossiers manquants. Aucun fichier ni dossier existant
155
- * n'est renommé ou remplacé quand l'utilisateur modifie les noms par défaut.
156
- */
157
- function ensureCaseFolderStructure(caseRoot, config = {}) {
158
- const requested = String(caseRoot || '').trim();
159
- if (!requested || !path.isAbsolute(requested)) throw new Error('Racine du dossier juridique invalide.');
160
- const root = path.resolve(requested);
161
- let rootStat;
162
- try {
163
- rootStat = fs.statSync(root);
164
- } catch {
165
- throw new Error('Racine du dossier juridique introuvable.');
166
- }
167
- if (!rootStat.isDirectory()) throw new Error('Racine du dossier juridique invalide.');
168
-
169
- const created = [];
170
- const manifest = readStructureManifest(root);
171
- const structure = manifest.structure || configuredCaseFolderStructure(config);
172
- const directories = caseFolderDirectoriesFromStructure(structure);
173
- for (const relative of directories) {
174
- const target = path.join(root, relative);
175
- if (fs.existsSync(target)) {
176
- if (!fs.statSync(target).isDirectory()) {
177
- throw new Error(`L’emplacement « ${relative} » existe mais n’est pas un dossier.`);
178
- }
179
- continue;
180
- }
181
- fs.mkdirSync(target, { recursive: true });
182
- created.push(relative.split(path.sep).join('/'));
183
- }
184
- if (!manifest.exists) writeStructureManifest(manifest.file, structure);
185
-
186
- return {
187
- file: manifest.file,
188
- names: structure,
189
- directories: directories.map((relative) => relative.split(path.sep).join('/')),
190
- created,
191
- };
192
- }
193
-
194
119
  function relativeSegments(relativePath) {
195
120
  const value = String(relativePath || '').replaceAll('\\', '/').replace(/^\.\//, '');
196
121
  if (!value || value.startsWith('/') || value.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
@@ -278,20 +203,7 @@ function structuredMarkdownCounterpart(absolutePath, caseRoot, config = {}) {
278
203
  }
279
204
 
280
205
  module.exports = {
281
- CASE_FOLDER_KEYS,
282
- CASE_FOLDER_STRUCTURE_KEY,
283
- CASE_FOLDER_STRUCTURE_RELATIVE,
284
- DEFAULT_CASE_FOLDER_STRUCTURE,
285
- caseConversionOutputDirectory,
286
- caseFolderDirectories,
287
- caseFolderStructureFile,
288
- classifyCaseFolderPath,
289
206
  classifyRelativeCaseFolderPath,
290
- configuredCaseFolderStructure,
291
- ensureCaseFolderStructure,
292
- isCaseGeneratedPath,
293
- normalizeCaseFolderStructure,
294
207
  readCaseFolderStructure,
295
208
  structuredMarkdownCounterpart,
296
- validateCaseFolderName,
297
209
  };
@@ -115,6 +115,157 @@ function byDescendingEntityLength(getEntity) {
115
115
  };
116
116
  }
117
117
 
118
+ function mappingSignature(entries) {
119
+ let signature = '';
120
+ for (const [key, value] of entries) signature += key + '\u0000' + value + '\u0001';
121
+ return signature;
122
+ }
123
+
124
+ function literalAnchor(value) {
125
+ const runs = String(value).match(/[A-Za-z0-9_]+/g) || [];
126
+ return runs.reduce((longest, run) => (run.length > longest.length ? run : longest), '');
127
+ }
128
+
129
+ const ANCHOR_WIDTH = 37;
130
+ const ANCHOR_SYMBOL = new Int8Array(128).fill(-1);
131
+ for (let letter = 0; letter < 26; letter += 1) {
132
+ ANCHOR_SYMBOL[65 + letter] = letter;
133
+ ANCHOR_SYMBOL[97 + letter] = letter;
134
+ }
135
+ for (let digit = 0; digit < 10; digit += 1) ANCHOR_SYMBOL[48 + digit] = 26 + digit;
136
+ ANCHOR_SYMBOL[95] = 36;
137
+ const KELVIN_SIGN = 0x212A;
138
+ const LONG_S = 0x017F;
139
+
140
+ function anchorSymbol(unit) {
141
+ if (unit < 128) return ANCHOR_SYMBOL[unit];
142
+ if (unit === KELVIN_SIGN) return ANCHOR_SYMBOL[107];
143
+ if (unit === LONG_S) return ANCHOR_SYMBOL[115];
144
+ return -1;
145
+ }
146
+
147
+ function buildAnchorScanner(anchors) {
148
+ let transitions = new Int32Array(ANCHOR_WIDTH * 16).fill(-1);
149
+ const outputs = [[]];
150
+ let size = 1;
151
+ anchors.forEach((anchor, index) => {
152
+ if (!anchor) return;
153
+ let node = 0;
154
+ for (let position = 0; position < anchor.length; position += 1) {
155
+ const slot = node * ANCHOR_WIDTH + ANCHOR_SYMBOL[anchor.charCodeAt(position)];
156
+ if (transitions[slot] === -1) {
157
+ if ((size + 1) * ANCHOR_WIDTH > transitions.length) {
158
+ const grown = new Int32Array(transitions.length * 2).fill(-1);
159
+ grown.set(transitions);
160
+ transitions = grown;
161
+ }
162
+ transitions[slot] = size;
163
+ outputs.push([]);
164
+ size += 1;
165
+ }
166
+ node = transitions[slot];
167
+ }
168
+ outputs[node].push(index);
169
+ });
170
+
171
+ const failure = new Int32Array(size);
172
+ const queue = [];
173
+ for (let symbol = 0; symbol < ANCHOR_WIDTH; symbol += 1) {
174
+ const child = transitions[symbol];
175
+ if (child === -1) transitions[symbol] = 0;
176
+ else queue.push(child);
177
+ }
178
+ for (let head = 0; head < queue.length; head += 1) {
179
+ const node = queue[head];
180
+ outputs[node] = outputs[node].concat(outputs[failure[node]]);
181
+ for (let symbol = 0; symbol < ANCHOR_WIDTH; symbol += 1) {
182
+ const slot = node * ANCHOR_WIDTH + symbol;
183
+ const child = transitions[slot];
184
+ if (child === -1) {
185
+ transitions[slot] = transitions[failure[node] * ANCHOR_WIDTH + symbol];
186
+ } else {
187
+ failure[child] = transitions[failure[node] * ANCHOR_WIDTH + symbol];
188
+ queue.push(child);
189
+ }
190
+ }
191
+ }
192
+
193
+ const next = new Int32Array(size * ANCHOR_WIDTH);
194
+ for (let slot = 0; slot < next.length; slot += 1) next[slot] = transitions[slot] * ANCHOR_WIDTH;
195
+ const emitted = new Array(size * ANCHOR_WIDTH).fill(null);
196
+ for (let node = 0; node < size; node += 1) if (outputs[node].length) emitted[node * ANCHOR_WIDTH] = outputs[node];
197
+
198
+ return (text) => {
199
+ const found = new Set();
200
+ let state = 0;
201
+ for (let position = 0; position < text.length; position += 1) {
202
+ const symbol = anchorSymbol(text.charCodeAt(position));
203
+ if (symbol === -1) {
204
+ state = 0;
205
+ continue;
206
+ }
207
+ state = next[state + symbol];
208
+ const matched = emitted[state];
209
+ if (matched !== null) for (const index of matched) found.add(index);
210
+ }
211
+ return found;
212
+ };
213
+ }
214
+
215
+ function compileReplacements(replacements) {
216
+ return { replacements, scan: buildAnchorScanner(replacements.map(({ anchor }) => anchor.toLowerCase())) };
217
+ }
218
+
219
+ function isDeeplyFrozen(object) {
220
+ return Object.isFrozen(object) && Object.values(object).every((value) => !Array.isArray(value) || Object.isFrozen(value));
221
+ }
222
+
223
+ const compiledMappings = new WeakMap();
224
+
225
+ function compileMapping(mapping) {
226
+ const cached = compiledMappings.get(mapping);
227
+ if (cached && cached.frozen) return cached;
228
+ const entries = Object.entries(mapping);
229
+ const signature = mappingSignature(entries);
230
+ if (cached && cached.signature === signature) return cached;
231
+
232
+ const codes = [...new Set(entries.map(([, code]) => String(code)).filter(Boolean))]
233
+ .sort((a, b) => b.length - a.length);
234
+ const replacements = [];
235
+ for (const [entity, code] of entries.sort(byDescendingEntityLength(([key]) => key))) {
236
+ const regex = buildEntityRegex(entity);
237
+ if (!regex) continue;
238
+ const trimmed = entity.trim();
239
+ replacements.push({ regex, code, anchor: literalAnchor(regex.flags.includes('i') ? trimmed : trimmed.toUpperCase()) });
240
+ }
241
+ const compiled = {
242
+ frozen: Object.isFrozen(mapping),
243
+ signature,
244
+ codes,
245
+ codeProbe: codes.length ? new RegExp(codes.map(escapeRegex).join('|')) : null,
246
+ replacements: compileReplacements(replacements),
247
+ };
248
+ compiledMappings.set(mapping, compiled);
249
+ return compiled;
250
+ }
251
+
252
+ function applyReplacements({ replacements, scan }, text) {
253
+ let output = text;
254
+ let present = null;
255
+ replacements.forEach(({ regex, code, anchor }, index) => {
256
+ if (anchor) {
257
+ if (present === null) present = scan(output);
258
+ if (!present.has(index)) return;
259
+ }
260
+ const next = output.replace(regex, code);
261
+ if (next !== output) {
262
+ output = next;
263
+ present = null;
264
+ }
265
+ });
266
+ return output;
267
+ }
268
+
118
269
  /**
119
270
  * Entité → code. Les entrées passent de la plus longue à la plus courte pour
120
271
  * qu'un nom contenu dans un autre ne consomme jamais le plus long en premier
@@ -122,41 +273,49 @@ function byDescendingEntityLength(getEntity) {
122
273
  */
123
274
  function applyMapping(text, mapping) {
124
275
  if (typeof text !== 'string' || !text) return text;
125
- const entries = Object.entries(mapping || {});
126
- if (!entries.length) return text;
127
-
128
- // Masquage préalable des codes déjà présents dans le texte. La substitution en
129
- // sous-chaîne (≥ 3 car.) et l'underscore-séparateur (acronymes 2 car.)
130
- // pourraient sinon réécrire l'intérieur d'un code — « SA » dans « URGOT SA »,
131
- // « Moral » dans « PERSONNE_MORALE_01 » — et le corrompre. Chaque code distinct
132
- // est remplacé par un caractère de zone privée (ni lettre ni chiffre, absent des
133
- // documents, jamais matché par une regex d'entité), on anonymise, puis on
134
- // restaure. C'est aussi ce qui garantit l'idempotence : réappliquer le mapping à
135
- // un texte déjà codé masque ses codes et ne touche à rien. Codes triés du plus
136
- // long au plus court pour qu'un code contenu dans un autre ne soit pas masqué
137
- // en premier.
138
- const codes = [...new Set(entries.map(([, code]) => String(code)).filter(Boolean))]
139
- .sort((a, b) => b.length - a.length);
276
+ if (!mapping || typeof mapping !== 'object') return text;
277
+ const compiled = compileMapping(mapping);
278
+ if (!compiled.replacements.replacements.length) return text;
279
+
140
280
  const restore = [];
141
281
  let masked = text;
142
- codes.forEach((code, idx) => {
143
- if (!masked.includes(code)) return;
144
- const token = String.fromCodePoint(0xE000 + idx);
145
- restore.push([token, code]);
146
- masked = masked.split(code).join(token);
147
- });
148
-
149
- let output = masked;
150
- for (const [entity, code] of entries.sort(byDescendingEntityLength(([key]) => key))) {
151
- const regex = buildEntityRegex(entity);
152
- if (!regex) continue;
153
- output = output.replace(regex, code);
282
+ if (compiled.codeProbe && compiled.codeProbe.test(text)) {
283
+ compiled.codes.forEach((code, idx) => {
284
+ if (!masked.includes(code)) return;
285
+ const token = String.fromCodePoint(0xE000 + idx);
286
+ restore.push([token, code]);
287
+ masked = masked.split(code).join(token);
288
+ });
154
289
  }
155
290
 
291
+ let output = applyReplacements(compiled.replacements, masked);
292
+ if (output === masked) return text;
293
+
156
294
  for (const [token, code] of restore) output = output.split(token).join(code);
157
295
  return output;
158
296
  }
159
297
 
298
+ const compiledReverseMappings = new WeakMap();
299
+
300
+ function compileReverseMapping(reverseMapping) {
301
+ const cached = compiledReverseMappings.get(reverseMapping);
302
+ if (cached && cached.frozen) return cached;
303
+ const entries = Object.entries(reverseMapping);
304
+ const signature = mappingSignature(entries.map(([code, variants]) => [code, Array.isArray(variants) ? variants[0] : variants]));
305
+ if (cached && cached.signature === signature) return cached;
306
+
307
+ const replacements = [];
308
+ for (const [code, variants] of entries.sort(byDescendingEntityLength(([key]) => key))) {
309
+ const canonical = Array.isArray(variants) ? variants[0] : variants;
310
+ if (!canonical) continue;
311
+ const regex = new RegExp(`${WORD_BOUNDARY_BEFORE}${escapeRegex(String(code))}${WORD_BOUNDARY_AFTER}`, 'giu');
312
+ replacements.push({ regex, code: String(canonical), anchor: literalAnchor(code) });
313
+ }
314
+ const compiled = { frozen: isDeeplyFrozen(reverseMapping), signature, replacements: compileReplacements(replacements) };
315
+ compiledReverseMappings.set(reverseMapping, compiled);
316
+ return compiled;
317
+ }
318
+
160
319
  /**
161
320
  * Code → entité. Le premier variant du tableau fait foi : c'est l'orthographe
162
321
  * canonique retenue par `consolidate_duplicate_entities`, celle qu'un humain
@@ -175,20 +334,9 @@ function applyMapping(text, mapping) {
175
334
  */
176
335
  function revertMapping(text, reverseMapping) {
177
336
  if (typeof text !== 'string' || !text) return text;
178
- const entries = Object.entries(reverseMapping || {});
179
- if (!entries.length) return text;
180
-
181
- let output = text;
182
- for (const [code, variants] of entries.sort(byDescendingEntityLength(([key]) => key))) {
183
- const canonical = Array.isArray(variants) ? variants[0] : variants;
184
- if (!canonical) continue;
185
- // Un code est un identifiant ASCII : pas de variantes Unicode à gérer, mais
186
- // les mêmes frontières de mots, pour ne pas réécrire un code cité dans un
187
- // mot plus long.
188
- const regex = new RegExp(`${WORD_BOUNDARY_BEFORE}${escapeRegex(String(code))}${WORD_BOUNDARY_AFTER}`, 'giu');
189
- output = output.replace(regex, String(canonical));
190
- }
191
- return output;
337
+ if (!reverseMapping || typeof reverseMapping !== 'object') return text;
338
+ const compiled = compileReverseMapping(reverseMapping);
339
+ return applyReplacements(compiled.replacements, text);
192
340
  }
193
341
 
194
342
  function normalizedPathName(value) {
@@ -14,9 +14,9 @@ skill sert de guide pour ranger un ajout personnel au bon endroit.
14
14
 
15
15
  | Type d'ajout | Emplacement | Portée |
16
16
  | --- | --- | --- |
17
- | Instructions ou préférences globales (style, rappels, habitudes) | `/Users/tsardet/.piecemaker/custom/perso.md` | Toutes les affaires — ce fichier est importé automatiquement dans chaque dossier via `/Users/tsardet/.piecemaker/CLAUDE.md`. |
18
- | Skill personnelle réutilisable | Nouveau dossier `/Users/tsardet/.claude/skills/<nom>/SKILL.md` | Toutes les sessions Claude Code — découverte automatique, jamais écrasée par PieceMaker (un fichier créé à la main n'est jamais remplacé par l'application). |
19
- | Gabarit personnel (acte, courrier type…) | `/Users/tsardet/.piecemaker/templates/` | Disponible pour toute rédaction, indépendamment du dépôt de l'application. |
17
+ | Instructions ou préférences globales (style, rappels, habitudes) | `~/.piecemaker/custom/perso.md` | Toutes les affaires — ce fichier est importé automatiquement dans chaque dossier via `~/.piecemaker/CLAUDE.md`. |
18
+ | Skill personnelle réutilisable | Nouveau dossier `~/.claude/skills/<nom>/SKILL.md` | Toutes les sessions Claude Code — découverte automatique, jamais écrasée par PieceMaker (un fichier créé à la main n'est jamais remplacé par l'application). |
19
+ | Gabarit personnel (acte, courrier type…) | `~/.piecemaker/templates/` | Disponible pour toute rédaction, indépendamment du dépôt de l'application. |
20
20
  | Élément propre à une seule affaire (parties, juridiction, échéances) | Section « Ce dossier » du `CLAUDE.md` / `AGENTS.md` de ce dossier | Uniquement ce dossier-là. |
21
21
 
22
22
  Ces emplacements sont hors du dépôt PieceMaker : une mise à jour de PieceMaker
@@ -26,7 +26,7 @@ PieceMaker — la garantie va dans les deux sens.
26
26
 
27
27
  ## Format minimal d'une skill personnelle
28
28
 
29
- Un fichier `/Users/tsardet/.claude/skills/<nom>/SKILL.md` avec un en-tête
29
+ Un fichier `~/.claude/skills/<nom>/SKILL.md` avec un en-tête
30
30
  puis du texte libre :
31
31
 
32
32
  ```markdown
@@ -50,17 +50,3 @@ Instructions pour l'assistant, en français, centrées sur la tâche.
50
50
  3. **Créer ou éditer le fichier correspondant**, en respectant le format
51
51
  attendu (front matter pour une skill, section « Ce dossier » pour un
52
52
  élément local, etc.).
53
- 4. **Pour un nouveau dossier d'affaire** qui n'a pas encore de wrapper : sur
54
- demande, proposer et créer `CLAUDE.md` et `AGENTS.md` à la racine du
55
- dossier, avec exactement ce contenu (chemin absolu, jamais `~`) :
56
-
57
- ```
58
- @/Users/tsardet/.piecemaker/CLAUDE.md
59
-
60
- # Ce dossier
61
- <!-- Éléments propres à ce dossier : parties, juridiction, échéances, particularités. À compléter. -->
62
- ```
63
-
64
- Si l'un de ces deux fichiers existe déjà dans le dossier, ne jamais
65
- l'écraser : le lire, et s'il ne contient pas déjà cette ligne d'import,
66
- l'ajouter en tête sans supprimer le contenu existant.