@openclawbrain/cli 0.4.35 → 0.4.36

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.
@@ -0,0 +1,745 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { describeStablePathTree } from "./import-export.js";
6
+
7
+ export const GRAPHIFY_RUN_BUNDLE_LAYOUT = {
8
+ command: "graphify-command.json",
9
+ graph: "graph.json",
10
+ html: "graph.html",
11
+ report: "GRAPH_REPORT.md",
12
+ summary: "graphify-summary.json",
13
+ benchmark: "benchmark.json",
14
+ labels: "labels.json",
15
+ };
16
+
17
+ function canonicalizeJsonValue(value) {
18
+ if (Array.isArray(value)) {
19
+ return value.map((entry) => canonicalizeJsonValue(entry));
20
+ }
21
+ if (value === null || typeof value !== "object") {
22
+ return value;
23
+ }
24
+ const result = {};
25
+ for (const key of Object.keys(value).sort((left, right) => left.localeCompare(right))) {
26
+ const nextValue = canonicalizeJsonValue(value[key]);
27
+ if (nextValue !== undefined) {
28
+ result[key] = nextValue;
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function stableJsonStringify(value) {
35
+ return JSON.stringify(canonicalizeJsonValue(value), null, 2) + "\n";
36
+ }
37
+
38
+ function writeJsonFile(filePath, value) {
39
+ writeFileSync(filePath, stableJsonStringify(value), "utf8");
40
+ }
41
+
42
+ function shortHash(value, length = 12) {
43
+ return value.slice(0, length);
44
+ }
45
+
46
+ function normalizeOptionalString(value) {
47
+ if (typeof value !== "string") {
48
+ return null;
49
+ }
50
+ const trimmed = value.trim();
51
+ return trimmed.length > 0 ? trimmed : null;
52
+ }
53
+
54
+ function loadJsonIfExists(filePath) {
55
+ if (!existsSync(filePath)) {
56
+ return null;
57
+ }
58
+ try {
59
+ return JSON.parse(readFileSync(filePath, "utf8"));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function loadSourceBundleManifest(sourceBundlePath) {
67
+ const resolvedPath = path.resolve(sourceBundlePath);
68
+ const stats = statSync(resolvedPath);
69
+ if (stats.isFile()) {
70
+ const data = loadJsonIfExists(resolvedPath);
71
+ return data === null ? null : { path: resolvedPath, data };
72
+ }
73
+ const candidateFiles = [
74
+ "corpus-manifest.json",
75
+ "manifest.json",
76
+ "normalized-event-export.json",
77
+ "workspace-metadata.json",
78
+ ];
79
+ for (const candidate of candidateFiles) {
80
+ const candidatePath = path.join(resolvedPath, candidate);
81
+ const data = loadJsonIfExists(candidatePath);
82
+ if (data !== null) {
83
+ return { path: candidatePath, data };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function extractLabelsFromManifest(manifest) {
90
+ if (manifest === null || manifest === undefined) {
91
+ return [];
92
+ }
93
+ const labels = new Set();
94
+ const pushLabel = (value) => {
95
+ if (typeof value !== "string") {
96
+ return;
97
+ }
98
+ const normalized = value.trim();
99
+ if (normalized.length > 0) {
100
+ labels.add(normalized);
101
+ }
102
+ };
103
+ const pushLabelList = (value) => {
104
+ if (!Array.isArray(value)) {
105
+ return;
106
+ }
107
+ for (const item of value) {
108
+ pushLabel(item);
109
+ }
110
+ };
111
+ pushLabel(manifest.label);
112
+ pushLabel(manifest.name);
113
+ pushLabel(manifest.contract);
114
+ pushLabelList(manifest.labels);
115
+ pushLabelList(manifest.tags);
116
+ pushLabelList(manifest.topics);
117
+ pushLabelList(manifest.sourceLabels);
118
+ pushLabelList(manifest.bundleLabels);
119
+ pushLabelList(manifest?.provenance?.labels);
120
+ pushLabelList(manifest?.metadata?.labels);
121
+ return [...labels].sort((left, right) => left.localeCompare(right));
122
+ }
123
+
124
+ function buildGraphifyLabels(input) {
125
+ const labels = new Set([
126
+ "graphify",
127
+ "managed-run",
128
+ `mode:${input.mode}`,
129
+ `version:${input.version}`,
130
+ `source-hash:${shortHash(input.sourceBundleHash)}`,
131
+ ]);
132
+ for (const label of input.sourceLabels ?? []) {
133
+ labels.add(label);
134
+ }
135
+ for (const label of input.requestedLabels ?? []) {
136
+ labels.add(label);
137
+ }
138
+ for (const flag of input.flags ?? []) {
139
+ labels.add(`flag:${flag}`);
140
+ }
141
+ return [...labels].sort((left, right) => left.localeCompare(right));
142
+ }
143
+
144
+ function deriveGraphifyRunId(input) {
145
+ const digest = createHash("sha256");
146
+ digest.update(input.sourceBundleHash);
147
+ digest.update("\u0000");
148
+ digest.update(input.version);
149
+ digest.update("\u0000");
150
+ digest.update(input.mode);
151
+ digest.update("\u0000");
152
+ digest.update(stableJsonStringify(input.config));
153
+ digest.update("\u0000");
154
+ for (const label of input.labels ?? []) {
155
+ digest.update(label);
156
+ digest.update("\u0000");
157
+ }
158
+ for (const flag of input.flags ?? []) {
159
+ digest.update(flag);
160
+ digest.update("\u0000");
161
+ }
162
+ return `graphify-${digest.digest("hex").slice(0, 16)}`;
163
+ }
164
+
165
+ function getNodePath(entryPath) {
166
+ return entryPath === "." ? "" : entryPath;
167
+ }
168
+
169
+ function buildGraphPayload(input) {
170
+ const treeEntries = input.sourceTree.entries;
171
+ const nodes = [
172
+ {
173
+ id: "source-bundle",
174
+ kind: input.sourceTree.kind,
175
+ path: ".",
176
+ label: path.basename(input.sourceBundlePath),
177
+ hash: input.sourceBundleHash,
178
+ fileCount: input.sourceTree.fileCount,
179
+ directoryCount: input.sourceTree.directoryCount,
180
+ symlinkCount: input.sourceTree.symlinkCount,
181
+ totalBytes: input.sourceTree.totalBytes,
182
+ },
183
+ ];
184
+ const edges = [];
185
+ const nodeIdsByPath = new Map([["", "source-bundle"]]);
186
+ for (const entry of treeEntries) {
187
+ const normalizedPath = getNodePath(entry.path);
188
+ if (entry.kind === "directory" && normalizedPath === "") {
189
+ continue;
190
+ }
191
+ const nodeId = `${entry.kind}:${normalizedPath}`;
192
+ const node = {
193
+ id: nodeId,
194
+ kind: entry.kind,
195
+ path: normalizedPath,
196
+ label: normalizedPath === "" ? path.basename(input.sourceBundlePath) : path.posix.basename(normalizedPath),
197
+ };
198
+ if (entry.kind === "file") {
199
+ node.hash = entry.hash;
200
+ node.size = entry.size;
201
+ const ext = path.posix.extname(normalizedPath);
202
+ node.extension = ext.length > 0 ? ext : null;
203
+ }
204
+ if (entry.kind === "symlink") {
205
+ node.target = entry.target;
206
+ }
207
+ nodes.push(node);
208
+ nodeIdsByPath.set(normalizedPath, nodeId);
209
+ const parentPath = normalizedPath.length === 0 ? "" : path.posix.dirname(normalizedPath);
210
+ const parentId = nodeIdsByPath.get(parentPath === "." ? "" : parentPath) ?? "source-bundle";
211
+ edges.push({
212
+ from: parentId,
213
+ to: nodeId,
214
+ kind: "contains",
215
+ });
216
+ }
217
+ const graph = {
218
+ contract: "graphify_graph.v1",
219
+ runId: input.runId,
220
+ sourceBundleHash: input.sourceBundleHash,
221
+ graphify: {
222
+ version: input.version,
223
+ mode: input.mode,
224
+ config: canonicalizeJsonValue(input.config),
225
+ flags: input.flags,
226
+ },
227
+ sourceBundle: {
228
+ path: input.sourceBundlePath,
229
+ kind: input.sourceTree.kind,
230
+ fileCount: input.sourceTree.fileCount,
231
+ directoryCount: input.sourceTree.directoryCount,
232
+ symlinkCount: input.sourceTree.symlinkCount,
233
+ totalBytes: input.sourceTree.totalBytes,
234
+ },
235
+ nodes,
236
+ edges,
237
+ };
238
+ const graphHash = createHash("sha256").update(stableJsonStringify(graph)).digest("hex");
239
+ graph.graphHash = graphHash;
240
+ return { graph, graphHash };
241
+ }
242
+
243
+ function summarizeGraphPayload(graphPayload) {
244
+ const nodes = Array.isArray(graphPayload?.nodes) ? graphPayload.nodes : [];
245
+ const edges = Array.isArray(graphPayload?.edges) ? graphPayload.edges : [];
246
+ const nodeKinds = new Map();
247
+ for (const node of nodes) {
248
+ const kind = typeof node?.kind === "string" ? node.kind : "unknown";
249
+ nodeKinds.set(kind, (nodeKinds.get(kind) ?? 0) + 1);
250
+ }
251
+ return {
252
+ nodeCount: nodes.length,
253
+ edgeCount: edges.length,
254
+ nodeKinds: Object.fromEntries([...nodeKinds.entries()].sort((left, right) => left[0].localeCompare(right[0]))),
255
+ graphHash: typeof graphPayload?.graphHash === "string" ? graphPayload.graphHash : null,
256
+ };
257
+ }
258
+
259
+ function buildGraphHtml(input) {
260
+ const json = stableJsonStringify(input.graph);
261
+ return [
262
+ "<!doctype html>",
263
+ '<html lang="en">',
264
+ "<head>",
265
+ ' <meta charset="utf-8" />',
266
+ ` <title>Graphify run ${input.runId}</title>`,
267
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
268
+ ' <style>body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.45;margin:24px;max-width:1100px} pre{background:#f6f8fa;padding:16px;overflow:auto;border-radius:8px} table{border-collapse:collapse} td,th{border:1px solid #ddd;padding:6px 10px;text-align:left}</style>',
269
+ "</head>",
270
+ "<body>",
271
+ ` <h1>Graphify run ${input.runId}</h1>`,
272
+ ' <p><strong>Managed off-path bundle.</strong> This HTML is reproducible from the source bundle hash and recorded Graphify metadata.</p>',
273
+ ' <table>',
274
+ ` <tr><th>Source bundle hash</th><td>${input.sourceBundleHash}</td></tr>`,
275
+ ` <tr><th>Graphify version</th><td>${input.version}</td></tr>`,
276
+ ` <tr><th>Mode</th><td>${input.mode}</td></tr>`,
277
+ ` <tr><th>Config hash</th><td>${input.configHash}</td></tr>`,
278
+ ` <tr><th>Graph hash</th><td>${input.graphHash}</td></tr>`,
279
+ ` <tr><th>Nodes</th><td>${input.summary.nodeCount}</td></tr>`,
280
+ ` <tr><th>Edges</th><td>${input.summary.edgeCount}</td></tr>`,
281
+ " </table>",
282
+ ' <h2>Graph JSON</h2>',
283
+ ` <script type="application/json" id="graph-json">${json.replace(/</g, "\\u003c")}</script>`,
284
+ " <pre id=\"graph-pre\"></pre>",
285
+ " <script>",
286
+ " document.getElementById('graph-pre').textContent = document.getElementById('graph-json').textContent;",
287
+ " </script>",
288
+ "</body>",
289
+ "</html>",
290
+ ].join("\n") + "\n";
291
+ }
292
+
293
+ function buildGraphReport(input) {
294
+ const lines = [
295
+ `# Graphify run ${input.runId}`,
296
+ "",
297
+ "## Managed execution summary",
298
+ `- Source bundle: ${input.sourceBundlePath}`,
299
+ `- Source bundle hash: ${input.sourceBundleHash}`,
300
+ `- Graphify version: ${input.version}`,
301
+ `- Mode: ${input.mode}`,
302
+ `- Config hash: ${input.configHash}`,
303
+ `- Flags: ${input.flags.length === 0 ? "none" : input.flags.join(", ")}`,
304
+ `- Execution state: ${input.execution.state}`,
305
+ `- Execution exit code: ${input.execution.exitCode === null ? "none" : input.execution.exitCode}`,
306
+ `- Execution failure: ${input.execution.failure === null ? "none" : input.execution.failure}`,
307
+ "",
308
+ "## Structural summary",
309
+ `- Files: ${input.sourceTree.fileCount}`,
310
+ `- Directories: ${input.sourceTree.directoryCount}`,
311
+ `- Symlinks: ${input.sourceTree.symlinkCount}`,
312
+ `- Bytes: ${input.sourceTree.totalBytes}`,
313
+ `- Nodes: ${input.summary.nodeCount}`,
314
+ `- Edges: ${input.summary.edgeCount}`,
315
+ `- Graph hash: ${input.graphHash}`,
316
+ "",
317
+ "## Labels",
318
+ input.labels.length === 0 ? "- none" : input.labels.map((label) => `- ${label}`).join("\n"),
319
+ ];
320
+ return lines.join("\n") + "\n";
321
+ }
322
+
323
+ function buildBenchmarkRecord(input) {
324
+ return {
325
+ contract: "graphify_benchmark.v1",
326
+ runId: input.runId,
327
+ sourceBundleHash: input.sourceBundleHash,
328
+ graphifyVersion: input.version,
329
+ graphifyMode: input.mode,
330
+ configHash: input.configHash,
331
+ execution: {
332
+ state: input.execution.state,
333
+ exitCode: input.execution.exitCode,
334
+ durationMs: input.execution.durationMs,
335
+ },
336
+ hashMs: input.hashMs,
337
+ synthesisMs: input.synthesisMs,
338
+ writeMs: input.writeMs,
339
+ totalMs: input.totalMs,
340
+ };
341
+ }
342
+
343
+ function buildCommandRecord(input) {
344
+ return {
345
+ contract: "graphify_command_record.v1",
346
+ runId: input.runId,
347
+ sourceBundle: {
348
+ path: input.sourceBundlePath,
349
+ kind: input.sourceTree.kind,
350
+ hash: input.sourceBundleHash,
351
+ fileCount: input.sourceTree.fileCount,
352
+ directoryCount: input.sourceTree.directoryCount,
353
+ symlinkCount: input.sourceTree.symlinkCount,
354
+ totalBytes: input.sourceTree.totalBytes,
355
+ manifest: input.manifest,
356
+ },
357
+ graphify: {
358
+ version: input.version,
359
+ versionSource: input.versionSource,
360
+ mode: input.mode,
361
+ config: canonicalizeJsonValue(input.config),
362
+ configHash: input.configHash,
363
+ flags: input.flags,
364
+ command: input.command === null ? null : {
365
+ executable: input.command.executable,
366
+ args: input.command.args,
367
+ },
368
+ },
369
+ execution: {
370
+ state: input.execution.state,
371
+ command: input.execution.command,
372
+ args: input.execution.args,
373
+ cwd: input.execution.cwd,
374
+ startedAt: input.execution.startedAt,
375
+ finishedAt: input.execution.finishedAt,
376
+ durationMs: input.execution.durationMs,
377
+ exitCode: input.execution.exitCode,
378
+ signal: input.execution.signal,
379
+ stdout: input.execution.stdout,
380
+ stderr: input.execution.stderr,
381
+ failure: input.execution.failure,
382
+ },
383
+ outputs: input.outputs,
384
+ reproducibility: {
385
+ runId: input.runId,
386
+ sourceBundleHash: input.sourceBundleHash,
387
+ graphifyVersion: input.version,
388
+ graphifyMode: input.mode,
389
+ configHash: input.configHash,
390
+ outputHash: input.graphHash,
391
+ },
392
+ };
393
+ }
394
+
395
+ function probeGraphifyVersion(command, flags) {
396
+ if (command === null) {
397
+ return { version: "unknown", versionSource: "unavailable" };
398
+ }
399
+ const probe = spawnSync(command.executable, ["--version", ...flags], {
400
+ cwd: process.cwd(),
401
+ encoding: "utf8",
402
+ stdio: "pipe",
403
+ });
404
+ const stdout = typeof probe.stdout === "string" ? probe.stdout.trim() : "";
405
+ const stderr = typeof probe.stderr === "string" ? probe.stderr.trim() : "";
406
+ const combined = [stdout, stderr].filter((value) => value.length > 0).join("\n").trim();
407
+ if (probe.error || probe.status !== 0) {
408
+ return {
409
+ version: normalizeOptionalString(combined) ?? "unknown",
410
+ versionSource: probe.error ? `probe-error:${probe.error.message}` : `probe-exit:${probe.status}`,
411
+ };
412
+ }
413
+ return {
414
+ version: normalizeOptionalString(combined) ?? "unknown",
415
+ versionSource: "probed",
416
+ };
417
+ }
418
+
419
+ function runOptionalManagedCommand(input) {
420
+ if (input.command === null) {
421
+ return {
422
+ state: "synthesized",
423
+ command: null,
424
+ args: [],
425
+ cwd: input.cwd,
426
+ startedAt: null,
427
+ finishedAt: null,
428
+ durationMs: 0,
429
+ exitCode: 0,
430
+ signal: null,
431
+ stdout: "",
432
+ stderr: "",
433
+ failure: null,
434
+ };
435
+ }
436
+ const startedAt = new Date().toISOString();
437
+ const startedMs = Date.now();
438
+ const result = spawnSync(input.command.executable, input.command.args, {
439
+ cwd: input.cwd,
440
+ encoding: "utf8",
441
+ stdio: "pipe",
442
+ env: {
443
+ ...process.env,
444
+ GRAPHIFY_RUN_DIR: input.cwd,
445
+ GRAPHIFY_SOURCE_BUNDLE: input.sourceBundlePath,
446
+ GRAPHIFY_SOURCE_BUNDLE_HASH: input.sourceBundleHash,
447
+ GRAPHIFY_GRAPH_JSON: input.outputs.graph,
448
+ GRAPHIFY_GRAPH_HTML: input.outputs.html,
449
+ GRAPHIFY_GRAPH_REPORT: input.outputs.report,
450
+ GRAPHIFY_GRAPHIFY_SUMMARY: input.outputs.summary,
451
+ GRAPHIFY_BENCHMARK: input.outputs.benchmark,
452
+ GRAPHIFY_LABELS: input.outputs.labels,
453
+ GRAPHIFY_VERSION: input.version,
454
+ GRAPHIFY_MODE: input.mode,
455
+ GRAPHIFY_CONFIG_JSON: stableJsonStringify(input.config),
456
+ GRAPHIFY_FLAGS_JSON: stableJsonStringify(input.flags),
457
+ GRAPHIFY_RUN_ID: input.runId,
458
+ },
459
+ });
460
+ const finishedAt = new Date().toISOString();
461
+ const stdout = typeof result.stdout === "string" ? result.stdout : "";
462
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
463
+ const failure = result.error || result.status !== 0
464
+ ? [
465
+ result.error instanceof Error ? result.error.message : null,
466
+ typeof result.status === "number" && result.status !== 0 ? `exitCode=${result.status}` : null,
467
+ typeof result.signal === "string" && result.signal.length > 0 ? `signal=${result.signal}` : null,
468
+ stderr.trim().length > 0 ? stderr.trim() : null,
469
+ ].filter((value) => value !== null).join(" | ")
470
+ : null;
471
+ return {
472
+ state: result.error || result.status !== 0 ? "failed" : "executed",
473
+ command: input.command.executable,
474
+ args: input.command.args,
475
+ cwd: input.cwd,
476
+ startedAt,
477
+ finishedAt,
478
+ durationMs: Date.now() - startedMs,
479
+ exitCode: typeof result.status === "number" ? result.status : null,
480
+ signal: typeof result.signal === "string" ? result.signal : null,
481
+ stdout,
482
+ stderr,
483
+ failure,
484
+ };
485
+ }
486
+
487
+ function ensureRunDirectory(runDir) {
488
+ rmSync(runDir, { recursive: true, force: true });
489
+ mkdirSync(runDir, { recursive: true });
490
+ }
491
+
492
+ export function runManagedGraphifyRunner(options) {
493
+ const startedAt = new Date().toISOString();
494
+ const startedMs = Date.now();
495
+ const sourceBundlePath = path.resolve(options.sourceBundlePath);
496
+ const outputRoot = path.resolve(options.outputRoot ?? path.join("artifacts", "graphify-runs"));
497
+ if (!existsSync(sourceBundlePath)) {
498
+ throw new Error(`Source bundle does not exist: ${sourceBundlePath}`);
499
+ }
500
+ const sourceTreeStartMs = Date.now();
501
+ const sourceTree = describeStablePathTree(sourceBundlePath);
502
+ const hashMs = Date.now() - sourceTreeStartMs;
503
+ const manifest = loadSourceBundleManifest(sourceBundlePath);
504
+ const manifestLabels = manifest === null ? [] : extractLabelsFromManifest(manifest.data);
505
+ const config = canonicalizeJsonValue(options.graphifyConfig ?? {});
506
+ const flags = Array.isArray(options.graphifyFlags) ? options.graphifyFlags.filter((flag) => typeof flag === "string" && flag.trim().length > 0) : [];
507
+ const mode = normalizeOptionalString(options.graphifyMode) ?? "managed-off-path";
508
+ const command = normalizeOptionalString(options.graphifyCommand);
509
+ const commandArgs = Array.isArray(options.graphifyArgs) ? options.graphifyArgs.filter((arg) => typeof arg === "string") : [];
510
+ const explicitVersion = normalizeOptionalString(options.graphifyVersion);
511
+ const commandObject = command === null ? null : {
512
+ executable: command,
513
+ args: commandArgs,
514
+ };
515
+ const versionProbe = explicitVersion === null ? probeGraphifyVersion(commandObject, []) : { version: explicitVersion, versionSource: "provided" };
516
+ const version = versionProbe.version;
517
+ const versionSource = versionProbe.versionSource;
518
+ const sourceBundleHash = sourceTree.hash;
519
+ const configHash = createHash("sha256").update(stableJsonStringify(config)).digest("hex");
520
+ const runId = normalizeOptionalString(options.runId) ?? deriveGraphifyRunId({
521
+ sourceBundleHash,
522
+ version,
523
+ mode,
524
+ config,
525
+ labels: Array.isArray(options.labels) ? options.labels.filter((label) => typeof label === "string" && label.trim().length > 0) : [],
526
+ flags,
527
+ });
528
+ const runDir = path.join(outputRoot, runId);
529
+ ensureRunDirectory(runDir);
530
+ const outputPaths = {
531
+ command: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.command),
532
+ graph: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.graph),
533
+ html: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.html),
534
+ report: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.report),
535
+ summary: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.summary),
536
+ benchmark: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.benchmark),
537
+ labels: path.join(runDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.labels),
538
+ };
539
+ const executionStartMs = Date.now();
540
+ const execution = runOptionalManagedCommand({
541
+ command: commandObject,
542
+ cwd: runDir,
543
+ sourceBundlePath,
544
+ sourceBundleHash,
545
+ version,
546
+ mode,
547
+ config,
548
+ runId,
549
+ outputs: outputPaths,
550
+ });
551
+ const executionDurationMs = Date.now() - executionStartMs;
552
+ const graphPayloadStartMs = Date.now();
553
+ const graphPayload = buildGraphPayload({
554
+ runId,
555
+ sourceBundlePath,
556
+ sourceBundleHash,
557
+ sourceTree,
558
+ version,
559
+ mode,
560
+ config,
561
+ flags,
562
+ });
563
+ const labels = buildGraphifyLabels({
564
+ sourceBundleHash,
565
+ version,
566
+ mode,
567
+ flags,
568
+ sourceLabels: manifestLabels,
569
+ requestedLabels: Array.isArray(options.labels) ? options.labels.filter((label) => typeof label === "string" && label.trim().length > 0) : [],
570
+ });
571
+ const graphHash = graphPayload.graphHash;
572
+ const graphSummary = summarizeGraphPayload(graphPayload.graph);
573
+ const runSummary = {
574
+ contract: "graphify_run_summary.v1",
575
+ runId,
576
+ sourceBundleHash,
577
+ graphifyVersion: version,
578
+ graphifyVersionSource: versionSource,
579
+ graphifyMode: mode,
580
+ graphifyConfigHash: configHash,
581
+ graphifyFlags: flags,
582
+ sourceBundle: {
583
+ path: sourceBundlePath,
584
+ kind: sourceTree.kind,
585
+ fileCount: sourceTree.fileCount,
586
+ directoryCount: sourceTree.directoryCount,
587
+ symlinkCount: sourceTree.symlinkCount,
588
+ totalBytes: sourceTree.totalBytes,
589
+ labels: manifestLabels,
590
+ },
591
+ manifest: manifest === null ? null : manifest.path,
592
+ graph: graphSummary,
593
+ labels,
594
+ execution: {
595
+ state: execution.state,
596
+ exitCode: execution.exitCode,
597
+ failure: execution.failure,
598
+ },
599
+ outputs: {
600
+ command: GRAPHIFY_RUN_BUNDLE_LAYOUT.command,
601
+ graph: GRAPHIFY_RUN_BUNDLE_LAYOUT.graph,
602
+ html: GRAPHIFY_RUN_BUNDLE_LAYOUT.html,
603
+ report: GRAPHIFY_RUN_BUNDLE_LAYOUT.report,
604
+ summary: GRAPHIFY_RUN_BUNDLE_LAYOUT.summary,
605
+ benchmark: GRAPHIFY_RUN_BUNDLE_LAYOUT.benchmark,
606
+ labels: GRAPHIFY_RUN_BUNDLE_LAYOUT.labels,
607
+ },
608
+ graphHash,
609
+ };
610
+ const graphWriteStartMs = Date.now();
611
+ writeJsonFile(outputPaths.graph, graphPayload.graph);
612
+ writeFileSync(outputPaths.html, buildGraphHtml({
613
+ runId,
614
+ sourceBundleHash,
615
+ version,
616
+ mode,
617
+ configHash,
618
+ graphHash,
619
+ graph: graphPayload.graph,
620
+ summary: graphSummary,
621
+ }), "utf8");
622
+ writeJsonFile(outputPaths.labels, {
623
+ contract: "graphify_labels.v1",
624
+ runId,
625
+ sourceBundleHash,
626
+ graphifyVersion: version,
627
+ graphifyMode: mode,
628
+ configHash,
629
+ labels,
630
+ manifestLabels,
631
+ });
632
+ const report = buildGraphReport({
633
+ runId,
634
+ sourceBundlePath,
635
+ sourceBundleHash,
636
+ version,
637
+ mode,
638
+ configHash,
639
+ flags,
640
+ execution,
641
+ sourceTree,
642
+ summary: graphSummary,
643
+ graphHash,
644
+ labels,
645
+ });
646
+ writeFileSync(outputPaths.report, report, "utf8");
647
+ writeJsonFile(outputPaths.summary, runSummary);
648
+ const benchmark = buildBenchmarkRecord({
649
+ runId,
650
+ sourceBundleHash,
651
+ version,
652
+ mode,
653
+ configHash,
654
+ execution: {
655
+ state: execution.state,
656
+ exitCode: execution.exitCode,
657
+ durationMs: executionDurationMs,
658
+ },
659
+ hashMs,
660
+ synthesisMs: Date.now() - graphPayloadStartMs,
661
+ writeMs: Date.now() - graphWriteStartMs,
662
+ totalMs: Date.now() - startedMs,
663
+ });
664
+ writeJsonFile(outputPaths.benchmark, benchmark);
665
+ const commandRecord = buildCommandRecord({
666
+ runId,
667
+ sourceBundlePath,
668
+ sourceTree,
669
+ sourceBundleHash,
670
+ manifest,
671
+ version,
672
+ versionSource,
673
+ mode,
674
+ config,
675
+ configHash,
676
+ flags,
677
+ command: commandObject,
678
+ execution: {
679
+ ...execution,
680
+ startedAt,
681
+ finishedAt: new Date().toISOString(),
682
+ },
683
+ outputs: outputPaths,
684
+ graphHash,
685
+ });
686
+ writeJsonFile(outputPaths.command, commandRecord);
687
+ const finishedAt = new Date().toISOString();
688
+ const result = {
689
+ ok: true,
690
+ runId,
691
+ outputRoot,
692
+ runDir,
693
+ sourceBundlePath,
694
+ sourceBundleHash,
695
+ sourceTree,
696
+ graphifyVersion: version,
697
+ graphifyVersionSource: versionSource,
698
+ graphifyMode: mode,
699
+ graphifyConfig: config,
700
+ graphifyConfigHash: configHash,
701
+ graphifyFlags: flags,
702
+ labels,
703
+ manifest,
704
+ command: commandObject,
705
+ execution: {
706
+ ...execution,
707
+ startedAt,
708
+ finishedAt,
709
+ },
710
+ graph: {
711
+ path: outputPaths.graph,
712
+ hash: graphHash,
713
+ nodeCount: graphSummary.nodeCount,
714
+ edgeCount: graphSummary.edgeCount,
715
+ nodeKinds: graphSummary.nodeKinds,
716
+ },
717
+ outputs: outputPaths,
718
+ benchmark,
719
+ startedAt,
720
+ finishedAt,
721
+ durationMs: Date.now() - startedMs,
722
+ };
723
+ return result;
724
+ }
725
+
726
+ export function describeGraphifyRunBundle(runDir) {
727
+ const resolvedRunDir = path.resolve(runDir);
728
+ const commandPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.command);
729
+ const graphPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.graph);
730
+ const htmlPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.html);
731
+ const reportPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.report);
732
+ const summaryPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.summary);
733
+ const benchmarkPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.benchmark);
734
+ const labelsPath = path.join(resolvedRunDir, GRAPHIFY_RUN_BUNDLE_LAYOUT.labels);
735
+ return {
736
+ runDir: resolvedRunDir,
737
+ command: loadJsonIfExists(commandPath),
738
+ graph: loadJsonIfExists(graphPath),
739
+ html: existsSync(htmlPath),
740
+ report: existsSync(reportPath),
741
+ summary: loadJsonIfExists(summaryPath),
742
+ benchmark: loadJsonIfExists(benchmarkPath),
743
+ labels: loadJsonIfExists(labelsPath),
744
+ };
745
+ }