chain-insights 0.3.4 → 0.3.5
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/README.md +19 -9
- package/dist/canvas-Cn-maEIh.mjs +203 -0
- package/dist/canvas-Cn-maEIh.mjs.map +1 -0
- package/dist/canvas-p-oKCMjc.cjs +251 -0
- package/dist/cases-Bz_9XKEw.cjs +19 -0
- package/dist/cases-TVcAifxu.mjs +16 -0
- package/dist/cases-TVcAifxu.mjs.map +1 -0
- package/dist/cli.cjs +74 -28
- package/dist/cli.mjs +74 -28
- package/dist/cli.mjs.map +1 -1
- package/dist/{data-extractor-DZUJu1Bz.mjs → data-extractor-B4nHw1wZ.mjs} +2 -2
- package/dist/{data-extractor-DZUJu1Bz.mjs.map → data-extractor-B4nHw1wZ.mjs.map} +1 -1
- package/dist/{data-extractor-Cavd7wHk.cjs → data-extractor-DS4rzy3M.cjs} +1 -1
- package/dist/{export-BqTCO9lP.mjs → export-CBhcJuZ6.mjs} +8 -205
- package/dist/export-CBhcJuZ6.mjs.map +1 -0
- package/dist/{export-DsXgtCwO.cjs → export-D4v4-6F4.cjs} +16 -214
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{init-DLBL_nVG.mjs → init-CKQ6F07J.mjs} +22 -5
- package/dist/init-CKQ6F07J.mjs.map +1 -0
- package/dist/{init-zqbd7i-_.cjs → init-Dhw8F23z.cjs} +21 -4
- package/dist/mcp-proxy.cjs +20 -20
- package/dist/mcp-proxy.mjs +20 -20
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-wJoAFDFa.mjs → public-tools-CyUZEz9B.mjs} +3 -3
- package/dist/{public-tools-wJoAFDFa.mjs.map → public-tools-CyUZEz9B.mjs.map} +1 -1
- package/dist/{public-tools-BvMb3H2P.cjs → public-tools-xfVNz9NE.cjs} +2 -2
- package/dist/{runner-BhZ4lnF1.cjs → runner-CVo41fjz.cjs} +2 -2
- package/dist/{runner-DIJSbkjc.mjs → runner-DWuSy1Se.mjs} +3 -3
- package/dist/{runner-DIJSbkjc.mjs.map → runner-DWuSy1Se.mjs.map} +1 -1
- package/dist/{selector-CF2o5gxN.mjs → selector-BvXM9jbe.mjs} +2 -2
- package/dist/{selector-CF2o5gxN.mjs.map → selector-BvXM9jbe.mjs.map} +1 -1
- package/dist/{selector-DfAMZEC9.cjs → selector-Dps_ZFxq.cjs} +1 -1
- package/dist/{store-CTtqQtaE.mjs → store-C2B_AssI.mjs} +2 -2
- package/dist/{store-CTtqQtaE.mjs.map → store-C2B_AssI.mjs.map} +1 -1
- package/dist/{store-CqPfs47P.cjs → store-CQhU8dz8.cjs} +0 -18
- package/dist/vault-B2y78Ypu.cjs +560 -0
- package/dist/vault-z35Dohdq.mjs +560 -0
- package/dist/vault-z35Dohdq.mjs.map +1 -0
- package/dist/{viz-Dqp3C5kb.cjs → viz-D1620cBX.cjs} +3 -3
- package/dist/{viz-5y24S5X1.mjs → viz-DB5XFG1z.mjs} +4 -4
- package/dist/{viz-5y24S5X1.mjs.map → viz-DB5XFG1z.mjs.map} +1 -1
- package/docs/graph-tools.md +15 -7
- package/docs/investigation-workspaces.md +36 -10
- package/docs/knowledge-exports.md +6 -2
- package/docs/mcp-proxy.md +14 -3
- package/docs/obsidian-vault.md +130 -0
- package/package.json +1 -1
- package/skills/chain-insights-developer-experience/SKILL.md +2 -2
- package/skills/chain-insights-investigation/SKILL.md +1 -1
- package/dist/cases-Cp9DUbEV.mjs +0 -6
- package/dist/cases-sTY5aXav.cjs +0 -9
- package/dist/export-BqTCO9lP.mjs.map +0 -1
- package/dist/init-DLBL_nVG.mjs.map +0 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
const require_data_extractor = require("./data-extractor-DS4rzy3M.cjs");
|
|
2
|
+
const require_frontmatter = require("./frontmatter-Dvqa5HX6.cjs");
|
|
3
|
+
const require_output_root = require("./output-root-YIbl6PwF.cjs");
|
|
4
|
+
const require_dossier = require("./dossier-BXy57V4-.cjs");
|
|
5
|
+
const require_store = require("./store-CQhU8dz8.cjs");
|
|
6
|
+
const require_evidence = require("./evidence-CvEesemA.cjs");
|
|
7
|
+
require("./cases-Bz_9XKEw.cjs");
|
|
8
|
+
const require_canvas = require("./canvas-p-oKCMjc.cjs");
|
|
9
|
+
const require_graph_normalizer = require("./graph-normalizer-DbjlbMpz.cjs");
|
|
10
|
+
let node_path = require("node:path");
|
|
11
|
+
let node_fs_promises = require("node:fs/promises");
|
|
12
|
+
//#region src/vault/schema.ts
|
|
13
|
+
const VAULT_DIRS = [
|
|
14
|
+
".obsidian",
|
|
15
|
+
"Canvases",
|
|
16
|
+
"Entities",
|
|
17
|
+
"Evidence",
|
|
18
|
+
"published"
|
|
19
|
+
];
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/vault/markdown.ts
|
|
22
|
+
function yamlString(value) {
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
function frontmatter(values) {
|
|
26
|
+
const lines = ["---"];
|
|
27
|
+
for (const [key, value] of Object.entries(values)) if (Array.isArray(value)) {
|
|
28
|
+
lines.push(`${key}:`);
|
|
29
|
+
for (const item of value) lines.push(` - ${yamlString(item)}`);
|
|
30
|
+
} else lines.push(`${key}: ${typeof value === "boolean" ? String(value) : yamlString(value)}`);
|
|
31
|
+
lines.push("---", "");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
function renderLiveCaseNote(input) {
|
|
35
|
+
return frontmatter({
|
|
36
|
+
type: "chain-insights-case",
|
|
37
|
+
case_id: input.id,
|
|
38
|
+
status: input.status,
|
|
39
|
+
tags: input.tags,
|
|
40
|
+
contains_sensitive_data: true,
|
|
41
|
+
source_of_truth: `cases/${input.id}/`
|
|
42
|
+
}) + [
|
|
43
|
+
`# ${input.name}`,
|
|
44
|
+
"",
|
|
45
|
+
input.description,
|
|
46
|
+
"",
|
|
47
|
+
"## Live Workspace",
|
|
48
|
+
"",
|
|
49
|
+
`Status: ${input.status}`,
|
|
50
|
+
`Evidence files: ${input.evidenceCount}`,
|
|
51
|
+
`Evidence manifest verified: ${input.evidenceVerified ? "yes" : "no"}`,
|
|
52
|
+
`Entities: ${input.entityCount}`,
|
|
53
|
+
"",
|
|
54
|
+
"## Navigation",
|
|
55
|
+
"",
|
|
56
|
+
"- [[Agent Console]]",
|
|
57
|
+
"- [[Graph.canvas]]",
|
|
58
|
+
"- [[Cases]]",
|
|
59
|
+
`- [[cases/${input.id}/Evidence|Evidence]]`,
|
|
60
|
+
`- [[cases/${input.id}/Entities|Entities]]`,
|
|
61
|
+
"- [[Graphs]]",
|
|
62
|
+
""
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
function renderCaseAgentConsole(input) {
|
|
66
|
+
return frontmatter({
|
|
67
|
+
type: "chain-insights-case-agent-console",
|
|
68
|
+
case_id: input.id,
|
|
69
|
+
contains_sensitive_data: true
|
|
70
|
+
}) + [
|
|
71
|
+
`# Agent Console: ${input.name}`,
|
|
72
|
+
"",
|
|
73
|
+
`Canonical case state: \`cases/${input.id}/\``,
|
|
74
|
+
"",
|
|
75
|
+
"## Case Files",
|
|
76
|
+
"",
|
|
77
|
+
"- [[Case]]",
|
|
78
|
+
"- [[Graph.canvas]]",
|
|
79
|
+
`- [[cases/${input.id}/Evidence|Evidence]]`,
|
|
80
|
+
`- [[cases/${input.id}/Entities|Entities]]`,
|
|
81
|
+
"",
|
|
82
|
+
"## Current Counts",
|
|
83
|
+
"",
|
|
84
|
+
`- Evidence files: ${input.evidenceCount}`,
|
|
85
|
+
`- Entities: ${input.entityCount}`,
|
|
86
|
+
`- Manifest verified: ${input.evidenceVerified ? "yes" : "no"}`,
|
|
87
|
+
""
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
function renderCaseEvidenceIndex(input, evidence) {
|
|
91
|
+
return frontmatter({
|
|
92
|
+
type: "chain-insights-case-evidence-index",
|
|
93
|
+
case_id: input.id,
|
|
94
|
+
contains_sensitive_data: true
|
|
95
|
+
}) + [
|
|
96
|
+
`# Evidence: ${input.name}`,
|
|
97
|
+
"",
|
|
98
|
+
`Canonical evidence directory: \`cases/${input.id}/evidence/\``,
|
|
99
|
+
`Evidence files: ${input.evidenceCount}`,
|
|
100
|
+
`Manifest verified: ${input.evidenceVerified ? "yes" : "no"}`,
|
|
101
|
+
"",
|
|
102
|
+
"## Evidence Notes",
|
|
103
|
+
"",
|
|
104
|
+
...evidence.length > 0 ? evidence.map((item) => `- [[${item.notePath.replace(/\.md$/, "")}|${item.id}]] (${item.source})`) : ["No evidence files recorded yet."],
|
|
105
|
+
""
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
function renderEvidenceNote(input, caseId) {
|
|
109
|
+
return frontmatter({
|
|
110
|
+
type: "chain-insights-evidence",
|
|
111
|
+
case_id: caseId,
|
|
112
|
+
evidence_id: input.id,
|
|
113
|
+
source: input.source,
|
|
114
|
+
source_file: `cases/${caseId}/evidence/${input.filename}`,
|
|
115
|
+
contains_sensitive_data: true
|
|
116
|
+
}) + [
|
|
117
|
+
`# Evidence: ${input.source}`,
|
|
118
|
+
"",
|
|
119
|
+
`Evidence ID: \`${input.id}\``,
|
|
120
|
+
`Source file: \`cases/${caseId}/evidence/${input.filename}\``,
|
|
121
|
+
`Captured: ${input.timestamp || "unknown"}`,
|
|
122
|
+
`Query params: ${input.queryParams || "none"}`,
|
|
123
|
+
"",
|
|
124
|
+
"## Case",
|
|
125
|
+
"",
|
|
126
|
+
`- [[cases/${caseId}/Case|${caseId}]]`,
|
|
127
|
+
"",
|
|
128
|
+
"## Body",
|
|
129
|
+
"",
|
|
130
|
+
input.body.trim() || "No evidence body recorded.",
|
|
131
|
+
""
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
function renderCaseEntityIndex(input, entities) {
|
|
135
|
+
return frontmatter({
|
|
136
|
+
type: "chain-insights-case-entity-index",
|
|
137
|
+
case_id: input.id,
|
|
138
|
+
contains_sensitive_data: true
|
|
139
|
+
}) + [
|
|
140
|
+
`# Entities: ${input.name}`,
|
|
141
|
+
"",
|
|
142
|
+
`Canonical dossier directory: \`cases/${input.id}/dossiers/\``,
|
|
143
|
+
`Entities: ${entities.length}`,
|
|
144
|
+
"",
|
|
145
|
+
"## Entity Notes",
|
|
146
|
+
"",
|
|
147
|
+
...entities.length > 0 ? entities.map((item) => `- [[${item.notePath.replace(/\.md$/, "")}|${item.label}]] (${item.entityType})`) : ["No entities recorded yet."],
|
|
148
|
+
""
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
function renderEntityNote(address, caseId, entityType) {
|
|
152
|
+
return frontmatter({
|
|
153
|
+
type: "chain-insights-entity",
|
|
154
|
+
case_id: caseId,
|
|
155
|
+
address,
|
|
156
|
+
entity_type: entityType,
|
|
157
|
+
contains_sensitive_data: true
|
|
158
|
+
}) + [
|
|
159
|
+
`# Entity: ${address}`,
|
|
160
|
+
"",
|
|
161
|
+
`Address: ${address}`,
|
|
162
|
+
`Type: ${entityType}`,
|
|
163
|
+
"",
|
|
164
|
+
"## Cases",
|
|
165
|
+
"",
|
|
166
|
+
`- [[cases/${caseId}/Case|${caseId}]]`,
|
|
167
|
+
""
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
function renderVaultHome() {
|
|
171
|
+
return frontmatter({
|
|
172
|
+
type: "chain-insights-vault-home",
|
|
173
|
+
product: "Chain Insights",
|
|
174
|
+
contains_sensitive_data: true
|
|
175
|
+
}) + [
|
|
176
|
+
"# Chain Insights Vault",
|
|
177
|
+
"",
|
|
178
|
+
"Chain Insights is an AML investigation CLI and MCP proxy layered on GraphRAG MCP.",
|
|
179
|
+
"",
|
|
180
|
+
"## Start Here",
|
|
181
|
+
"",
|
|
182
|
+
"- [[Cases]]",
|
|
183
|
+
"- [[Entities]]",
|
|
184
|
+
"- [[Evidence]]",
|
|
185
|
+
"- [[Graphs]]",
|
|
186
|
+
"- [[Agent Console]]",
|
|
187
|
+
""
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
function renderRootIndex(title, type, links) {
|
|
191
|
+
return frontmatter({
|
|
192
|
+
type,
|
|
193
|
+
product: "Chain Insights",
|
|
194
|
+
contains_sensitive_data: true
|
|
195
|
+
}) + [
|
|
196
|
+
`# ${title}`,
|
|
197
|
+
"",
|
|
198
|
+
...links.map((link) => `- [[${link}]]`),
|
|
199
|
+
""
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
function renderRootAgentConsole() {
|
|
203
|
+
return frontmatter({
|
|
204
|
+
type: "chain-insights-agent-console",
|
|
205
|
+
product: "Chain Insights",
|
|
206
|
+
contains_sensitive_data: true
|
|
207
|
+
}) + [
|
|
208
|
+
"# Agent Console",
|
|
209
|
+
"",
|
|
210
|
+
"Use Chain Insights case evidence as canonical local state and GraphRAG MCP for fresh graph facts.",
|
|
211
|
+
"",
|
|
212
|
+
"## Reading Order",
|
|
213
|
+
"",
|
|
214
|
+
"1. [[Home]]",
|
|
215
|
+
"2. [[Cases]]",
|
|
216
|
+
"3. [[Entities]]",
|
|
217
|
+
"4. [[Evidence]]",
|
|
218
|
+
"5. [[Graphs]]",
|
|
219
|
+
""
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function renderObsidianAppConfig() {
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
useMarkdownLinks: false,
|
|
225
|
+
newLinkFormat: "shortest",
|
|
226
|
+
alwaysUpdateLinks: true
|
|
227
|
+
}, null, 2) + "\n";
|
|
228
|
+
}
|
|
229
|
+
function renderObsidianGraphConfig() {
|
|
230
|
+
return JSON.stringify({
|
|
231
|
+
"collapse-filter": true,
|
|
232
|
+
search: "",
|
|
233
|
+
showTags: true,
|
|
234
|
+
showAttachments: true,
|
|
235
|
+
hideUnresolved: false,
|
|
236
|
+
showOrphans: true
|
|
237
|
+
}, null, 2) + "\n";
|
|
238
|
+
}
|
|
239
|
+
function renderObsidianTemplatesConfig() {
|
|
240
|
+
return JSON.stringify({
|
|
241
|
+
folder: "",
|
|
242
|
+
dateFormat: "YYYY-MM-DD",
|
|
243
|
+
timeFormat: "HH:mm"
|
|
244
|
+
}, null, 2) + "\n";
|
|
245
|
+
}
|
|
246
|
+
function renderVaultGitignore() {
|
|
247
|
+
return [
|
|
248
|
+
"# Chain Insights local runtime state",
|
|
249
|
+
".chain-insights/runtime/",
|
|
250
|
+
"",
|
|
251
|
+
"# Obsidian local UI state",
|
|
252
|
+
".obsidian/workspace.json",
|
|
253
|
+
".obsidian/workspace-mobile.json",
|
|
254
|
+
".obsidian/workspaces.json",
|
|
255
|
+
"",
|
|
256
|
+
"# Private export bundles are local by default",
|
|
257
|
+
"published/",
|
|
258
|
+
""
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/vault/index.ts
|
|
263
|
+
const VAULT_FILES = [
|
|
264
|
+
{
|
|
265
|
+
path: ".obsidian/app.json",
|
|
266
|
+
content: renderObsidianAppConfig()
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
path: ".obsidian/graph.json",
|
|
270
|
+
content: renderObsidianGraphConfig()
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
path: ".obsidian/templates.json",
|
|
274
|
+
content: renderObsidianTemplatesConfig()
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
path: ".gitignore",
|
|
278
|
+
content: renderVaultGitignore()
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
path: "Home.md",
|
|
282
|
+
content: renderVaultHome()
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
path: "Cases.md",
|
|
286
|
+
content: renderRootIndex("Cases", "chain-insights-vault-cases-index", ["Home", "Agent Console"])
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
path: "Entities.md",
|
|
290
|
+
content: renderRootIndex("Entities", "chain-insights-vault-entities-index", [
|
|
291
|
+
"Home",
|
|
292
|
+
"Cases",
|
|
293
|
+
"Evidence"
|
|
294
|
+
])
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
path: "Evidence.md",
|
|
298
|
+
content: renderRootIndex("Evidence", "chain-insights-vault-evidence-index", [
|
|
299
|
+
"Home",
|
|
300
|
+
"Cases",
|
|
301
|
+
"Entities"
|
|
302
|
+
])
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
path: "Graphs.md",
|
|
306
|
+
content: renderRootIndex("Graphs", "chain-insights-vault-graphs-index", [
|
|
307
|
+
"Home",
|
|
308
|
+
"Cases",
|
|
309
|
+
"Entities",
|
|
310
|
+
"Evidence"
|
|
311
|
+
])
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
path: "Agent Console.md",
|
|
315
|
+
content: renderRootAgentConsole()
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
path: "Canvases/README.md",
|
|
319
|
+
content: renderRootIndex("Canvases", "chain-insights-vault-canvases-readme", ["Home", "Graphs"])
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
path: "Entities/README.md",
|
|
323
|
+
content: renderRootIndex("Entities Folder", "chain-insights-vault-entities-readme", ["Entities", "Cases"])
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
path: "Evidence/README.md",
|
|
327
|
+
content: renderRootIndex("Evidence Folder", "chain-insights-vault-evidence-readme", ["Evidence", "Cases"])
|
|
328
|
+
}
|
|
329
|
+
];
|
|
330
|
+
async function scaffoldVault(options) {
|
|
331
|
+
const workspaceRoot = (0, node_path.resolve)(options.workspaceRoot);
|
|
332
|
+
const force = options.force === true;
|
|
333
|
+
if (!force) await assertNoVaultFileCollisions(workspaceRoot);
|
|
334
|
+
for (const dir of VAULT_DIRS) await (0, node_fs_promises.mkdir)((0, node_path.join)(workspaceRoot, dir), { recursive: true });
|
|
335
|
+
const filesWritten = [];
|
|
336
|
+
for (const file of VAULT_FILES) {
|
|
337
|
+
await writeVaultFile(workspaceRoot, file, force);
|
|
338
|
+
filesWritten.push(file.path);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
workspaceRoot,
|
|
342
|
+
filesWritten
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
async function refreshCaseVault(options) {
|
|
346
|
+
const workspace = require_output_root.workspaceOutputPaths();
|
|
347
|
+
const force = options.force === true;
|
|
348
|
+
const [caseInfo, evidenceVerification, evidence, dossiers, graph] = await Promise.all([
|
|
349
|
+
require_store.CaseStore.get(options.caseId),
|
|
350
|
+
require_evidence.EvidenceStore.verifyManifest(options.caseId),
|
|
351
|
+
readCaseEvidence(options.caseId),
|
|
352
|
+
require_dossier.DossierStore.listSummaries(options.caseId),
|
|
353
|
+
loadLiveCaseGraph(options.caseId)
|
|
354
|
+
]);
|
|
355
|
+
const caseSummary = {
|
|
356
|
+
id: caseInfo.id,
|
|
357
|
+
name: caseInfo.name,
|
|
358
|
+
status: caseInfo.status,
|
|
359
|
+
tags: caseInfo.tags,
|
|
360
|
+
description: caseInfo.description,
|
|
361
|
+
evidenceCount: evidenceVerification.count,
|
|
362
|
+
evidenceVerified: evidenceVerification.ok,
|
|
363
|
+
entityCount: dossiers.length
|
|
364
|
+
};
|
|
365
|
+
const canvasGraph = {
|
|
366
|
+
...graph,
|
|
367
|
+
nodes: mergeDossierNodes(graph.nodes, dossiers)
|
|
368
|
+
};
|
|
369
|
+
const canvas = graphToCaseVaultCanvas(require_canvas.graphToCanvas(canvasGraph), caseInfo.id, canvasGraph.nodes);
|
|
370
|
+
const evidenceSummaries = evidence.map((evidenceDoc) => ({
|
|
371
|
+
...evidenceDoc,
|
|
372
|
+
notePath: `Evidence/${require_canvas.safeFilename(`${evidenceDoc.filename.replace(/\.md$/, "")}-${caseInfo.id}`)}`
|
|
373
|
+
}));
|
|
374
|
+
const entityFiles = entityFilesForCase(caseInfo.id, canvasGraph.nodes, dossiers);
|
|
375
|
+
const entitySummaries = [...entityFiles.values()].map((entry) => entry.summary).sort((left, right) => left.label.localeCompare(right.label));
|
|
376
|
+
const files = [
|
|
377
|
+
{
|
|
378
|
+
path: `cases/${caseInfo.id}/Case.md`,
|
|
379
|
+
content: renderLiveCaseNote(caseSummary)
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
path: `cases/${caseInfo.id}/Agent Console.md`,
|
|
383
|
+
content: renderCaseAgentConsole(caseSummary)
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
path: `cases/${caseInfo.id}/Evidence.md`,
|
|
387
|
+
content: renderCaseEvidenceIndex(caseSummary, evidenceSummaries)
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
path: `cases/${caseInfo.id}/Entities.md`,
|
|
391
|
+
content: renderCaseEntityIndex(caseSummary, entitySummaries)
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
path: `cases/${caseInfo.id}/Graph.canvas`,
|
|
395
|
+
content: JSON.stringify(canvas, null, 2) + "\n"
|
|
396
|
+
},
|
|
397
|
+
...evidenceSummaries.map((evidenceDoc) => ({
|
|
398
|
+
path: evidenceDoc.notePath,
|
|
399
|
+
content: renderEvidenceNote(evidenceDoc, caseInfo.id)
|
|
400
|
+
})),
|
|
401
|
+
...[...entityFiles.values()].map((entry) => ({
|
|
402
|
+
path: entry.summary.notePath,
|
|
403
|
+
content: entry.content
|
|
404
|
+
}))
|
|
405
|
+
];
|
|
406
|
+
if (!force) await assertNoFileCollisions(workspace.root, files);
|
|
407
|
+
const filesWritten = [];
|
|
408
|
+
for (const file of files) {
|
|
409
|
+
await writeVaultFile(workspace.root, file, force);
|
|
410
|
+
filesWritten.push(file.path);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
caseId: caseInfo.id,
|
|
414
|
+
filesWritten,
|
|
415
|
+
nextFile: `cases/${caseInfo.id}/Case.md`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function assertNoVaultFileCollisions(workspaceRoot) {
|
|
419
|
+
await assertNoFileCollisions(workspaceRoot, VAULT_FILES);
|
|
420
|
+
}
|
|
421
|
+
async function assertNoFileCollisions(workspaceRoot, files) {
|
|
422
|
+
for (const file of files) try {
|
|
423
|
+
await (0, node_fs_promises.access)((0, node_path.join)(workspaceRoot, file.path));
|
|
424
|
+
throw new Error(`Refusing to overwrite existing vault file: ${file.path}`);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (isNotFoundError(error)) continue;
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function writeVaultFile(workspaceRoot, file, force) {
|
|
431
|
+
try {
|
|
432
|
+
const filePath = (0, node_path.join)(workspaceRoot, file.path);
|
|
433
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
|
|
434
|
+
await (0, node_fs_promises.writeFile)(filePath, file.content, {
|
|
435
|
+
encoding: "utf8",
|
|
436
|
+
flag: force ? "w" : "wx"
|
|
437
|
+
});
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (!force && isFileExistsError(error)) throw new Error(`Refusing to overwrite existing vault file: ${file.path}`);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function isFileExistsError(error) {
|
|
444
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
445
|
+
}
|
|
446
|
+
function isNotFoundError(error) {
|
|
447
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
448
|
+
}
|
|
449
|
+
function mergeDossierNodes(graphNodes, dossiers) {
|
|
450
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
451
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
452
|
+
graphNodes.forEach((node, index) => {
|
|
453
|
+
const id = String(node["id"] ?? node["address"] ?? `node-${index + 1}`);
|
|
454
|
+
nodesById.set(id, node);
|
|
455
|
+
aliases.set(id, id);
|
|
456
|
+
if (typeof node["address"] === "string") aliases.set(node["address"], id);
|
|
457
|
+
});
|
|
458
|
+
for (const dossier of dossiers) {
|
|
459
|
+
const existingId = aliases.get(dossier.address);
|
|
460
|
+
if (existingId) {
|
|
461
|
+
const existing = nodesById.get(existingId);
|
|
462
|
+
if (existing) nodesById.set(existingId, enrichDossierNode(existing, dossier.type));
|
|
463
|
+
} else nodesById.set(dossier.address, {
|
|
464
|
+
id: dossier.address,
|
|
465
|
+
address: dossier.address,
|
|
466
|
+
node_type: dossier.type,
|
|
467
|
+
roles: [dossier.type]
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return [...nodesById.values()];
|
|
471
|
+
}
|
|
472
|
+
async function readCaseEvidence(caseId) {
|
|
473
|
+
const evidenceDir = (0, node_path.join)(require_output_root.workspaceOutputPaths().casesRoot, caseId, "evidence");
|
|
474
|
+
const files = await (0, node_fs_promises.readdir)(evidenceDir).catch(() => []);
|
|
475
|
+
const docs = [];
|
|
476
|
+
for (const filename of files.filter((file) => file.endsWith(".md")).sort()) {
|
|
477
|
+
const { frontmatter, body } = require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)((0, node_path.join)(evidenceDir, filename), "utf8"));
|
|
478
|
+
docs.push({
|
|
479
|
+
id: frontmatter["id"] || filename.replace(/\.md$/, ""),
|
|
480
|
+
filename,
|
|
481
|
+
source: frontmatter["source"] || "unknown",
|
|
482
|
+
timestamp: frontmatter["timestamp"] || "",
|
|
483
|
+
queryParams: frontmatter["queryParams"] || "",
|
|
484
|
+
body
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return docs;
|
|
488
|
+
}
|
|
489
|
+
function entityFilesForCase(caseId, graphNodes, dossiers) {
|
|
490
|
+
const files = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const [index, node] of graphNodes.entries()) {
|
|
492
|
+
const label = entityLabelForGraphNode(node, index);
|
|
493
|
+
const entityType = String(node["entityType"] ?? node["node_type"] ?? node["nodeType"] ?? "unknown");
|
|
494
|
+
const notePath = `Entities/${require_canvas.safeFilename(label)}`;
|
|
495
|
+
files.set(notePath, {
|
|
496
|
+
summary: {
|
|
497
|
+
label,
|
|
498
|
+
notePath,
|
|
499
|
+
entityType
|
|
500
|
+
},
|
|
501
|
+
content: renderEntityNote(label, caseId, entityType)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
for (const dossier of dossiers) {
|
|
505
|
+
const notePath = `Entities/${require_canvas.safeFilename(dossier.address)}`;
|
|
506
|
+
files.set(notePath, {
|
|
507
|
+
summary: {
|
|
508
|
+
label: dossier.address,
|
|
509
|
+
notePath,
|
|
510
|
+
entityType: dossier.type
|
|
511
|
+
},
|
|
512
|
+
content: renderEntityNote(dossier.address, caseId, dossier.type)
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return files;
|
|
516
|
+
}
|
|
517
|
+
function entityLabelForGraphNode(node, index) {
|
|
518
|
+
return String(node["address"] ?? node["id"] ?? `node-${index + 1}`);
|
|
519
|
+
}
|
|
520
|
+
function graphToCaseVaultCanvas(canvas, caseId, graphNodes) {
|
|
521
|
+
return {
|
|
522
|
+
nodes: canvas.nodes.map((node) => {
|
|
523
|
+
if (node.id === "case" && node.type === "file") return {
|
|
524
|
+
...node,
|
|
525
|
+
file: `cases/${caseId}/Case.md`
|
|
526
|
+
};
|
|
527
|
+
const entityIndex = /^entity-(\d+)$/.exec(node.id)?.[1];
|
|
528
|
+
if (node.type === "file" && entityIndex) {
|
|
529
|
+
const graphNode = graphNodes[Number(entityIndex) - 1];
|
|
530
|
+
if (graphNode) return {
|
|
531
|
+
...node,
|
|
532
|
+
file: `Entities/${require_canvas.safeFilename(entityLabelForGraphNode(graphNode, Number(entityIndex) - 1))}`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return node;
|
|
536
|
+
}),
|
|
537
|
+
edges: canvas.edges
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function loadLiveCaseGraph(caseId) {
|
|
541
|
+
const graph = await require_data_extractor.extractGraphFromCase(caseId);
|
|
542
|
+
return require_graph_normalizer.normalizeGraphPayload({
|
|
543
|
+
schema: "chain-insights.graph.v1",
|
|
544
|
+
nodes: graph.nodes,
|
|
545
|
+
edges: graph.edges,
|
|
546
|
+
flows: [],
|
|
547
|
+
edge_anchors: [],
|
|
548
|
+
metadata: graph.metadata
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function enrichDossierNode(node, dossierType) {
|
|
552
|
+
const enriched = { ...node };
|
|
553
|
+
if (typeof enriched["node_type"] !== "string" || enriched["node_type"] === "unknown") enriched["node_type"] = dossierType;
|
|
554
|
+
if (!Array.isArray(enriched["roles"]) || enriched["roles"].length === 0) enriched["roles"] = [dossierType];
|
|
555
|
+
return enriched;
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
exports.assertNoVaultFileCollisions = assertNoVaultFileCollisions;
|
|
559
|
+
exports.refreshCaseVault = refreshCaseVault;
|
|
560
|
+
exports.scaffoldVault = scaffoldVault;
|