@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.
- package/dist/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BucOHLdm.js} +1 -1
- package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js} +1 -1
- package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js} +4 -4
- package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js} +1 -1
- package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-D86PckZ2.js} +1 -1
- package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DLETP-7v.js} +1 -1
- package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-CwdQBXeb.js} +1 -1
- package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js} +1 -1
- package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-XFJ9gTkC.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-DiCrND_Y.js} +1 -1
- package/dist/assets/{arc-BK1oUIk7.js → arc-DOjgbMYz.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-brzIFVhl.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-C8JhTjQQ.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-jfYxccyk.js} +1 -1
- package/dist/assets/channel-Dajzu2W1.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-CrlEShp9.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-DzPlUHGq.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-Cp9cpeWU.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-BifUldEN.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-ggPEIkWu.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-3h_nayRX.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-CH29aG6J.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-PluJUM5k.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-DXU39Vwg.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-Dz6roYrN.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-BLzrpSit.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-kfxnERDZ.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-DP57bp-N.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-DAlsrQVW.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-By5yrMer.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-Dx89kryN.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-B9fARitZ.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-DC15CEH1.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-BKQzghGF.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-CPY6RM7G.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-4dL4GJcf.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-D4-cVcfb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BMeVIEiX.js} +1 -1
- package/dist/assets/{index-Bk2iXKnJ.js → index-DNtFDOni.js} +3 -3
- package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-D5qMSUEU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-BDnyKRHm.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-dGz5ZGzq.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-D7UAz7o_.js} +1 -1
- package/dist/assets/{layout-CTfb1sAF.js → layout-DKrmEGrz.js} +1 -1
- package/dist/assets/{linear-Dfgi6vlo.js → linear-CrTdMY-p.js} +1 -1
- package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-CXQOT5G6.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-VgEiYfay.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-9IqT8gGx.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-D98pRE-t.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-b9PwkYzL.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dz87hsPE.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-CkcV73dK.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-UBN31Yrs.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-CpQxd5Za.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-mCsFmyzw.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-B0MTBN-3.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-DzBPN39W.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-Dzydjm0e.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-Dz0toOLu.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-DOWp7dkf.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-CUxsztUB.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-DS96vNED.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-server/server/piecemaker/agent-instructions.js +148 -0
- package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
- package/dist-server/server/piecemaker/index.js +4 -0
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
- package/server/piecemaker/agent-instructions.ts +148 -0
- package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
- package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
- package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
- package/server/piecemaker/anonymizer/service.cjs +7 -6
- package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +12 -1
- package/server/piecemaker/index.ts +4 -0
- package/server/piecemaker/router.cjs +0 -1
- package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
- package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -30
- package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
- package/dist/assets/channel-DVyNr2Yg.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
140
|
-
const
|
|
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
|
|
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,
|
|
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 =
|
|
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(
|
|
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(
|
|
120
|
-
const ptyReady = repairNodePtySpawnHelpers(
|
|
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
|
-
*
|
|
111
|
-
*
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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) |
|
|
18
|
-
| Skill personnelle réutilisable | Nouveau dossier
|
|
19
|
-
| Gabarit personnel (acte, courrier type…) |
|
|
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
|
|
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.
|