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