@piecemaker-legal/piecemaker 1.1.1 → 2.0.2
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/{abnfDiagram-VCTEODGH-DEXWh3LH.js → abnfDiagram-VCTEODGH-BeQS_HnO.js} +1 -1
- package/dist/assets/{arc-CoTRLXKW.js → arc-ClYjD3aM.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-Cg-jO9oR.js → architectureDiagram-5GKGNRK7-Csdihb1L.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-ChXwQSIq.js → blockDiagram-I7D4REHJ-BuXey9ht.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-Bw2VGggd.js → c4Diagram-7LVT6UL2-DdO-ovOy.js} +1 -1
- package/dist/assets/channel-DhuuSCHK.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-C1WdqrLP.js → chunk-2Q5K7J3B-D3hzWzSc.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BerUZBa1.js → chunk-5VM5RSS4-DeT3cR6k.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-5W7ax2gB.js → chunk-F27PBJKO-bazGi1ru.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-JgDN1h4T.js → chunk-IMKFNOWR-BE3ZXrPd.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-ulNHnEdQ.js → chunk-JWPE2WC7-dTAsgByE.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-D4aTDC2T.js → chunk-POPQ4Y6H-DKYrzMSd.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-BA5zX4XF.js → chunk-SVP7TREG-BTtrMvhE.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-DhOULVhX.js → chunk-TICWLB2K-CZPHShPS.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CkpCgrVg.js → chunk-XXDRQBXY-B0byFHaU.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-CO0E82Dl.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-CO0E82Dl.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-0xUSfjfV.js → cose-bilkent-JH36ORCC-BbLCzlaU.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BE6fNu2_.js → cynefin-OW5HDTMX-CQ8ui4YD.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-rFMnZ9b3.js → cynefinDiagram-5FMLGOSQ-T-GL1aIf.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-C1p4ffd7.js → dagre-GXQ25YYZ-CXxVKznH.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-BV_3Xj4O.js → diagram-S7CK7UJ4-B_-1Mf_7.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-Bb_Evkrm.js → diagram-UQ7AKVKN-Bs-OB8xI.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-BzgupjxD.js → diagram-VSXAHHWV-BpUqq93o.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-D1c67z76.js → diagram-VX7I27RA-CZ6hSUo6.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-DsJAJGKE.js → diagram-Z3DM3KII-DVZiDO6S.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-B1ge_obq.js → ebnfDiagram-PWID7BFC-D_1izoKK.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-DrV-tggw.js → erDiagram-RLTQ6QDP-_jSi4sg-.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-CVzDiSQ3.js → flowDiagram-HODETNUW-BtBM7ez3.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-CGRdSN9D.js → ganttDiagram-EL5Y4UJY-Bz2nxw7c.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CAfvFXzL.js → gitGraphDiagram-WWUBYQGX-8Q3I3hoE.js} +1 -1
- package/dist/assets/{index-DaxH81ZJ.js → index-CZUj0Tlu.js} +152 -152
- package/dist/assets/index-DcfdIMj-.css +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-DTshEkGK.js → infoDiagram-27XIBGKW-Cc6eKjfU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-DkbshVxp.js → ishikawaDiagram-5VMMS53U-Bd3w97qM.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-v5qE8xpn.js → journeyDiagram-3NMN7TZE-Cy6XbK5X.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-a9CNMFLt.js → kanban-definition-UXKFOSKX-kyLqWDpr.js} +1 -1
- package/dist/assets/{layout-DrW4R1TH.js → layout-DXpPSEft.js} +1 -1
- package/dist/assets/{linear-Cs3qpXT_.js → linear-DH_h9cDZ.js} +1 -1
- package/dist/assets/{mermaid.core-C7Ti-Fty.js → mermaid.core-BghXiB7p.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-B0VDdIcG.js → mindmap-definition-YA3MSWOX-C2xxEXsP.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-CV1L7MjP.js → pegDiagram-XKGWAZYB-DuppHX4-.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-DSCuzVTV.js → pieDiagram-E7YTZNPT-BCukPZ2a.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-hzX-9AKo.js → quadrantDiagram-AXDQQJYC-Dm_ojY_7.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-BbmzIqcp.js → railroadDiagram-O6MQD6OU-BzdOv7BJ.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CQCRRBSL.js → requirementDiagram-BXWQKSXE-DgewPP_g.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-kiaLcW6X.js → sankeyDiagram-P5KCCOFB-vEIMvCSt.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bgx8ST7Q.js → sequenceDiagram-WJ2MYXX4-BL_31NoY.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BTjJ-ygN.js → sizeCapture-INFHLROL-Ch6u4xo6.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-D48RR1FL.js → stateDiagram-D77RDMKH-ByquMuJv.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-B-XwSR-z.js → stateDiagram-v2-MP3YSRHH-_QRJP_vn.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-BhE-QuEZ.js → swimlanes-42K2YHIH-BZyXEHOb.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-sqZwZwbP.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-Cm7EUWWt.js → timeline-definition-24CTP7MA-CLBivayK.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-X-nC9wZ0.js → vennDiagram-4TSXK5OY-3dJ2PRqR.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-D33YvEQZ.js → wardleyDiagram-VM6X3IG4-C279LT36.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-lziwBKsM.js → xychartDiagram-S5SC5T6Z-FoqPDrei.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/server/index.js +3 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/piecemaker/harness/citations.routes.js +18 -1
- package/dist-server/server/piecemaker/harness/citations.routes.js.map +1 -1
- package/dist-server/server/piecemaker/harness/legifrance-html.js +112 -0
- package/dist-server/server/piecemaker/harness/legifrance-html.js.map +1 -0
- package/dist-server/server/piecemaker/index.js +7 -3
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/index.js +13 -6
- package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
- package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
- package/dist-server/server/piecemaker/library/index.js +14 -41
- package/dist-server/server/piecemaker/library/index.js.map +1 -1
- package/dist-server/server/piecemaker/library/store.js +11 -3
- package/dist-server/server/piecemaker/library/store.js.map +1 -1
- package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
- package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/service.js +8 -7
- package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/store.js +5 -3
- package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
- package/package.json +1 -1
- package/product.config.json +2 -0
- package/scripts/piecemaker/build-app-icon.py +88 -0
- package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
- package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
- package/scripts/release/assert-piecemaker-version.mjs +25 -0
- package/server/index.ts +4 -1
- package/server/piecemaker/anonymizer/client-config.cjs +45 -162
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
- package/server/piecemaker/anonymizer/providers.cjs +14 -135
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
- package/server/piecemaker/anonymizer/service.cjs +144 -28
- package/server/piecemaker/harness/citations.routes.ts +23 -1
- package/server/piecemaker/harness/legifrance-html.ts +107 -0
- package/server/piecemaker/index.ts +8 -3
- package/server/piecemaker/knowledge/index.ts +14 -6
- package/server/piecemaker/knowledge/local-routes.ts +72 -0
- package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
- package/server/piecemaker/library/index.ts +14 -34
- package/server/piecemaker/library/store.ts +11 -3
- package/server/piecemaker/library/workspace-installation.ts +20 -3
- package/server/piecemaker/timesheet/service.ts +9 -7
- package/server/piecemaker/timesheet/store.ts +5 -3
- package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
- package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
- package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
- package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
- package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
- package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
- package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
- package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
- package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
- package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
- package/dist/assets/channel-DdzrPZ6Y.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-Dim2o8E5.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-Dim2o8E5.js +0 -1
- package/dist/assets/index-D_Dc_nQv.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-DR525d5V.js +0 -8
- package/server/piecemaker/anonymizer/proxy.cjs +0 -365
- package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
- package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
- package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
- package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
- package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
- package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
- package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
|
@@ -13,10 +13,12 @@ const path = require('path');
|
|
|
13
13
|
/** Racine du dépôt git, quatre niveaux au-dessus de ce fichier vendorisé. */
|
|
14
14
|
const GIT_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const DEPRECATED_HOOKS = new Set([
|
|
17
17
|
'anonymize-read.mjs',
|
|
18
18
|
'deanonymize-write.mjs',
|
|
19
19
|
'piecemaker-central-anonymize.mjs',
|
|
20
|
+
'verify-citations.mjs',
|
|
21
|
+
'decision-cache.mjs',
|
|
20
22
|
]);
|
|
21
23
|
|
|
22
24
|
function settingsPath(userHome) {
|
|
@@ -41,24 +43,24 @@ function commandScriptName(command) {
|
|
|
41
43
|
return match?.[1] || '';
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function
|
|
46
|
+
function deprecatedHookName(command) {
|
|
45
47
|
const text = String(command || '');
|
|
46
|
-
return [...
|
|
48
|
+
return [...DEPRECATED_HOOKS].find((name) => text.includes(name)) || '';
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function
|
|
51
|
+
function deprecatedHooks(settings) {
|
|
50
52
|
if (!settings?.hooks || typeof settings.hooks !== 'object') return [];
|
|
51
53
|
return Object.entries(settings.hooks).flatMap(([event, groups]) =>
|
|
52
54
|
(Array.isArray(groups) ? groups : []).flatMap((group) =>
|
|
53
55
|
(Array.isArray(group?.hooks) ? group.hooks : [])
|
|
54
|
-
.map((hook) => ({ event, command: hook?.command, script:
|
|
56
|
+
.map((hook) => ({ event, command: hook?.command, script: deprecatedHookName(hook?.command) }))
|
|
55
57
|
.filter((entry) => entry.script),
|
|
56
58
|
),
|
|
57
59
|
);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
/** Retire les
|
|
61
|
-
function
|
|
62
|
+
/** Retire les hooks hérités dont la fonction est passée dans le proxy PII. */
|
|
63
|
+
function removeDeprecatedHooks(userHome) {
|
|
62
64
|
const target = settingsPath(userHome);
|
|
63
65
|
let changed = false;
|
|
64
66
|
let removed = 0;
|
|
@@ -77,7 +79,7 @@ function removeDeprecatedMappingHooks(userHome) {
|
|
|
77
79
|
continue;
|
|
78
80
|
}
|
|
79
81
|
const hooks = group.hooks;
|
|
80
|
-
const kept = hooks.filter((hook) => !
|
|
82
|
+
const kept = hooks.filter((hook) => !deprecatedHookName(hook?.command));
|
|
81
83
|
const removedFromGroup = hooks.length - kept.length;
|
|
82
84
|
removed += removedFromGroup;
|
|
83
85
|
eventChanged ||= removedFromGroup > 0;
|
|
@@ -155,7 +157,7 @@ function claudeHooksStatus(repoRoot, userHome) {
|
|
|
155
157
|
const expected = expectedCommands(repoRoot);
|
|
156
158
|
if (!expected) return { ok: false, reason: 'source-hooks-absent', missing: [] };
|
|
157
159
|
const settings = readJson(settingsPath(userHome), {});
|
|
158
|
-
const deprecated =
|
|
160
|
+
const deprecated = deprecatedHooks(settings);
|
|
159
161
|
const missing = expected.filter((entry) => {
|
|
160
162
|
const current = findHook(settings, entry.event, entry.script);
|
|
161
163
|
return current?.hook.command !== entry.command
|
|
@@ -167,9 +169,9 @@ function claudeHooksStatus(repoRoot, userHome) {
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
function installClaudeHooks(repoRoot, userHome) {
|
|
170
|
-
const cleanup =
|
|
172
|
+
const cleanup = removeDeprecatedHooks(userHome);
|
|
171
173
|
if (!cleanup.ok) {
|
|
172
|
-
return { ok: false, changed: cleanup.changed, reason: `Nettoyage des
|
|
174
|
+
return { ok: false, changed: cleanup.changed, reason: `Nettoyage des hooks hérités impossible : ${cleanup.reason}` };
|
|
173
175
|
}
|
|
174
176
|
const expected = expectedCommands(repoRoot);
|
|
175
177
|
if (!expected) {
|
|
@@ -375,6 +377,6 @@ module.exports = {
|
|
|
375
377
|
installClaudeHooks,
|
|
376
378
|
installClaudeSessionHook,
|
|
377
379
|
installClaudeStatusLine,
|
|
378
|
-
|
|
380
|
+
removeDeprecatedHooks,
|
|
379
381
|
settingsPath,
|
|
380
382
|
};
|
|
@@ -24,21 +24,15 @@ const {
|
|
|
24
24
|
} = require('./process-group.cjs');
|
|
25
25
|
|
|
26
26
|
const {
|
|
27
|
-
createCommit,
|
|
28
27
|
originalFilesOverview,
|
|
29
|
-
resolveCase,
|
|
30
28
|
safeCaseFiles,
|
|
31
29
|
} = require('../piecemaker-plugin/scripts/lib/commits.cjs');
|
|
32
|
-
const { documentKey
|
|
30
|
+
const { documentKey } = require('../piecemaker-plugin/scripts/lib/protection.cjs');
|
|
33
31
|
const {
|
|
34
|
-
caseConversionOutputDirectory,
|
|
35
32
|
classifyRelativeCaseFolderPath,
|
|
36
33
|
readCaseFolderStructure,
|
|
37
34
|
} = require('./case-folder-structure.cjs');
|
|
38
|
-
const {
|
|
39
|
-
markFilesAnonymized,
|
|
40
|
-
markFilesConverted,
|
|
41
|
-
} = require('../piecemaker-plugin/scripts/lib/anonymization-state.cjs');
|
|
35
|
+
const { markFilesAnonymized } = require('../piecemaker-plugin/scripts/lib/anonymization-state.cjs');
|
|
42
36
|
// Le mapping vit dans le plugin : c'est le seul des trois consommateurs (hooks,
|
|
43
37
|
// pipeline, routeur du task pane) qui soit distribué seul.
|
|
44
38
|
const {
|
|
@@ -58,8 +52,6 @@ const { isSocieteCode, societeCounterKey, detectCompanySigle, LEGAL_FORM_TOKENS
|
|
|
58
52
|
const { syncCentralMapping } = require('../piecemaker-plugin/scripts/lib/central-mapping.cjs');
|
|
59
53
|
|
|
60
54
|
const SCRIPTS_DIR = path.join(__dirname, 'scripts');
|
|
61
|
-
const CONVERTER_SCRIPT = () => process.env.SMART_CONVERTER_PATH || path.join(SCRIPTS_DIR, 'smart_converter.py');
|
|
62
|
-
const PIPELINE_SCRIPT = () => process.env.PIECEMAKER_PIPELINE_PATH || path.join(SCRIPTS_DIR, 'convert_and_scan_pipeline.py');
|
|
63
55
|
const PIECEMAKER_HOME = path.join(os.homedir(), '.piecemaker');
|
|
64
56
|
const configuredPythonPath = () => {
|
|
65
57
|
try {
|
|
@@ -466,67 +458,6 @@ async function migrateLegacySensitiveMaps(caseRoot) {
|
|
|
466
458
|
|
|
467
459
|
// ── Travaux de conversion / anonymisation ──────────────────────────────────
|
|
468
460
|
|
|
469
|
-
const jobs = new Map();
|
|
470
|
-
const JOB_RETENTION_MS = 30 * 60 * 1000;
|
|
471
|
-
|
|
472
|
-
function pruneJobs() {
|
|
473
|
-
const now = Date.now();
|
|
474
|
-
for (const [id, job] of jobs) {
|
|
475
|
-
if (job.state !== 'running' && job.finishedAt && now - Date.parse(job.finishedAt) > JOB_RETENTION_MS) jobs.delete(id);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/** Travail rendu déjà terminé — rien à traiter, aucun processus lancé. */
|
|
480
|
-
function finishedJob({ case: caseName, caseRoot, action, total, files, result }) {
|
|
481
|
-
const now = new Date().toISOString();
|
|
482
|
-
return {
|
|
483
|
-
id: crypto.randomUUID(),
|
|
484
|
-
case: caseName,
|
|
485
|
-
caseRoot,
|
|
486
|
-
action,
|
|
487
|
-
state: 'done',
|
|
488
|
-
phase: 'mapping',
|
|
489
|
-
percent: 100,
|
|
490
|
-
processed: total,
|
|
491
|
-
total,
|
|
492
|
-
skipped: result?.skipped || 0,
|
|
493
|
-
files,
|
|
494
|
-
log: [],
|
|
495
|
-
error: null,
|
|
496
|
-
result,
|
|
497
|
-
cancelled: false,
|
|
498
|
-
child: null,
|
|
499
|
-
startedAt: now,
|
|
500
|
-
finishedAt: now,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Forme exposée par l'API. `caseRoot`, `child` et `reserveBytes` en sont retirés :
|
|
506
|
-
* ils ne servent qu'à l'ordonnancement interne, et la réponse de
|
|
507
|
-
* `GET /api/admin/originals/job` n'a pas à véhiculer un chemin absolu du disque du
|
|
508
|
-
* cabinet ni des octets de réservation. `queuePosition` est ajouté pour un job en
|
|
509
|
-
* file, pour que l'admin puisse afficher « N devant ».
|
|
510
|
-
*/
|
|
511
|
-
function publicJob(job) {
|
|
512
|
-
if (!job) return null;
|
|
513
|
-
const {
|
|
514
|
-
cancelReason,
|
|
515
|
-
child,
|
|
516
|
-
completion,
|
|
517
|
-
processGroupId,
|
|
518
|
-
reserveBytes,
|
|
519
|
-
stopPromise,
|
|
520
|
-
timeoutId,
|
|
521
|
-
...rest
|
|
522
|
-
} = job;
|
|
523
|
-
if (job.state === 'queued') {
|
|
524
|
-
const position = waiting.findIndex((entry) => entry.job.id === job.id);
|
|
525
|
-
rest.queuePosition = position >= 0 ? position + 1 : 1;
|
|
526
|
-
}
|
|
527
|
-
return rest;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
461
|
function appendLog(job, line) {
|
|
531
462
|
const text = String(line || '').trim();
|
|
532
463
|
if (!text) return;
|
|
@@ -534,24 +465,6 @@ function appendLog(job, line) {
|
|
|
534
465
|
if (job.log.length > MAX_LOG_LINES) job.log.splice(0, job.log.length - MAX_LOG_LINES);
|
|
535
466
|
}
|
|
536
467
|
|
|
537
|
-
/**
|
|
538
|
-
* Le verrou porte sur la **racine** du dossier, pas sur son nom : deux dossiers
|
|
539
|
-
* juridiques enregistrés distincts peuvent porter le même nom, et un
|
|
540
|
-
* traitement lent sur l'une bloquait alors tout traitement sur l'autre. Un job en
|
|
541
|
-
* file compte aussi : sinon un double-clic empilerait deux traitements du même
|
|
542
|
-
* dossier au lieu d'un seul.
|
|
543
|
-
*/
|
|
544
|
-
function runningJobForCase(caseRoot) {
|
|
545
|
-
for (const job of jobs.values()) {
|
|
546
|
-
if (['running', 'queued'].includes(job.state) && job.caseRoot === caseRoot) return job;
|
|
547
|
-
}
|
|
548
|
-
return null;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function getJob(jobId) {
|
|
552
|
-
return publicJob(jobs.get(String(jobId || '')));
|
|
553
|
-
}
|
|
554
|
-
|
|
555
468
|
// ── Contrôle d'admission : sérialise GLiNER, plafonne les conversions par la RAM ─
|
|
556
469
|
|
|
557
470
|
/** Descripteurs de traitements admis mais pas encore lancés, du plus ancien au plus récent. */
|
|
@@ -560,17 +473,12 @@ let reservedBytes = 0;
|
|
|
560
473
|
let acceptingJobs = true;
|
|
561
474
|
|
|
562
475
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* reste vraie dans les deux sens, quel que soit le consommateur qui a lancé
|
|
566
|
-
* le traitement en cours.
|
|
476
|
+
* Traitements en cours, tous consommateurs confondus : l'exclusivité GLiNER
|
|
477
|
+
* doit rester vraie quel que soit celui qui a lancé le traitement.
|
|
567
478
|
*/
|
|
568
479
|
const runningManaged = new Set();
|
|
569
480
|
|
|
570
481
|
function runningAnonymize() {
|
|
571
|
-
for (const job of jobs.values()) {
|
|
572
|
-
if (job.state === 'running' && job.action === 'anonymize') return true;
|
|
573
|
-
}
|
|
574
482
|
for (const job of runningManaged) {
|
|
575
483
|
if (job.action === 'anonymize') return true;
|
|
576
484
|
}
|
|
@@ -590,9 +498,7 @@ function canAdmit(job) {
|
|
|
590
498
|
|
|
591
499
|
/**
|
|
592
500
|
* Admet les traitements en file qui rentrent désormais, du plus ancien au plus
|
|
593
|
-
* récent.
|
|
594
|
-
* `run`, sans `legalCase`) suit son propre lancement : même file, même
|
|
595
|
-
* contrôle d'admission, exécution différente.
|
|
501
|
+
* récent.
|
|
596
502
|
*/
|
|
597
503
|
function pumpQueue() {
|
|
598
504
|
if (!acceptingJobs) return;
|
|
@@ -601,8 +507,7 @@ function pumpQueue() {
|
|
|
601
507
|
if (!canAdmit(descriptor.job)) continue;
|
|
602
508
|
waiting.splice(index, 1);
|
|
603
509
|
index -= 1;
|
|
604
|
-
|
|
605
|
-
else launchJob(descriptor);
|
|
510
|
+
launchManagedJob(descriptor);
|
|
606
511
|
}
|
|
607
512
|
}
|
|
608
513
|
|
|
@@ -613,9 +518,9 @@ function pumpQueue() {
|
|
|
613
518
|
* stderr conservé pour diagnostiquer un échec.
|
|
614
519
|
*
|
|
615
520
|
* `progressScale` reporte le pourcentage 0-100 de cet appel dans la part
|
|
616
|
-
* `[offset, offset + weight]` du travail global : un
|
|
617
|
-
* plusieurs appels
|
|
618
|
-
*
|
|
521
|
+
* `[offset, offset + weight]` du travail global : un appelant qui enchaîne
|
|
522
|
+
* plusieurs appels garde ainsi une progression monotone plutôt qu'un
|
|
523
|
+
* pourcentage qui repart de zéro à chaque appel.
|
|
619
524
|
*
|
|
620
525
|
* `convert_and_scan_pipeline.py` enchaîne lui-même CONVERT (markitdown) puis
|
|
621
526
|
* SCAN/CHUNKS (GLiNER) au sein d'un même appel, chacun avec son propre 0-100 :
|
|
@@ -771,9 +676,9 @@ function spawnTracked(job, script, args, progressScale = {}) {
|
|
|
771
676
|
* contrôle d'admission que les pièces originales (exclusivité GLiNER, budget
|
|
772
677
|
* RAM, file d'attente, nice, timeout, arrêt propre du groupe de process).
|
|
773
678
|
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
679
|
+
* Cette fonction ne connaît aucune notion de dossier juridique enregistré
|
|
680
|
+
* (`legalCase`, mapping, commit) : elle sert les consommateurs qui ont leur
|
|
681
|
+
* propre gestion du résultat (le
|
|
777
682
|
* pipeline `knowledge`, dont le `projectId` n'est pas un dossier juridique du
|
|
778
683
|
* registre) mais qui doivent néanmoins partager le même verrou GLiNER et le
|
|
779
684
|
* même budget RAM que l'administration — sinon rien n'empêche deux scans
|
|
@@ -835,7 +740,7 @@ function runManagedPythonJob({ action, script, args, onProgress, signal } = {})
|
|
|
835
740
|
});
|
|
836
741
|
}
|
|
837
742
|
|
|
838
|
-
/**
|
|
743
|
+
/** Lancement d'un descripteur `runManagedPythonJob` (pas de `legalCase`/commit à gérer ici). */
|
|
839
744
|
function launchManagedJob(descriptor) {
|
|
840
745
|
const { job, run, resolve, reject } = descriptor;
|
|
841
746
|
reservedBytes += job.reserveBytes;
|
|
@@ -868,334 +773,6 @@ function launchManagedJob(descriptor) {
|
|
|
868
773
|
})();
|
|
869
774
|
}
|
|
870
775
|
|
|
871
|
-
/**
|
|
872
|
-
* Migration « au prochain traitement », rattachée aux pièces du lot : range dans
|
|
873
|
-
* le dossier de conversion métier le Markdown que d'anciennes versions ont laissé à la racine
|
|
874
|
-
* pour une pièce effectivement (re)traitée. Le rattachement au lot est délibéré —
|
|
875
|
-
* comme `sessionArtifactPaths` — pour ne jamais déplacer un document de travail
|
|
876
|
-
* de l'utilisateur ni un fichier modifié en parallèle. Retourne les chemins
|
|
877
|
-
* racine retirés (POSIX) : le commit du lot les enregistre comme suppressions,
|
|
878
|
-
* sinon la relocalisation resterait un changement en attente.
|
|
879
|
-
*
|
|
880
|
-
* Le mapping n'est **pas** relocalisé ici : sa consolidation reste l'apanage du
|
|
881
|
-
* chemin de succès (`writeCaseMapping`), qui lit les copies racine puis les
|
|
882
|
-
* supprime. Un scan en échec ne doit pas migrer prématurément un ancien mapping.
|
|
883
|
-
*/
|
|
884
|
-
function outputDirectoryForOriginal(caseRoot, absoluteFile) {
|
|
885
|
-
const relative = path.relative(caseRoot, absoluteFile).split(path.sep).join('/');
|
|
886
|
-
return caseConversionOutputDirectory(caseRoot, relative)
|
|
887
|
-
|| path.join(caseRoot, WORKSPACE_SUBDIR);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function migrateRootArtifacts(caseRoot, absoluteFiles) {
|
|
891
|
-
const sourceByKey = new Map(absoluteFiles.map((file) => [documentKey(file), file]));
|
|
892
|
-
if (!sourceByKey.size) return [];
|
|
893
|
-
let entries;
|
|
894
|
-
try {
|
|
895
|
-
entries = fs.readdirSync(caseRoot, { withFileTypes: true });
|
|
896
|
-
} catch {
|
|
897
|
-
return [];
|
|
898
|
-
}
|
|
899
|
-
const toMove = entries.filter((entry) =>
|
|
900
|
-
entry.isFile()
|
|
901
|
-
&& path.extname(entry.name).toLowerCase() === '.md'
|
|
902
|
-
&& sourceByKey.has(documentKey(entry.name)));
|
|
903
|
-
if (!toMove.length) return [];
|
|
904
|
-
const removed = [];
|
|
905
|
-
for (const entry of toMove) {
|
|
906
|
-
const source = path.join(caseRoot, entry.name);
|
|
907
|
-
const outputDirectory = outputDirectoryForOriginal(caseRoot, sourceByKey.get(documentKey(entry.name)));
|
|
908
|
-
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
909
|
-
const destination = path.join(outputDirectory, entry.name);
|
|
910
|
-
// Le sous-dossier fait autorité : un doublon plus récent y gagne, on se
|
|
911
|
-
// contente d'écarter la copie racine périmée.
|
|
912
|
-
if (fs.existsSync(destination)) fs.rmSync(source, { force: true });
|
|
913
|
-
else fs.renameSync(source, destination);
|
|
914
|
-
removed.push(entry.name);
|
|
915
|
-
}
|
|
916
|
-
return removed;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
async function runJob(job, legalCase, absoluteFiles, options) {
|
|
920
|
-
job.migratedFromRoot = migrateRootArtifacts(legalCase.root, absoluteFiles);
|
|
921
|
-
if (job.cancelled) throw cancellationError(job);
|
|
922
|
-
if (job.action === 'convert') {
|
|
923
|
-
// `smart_converter.py` ne prend qu'un fichier : on avance dossier par
|
|
924
|
-
// dossier pour garder une progression lisible même sans ligne PROGRESS.
|
|
925
|
-
job.phase = 'convert';
|
|
926
|
-
for (const [index, absolute] of absoluteFiles.entries()) {
|
|
927
|
-
if (job.cancelled) throw cancellationError(job);
|
|
928
|
-
job.processed = index;
|
|
929
|
-
job.percent = Math.round((index / absoluteFiles.length) * 100);
|
|
930
|
-
const outputDirectory = outputDirectoryForOriginal(legalCase.root, absolute);
|
|
931
|
-
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
932
|
-
const args = [absolute, '-o', outputDirectory];
|
|
933
|
-
if (options.engine) args.push('--engine', options.engine);
|
|
934
|
-
if (options.mode) args.push('--mode', options.mode);
|
|
935
|
-
if (options.lang) args.push('--lang', options.lang);
|
|
936
|
-
await spawnTracked(job, CONVERTER_SCRIPT(), args);
|
|
937
|
-
markFilesConverted(legalCase.root, [absolute]);
|
|
938
|
-
job.processed = index + 1;
|
|
939
|
-
job.percent = Math.round(((index + 1) / absoluteFiles.length) * 100);
|
|
940
|
-
appendLog(job, `Conversion ${job.processed}/${absoluteFiles.length}`);
|
|
941
|
-
}
|
|
942
|
-
return { converted: absoluteFiles.length };
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const before = readCaseMapping(legalCase.root);
|
|
946
|
-
const mappingFile = caseMappingFile(legalCase.root);
|
|
947
|
-
|
|
948
|
-
// `--state-file` découple le statut « analysé » du contenu sensible ; les
|
|
949
|
-
// cartes brutes restent temporaires et ne sont jamais déposées ici.
|
|
950
|
-
// Le script n'accepte qu'un répertoire de sortie. On regroupe donc les
|
|
951
|
-
// pièces par répertoire de sortie (zones métier et sous-dossier de travail
|
|
952
|
-
// générique confondus).
|
|
953
|
-
const groups = new Map();
|
|
954
|
-
for (const absolute of absoluteFiles) {
|
|
955
|
-
const outputDirectory = outputDirectoryForOriginal(legalCase.root, absolute);
|
|
956
|
-
if (!groups.has(outputDirectory)) groups.set(outputDirectory, []);
|
|
957
|
-
groups.get(outputDirectory).push(absolute);
|
|
958
|
-
}
|
|
959
|
-
// Chaque groupe ne rapporte qu'une part de `job.percent`, proportionnelle
|
|
960
|
-
// à sa part du nombre total de fichiers du lot : sans quoi la barre
|
|
961
|
-
// repartirait de zéro à chaque nouveau groupe.
|
|
962
|
-
let filesDone = 0;
|
|
963
|
-
for (const [outputDirectory, groupFiles] of groups) {
|
|
964
|
-
if (job.cancelled) throw cancellationError(job);
|
|
965
|
-
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
966
|
-
const args = [...groupFiles, '-o', outputDirectory, '--mapping-file', mappingFile];
|
|
967
|
-
// `--case-root` découple la clé du manifeste de `--output` : les pièces
|
|
968
|
-
// vivent sous le dossier juridique, pas sous le sous-dossier de sortie.
|
|
969
|
-
args.push('--case-root', legalCase.root);
|
|
970
|
-
args.push('--state-file', path.join(legalCase.root, '.piecemaker', 'anonymization-state.json'));
|
|
971
|
-
if (options.skipExisting) args.push('--skip-existing');
|
|
972
|
-
if (options.engine) args.push('--engine', options.engine);
|
|
973
|
-
if (options.mode) args.push('--mode', options.mode);
|
|
974
|
-
if (options.lang) args.push('--lang', options.lang);
|
|
975
|
-
const offset = absoluteFiles.length ? (filesDone / absoluteFiles.length) * 100 : 0;
|
|
976
|
-
const weight = absoluteFiles.length ? (groupFiles.length / absoluteFiles.length) * 100 : 100;
|
|
977
|
-
await spawnTracked(job, PIPELINE_SCRIPT(), args, { offset, weight });
|
|
978
|
-
filesDone += groupFiles.length;
|
|
979
|
-
}
|
|
980
|
-
job.phase = 'mapping';
|
|
981
|
-
job.percent = 100;
|
|
982
|
-
// Migration unique des dossiers créés par les anciennes versions : leurs
|
|
983
|
-
// scans sont absorbés dans le mapping et leur état technique avant suppression.
|
|
984
|
-
const mapping = await rebuildCaseMapping(legalCase.root);
|
|
985
|
-
const mappingAdded = Math.max(0, mapping.total - Object.keys(before.mapping).length);
|
|
986
|
-
appendLog(job, `Mapping : ${mappingAdded} nouvelle(s) entrée(s), ${mapping.total} au total`);
|
|
987
|
-
if (mapping.migratedScans) appendLog(job, `${mapping.migratedScans} ancien(s) sensitive map migré(s)`);
|
|
988
|
-
return { scanned: absoluteFiles.length, mappingAdded, mappingTotal: mapping.total };
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Chemins produits par le lot courant. Le filtre est volontairement rattaché
|
|
993
|
-
* aux pièces sélectionnées : un autre fichier Markdown/JSON modifié pendant un
|
|
994
|
-
* OCR long ne doit jamais entrer dans le commit de cette session.
|
|
995
|
-
*/
|
|
996
|
-
async function sessionArtifactPaths(legalCase, absoluteFiles, action) {
|
|
997
|
-
const documentKeys = new Set(absoluteFiles.map((file) => documentKey(file)));
|
|
998
|
-
const mappingPath = path.relative(legalCase.root, caseMappingFile(legalCase.root)).split(path.sep).join('/');
|
|
999
|
-
const safeFiles = await safeCaseFiles(legalCase.root);
|
|
1000
|
-
return safeFiles.filter((relative) => {
|
|
1001
|
-
if (action === 'anonymize' && relative === mappingPath) return true;
|
|
1002
|
-
const segments = relative.split('/');
|
|
1003
|
-
const basename = segments.at(-1) || '';
|
|
1004
|
-
const extension = path.extname(basename).toLowerCase();
|
|
1005
|
-
const insideDocumentOutput = segments.slice(0, -1).some((segment) => documentKeys.has(documentKey(segment)));
|
|
1006
|
-
if (insideDocumentOutput) return true;
|
|
1007
|
-
if (extension === '.md') return documentKeys.has(documentKey(basename));
|
|
1008
|
-
return false;
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
async function commitJobArtifacts(job, legalCase, absoluteFiles, homeDir) {
|
|
1013
|
-
if (!homeDir) return null;
|
|
1014
|
-
const artifactPaths = await sessionArtifactPaths(legalCase, absoluteFiles, job.action);
|
|
1015
|
-
// Les Markdown relocalisés depuis la racine rejoignent le commit du lot pour
|
|
1016
|
-
// que leur suppression à l'ancien emplacement y soit enregistrée proprement.
|
|
1017
|
-
const paths = [...new Set([...artifactPaths, ...(job.migratedFromRoot || [])])];
|
|
1018
|
-
if (!paths.length) throw new Error('Traitement terminé, mais aucun fichier produit ne peut être commité.');
|
|
1019
|
-
job.phase = 'commit';
|
|
1020
|
-
appendLog(job, `Commit automatique de ${paths.length} fichier(s)`);
|
|
1021
|
-
const count = absoluteFiles.length;
|
|
1022
|
-
const commit = await createCommit({
|
|
1023
|
-
casesRoot: legalCase.casesRoot,
|
|
1024
|
-
caseName: legalCase.name,
|
|
1025
|
-
homeDir,
|
|
1026
|
-
label: job.action === 'convert'
|
|
1027
|
-
? `Conversion de ${count} pièce${count > 1 ? 's' : ''}`
|
|
1028
|
-
: `Conversion et analyse PII de ${count} pièce${count > 1 ? 's' : ''}`,
|
|
1029
|
-
sessionId: job.id,
|
|
1030
|
-
event: job.action === 'convert' ? 'admin-conversion' : 'admin-scan',
|
|
1031
|
-
paths,
|
|
1032
|
-
waitForLockMs: 10_000,
|
|
1033
|
-
});
|
|
1034
|
-
if (commit.skipped === 'busy') throw new Error('Traitement terminé, mais l’historique est occupé : commit automatique non créé.');
|
|
1035
|
-
return {
|
|
1036
|
-
created: commit.created,
|
|
1037
|
-
hash: commit.commit || null,
|
|
1038
|
-
files: commit.files || [],
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Démarre un travail sur les pièces d'un dossier.
|
|
1044
|
-
* `action` vaut `convert` (Markdown seul) ou `anonymize` (Markdown + scan PII
|
|
1045
|
-
* + régénération du mapping). `files` contient des chemins relatifs au dossier
|
|
1046
|
-
* juridique, tels que renvoyés par `listOriginals`.
|
|
1047
|
-
*/
|
|
1048
|
-
async function startOriginalsJob({ casesRoot, caseName, action, files = [], options = {}, homeDir = null } = {}) {
|
|
1049
|
-
if (!['convert', 'anonymize'].includes(action)) throw new Error('Action inconnue sur les pièces originales.');
|
|
1050
|
-
if (!acceptingJobs) throw new Error('Le serveur est en cours d’arrêt : aucun nouveau traitement ne peut démarrer.');
|
|
1051
|
-
const legalCase = resolveCase(casesRoot, caseName);
|
|
1052
|
-
const busy = runningJobForCase(legalCase.root);
|
|
1053
|
-
if (busy) throw new Error('Un traitement est déjà en cours sur ce dossier.');
|
|
1054
|
-
|
|
1055
|
-
const originals = await listOriginals(legalCase.root);
|
|
1056
|
-
const wanted = new Set(files.map((file) => String(file || '').replaceAll('\\', '/')));
|
|
1057
|
-
// Les ressources sont volontairement hors périmètre : accessibles à l'IA telles
|
|
1058
|
-
// quelles, elles ne sont ni converties ni scannées. On les écarte même si elles
|
|
1059
|
-
// sont explicitement cochées, pour que le drapeau reste la seule vérité.
|
|
1060
|
-
const selected = (wanted.size ? originals.filter((file) => wanted.has(file.path)) : originals)
|
|
1061
|
-
.filter((file) => !file.resource);
|
|
1062
|
-
if (!selected.length) {
|
|
1063
|
-
throw new Error('Aucune pièce à traiter dans ce dossier.');
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Sans sélection, le travail porte sur tout le dossier et ne refait que ce
|
|
1067
|
-
// qui manque : le modèle GLiNER ne se charge pas si tout est déjà scanné.
|
|
1068
|
-
// Cocher des pièces vaut demande explicite de les retraiter.
|
|
1069
|
-
const forced = wanted.size > 0 || options.force === true;
|
|
1070
|
-
const pending = forced
|
|
1071
|
-
? selected
|
|
1072
|
-
: selected.filter((file) => (action === 'convert' ? !file.converted : !(file.converted && file.scanned)));
|
|
1073
|
-
const skipped = selected.length - pending.length;
|
|
1074
|
-
|
|
1075
|
-
pruneJobs();
|
|
1076
|
-
if (!pending.length) {
|
|
1077
|
-
// Rien à faire : on rend un travail déjà terminé plutôt qu'une erreur, pour
|
|
1078
|
-
// que l'administration affiche « à jour » et non un échec.
|
|
1079
|
-
const job = finishedJob({
|
|
1080
|
-
case: legalCase.name,
|
|
1081
|
-
caseRoot: legalCase.root,
|
|
1082
|
-
action,
|
|
1083
|
-
total: 0,
|
|
1084
|
-
files: [],
|
|
1085
|
-
result: { converted: 0, scanned: 0, skipped, upToDate: true },
|
|
1086
|
-
});
|
|
1087
|
-
jobs.set(job.id, job);
|
|
1088
|
-
return publicJob(job);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// Les chemins sont relatifs au dossier juridique lui-même : les pièces ne
|
|
1092
|
-
// vivent plus dans un sous-dossier dédié, elles sont là où le cabinet les a
|
|
1093
|
-
// rangées, racine et sous-dossiers confondus.
|
|
1094
|
-
const absoluteFiles = pending.map((file) => {
|
|
1095
|
-
const absolute = path.resolve(legalCase.root, ...file.path.split('/'));
|
|
1096
|
-
if (!absolute.startsWith(`${legalCase.root}${path.sep}`)) {
|
|
1097
|
-
throw new Error('Pièce hors du dossier juridique.');
|
|
1098
|
-
}
|
|
1099
|
-
if (!fs.existsSync(absolute)) throw new Error(`Pièce introuvable : ${file.name}`);
|
|
1100
|
-
return absolute;
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
const job = {
|
|
1104
|
-
id: crypto.randomUUID(),
|
|
1105
|
-
case: legalCase.name,
|
|
1106
|
-
caseRoot: legalCase.root,
|
|
1107
|
-
action,
|
|
1108
|
-
state: 'queued',
|
|
1109
|
-
phase: 'convert',
|
|
1110
|
-
percent: 0,
|
|
1111
|
-
processed: 0,
|
|
1112
|
-
total: pending.length,
|
|
1113
|
-
skipped,
|
|
1114
|
-
files: pending.map((file) => file.path),
|
|
1115
|
-
reserveBytes: JOB_RESERVE_BYTES(action),
|
|
1116
|
-
log: [],
|
|
1117
|
-
error: null,
|
|
1118
|
-
result: null,
|
|
1119
|
-
cancelled: false,
|
|
1120
|
-
cancelReason: null,
|
|
1121
|
-
child: null,
|
|
1122
|
-
completion: null,
|
|
1123
|
-
processGroupId: null,
|
|
1124
|
-
stopPromise: null,
|
|
1125
|
-
timeoutId: null,
|
|
1126
|
-
queuedAt: new Date().toISOString(),
|
|
1127
|
-
startedAt: null,
|
|
1128
|
-
finishedAt: null,
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
// Le descripteur porte tout ce dont `launchJob` a besoin, qu'il démarre
|
|
1132
|
-
// maintenant ou plus tard depuis la file.
|
|
1133
|
-
return admitOrQueue({ job, legalCase, absoluteFiles, options, homeDir, skipped, forced });
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/** Démarre le traitement maintenant s'il rentre, sinon le met en file d'attente. */
|
|
1137
|
-
function admitOrQueue(descriptor) {
|
|
1138
|
-
const { job } = descriptor;
|
|
1139
|
-
jobs.set(job.id, job);
|
|
1140
|
-
if (canAdmit(job)) {
|
|
1141
|
-
launchJob(descriptor);
|
|
1142
|
-
} else {
|
|
1143
|
-
job.state = 'queued';
|
|
1144
|
-
waiting.push(descriptor);
|
|
1145
|
-
}
|
|
1146
|
-
return publicJob(job);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Lance réellement le traitement : réserve sa RAM, exécute le script Python, puis
|
|
1151
|
-
* — quoi qu'il arrive — libère la réservation et relance la file. Le bloc
|
|
1152
|
-
* `finally` est le seul endroit qui rend une place : un traitement qui échoue
|
|
1153
|
-
* ne doit pas bloquer la file pour autant.
|
|
1154
|
-
*/
|
|
1155
|
-
function launchJob(descriptor) {
|
|
1156
|
-
const { job, legalCase, absoluteFiles, options, homeDir, skipped, forced } = descriptor;
|
|
1157
|
-
job.state = 'running';
|
|
1158
|
-
job.startedAt = new Date().toISOString();
|
|
1159
|
-
reservedBytes += job.reserveBytes;
|
|
1160
|
-
job.timeoutId = setTimeout(() => {
|
|
1161
|
-
void stopRunningJob(job, 'timeout').catch((error) => {
|
|
1162
|
-
appendLog(job, `Nettoyage après délai dépassé : ${error.message}`);
|
|
1163
|
-
});
|
|
1164
|
-
}, JOB_TIMEOUT_MS());
|
|
1165
|
-
|
|
1166
|
-
job.completion = (async () => {
|
|
1167
|
-
try {
|
|
1168
|
-
const result = await runJob(job, legalCase, absoluteFiles, { ...options, skipExisting: !forced });
|
|
1169
|
-
if (job.cancelled) throw cancellationError(job);
|
|
1170
|
-
const commit = await commitJobArtifacts(job, legalCase, absoluteFiles, homeDir);
|
|
1171
|
-
if (job.cancelled) throw cancellationError(job);
|
|
1172
|
-
job.state = 'done';
|
|
1173
|
-
job.percent = 100;
|
|
1174
|
-
job.processed = job.total;
|
|
1175
|
-
job.result = { ...result, skipped, commit };
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
job.state = 'error';
|
|
1178
|
-
job.error = error.message;
|
|
1179
|
-
} finally {
|
|
1180
|
-
clearTimeout(job.timeoutId);
|
|
1181
|
-
job.timeoutId = null;
|
|
1182
|
-
const processGroupId = job.processGroupId;
|
|
1183
|
-
try {
|
|
1184
|
-
await verifyJobProcessTreeStopped(job, processGroupId);
|
|
1185
|
-
} catch (error) {
|
|
1186
|
-
job.state = 'error';
|
|
1187
|
-
job.error = `Nettoyage incomplet du pipeline : ${error.message}`;
|
|
1188
|
-
}
|
|
1189
|
-
job.child = null;
|
|
1190
|
-
job.processGroupId = null;
|
|
1191
|
-
job.stopPromise = null;
|
|
1192
|
-
job.finishedAt = new Date().toISOString();
|
|
1193
|
-
reservedBytes -= job.reserveBytes;
|
|
1194
|
-
pumpQueue();
|
|
1195
|
-
}
|
|
1196
|
-
})();
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
776
|
let stopAllPromise = null;
|
|
1200
777
|
|
|
1201
778
|
/**
|
|
@@ -1211,23 +788,12 @@ function stopOriginalsJobs() {
|
|
|
1211
788
|
const { job } = descriptor;
|
|
1212
789
|
job.cancelled = true;
|
|
1213
790
|
job.cancelReason = 'server-shutdown';
|
|
1214
|
-
|
|
1215
|
-
// Descripteur `runManagedPythonJob` : rejeter sa promesse est le seul
|
|
1216
|
-
// moyen de le débloquer, il n'a pas d'état `job.state`/`jobs` propre.
|
|
1217
|
-
descriptor.reject(cancellationError(job));
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
job.state = 'error';
|
|
1221
|
-
job.error = 'Traitement interrompu par l’arrêt du serveur.';
|
|
1222
|
-
job.finishedAt = new Date().toISOString();
|
|
791
|
+
descriptor.reject(cancellationError(job));
|
|
1223
792
|
}
|
|
1224
793
|
|
|
1225
|
-
const running = [...
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
const groups = [...new Set(allRunning.map((job) => job.processGroupId).filter(Boolean))];
|
|
1229
|
-
await Promise.allSettled(allRunning.map((job) => stopRunningJob(job, 'server-shutdown')));
|
|
1230
|
-
await Promise.allSettled(running.map((job) => job.completion).filter(Boolean));
|
|
794
|
+
const running = [...runningManaged];
|
|
795
|
+
const groups = [...new Set(running.map((job) => job.processGroupId).filter(Boolean))];
|
|
796
|
+
await Promise.allSettled(running.map((job) => stopRunningJob(job, 'server-shutdown')));
|
|
1231
797
|
|
|
1232
798
|
// Contrôle défensif final, y compris si un enfant a fermé ses pipes avant
|
|
1233
799
|
// que son événement `exit` ait été traité par le suivi normal du job.
|
|
@@ -1238,29 +804,7 @@ function stopOriginalsJobs() {
|
|
|
1238
804
|
return stopAllPromise;
|
|
1239
805
|
}
|
|
1240
806
|
|
|
1241
|
-
function cancelOriginalsJob(jobId) {
|
|
1242
|
-
const job = jobs.get(String(jobId || ''));
|
|
1243
|
-
if (!job) return null;
|
|
1244
|
-
// Un job encore en file n'a pas de process : on le retire de la file et on
|
|
1245
|
-
// laisse la place au suivant.
|
|
1246
|
-
if (job.state === 'queued') {
|
|
1247
|
-
const index = waiting.findIndex((entry) => entry.job.id === job.id);
|
|
1248
|
-
if (index >= 0) waiting.splice(index, 1);
|
|
1249
|
-
job.state = 'error';
|
|
1250
|
-
job.error = 'Traitement interrompu.';
|
|
1251
|
-
job.finishedAt = new Date().toISOString();
|
|
1252
|
-
pumpQueue();
|
|
1253
|
-
return publicJob(job);
|
|
1254
|
-
}
|
|
1255
|
-
if (job.state !== 'running') return null;
|
|
1256
|
-
void stopRunningJob(job, 'user-cancellation').catch((error) => {
|
|
1257
|
-
appendLog(job, `Nettoyage après annulation : ${error.message}`);
|
|
1258
|
-
});
|
|
1259
|
-
return publicJob(job);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
807
|
module.exports = {
|
|
1263
|
-
cancelOriginalsJob,
|
|
1264
808
|
groupEntityHits,
|
|
1265
809
|
// Point d'entrée partagé : tout consommateur d'un pipeline GLiNER/markitdown
|
|
1266
810
|
// (même hors dossier juridique enregistré, ex. `knowledge/pipeline.ts`) doit
|
|
@@ -1270,12 +814,9 @@ module.exports = {
|
|
|
1270
814
|
// Ré-exportés pour les routes de l'administration : l'implémentation vit
|
|
1271
815
|
// désormais dans `piecemaker-plugin/scripts/lib/mapping.cjs`.
|
|
1272
816
|
caseMappingFile,
|
|
1273
|
-
getJob,
|
|
1274
817
|
listOriginals,
|
|
1275
818
|
readCaseMapping,
|
|
1276
819
|
rebuildCaseMapping,
|
|
1277
820
|
saveCaseMapping,
|
|
1278
|
-
sessionArtifactPaths,
|
|
1279
|
-
startOriginalsJob,
|
|
1280
821
|
writeCaseMapping,
|
|
1281
822
|
};
|