@tracegraph/cli 0.1.0

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/index.js ADDED
@@ -0,0 +1,2597 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+ var import_shared_types14 = require("@tracegraph/shared-types");
29
+
30
+ // src/commands/run.ts
31
+ var import_child_process = require("child_process");
32
+ var import_fs2 = __toESM(require("fs"));
33
+ var import_path2 = __toESM(require("path"));
34
+ var import_trace_core = require("@tracegraph/trace-core");
35
+ var import_shared_types = require("@tracegraph/shared-types");
36
+
37
+ // src/protocol.ts
38
+ function emit(fields) {
39
+ const envelope = {
40
+ protocol: "tracegraph.cli.v1",
41
+ timestamp: Date.now(),
42
+ ...fields
43
+ };
44
+ process.stdout.write(JSON.stringify(envelope) + "\n");
45
+ }
46
+ function emitError(runId, message, detail) {
47
+ process.stderr.write(`[tracegraph] Error: ${message}
48
+ `);
49
+ if (detail) {
50
+ process.stderr.write(`[tracegraph] Detail: ${String(detail)}
51
+ `);
52
+ }
53
+ emit({
54
+ type: "error",
55
+ runId,
56
+ payload: { message, detail: String(detail ?? "") }
57
+ });
58
+ }
59
+
60
+ // src/config.ts
61
+ var import_fs = __toESM(require("fs"));
62
+ var import_path = __toESM(require("path"));
63
+ var CONFIG_FILENAME = "tracegraph.config.json";
64
+ function loadConfig(workspaceRoot) {
65
+ const configPath = import_path.default.join(workspaceRoot, CONFIG_FILENAME);
66
+ if (!import_fs.default.existsSync(configPath)) {
67
+ return {};
68
+ }
69
+ try {
70
+ return JSON.parse(import_fs.default.readFileSync(configPath, "utf8"));
71
+ } catch (err) {
72
+ process.stderr.write(
73
+ `[tracegraph] Warning: could not parse ${configPath}: ${String(err)}
74
+ [tracegraph] Using default configuration.
75
+ `
76
+ );
77
+ return {};
78
+ }
79
+ }
80
+
81
+ // src/commands/run.ts
82
+ function detectAndInjectReporter(args, workspaceRoot) {
83
+ const messages = [];
84
+ let runner = null;
85
+ for (const arg of args) {
86
+ const base = import_path2.default.basename(arg).toLowerCase();
87
+ if (/^vitest(\.cmd)?$/.test(base)) {
88
+ runner = "vitest";
89
+ break;
90
+ }
91
+ if (/^jest(\.cmd)?$/.test(base)) {
92
+ runner = "jest";
93
+ break;
94
+ }
95
+ }
96
+ if (!runner) {
97
+ const joined = args.join(" ").toLowerCase();
98
+ if (/\bvitest\b/.test(joined)) runner = "vitest";
99
+ else if (/\bjest\b/.test(joined)) runner = "jest";
100
+ }
101
+ if (!runner) {
102
+ messages.push(
103
+ "TraceGraph: no test reporter detected \u2014 capture level will be 0\u20131",
104
+ "Recommendation: add @tracegraph/vitest or @tracegraph/jest for test-level tracing"
105
+ );
106
+ return { args, messages, injected: false, runner: null };
107
+ }
108
+ const alreadyPresent = args.some(
109
+ (a) => /--reporters?=@tracegraph\//i.test(a)
110
+ );
111
+ let alreadyPresentByPair = false;
112
+ for (let i = 0; i < args.length - 1; i++) {
113
+ if ((args[i] === "--reporter" || args[i] === "--reporters") && (args[i + 1] ?? "").startsWith("@tracegraph/")) {
114
+ alreadyPresentByPair = true;
115
+ break;
116
+ }
117
+ }
118
+ const explicitConfig = extractConfigArg(args);
119
+ const hasReporterInConfig = checkVitestConfigForTraceGraphReporter(workspaceRoot, explicitConfig);
120
+ if (alreadyPresent || alreadyPresentByPair || hasReporterInConfig) {
121
+ messages.push(
122
+ `TraceGraph: detected ${runner} \u2014 @tracegraph/${runner} reporter already present, skipping injection`
123
+ );
124
+ return { args, messages, injected: false, runner };
125
+ }
126
+ let injectedArgs;
127
+ if (runner === "vitest") {
128
+ injectedArgs = [...args, "--reporter=default", "--reporter=@tracegraph/vitest"];
129
+ messages.push(
130
+ `TraceGraph: detected Vitest \u2014 injecting @tracegraph/vitest reporter (Level 5)`
131
+ );
132
+ } else {
133
+ injectedArgs = [...args, "--reporters=default", "--reporters=@tracegraph/jest"];
134
+ messages.push(
135
+ `TraceGraph: detected Jest \u2014 injecting @tracegraph/jest reporter (Level 5)`
136
+ );
137
+ }
138
+ return { args: injectedArgs, messages, injected: true, runner };
139
+ }
140
+ function checkVitestConfigForTraceGraphReporter(workspaceRoot, extraConfigPath) {
141
+ const configCandidates = [
142
+ "vitest.config.ts",
143
+ "vitest.config.js",
144
+ "vitest.config.mts",
145
+ "vitest.config.mjs"
146
+ ];
147
+ const pathsToCheck = configCandidates.map((n) => import_path2.default.join(workspaceRoot, n));
148
+ if (extraConfigPath) {
149
+ pathsToCheck.push(
150
+ import_path2.default.isAbsolute(extraConfigPath) ? extraConfigPath : import_path2.default.join(workspaceRoot, extraConfigPath)
151
+ );
152
+ }
153
+ for (const p of pathsToCheck) {
154
+ if (import_fs2.default.existsSync(p)) {
155
+ try {
156
+ const content = import_fs2.default.readFileSync(p, "utf8");
157
+ if (content.includes("@tracegraph/vitest") || content.includes("TraceGraphReporter")) {
158
+ return true;
159
+ }
160
+ } catch {
161
+ }
162
+ }
163
+ }
164
+ return false;
165
+ }
166
+ function extractConfigArg(args) {
167
+ for (let i = 0; i < args.length; i++) {
168
+ const arg = args[i];
169
+ if (arg.startsWith("--config=")) return arg.slice("--config=".length);
170
+ if (arg === "--config" || arg === "-c") return args[i + 1];
171
+ }
172
+ return void 0;
173
+ }
174
+ async function runCommand(wrappedArgs2, options) {
175
+ if (wrappedArgs2.length === 0) {
176
+ process.stderr.write(
177
+ "Usage: tracegraph run [options] -- <command> [args]\nExample: tracegraph run -- npm test\n"
178
+ );
179
+ return import_shared_types.EXIT_CODES.CLI_ERROR;
180
+ }
181
+ const workspaceRoot = process.cwd();
182
+ const config = loadConfig(workspaceRoot);
183
+ const runId = options.runId ?? (0, import_trace_core.createRunId)();
184
+ const traceId = (0, import_trace_core.createTraceId)();
185
+ const sessionId = (0, import_trace_core.createSessionId)();
186
+ const startedAt = Date.now();
187
+ const tracegraphDir = import_path2.default.join(workspaceRoot, ".tracegraph");
188
+ const runDir = import_path2.default.join(tracegraphDir, "runs", runId);
189
+ const tracesDir = import_path2.default.join(tracegraphDir, "traces");
190
+ import_fs2.default.mkdirSync(runDir, { recursive: true });
191
+ import_fs2.default.mkdirSync(tracesDir, { recursive: true });
192
+ const injection = detectAndInjectReporter(wrappedArgs2, workspaceRoot);
193
+ for (const msg of injection.messages) {
194
+ process.stderr.write(`${msg}
195
+ `);
196
+ }
197
+ const effectiveArgs = injection.args;
198
+ const [command, ...commandArgs] = effectiveArgs;
199
+ const commandStr = wrappedArgs2.join(" ");
200
+ const entrypoint = { type: "cli_command", command: commandStr };
201
+ emit({ type: "run.started", runId });
202
+ emit({ type: "trace.started", runId, traceId, payload: { entrypoint } });
203
+ const jsonlTmpPath = import_path2.default.join(runDir, `${traceId}.events.jsonl.tmp`);
204
+ const writer = new import_trace_core.TraceEventWriter(jsonlTmpPath);
205
+ const traceStartEventId = (0, import_trace_core.createEventId)();
206
+ const traceStartEvent = {
207
+ schemaVersion: import_shared_types.SCHEMA_VERSIONS.event,
208
+ eventId: traceStartEventId,
209
+ traceId,
210
+ parentEventId: null,
211
+ type: "trace_start",
212
+ language: "javascript",
213
+ name: "trace_start",
214
+ startTime: startedAt,
215
+ metadata: { command: commandStr }
216
+ };
217
+ writer.write(traceStartEvent);
218
+ await writer.close();
219
+ let exitCode = 0;
220
+ let spawnErrorMsg;
221
+ const childEnv = {
222
+ ...process.env,
223
+ TRACEGRAPH_ENABLED: "1",
224
+ TRACEGRAPH_RUN_DIR: runDir,
225
+ TRACEGRAPH_TRACE_ID: traceId,
226
+ TRACEGRAPH_RUN_ID: runId,
227
+ TRACEGRAPH_SESSION_ID: sessionId,
228
+ TRACEGRAPH_ROOT_EVENT_ID: traceStartEventId
229
+ };
230
+ try {
231
+ if (!command) {
232
+ spawnErrorMsg = "No command provided";
233
+ exitCode = import_shared_types.EXIT_CODES.CLI_ERROR;
234
+ } else {
235
+ const result = (0, import_child_process.spawnSync)(command, commandArgs, {
236
+ stdio: "inherit",
237
+ // pass through so the user sees the command's output
238
+ shell: false,
239
+ env: childEnv
240
+ });
241
+ if (result.error) {
242
+ spawnErrorMsg = result.error.message;
243
+ exitCode = result.error.message.includes("ENOENT") ? import_shared_types.EXIT_CODES.CLI_ERROR : import_shared_types.EXIT_CODES.COMMAND_FAILURE;
244
+ } else {
245
+ exitCode = result.status ?? import_shared_types.EXIT_CODES.SUCCESS;
246
+ }
247
+ }
248
+ } catch (err) {
249
+ spawnErrorMsg = err instanceof Error ? err.message : String(err);
250
+ exitCode = import_shared_types.EXIT_CODES.CLI_ERROR;
251
+ emitError(runId, "Failed to spawn command", spawnErrorMsg);
252
+ }
253
+ const endedAt = Date.now();
254
+ const status = spawnErrorMsg !== void 0 ? "error" : exitCode === import_shared_types.EXIT_CODES.SUCCESS ? "passed" : "failed";
255
+ const traceEndEvent = {
256
+ schemaVersion: import_shared_types.SCHEMA_VERSIONS.event,
257
+ eventId: (0, import_trace_core.createEventId)(),
258
+ traceId,
259
+ parentEventId: traceStartEventId,
260
+ type: "trace_end",
261
+ language: "javascript",
262
+ name: "trace_end",
263
+ startTime: endedAt,
264
+ endTime: endedAt,
265
+ durationMs: endedAt - startedAt,
266
+ metadata: {
267
+ exitCode,
268
+ ...spawnErrorMsg ? { error: spawnErrorMsg } : {}
269
+ }
270
+ };
271
+ try {
272
+ import_fs2.default.appendFileSync(jsonlTmpPath, JSON.stringify(traceEndEvent) + "\n", "utf8");
273
+ } catch (err) {
274
+ emitError(runId, "Failed to write trace_end event", err);
275
+ }
276
+ let captureLevel = {
277
+ overall: 0,
278
+ label: "Runner metadata only",
279
+ adapters: {}
280
+ };
281
+ const captureLevelFile = import_path2.default.join(runDir, "capture-level.json");
282
+ if (import_fs2.default.existsSync(captureLevelFile)) {
283
+ try {
284
+ captureLevel = JSON.parse(import_fs2.default.readFileSync(captureLevelFile, "utf8"));
285
+ } catch {
286
+ }
287
+ }
288
+ let traceLanguage = "javascript";
289
+ let traceFramework;
290
+ const metaFile = import_path2.default.join(runDir, "meta.json");
291
+ if (import_fs2.default.existsSync(metaFile)) {
292
+ try {
293
+ const meta = JSON.parse(import_fs2.default.readFileSync(metaFile, "utf8"));
294
+ if (meta.language === "php") traceLanguage = "php";
295
+ if (meta.framework) traceFramework = meta.framework;
296
+ } catch {
297
+ }
298
+ }
299
+ const testsRunDir = import_path2.default.join(runDir, "tests");
300
+ const testTracePaths = [];
301
+ if (import_fs2.default.existsSync(testsRunDir)) {
302
+ const testJsonlFiles = import_fs2.default.readdirSync(testsRunDir).filter((f) => f.endsWith(".events.jsonl.tmp")).sort();
303
+ for (const jsonlFile of testJsonlFiles) {
304
+ const testTraceId = jsonlFile.replace(".events.jsonl.tmp", "");
305
+ try {
306
+ const testPath = await (0, import_trace_core.finaliseTrace)({
307
+ runDir: testsRunDir,
308
+ // reporter wrote files into the tests/ subdir
309
+ traceId: testTraceId,
310
+ tracesDir,
311
+ workspaceRoot,
312
+ sessionId,
313
+ runId,
314
+ ...options.scenarioId ? { scenarioId: options.scenarioId } : {},
315
+ language: traceLanguage,
316
+ ...traceFramework ? { framework: traceFramework } : {},
317
+ entrypoint,
318
+ startedAt,
319
+ endedAt,
320
+ status,
321
+ captureLevel
322
+ });
323
+ testTracePaths.push(testPath);
324
+ } catch {
325
+ }
326
+ }
327
+ }
328
+ let finalPath;
329
+ try {
330
+ finalPath = await (0, import_trace_core.finaliseTrace)({
331
+ runDir,
332
+ traceId,
333
+ tracesDir,
334
+ workspaceRoot,
335
+ sessionId,
336
+ runId,
337
+ ...options.scenarioId ? { scenarioId: options.scenarioId } : {},
338
+ language: traceLanguage,
339
+ ...traceFramework ? { framework: traceFramework } : {},
340
+ entrypoint,
341
+ startedAt,
342
+ endedAt,
343
+ status,
344
+ captureLevel
345
+ });
346
+ } catch (err) {
347
+ emitError(runId, "Failed to finalise trace", err);
348
+ emit({ type: "run.completed", runId, payload: { status: "error" } });
349
+ return import_shared_types.EXIT_CODES.CLI_ERROR;
350
+ }
351
+ try {
352
+ (0, import_trace_core.updateTraceIndex)(tracegraphDir, {
353
+ traceId,
354
+ runId,
355
+ file: import_path2.default.relative(workspaceRoot, finalPath).replace(/\\/g, "/"),
356
+ status,
357
+ createdAt: startedAt,
358
+ entrypoint
359
+ });
360
+ } catch {
361
+ }
362
+ try {
363
+ const testTraceIds = testTracePaths.map(
364
+ (p) => import_path2.default.basename(p, ".trace.json")
365
+ );
366
+ const latestPointer = {
367
+ latestRunId: runId,
368
+ latestTraceIds: [traceId, ...testTraceIds],
369
+ latestReportId: null,
370
+ updatedAt: Date.now()
371
+ };
372
+ import_fs2.default.writeFileSync(
373
+ import_path2.default.join(tracegraphDir, "latest.json"),
374
+ JSON.stringify(latestPointer, null, 2) + "\n",
375
+ "utf8"
376
+ );
377
+ } catch {
378
+ }
379
+ if (config.storage?.pruneOnRun !== false) {
380
+ try {
381
+ new import_trace_core.StorageManager(tracegraphDir, config.storage).prune();
382
+ } catch {
383
+ }
384
+ }
385
+ emit({
386
+ type: "trace.completed",
387
+ runId,
388
+ traceId,
389
+ captureLevel: { overall: captureLevel.overall, label: captureLevel.label },
390
+ payload: {
391
+ file: import_path2.default.relative(workspaceRoot, finalPath).replace(/\\/g, "/"),
392
+ status
393
+ }
394
+ });
395
+ emit({
396
+ type: "run.completed",
397
+ runId,
398
+ captureLevel: { overall: captureLevel.overall, label: captureLevel.label },
399
+ payload: { status }
400
+ });
401
+ return exitCode;
402
+ }
403
+
404
+ // src/commands/clean.ts
405
+ var import_path3 = __toESM(require("path"));
406
+ var import_trace_core2 = require("@tracegraph/trace-core");
407
+ function cleanCommand(options) {
408
+ const workspaceRoot = process.cwd();
409
+ const config = loadConfig(workspaceRoot);
410
+ const tracegraphDir = import_path3.default.join(workspaceRoot, ".tracegraph");
411
+ const storage = new import_trace_core2.StorageManager(tracegraphDir, config.storage);
412
+ const cleanOpts = {};
413
+ if (options.allRuns) cleanOpts.allRuns = true;
414
+ if (options.keepLast !== void 0) cleanOpts.keepLast = options.keepLast;
415
+ if (options.olderThan) cleanOpts.olderThan = options.olderThan;
416
+ storage.clean(cleanOpts);
417
+ const status = storage.status();
418
+ process.stderr.write(
419
+ `[tracegraph] clean: done. Runs remaining: ${status.runs}. Total size: ${status.totalSizeMB} MB
420
+ `
421
+ );
422
+ }
423
+
424
+ // src/commands/storage-status.ts
425
+ var import_path4 = __toESM(require("path"));
426
+ var import_trace_core3 = require("@tracegraph/trace-core");
427
+ function storageStatusCommand() {
428
+ const workspaceRoot = process.cwd();
429
+ const config = loadConfig(workspaceRoot);
430
+ const tracegraphDir = import_path4.default.join(workspaceRoot, ".tracegraph");
431
+ const storage = new import_trace_core3.StorageManager(tracegraphDir, config.storage);
432
+ const status = storage.status();
433
+ const lines = [
434
+ "",
435
+ "TraceGraph Storage",
436
+ "\u2500".repeat(40),
437
+ ` Runs: ${status.runs}`,
438
+ ` Traces: ${status.traces}`,
439
+ ` Baselines: ${status.baselines}`,
440
+ ` Total size: ${status.totalSizeMB} MB`,
441
+ ` Location: ${status.location}`,
442
+ ""
443
+ ];
444
+ process.stdout.write(lines.join("\n"));
445
+ }
446
+
447
+ // src/commands/open.ts
448
+ var import_fs3 = __toESM(require("fs"));
449
+ var import_path5 = __toESM(require("path"));
450
+ var import_trace_core4 = require("@tracegraph/trace-core");
451
+ var import_shared_types2 = require("@tracegraph/shared-types");
452
+ function openCommand(traceFilePath, options) {
453
+ const absTracePath = import_path5.default.resolve(process.cwd(), traceFilePath);
454
+ if (!import_fs3.default.existsSync(absTracePath)) {
455
+ process.stderr.write(`[tracegraph] Trace file not found: ${absTracePath}
456
+ `);
457
+ process.exit(import_shared_types2.EXIT_CODES.CLI_ERROR);
458
+ }
459
+ let trace;
460
+ try {
461
+ trace = (0, import_trace_core4.readTrace)(absTracePath);
462
+ } catch (err) {
463
+ process.stderr.write(`[tracegraph] Failed to read trace: ${String(err)}
464
+ `);
465
+ process.exit(import_shared_types2.EXIT_CODES.CLI_ERROR);
466
+ }
467
+ const bundlePath = resolveBundle();
468
+ if (!bundlePath) {
469
+ process.stderr.write(
470
+ "[tracegraph] Webview bundle not found.\n Build it with: pnpm --filter @tracegraph/webview build\n Or set: TRACEGRAPH_WEBVIEW_BUNDLE=/path/to/tracegraph-viewer.iife.js\n"
471
+ );
472
+ process.exit(import_shared_types2.EXIT_CODES.CLI_ERROR);
473
+ }
474
+ const bundleJs = import_fs3.default.readFileSync(bundlePath, "utf8");
475
+ const bundleCss = readCssIfExists(bundlePath);
476
+ const traceJson = JSON.stringify(trace);
477
+ const title = buildTitle(trace);
478
+ const html = buildHtml({ title, traceJson, bundleJs, bundleCss });
479
+ const outPath = resolveOutPath(absTracePath, options.out, trace.traceId);
480
+ import_fs3.default.mkdirSync(import_path5.default.dirname(outPath), { recursive: true });
481
+ import_fs3.default.writeFileSync(outPath, html, "utf8");
482
+ process.stdout.write(`[tracegraph] HTML report: ${outPath}
483
+ `);
484
+ if (!options.noOpen) {
485
+ openInBrowser(outPath);
486
+ }
487
+ }
488
+ function resolveBundle() {
489
+ const envPath = process.env["TRACEGRAPH_WEBVIEW_BUNDLE"];
490
+ if (envPath && import_fs3.default.existsSync(envPath)) return envPath;
491
+ const candidates = [
492
+ // From packages/cli/src/commands/ → ../../../../apps/webview/dist/
493
+ import_path5.default.resolve(__dirname, "../../../../apps/webview/dist/tracegraph-viewer.iife.js"),
494
+ // From packages/cli/dist/commands/ (built) → ../../../../apps/webview/dist/
495
+ import_path5.default.resolve(__dirname, "../../../../apps/webview/dist/tracegraph-viewer.iife.js"),
496
+ // Relative to process.cwd() (workspace root)
497
+ import_path5.default.resolve(process.cwd(), "apps/webview/dist/tracegraph-viewer.iife.js"),
498
+ // Production: next to the CLI entry
499
+ import_path5.default.resolve(__dirname, "../webview-bundle/tracegraph-viewer.iife.js")
500
+ ];
501
+ for (const c of candidates) {
502
+ if (import_fs3.default.existsSync(c)) return c;
503
+ }
504
+ return null;
505
+ }
506
+ function readCssIfExists(bundlePath) {
507
+ const cssPath = bundlePath.replace(".iife.js", ".css");
508
+ if (import_fs3.default.existsSync(cssPath)) {
509
+ return import_fs3.default.readFileSync(cssPath, "utf8");
510
+ }
511
+ return "";
512
+ }
513
+ function buildTitle(trace) {
514
+ const ep = trace.entrypoint;
515
+ if (ep.type === "http_request") return `${ep.method} ${ep.path}`;
516
+ if (ep.type === "cli_command") return ep.command;
517
+ if (ep.type === "test_case") return ep.testName;
518
+ return trace.traceId;
519
+ }
520
+ function buildHtml({ title, traceJson, bundleJs, bundleCss }) {
521
+ return `<!DOCTYPE html>
522
+ <html lang="en">
523
+ <head>
524
+ <meta charset="utf-8" />
525
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
526
+ <title>TraceGraph \u2014 ${escapeHtml(title)}</title>
527
+ <meta name="generator" content="tracegraph" />
528
+ ${bundleCss ? ` <style>
529
+ ${bundleCss}
530
+ </style>` : ""}
531
+ </head>
532
+ <body>
533
+ <div id="root"></div>
534
+ <script id="tracegraph-data" type="application/json">
535
+ ${traceJson}
536
+ </script>
537
+ <script>
538
+ ${bundleJs}
539
+ </script>
540
+ </body>
541
+ </html>`;
542
+ }
543
+ function escapeHtml(s) {
544
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
545
+ }
546
+ function resolveOutPath(traceFilePath, out, traceId) {
547
+ if (out) return import_path5.default.resolve(process.cwd(), out);
548
+ const tracegraphDir = findTracegraphDir(traceFilePath);
549
+ return import_path5.default.join(tracegraphDir, "reports", `${traceId}.html`);
550
+ }
551
+ function findTracegraphDir(traceFilePath) {
552
+ let dir = import_path5.default.dirname(traceFilePath);
553
+ while (true) {
554
+ if (import_path5.default.basename(dir) === ".tracegraph") return dir;
555
+ const parent = import_path5.default.dirname(dir);
556
+ if (parent === dir) break;
557
+ dir = parent;
558
+ }
559
+ return import_path5.default.dirname(traceFilePath);
560
+ }
561
+ function openInBrowser(filePath) {
562
+ const url = `file://${filePath}`;
563
+ try {
564
+ const { execSync } = require("child_process");
565
+ if (process.platform === "win32") {
566
+ execSync(`start "" "${filePath}"`, { stdio: "ignore" });
567
+ } else if (process.platform === "darwin") {
568
+ execSync(`open "${filePath}"`, { stdio: "ignore" });
569
+ } else {
570
+ execSync(`xdg-open "${filePath}"`, { stdio: "ignore" });
571
+ }
572
+ } catch {
573
+ process.stdout.write(`[tracegraph] Open in browser: ${url}
574
+ `);
575
+ }
576
+ }
577
+
578
+ // src/commands/init.ts
579
+ var import_fs4 = __toESM(require("fs"));
580
+ var import_path6 = __toESM(require("path"));
581
+ function initCommand() {
582
+ const cwd = process.cwd();
583
+ const pm = detectPackageManager(cwd);
584
+ const testRunner = detectTestRunner(cwd);
585
+ const framework = detectFramework(cwd);
586
+ process.stdout.write("[tracegraph] Initialising project...\n\n");
587
+ addPackageJsonScripts(cwd, pm, testRunner);
588
+ createConfig(cwd, framework);
589
+ updateGitignore(cwd);
590
+ process.stdout.write("\n[tracegraph] Done! Next steps:\n\n");
591
+ process.stdout.write(` 1. Install the adapter: ${pm} add -D @tracegraph/trace-js
592
+ `);
593
+ process.stdout.write(` 2. Add middleware: app.use(traceExpress()) // before routes
594
+ `);
595
+ process.stdout.write(` 3. Run a trace: ${pm} run trace:test
596
+ `);
597
+ process.stdout.write(` 4. View the graph: ${pm} run trace:report
598
+
599
+ `);
600
+ }
601
+ function detectPackageManager(cwd) {
602
+ if (import_fs4.default.existsSync(import_path6.default.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
603
+ if (import_fs4.default.existsSync(import_path6.default.join(cwd, "yarn.lock"))) return "yarn";
604
+ if (import_fs4.default.existsSync(import_path6.default.join(cwd, "bun.lockb"))) return "bun";
605
+ return "npm";
606
+ }
607
+ function detectTestRunner(cwd) {
608
+ try {
609
+ const pkg = JSON.parse(import_fs4.default.readFileSync(import_path6.default.join(cwd, "package.json"), "utf8"));
610
+ const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
611
+ if ("vitest" in all) return "vitest run";
612
+ if ("jest" in all) return "jest";
613
+ if ("playwright" in all) return "playwright test";
614
+ if ("mocha" in all) return "mocha";
615
+ } catch {
616
+ }
617
+ return "test";
618
+ }
619
+ function detectFramework(cwd) {
620
+ try {
621
+ const pkg = JSON.parse(import_fs4.default.readFileSync(import_path6.default.join(cwd, "package.json"), "utf8"));
622
+ const deps = pkg.dependencies ?? {};
623
+ if ("express" in deps) return "express";
624
+ if ("fastify" in deps) return "fastify";
625
+ if ("next" in deps) return "nextjs";
626
+ if ("@nestjs/core" in deps) return "nestjs";
627
+ } catch {
628
+ }
629
+ return "plain";
630
+ }
631
+ function addPackageJsonScripts(cwd, pm, testRunner) {
632
+ const pkgPath = import_path6.default.join(cwd, "package.json");
633
+ if (!import_fs4.default.existsSync(pkgPath)) {
634
+ process.stderr.write("[tracegraph] No package.json found \u2014 skipping script injection\n");
635
+ return;
636
+ }
637
+ let pkg;
638
+ try {
639
+ pkg = JSON.parse(import_fs4.default.readFileSync(pkgPath, "utf8"));
640
+ } catch {
641
+ process.stderr.write("[tracegraph] Failed to parse package.json \u2014 skipping script injection\n");
642
+ return;
643
+ }
644
+ const scripts = pkg.scripts ?? {};
645
+ const added = [];
646
+ const newScripts = {
647
+ "trace:test": `tracegraph run -- ${pm} run ${testRunner}`,
648
+ "trace:baseline": "tracegraph baseline create",
649
+ "trace:compare": "tracegraph compare",
650
+ "trace:report": "tracegraph open --html .tracegraph/reports/latest.report.json"
651
+ };
652
+ for (const [key, value] of Object.entries(newScripts)) {
653
+ if (!scripts[key]) {
654
+ scripts[key] = value;
655
+ added.push(key);
656
+ } else {
657
+ process.stdout.write(` [skip] ${key} already exists in package.json
658
+ `);
659
+ }
660
+ }
661
+ pkg.scripts = scripts;
662
+ import_fs4.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
663
+ if (added.length > 0) {
664
+ process.stdout.write(` [ok] Added scripts to package.json: ${added.join(", ")}
665
+ `);
666
+ }
667
+ }
668
+ function createConfig(cwd, framework) {
669
+ const configPath = import_path6.default.join(cwd, "tracegraph.config.json");
670
+ if (import_fs4.default.existsSync(configPath)) {
671
+ process.stdout.write(" [skip] tracegraph.config.json already exists\n");
672
+ return;
673
+ }
674
+ const config = {
675
+ language: "typescript",
676
+ framework,
677
+ sanitize: {
678
+ redactKeys: [],
679
+ maxDepth: 4,
680
+ maxStringLength: 500,
681
+ maxArrayLength: 50
682
+ },
683
+ storage: {
684
+ maxRuns: 20,
685
+ maxAgeDays: 7,
686
+ maxSizeMB: 500,
687
+ keepFailedRuns: 50,
688
+ pruneOnRun: true
689
+ }
690
+ };
691
+ import_fs4.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
692
+ process.stdout.write(" [ok] Created tracegraph.config.json\n");
693
+ }
694
+ var GITIGNORE_ENTRIES = [
695
+ "# TraceGraph",
696
+ ".tracegraph/runs/",
697
+ ".tracegraph/traces/",
698
+ ".tracegraph/reports/",
699
+ ".tracegraph/bundles/",
700
+ ".tracegraph/index.json"
701
+ ];
702
+ function updateGitignore(cwd) {
703
+ const gitignorePath = import_path6.default.join(cwd, ".gitignore");
704
+ const existing = import_fs4.default.existsSync(gitignorePath) ? import_fs4.default.readFileSync(gitignorePath, "utf8") : "";
705
+ const toAdd = GITIGNORE_ENTRIES.filter(
706
+ (entry) => !entry.startsWith("#") && !existing.includes(entry)
707
+ );
708
+ if (toAdd.length === 0) {
709
+ process.stdout.write(" [skip] .gitignore already contains TraceGraph entries\n");
710
+ return;
711
+ }
712
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
713
+ const addition = separator + GITIGNORE_ENTRIES.join("\n") + "\n";
714
+ import_fs4.default.appendFileSync(gitignorePath, addition, "utf8");
715
+ process.stdout.write(" [ok] Updated .gitignore\n");
716
+ }
717
+
718
+ // src/commands/baseline.ts
719
+ var import_fs5 = __toESM(require("fs"));
720
+ var import_path7 = __toESM(require("path"));
721
+ var import_shared_types3 = require("@tracegraph/shared-types");
722
+ var import_graph_engine = require("@tracegraph/graph-engine");
723
+ function baselineCreateCommand(options) {
724
+ const cwd = process.cwd();
725
+ const tracegraphDir = import_path7.default.join(cwd, ".tracegraph");
726
+ const tracesDir = import_path7.default.join(tracegraphDir, "traces");
727
+ const baselinesDir = import_path7.default.join(tracegraphDir, "baselines");
728
+ if (!import_fs5.default.existsSync(tracesDir)) {
729
+ process.stderr.write(
730
+ "[tracegraph] No traces found. Run `tracegraph run -- <your-test-command>` first.\n"
731
+ );
732
+ return import_shared_types3.EXIT_CODES.CLI_ERROR;
733
+ }
734
+ const traceFiles = resolveTraceFiles(tracesDir, tracegraphDir, options);
735
+ if (traceFiles.length === 0) {
736
+ process.stderr.write("[tracegraph] No .trace.json files found for the selected scope.\n");
737
+ process.stderr.write(" Try: tracegraph baseline create --all-traces\n");
738
+ return import_shared_types3.EXIT_CODES.CLI_ERROR;
739
+ }
740
+ const approvedBy = options.approvedBy ?? process.env["USER"] ?? process.env["USERNAME"] ?? "system";
741
+ const reason = options.reason ?? "Baseline created by tracegraph CLI";
742
+ import_fs5.default.mkdirSync(baselinesDir, { recursive: true });
743
+ let created = 0;
744
+ let skipped = 0;
745
+ for (const traceFile of traceFiles) {
746
+ let session;
747
+ try {
748
+ session = JSON.parse(import_fs5.default.readFileSync(traceFile, "utf8"));
749
+ } catch {
750
+ process.stderr.write(`[tracegraph] Skipping unreadable trace: ${traceFile}
751
+ `);
752
+ skipped++;
753
+ continue;
754
+ }
755
+ if (session.schemaVersion !== import_shared_types3.SCHEMA_VERSIONS.trace) {
756
+ process.stderr.write(
757
+ `[tracegraph] Schema mismatch in ${import_path7.default.basename(traceFile)} \u2014 expected ${import_shared_types3.SCHEMA_VERSIONS.trace}, got ${session.schemaVersion}
758
+ Run: tracegraph baseline migrate
759
+ `
760
+ );
761
+ skipped++;
762
+ continue;
763
+ }
764
+ const baseline = (0, import_graph_engine.sessionToBaseline)(session, { approvedBy, reason });
765
+ const outFile = import_path7.default.join(baselinesDir, `${baseline.testId}.baseline.json`);
766
+ if (import_fs5.default.existsSync(outFile) && !options.all) {
767
+ const existing = JSON.parse(import_fs5.default.readFileSync(outFile, "utf8"));
768
+ process.stdout.write(
769
+ ` [skip] Baseline already exists for testId ${baseline.testId} (approved ${new Date(existing.approvedAt).toISOString().slice(0, 10)}). Use --all to overwrite.
770
+ `
771
+ );
772
+ skipped++;
773
+ continue;
774
+ }
775
+ import_fs5.default.writeFileSync(outFile, JSON.stringify(baseline, null, 2) + "\n", "utf8");
776
+ process.stdout.write(` [ok] Baseline created: ${import_path7.default.relative(cwd, outFile)}
777
+ `);
778
+ created++;
779
+ }
780
+ process.stdout.write(
781
+ `
782
+ [tracegraph] baseline create: ${created} created, ${skipped} skipped
783
+ `
784
+ );
785
+ return import_shared_types3.EXIT_CODES.SUCCESS;
786
+ }
787
+ function resolveTraceFiles(tracesDir, tracegraphDir, options) {
788
+ let files;
789
+ if (options.allTraces) {
790
+ files = allTraceFiles(tracesDir);
791
+ process.stdout.write(`[tracegraph] Scoping to ALL ${files.length} trace(s) in .tracegraph/traces/
792
+ `);
793
+ } else if (options.runId) {
794
+ const allFiles = allTraceFiles(tracesDir);
795
+ files = allFiles.filter((f) => {
796
+ try {
797
+ const session = JSON.parse(import_fs5.default.readFileSync(f, "utf8"));
798
+ return session.runId === options.runId;
799
+ } catch {
800
+ return false;
801
+ }
802
+ });
803
+ process.stdout.write(
804
+ `[tracegraph] Scoping to run ${options.runId}: ${files.length} trace(s) found.
805
+ `
806
+ );
807
+ } else {
808
+ const latestPath = import_path7.default.join(tracegraphDir, "latest.json");
809
+ if (import_fs5.default.existsSync(latestPath)) {
810
+ try {
811
+ const ptr = JSON.parse(import_fs5.default.readFileSync(latestPath, "utf8"));
812
+ const resolved = ptr.latestTraceIds.map((id) => import_path7.default.join(tracesDir, `${id}.trace.json`)).filter((p) => import_fs5.default.existsSync(p));
813
+ if (resolved.length > 0) {
814
+ process.stdout.write(
815
+ `[tracegraph] Scoping to latest run ${ptr.latestRunId} (${resolved.length} trace(s)). Use --all-traces to baseline everything.
816
+ `
817
+ );
818
+ files = resolved;
819
+ } else {
820
+ process.stderr.write(
821
+ "[tracegraph] latest.json found but no matching trace files \u2014 falling back to all traces.\n"
822
+ );
823
+ files = allTraceFiles(tracesDir);
824
+ }
825
+ } catch {
826
+ files = allTraceFiles(tracesDir);
827
+ }
828
+ } else {
829
+ process.stderr.write(
830
+ "[tracegraph] .tracegraph/latest.json not found. Baseline will include ALL traces in .tracegraph/traces/.\n Run `tracegraph run -- <command>` first to create a latest.json pointer.\n"
831
+ );
832
+ files = allTraceFiles(tracesDir);
833
+ }
834
+ }
835
+ if (options.onlyPassed) {
836
+ const before = files.length;
837
+ files = files.filter((f) => {
838
+ try {
839
+ const session = JSON.parse(import_fs5.default.readFileSync(f, "utf8"));
840
+ return session.status === "passed";
841
+ } catch {
842
+ return false;
843
+ }
844
+ });
845
+ const skipped = before - files.length;
846
+ if (skipped > 0) {
847
+ process.stdout.write(`[tracegraph] --only-passed: skipped ${skipped} non-passing trace(s).
848
+ `);
849
+ }
850
+ }
851
+ return files;
852
+ }
853
+ function allTraceFiles(tracesDir) {
854
+ return import_fs5.default.readdirSync(tracesDir).filter((f) => f.endsWith(".trace.json")).map((f) => import_path7.default.join(tracesDir, f));
855
+ }
856
+ function baselineListCommand() {
857
+ const cwd = process.cwd();
858
+ const baselinesDir = import_path7.default.join(cwd, ".tracegraph", "baselines");
859
+ if (!import_fs5.default.existsSync(baselinesDir)) {
860
+ process.stdout.write("[tracegraph] No baselines found. Run `tracegraph baseline create` first.\n");
861
+ return import_shared_types3.EXIT_CODES.SUCCESS;
862
+ }
863
+ const files = import_fs5.default.readdirSync(baselinesDir).filter((f) => f.endsWith(".baseline.json"));
864
+ if (files.length === 0) {
865
+ process.stdout.write("[tracegraph] No baselines found.\n");
866
+ return import_shared_types3.EXIT_CODES.SUCCESS;
867
+ }
868
+ process.stdout.write(
869
+ ["testId".padEnd(16), "events".padEnd(8), "captureLevel".padEnd(14), "approvedBy".padEnd(16), "approvedAt"].join(" | ") + "\n"
870
+ );
871
+ process.stdout.write("\u2500".repeat(72) + "\n");
872
+ for (const file of files) {
873
+ try {
874
+ const b = JSON.parse(
875
+ import_fs5.default.readFileSync(import_path7.default.join(baselinesDir, file), "utf8")
876
+ );
877
+ const row = [
878
+ b.testId.slice(0, 14).padEnd(16),
879
+ String(b.events.length).padEnd(8),
880
+ String(b.captureLevel).padEnd(14),
881
+ (b.approvedBy ?? "unknown").slice(0, 14).padEnd(16),
882
+ new Date(b.approvedAt).toISOString().slice(0, 10)
883
+ ];
884
+ process.stdout.write(row.join(" | ") + "\n");
885
+ } catch {
886
+ process.stderr.write(` [warn] Could not read ${file}
887
+ `);
888
+ }
889
+ }
890
+ return import_shared_types3.EXIT_CODES.SUCCESS;
891
+ }
892
+ function baselineApproveCommand(baselineIdOrTestId, options) {
893
+ const cwd = process.cwd();
894
+ const baselinesDir = import_path7.default.join(cwd, ".tracegraph", "baselines");
895
+ if (!import_fs5.default.existsSync(baselinesDir)) {
896
+ process.stderr.write("[tracegraph] No baselines directory found.\n");
897
+ return import_shared_types3.EXIT_CODES.CLI_ERROR;
898
+ }
899
+ const files = import_fs5.default.readdirSync(baselinesDir).filter((f) => f.endsWith(".baseline.json"));
900
+ let found = null;
901
+ for (const file of files) {
902
+ if (file.startsWith(baselineIdOrTestId)) {
903
+ found = import_path7.default.join(baselinesDir, file);
904
+ break;
905
+ }
906
+ try {
907
+ const b = JSON.parse(import_fs5.default.readFileSync(import_path7.default.join(baselinesDir, file), "utf8"));
908
+ if (b.baselineId === baselineIdOrTestId || b.testId === baselineIdOrTestId) {
909
+ found = import_path7.default.join(baselinesDir, file);
910
+ break;
911
+ }
912
+ } catch {
913
+ }
914
+ }
915
+ if (!found) {
916
+ process.stderr.write(`[tracegraph] Baseline not found: ${baselineIdOrTestId}
917
+ `);
918
+ return import_shared_types3.EXIT_CODES.CLI_ERROR;
919
+ }
920
+ const baseline = JSON.parse(import_fs5.default.readFileSync(found, "utf8"));
921
+ baseline.approvedBy = options.approvedBy ?? process.env["USER"] ?? process.env["USERNAME"] ?? "system";
922
+ baseline.reason = options.reason;
923
+ baseline.approvedAt = Date.now();
924
+ import_fs5.default.writeFileSync(found, JSON.stringify(baseline, null, 2) + "\n", "utf8");
925
+ process.stdout.write(`[tracegraph] Baseline approved: ${import_path7.default.relative(cwd, found)}
926
+ `);
927
+ return import_shared_types3.EXIT_CODES.SUCCESS;
928
+ }
929
+ function findBaselineForSession(baselinesDir, session) {
930
+ const testId = (0, import_graph_engine.deriveTestId)(session.entrypoint);
931
+ const file = import_path7.default.join(baselinesDir, `${testId}.baseline.json`);
932
+ if (!import_fs5.default.existsSync(file)) return null;
933
+ try {
934
+ return JSON.parse(import_fs5.default.readFileSync(file, "utf8"));
935
+ } catch {
936
+ return null;
937
+ }
938
+ }
939
+
940
+ // src/commands/compare.ts
941
+ var import_fs6 = __toESM(require("fs"));
942
+ var import_path8 = __toESM(require("path"));
943
+ var import_node_crypto = require("crypto");
944
+ var import_shared_types4 = require("@tracegraph/shared-types");
945
+ var import_graph_engine2 = require("@tracegraph/graph-engine");
946
+ function compareCommand(options) {
947
+ const cwd = process.cwd();
948
+ const tracegraphDir = import_path8.default.join(cwd, ".tracegraph");
949
+ const baselinesDir = options.baseline ? import_path8.default.resolve(cwd, options.baseline) : import_path8.default.join(tracegraphDir, "baselines");
950
+ const candidateFiles = options.bundle ? resolveBundleTraceFiles(options.bundle, tracegraphDir, cwd) : resolveCandidateFiles(options.candidate, tracegraphDir, cwd, options.latest);
951
+ if (candidateFiles.length === 0) {
952
+ process.stderr.write(
953
+ "[tracegraph] No candidate traces found. Run `tracegraph run -- <command>` first, or specify --candidate.\n"
954
+ );
955
+ return import_shared_types4.EXIT_CODES.CLI_ERROR;
956
+ }
957
+ const suppressions = loadSuppressions(tracegraphDir);
958
+ const approvals = loadApprovals(tracegraphDir);
959
+ const diffs = [];
960
+ const allEvaluated = [];
961
+ let tracesCompared = 0;
962
+ for (const candidateFile of candidateFiles) {
963
+ let session;
964
+ try {
965
+ session = JSON.parse(import_fs6.default.readFileSync(candidateFile, "utf8"));
966
+ } catch (err) {
967
+ process.stderr.write(`[tracegraph] Skipping unreadable trace: ${candidateFile}
968
+ `);
969
+ continue;
970
+ }
971
+ if (session.schemaVersion !== import_shared_types4.SCHEMA_VERSIONS.trace) {
972
+ process.stderr.write(
973
+ `[tracegraph] Schema mismatch: ${import_path8.default.basename(candidateFile)} \u2014 expected ${import_shared_types4.SCHEMA_VERSIONS.trace}, got ${session.schemaVersion}
974
+ `
975
+ );
976
+ continue;
977
+ }
978
+ const baseline = findBaselineForSession(baselinesDir, session);
979
+ if (!baseline) {
980
+ process.stderr.write(
981
+ `[tracegraph] No baseline found for trace ${session.traceId} \u2014 skipping comparison.
982
+ Run: tracegraph baseline create
983
+ `
984
+ );
985
+ continue;
986
+ }
987
+ tracesCompared++;
988
+ const diff = (0, import_graph_engine2.diffBaseline)(baseline, session);
989
+ diffs.push(diff);
990
+ const rawFindings = [
991
+ ...(0, import_graph_engine2.diffToFindings)(diff),
992
+ ...(0, import_graph_engine2.analyseTraceFindings)(session)
993
+ ];
994
+ const evaluated = (0, import_graph_engine2.evaluateFindings)(rawFindings, session, suppressions, approvals);
995
+ allEvaluated.push(...evaluated);
996
+ for (const f of evaluated.filter((e) => e.status === "open")) {
997
+ emit({
998
+ type: "finding",
999
+ runId: session.runId,
1000
+ payload: {
1001
+ fingerprint: f.fingerprint,
1002
+ ruleId: f.ruleId,
1003
+ severity: f.severity,
1004
+ title: f.title
1005
+ }
1006
+ });
1007
+ }
1008
+ }
1009
+ const suppressionsModified = checkSuppressionsFileModified(tracegraphDir);
1010
+ if (suppressionsModified) {
1011
+ const policyFinding = buildSuppressionsModifiedFinding();
1012
+ allEvaluated.push({ ...policyFinding, status: "open" });
1013
+ }
1014
+ const findingsBySeverity = {
1015
+ critical: 0,
1016
+ high: 0,
1017
+ medium: 0,
1018
+ low: 0,
1019
+ info: 0
1020
+ };
1021
+ for (const f of allEvaluated) {
1022
+ if (f.status === "open") {
1023
+ findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] ?? 0) + 1;
1024
+ }
1025
+ }
1026
+ const hasOpenCritical = (findingsBySeverity.critical ?? 0) > 0;
1027
+ const report = {
1028
+ schemaVersion: import_shared_types4.SCHEMA_VERSIONS.report,
1029
+ reportId: `report_${(0, import_node_crypto.createHash)("sha256").update(Date.now().toString()).digest("hex").slice(0, 12)}`,
1030
+ createdAt: Date.now(),
1031
+ baselineDir: import_path8.default.relative(cwd, baselinesDir),
1032
+ candidateFiles: candidateFiles.map((f) => import_path8.default.relative(cwd, f)),
1033
+ diffs,
1034
+ findings: allEvaluated,
1035
+ summary: {
1036
+ tracesCompared,
1037
+ findingsBySeverity,
1038
+ hasOpenCritical,
1039
+ suppressionsModified
1040
+ }
1041
+ };
1042
+ const outPath = resolveOutPath2(options.out, tracegraphDir, report.reportId, cwd);
1043
+ import_fs6.default.mkdirSync(import_path8.default.dirname(outPath), { recursive: true });
1044
+ import_fs6.default.writeFileSync(outPath, JSON.stringify(report, null, 2) + "\n", "utf8");
1045
+ process.stdout.write(`[tracegraph] Report: ${import_path8.default.relative(cwd, outPath)}
1046
+ `);
1047
+ const openCount = allEvaluated.filter((f) => f.status === "open").length;
1048
+ if (openCount > 0) {
1049
+ process.stdout.write(
1050
+ `[tracegraph] ${openCount} open finding(s): ` + SEVERITY_ORDER.filter((s) => (findingsBySeverity[s] ?? 0) > 0).map((s) => `${findingsBySeverity[s]} ${s}`).join(", ") + "\n"
1051
+ );
1052
+ } else {
1053
+ process.stdout.write("[tracegraph] No open findings.\n");
1054
+ }
1055
+ emit({
1056
+ type: "report.created",
1057
+ runId: `compare_${Date.now()}`,
1058
+ payload: {
1059
+ file: import_path8.default.relative(cwd, outPath),
1060
+ openFindings: openCount,
1061
+ hasOpenCritical
1062
+ }
1063
+ });
1064
+ updateLatestReport(tracegraphDir, report.reportId);
1065
+ if (suppressionsModified) return import_shared_types4.EXIT_CODES.POLICY_REVIEW;
1066
+ if (hasOpenCritical && options.failOnCritical) return import_shared_types4.EXIT_CODES.FINDINGS_THRESHOLD;
1067
+ return import_shared_types4.EXIT_CODES.SUCCESS;
1068
+ }
1069
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
1070
+ function updateLatestReport(tracegraphDir, reportId) {
1071
+ const latestPath = import_path8.default.join(tracegraphDir, "latest.json");
1072
+ try {
1073
+ let existing = {
1074
+ latestRunId: "",
1075
+ latestTraceIds: [],
1076
+ latestReportId: null,
1077
+ updatedAt: Date.now()
1078
+ };
1079
+ if (import_fs6.default.existsSync(latestPath)) {
1080
+ existing = JSON.parse(import_fs6.default.readFileSync(latestPath, "utf8"));
1081
+ }
1082
+ existing.latestReportId = reportId;
1083
+ existing.updatedAt = Date.now();
1084
+ import_fs6.default.writeFileSync(latestPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ function resolveBundleTraceFiles(bundleArg, tracegraphDir, cwd) {
1089
+ const abs = import_path8.default.resolve(cwd, bundleArg);
1090
+ if (!import_fs6.default.existsSync(abs)) {
1091
+ process.stderr.write(`[tracegraph] Bundle file not found: ${abs}
1092
+ `);
1093
+ return [];
1094
+ }
1095
+ let bundle;
1096
+ try {
1097
+ bundle = JSON.parse(import_fs6.default.readFileSync(abs, "utf8"));
1098
+ } catch (err) {
1099
+ process.stderr.write(`[tracegraph] Cannot parse bundle file: ${abs} \u2014 ${String(err)}
1100
+ `);
1101
+ return [];
1102
+ }
1103
+ if (!Array.isArray(bundle.traces)) {
1104
+ process.stderr.write(`[tracegraph] Bundle has no traces array: ${abs}
1105
+ `);
1106
+ return [];
1107
+ }
1108
+ const resolved = [];
1109
+ for (const entry of bundle.traces) {
1110
+ const tracePath = import_path8.default.join(tracegraphDir, entry.file);
1111
+ if (import_fs6.default.existsSync(tracePath)) {
1112
+ resolved.push(tracePath);
1113
+ } else {
1114
+ process.stderr.write(
1115
+ `[tracegraph] Bundle trace file not found, skipping: ${entry.file}
1116
+ `
1117
+ );
1118
+ }
1119
+ }
1120
+ if (resolved.length > 0) {
1121
+ process.stderr.write(
1122
+ `[tracegraph] Bundle "${bundle.scenarioId}" \u2014 ${resolved.length}/${bundle.traces.length} trace(s) resolved.
1123
+ `
1124
+ );
1125
+ }
1126
+ return resolved;
1127
+ }
1128
+ function resolveCandidateFiles(candidateArg, tracegraphDir, cwd, useLatest) {
1129
+ if (candidateArg) {
1130
+ const abs = import_path8.default.resolve(cwd, candidateArg);
1131
+ if (!import_fs6.default.existsSync(abs)) return [];
1132
+ if (import_fs6.default.statSync(abs).isDirectory()) {
1133
+ return import_fs6.default.readdirSync(abs).filter((f) => f.endsWith(".trace.json")).map((f) => import_path8.default.join(abs, f));
1134
+ }
1135
+ return [abs];
1136
+ }
1137
+ const tracesDir = import_path8.default.join(tracegraphDir, "traces");
1138
+ if (useLatest || !candidateArg) {
1139
+ const latestPath = import_path8.default.join(tracegraphDir, "latest.json");
1140
+ if (import_fs6.default.existsSync(latestPath)) {
1141
+ try {
1142
+ const ptr = JSON.parse(import_fs6.default.readFileSync(latestPath, "utf8"));
1143
+ const resolved = ptr.latestTraceIds.map((id) => import_path8.default.join(tracesDir, `${id}.trace.json`)).filter((p) => import_fs6.default.existsSync(p));
1144
+ if (resolved.length > 0) {
1145
+ process.stderr.write(
1146
+ `[tracegraph] Using latest run ${ptr.latestRunId} (${resolved.length} trace(s)).
1147
+ Pass --candidate <dir> to compare a different set.
1148
+ `
1149
+ );
1150
+ return resolved;
1151
+ }
1152
+ } catch {
1153
+ }
1154
+ }
1155
+ }
1156
+ if (!import_fs6.default.existsSync(tracesDir)) return [];
1157
+ return import_fs6.default.readdirSync(tracesDir).filter((f) => f.endsWith(".trace.json")).map((f) => import_path8.default.join(tracesDir, f));
1158
+ }
1159
+ function loadSuppressions(tracegraphDir) {
1160
+ const suppressionFile = import_path8.default.join(tracegraphDir, "suppressions", "tracegraph.suppressions.json");
1161
+ if (!import_fs6.default.existsSync(suppressionFile)) return [];
1162
+ try {
1163
+ const data = JSON.parse(import_fs6.default.readFileSync(suppressionFile, "utf8"));
1164
+ return data.suppressions ?? [];
1165
+ } catch {
1166
+ return [];
1167
+ }
1168
+ }
1169
+ function loadApprovals(tracegraphDir) {
1170
+ const approvalFile = import_path8.default.join(tracegraphDir, "approvals", "findings.json");
1171
+ if (!import_fs6.default.existsSync(approvalFile)) return [];
1172
+ try {
1173
+ const data = JSON.parse(import_fs6.default.readFileSync(approvalFile, "utf8"));
1174
+ return data.approvals ?? [];
1175
+ } catch {
1176
+ return [];
1177
+ }
1178
+ }
1179
+ function checkSuppressionsFileModified(tracegraphDir) {
1180
+ const suppressionFile = import_path8.default.join(
1181
+ tracegraphDir,
1182
+ "suppressions",
1183
+ "tracegraph.suppressions.json"
1184
+ );
1185
+ if (!import_fs6.default.existsSync(suppressionFile)) return false;
1186
+ try {
1187
+ const { spawnSync: spawnSync2 } = require("child_process");
1188
+ const result = spawnSync2(
1189
+ "git",
1190
+ ["status", "--porcelain", suppressionFile],
1191
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
1192
+ );
1193
+ if (result.error || result.status !== 0) return false;
1194
+ return (result.stdout ?? "").trim().length > 0;
1195
+ } catch {
1196
+ return false;
1197
+ }
1198
+ }
1199
+ function resolveOutPath2(outArg, tracegraphDir, reportId, cwd) {
1200
+ if (outArg) return import_path8.default.resolve(cwd, outArg);
1201
+ return import_path8.default.join(tracegraphDir, "reports", `${reportId}.report.json`);
1202
+ }
1203
+ function buildSuppressionsModifiedFinding() {
1204
+ const ruleId = "policy.suppressions_modified";
1205
+ const fingerprint = (0, import_node_crypto.createHash)("sha256").update(`${ruleId}:tracegraph.suppressions.json`).digest("hex").slice(0, 16);
1206
+ return {
1207
+ id: `find_${fingerprint}`,
1208
+ fingerprint,
1209
+ ruleId,
1210
+ severity: "high",
1211
+ category: "tracegraph_policy_change",
1212
+ title: "Suppressions file modified in this change",
1213
+ description: "The file .tracegraph/suppressions/tracegraph.suppressions.json has uncommitted changes. Modifications to the suppressions file alter which findings are silenced, which is a policy-level change that requires explicit human review.",
1214
+ evidence: [{ traceId: "policy", eventIds: [] }],
1215
+ recommendation: "Commit and review the suppressions change separately, or revert it if unintentional. Suppressions should be version-controlled and reviewed like code."
1216
+ };
1217
+ }
1218
+
1219
+ // src/commands/finding.ts
1220
+ var import_fs7 = __toESM(require("fs"));
1221
+ var import_path9 = __toESM(require("path"));
1222
+ var import_shared_types5 = require("@tracegraph/shared-types");
1223
+ var import_node_crypto2 = require("crypto");
1224
+ var SEVERITY_EMOJI = {
1225
+ critical: "\u{1F534}",
1226
+ high: "\u{1F7E0}",
1227
+ medium: "\u{1F7E1}",
1228
+ low: "\u{1F535}",
1229
+ info: "\u26AA"
1230
+ };
1231
+ var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "info"];
1232
+ var STATUS_ICON = {
1233
+ open: "\u25CF",
1234
+ approved: "\u2713",
1235
+ suppressed: "\u25CB"
1236
+ };
1237
+ function findingListCommand(reportArg) {
1238
+ const cwd = process.cwd();
1239
+ const report = loadReport(reportArg, cwd);
1240
+ if (!report) {
1241
+ process.stderr.write(
1242
+ "[tracegraph] No report found. Run `tracegraph compare` first.\n"
1243
+ );
1244
+ return import_shared_types5.EXIT_CODES.CLI_ERROR;
1245
+ }
1246
+ const findings = report.findings;
1247
+ if (findings.length === 0) {
1248
+ process.stdout.write("[tracegraph] No findings.\n");
1249
+ return import_shared_types5.EXIT_CODES.SUCCESS;
1250
+ }
1251
+ process.stdout.write(`
1252
+ Findings \u2014 ${new Date(report.createdAt).toISOString().slice(0, 16)}
1253
+
1254
+ `);
1255
+ for (const sev of SEVERITY_ORDER2) {
1256
+ const group = findings.filter((f) => f.severity === sev);
1257
+ if (group.length === 0) continue;
1258
+ for (const f of group) {
1259
+ const icon = STATUS_ICON[f.status] ?? "?";
1260
+ process.stdout.write(
1261
+ ` ${SEVERITY_EMOJI[sev]} ${icon} ${f.title}
1262
+ rule: ${f.ruleId} | fingerprint: ${f.fingerprint} | status: ${f.status}
1263
+
1264
+ `
1265
+ );
1266
+ }
1267
+ }
1268
+ return import_shared_types5.EXIT_CODES.SUCCESS;
1269
+ }
1270
+ function findingApproveCommand(fingerprint, options) {
1271
+ const cwd = process.cwd();
1272
+ const tracegraphDir = import_path9.default.join(cwd, ".tracegraph");
1273
+ const report = loadReport(void 0, cwd);
1274
+ let ruleId = "unknown";
1275
+ if (report) {
1276
+ const f = report.findings.find((f2) => f2.fingerprint === fingerprint || f2.id === fingerprint);
1277
+ if (f) ruleId = f.ruleId;
1278
+ else {
1279
+ process.stderr.write(`[tracegraph] Finding not found in latest report: ${fingerprint}
1280
+ `);
1281
+ return import_shared_types5.EXIT_CODES.CLI_ERROR;
1282
+ }
1283
+ }
1284
+ const approval = {
1285
+ findingFingerprint: fingerprint,
1286
+ ruleId,
1287
+ semanticTarget: {},
1288
+ approvedBy: options.approvedBy ?? process.env["USER"] ?? process.env["USERNAME"] ?? "system",
1289
+ reason: options.reason,
1290
+ expiresAt: options.expiresAt ?? oneYearFromNow(),
1291
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1292
+ };
1293
+ const approvalDir = import_path9.default.join(tracegraphDir, "approvals");
1294
+ const approvalFile = import_path9.default.join(approvalDir, "findings.json");
1295
+ import_fs7.default.mkdirSync(approvalDir, { recursive: true });
1296
+ let existing = {
1297
+ schemaVersion: import_shared_types5.SCHEMA_VERSIONS.findingApproval,
1298
+ approvals: []
1299
+ };
1300
+ if (import_fs7.default.existsSync(approvalFile)) {
1301
+ try {
1302
+ existing = JSON.parse(import_fs7.default.readFileSync(approvalFile, "utf8"));
1303
+ } catch {
1304
+ }
1305
+ }
1306
+ existing.approvals = existing.approvals.filter(
1307
+ (a) => a.findingFingerprint !== fingerprint
1308
+ );
1309
+ existing.approvals.push(approval);
1310
+ import_fs7.default.writeFileSync(approvalFile, JSON.stringify(existing, null, 2) + "\n", "utf8");
1311
+ process.stdout.write(`[tracegraph] Finding approved: ${fingerprint}
1312
+ `);
1313
+ return import_shared_types5.EXIT_CODES.SUCCESS;
1314
+ }
1315
+ function findingSuppressCommand(fingerprint, options) {
1316
+ const cwd = process.cwd();
1317
+ const tracegraphDir = import_path9.default.join(cwd, ".tracegraph");
1318
+ const report = loadReport(void 0, cwd);
1319
+ let ruleId = "unknown";
1320
+ if (report) {
1321
+ const f = report.findings.find((f2) => f2.fingerprint === fingerprint || f2.id === fingerprint);
1322
+ if (f) ruleId = f.ruleId;
1323
+ else {
1324
+ process.stderr.write(`[tracegraph] Finding not found in latest report: ${fingerprint}
1325
+ `);
1326
+ return import_shared_types5.EXIT_CODES.CLI_ERROR;
1327
+ }
1328
+ }
1329
+ const requiresEvidence = options.requiresEvidence ? options.requiresEvidence.split(",").map((item) => {
1330
+ const [type, ...rest] = item.trim().split(":");
1331
+ return { type: type ?? "", name: rest.join(":") || "*" };
1332
+ }) : void 0;
1333
+ const suppression = {
1334
+ id: `suppress_${(0, import_node_crypto2.createHash)("sha256").update(fingerprint + Date.now()).digest("hex").slice(0, 12)}`,
1335
+ ruleId,
1336
+ semanticTarget: {},
1337
+ requiresEvidence,
1338
+ reason: options.reason,
1339
+ expiresAt: options.expiresAt ?? oneYearFromNow(),
1340
+ approvedBy: options.approvedBy ?? process.env["USER"] ?? process.env["USERNAME"] ?? "system",
1341
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1342
+ };
1343
+ const suppressDir = import_path9.default.join(tracegraphDir, "suppressions");
1344
+ const suppressFile = import_path9.default.join(suppressDir, "tracegraph.suppressions.json");
1345
+ import_fs7.default.mkdirSync(suppressDir, { recursive: true });
1346
+ let existing = {
1347
+ schemaVersion: import_shared_types5.SCHEMA_VERSIONS.suppression,
1348
+ suppressions: []
1349
+ };
1350
+ if (import_fs7.default.existsSync(suppressFile)) {
1351
+ try {
1352
+ existing = JSON.parse(import_fs7.default.readFileSync(suppressFile, "utf8"));
1353
+ } catch {
1354
+ }
1355
+ }
1356
+ existing.suppressions.push(suppression);
1357
+ import_fs7.default.writeFileSync(suppressFile, JSON.stringify(existing, null, 2) + "\n", "utf8");
1358
+ process.stdout.write(`[tracegraph] Suppression added for finding ${fingerprint}
1359
+ `);
1360
+ if (requiresEvidence) {
1361
+ process.stdout.write(
1362
+ ` requiresEvidence: ${requiresEvidence.map((e) => `${e.type}:${e.name}`).join(", ")}
1363
+ `
1364
+ );
1365
+ }
1366
+ return import_shared_types5.EXIT_CODES.SUCCESS;
1367
+ }
1368
+ function loadReport(reportArg, cwd) {
1369
+ const tracegraphDir = import_path9.default.join(cwd, ".tracegraph");
1370
+ let reportFile;
1371
+ if (reportArg) {
1372
+ reportFile = import_path9.default.resolve(cwd, reportArg);
1373
+ } else {
1374
+ const reportsDir = import_path9.default.join(tracegraphDir, "reports");
1375
+ if (!import_fs7.default.existsSync(reportsDir)) return null;
1376
+ const files = import_fs7.default.readdirSync(reportsDir).filter((f) => f.endsWith(".report.json")).map((f) => ({
1377
+ name: f,
1378
+ mtime: import_fs7.default.statSync(import_path9.default.join(reportsDir, f)).mtimeMs
1379
+ })).sort((a, b) => b.mtime - a.mtime);
1380
+ if (files.length === 0) return null;
1381
+ reportFile = import_path9.default.join(reportsDir, files[0].name);
1382
+ }
1383
+ if (!import_fs7.default.existsSync(reportFile)) return null;
1384
+ try {
1385
+ return JSON.parse(import_fs7.default.readFileSync(reportFile, "utf8"));
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ function oneYearFromNow() {
1391
+ const d = /* @__PURE__ */ new Date();
1392
+ d.setFullYear(d.getFullYear() + 1);
1393
+ return d.toISOString();
1394
+ }
1395
+
1396
+ // src/commands/explain.ts
1397
+ var import_fs8 = __toESM(require("fs"));
1398
+ var import_path10 = __toESM(require("path"));
1399
+ var import_shared_types6 = require("@tracegraph/shared-types");
1400
+ var SEVERITY_EMOJI2 = {
1401
+ critical: "\u{1F534}",
1402
+ high: "\u{1F7E0}",
1403
+ medium: "\u{1F7E1}",
1404
+ low: "\u{1F535}",
1405
+ info: "\u26AA"
1406
+ };
1407
+ var SEVERITY_LABEL = {
1408
+ critical: "CRITICAL",
1409
+ high: "HIGH",
1410
+ medium: "MEDIUM",
1411
+ low: "LOW",
1412
+ info: "INFO"
1413
+ };
1414
+ var STATUS_LABEL = {
1415
+ open: "Open",
1416
+ approved: "Approved",
1417
+ suppressed: "Suppressed"
1418
+ };
1419
+ function findingExplainCommand(fingerprint, options) {
1420
+ const cwd = process.cwd();
1421
+ const report = loadReport2(options.report, cwd);
1422
+ if (!report) {
1423
+ process.stderr.write(
1424
+ "[tracegraph] No report found. Run `tracegraph compare` first.\n"
1425
+ );
1426
+ return import_shared_types6.EXIT_CODES.CLI_ERROR;
1427
+ }
1428
+ const finding = report.findings.find(
1429
+ (f) => f.fingerprint === fingerprint || f.id === fingerprint || f.fingerprint.startsWith(fingerprint)
1430
+ );
1431
+ if (!finding) {
1432
+ process.stderr.write(
1433
+ `[tracegraph] Finding not found in report: ${fingerprint}
1434
+ Run \`tracegraph finding list\` to see all fingerprints.
1435
+ `
1436
+ );
1437
+ return import_shared_types6.EXIT_CODES.CLI_ERROR;
1438
+ }
1439
+ if (options.json) {
1440
+ process.stdout.write(JSON.stringify(finding, null, 2) + "\n");
1441
+ return import_shared_types6.EXIT_CODES.SUCCESS;
1442
+ }
1443
+ process.stdout.write(renderFindingExplanation(finding));
1444
+ return import_shared_types6.EXIT_CODES.SUCCESS;
1445
+ }
1446
+ function renderFindingExplanation(f) {
1447
+ const sev = SEVERITY_LABEL[f.severity] ?? f.severity.toUpperCase();
1448
+ const emoji = SEVERITY_EMOJI2[f.severity] ?? "";
1449
+ const status = STATUS_LABEL[f.status] ?? f.status;
1450
+ const divider = "\u2500".repeat(64);
1451
+ const lines = [];
1452
+ lines.push("");
1453
+ lines.push(`${emoji} ${f.title}`);
1454
+ lines.push(divider);
1455
+ lines.push(` Rule: ${f.ruleId}`);
1456
+ lines.push(` Fingerprint: ${f.fingerprint}`);
1457
+ lines.push(` Severity: ${sev}`);
1458
+ lines.push(` Category: ${f.category}`);
1459
+ lines.push(` Status: ${status}`);
1460
+ if (f.status === "approved" && f.approvedBy) {
1461
+ lines.push(` Approved by: ${f.approvedBy}`);
1462
+ if (f.approvedReason) lines.push(` Reason: ${f.approvedReason}`);
1463
+ }
1464
+ if (f.status === "suppressed" && f.suppressedBy) {
1465
+ lines.push(` Suppressed by: ${f.suppressedBy}`);
1466
+ }
1467
+ lines.push("");
1468
+ lines.push("Description:");
1469
+ for (const line of wrapText(f.description, 72)) {
1470
+ lines.push(` ${line}`);
1471
+ }
1472
+ if (f.recommendation) {
1473
+ lines.push("");
1474
+ lines.push("Recommendation:");
1475
+ for (const line of wrapText(f.recommendation, 72)) {
1476
+ lines.push(` ${line}`);
1477
+ }
1478
+ }
1479
+ if (f.evidence.length > 0) {
1480
+ lines.push("");
1481
+ lines.push("Evidence:");
1482
+ for (const ev of f.evidence) {
1483
+ lines.push(` Trace: ${ev.traceId}`);
1484
+ if (ev.eventIds.length > 0) {
1485
+ lines.push(` Events: ${ev.eventIds.join(", ")}`);
1486
+ } else {
1487
+ lines.push(" Events: (none recorded)");
1488
+ }
1489
+ if (ev.file) {
1490
+ const loc = ev.line ? `${ev.file}:${ev.line}` : ev.file;
1491
+ lines.push(` File: ${loc}`);
1492
+ }
1493
+ }
1494
+ }
1495
+ lines.push("");
1496
+ if (f.status === "open") {
1497
+ lines.push("Actions:");
1498
+ lines.push(` tracegraph finding approve ${f.fingerprint} --reason "..."`);
1499
+ lines.push(` tracegraph finding suppress ${f.fingerprint} --reason "..."`);
1500
+ lines.push("");
1501
+ }
1502
+ return lines.join("\n");
1503
+ }
1504
+ function loadReport2(reportArg, cwd) {
1505
+ const tracegraphDir = import_path10.default.join(cwd, ".tracegraph");
1506
+ let reportFile;
1507
+ if (reportArg) {
1508
+ reportFile = import_path10.default.resolve(cwd, reportArg);
1509
+ } else {
1510
+ const reportsDir = import_path10.default.join(tracegraphDir, "reports");
1511
+ if (!import_fs8.default.existsSync(reportsDir)) return null;
1512
+ const files = import_fs8.default.readdirSync(reportsDir).filter((f) => f.endsWith(".report.json")).map((f) => ({ name: f, mtime: import_fs8.default.statSync(import_path10.default.join(reportsDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
1513
+ if (files.length === 0) return null;
1514
+ reportFile = import_path10.default.join(reportsDir, files[0].name);
1515
+ }
1516
+ if (!import_fs8.default.existsSync(reportFile)) return null;
1517
+ try {
1518
+ return JSON.parse(import_fs8.default.readFileSync(reportFile, "utf8"));
1519
+ } catch {
1520
+ return null;
1521
+ }
1522
+ }
1523
+ function wrapText(text, maxWidth) {
1524
+ const words = text.split(/\s+/);
1525
+ const lines = [];
1526
+ let current = "";
1527
+ for (const word of words) {
1528
+ if (!word) continue;
1529
+ if (current.length === 0) {
1530
+ current = word;
1531
+ } else if (current.length + 1 + word.length <= maxWidth) {
1532
+ current += " " + word;
1533
+ } else {
1534
+ lines.push(current);
1535
+ current = word;
1536
+ }
1537
+ }
1538
+ if (current) lines.push(current);
1539
+ return lines.length > 0 ? lines : [""];
1540
+ }
1541
+
1542
+ // src/commands/report.ts
1543
+ var import_fs9 = __toESM(require("fs"));
1544
+ var import_path11 = __toESM(require("path"));
1545
+ var import_shared_types7 = require("@tracegraph/shared-types");
1546
+ var import_ci_reporter = require("@tracegraph/ci-reporter");
1547
+ function reportCommand(options) {
1548
+ const cwd = process.cwd();
1549
+ const tracegraphDir = import_path11.default.join(cwd, ".tracegraph");
1550
+ let reportFile;
1551
+ if (options.input) {
1552
+ reportFile = import_path11.default.resolve(cwd, options.input);
1553
+ } else {
1554
+ const reportsDir = import_path11.default.join(tracegraphDir, "reports");
1555
+ if (!import_fs9.default.existsSync(reportsDir)) {
1556
+ process.stderr.write(
1557
+ "[tracegraph] No reports found. Run `tracegraph compare` first.\n"
1558
+ );
1559
+ return import_shared_types7.EXIT_CODES.CLI_ERROR;
1560
+ }
1561
+ const files = import_fs9.default.readdirSync(reportsDir).filter((f) => f.endsWith(".report.json")).map((f) => ({
1562
+ name: f,
1563
+ mtime: import_fs9.default.statSync(import_path11.default.join(reportsDir, f)).mtimeMs
1564
+ })).sort((a, b) => b.mtime - a.mtime);
1565
+ if (files.length === 0) {
1566
+ process.stderr.write("[tracegraph] No .report.json files found.\n");
1567
+ return import_shared_types7.EXIT_CODES.CLI_ERROR;
1568
+ }
1569
+ reportFile = import_path11.default.join(reportsDir, files[0].name);
1570
+ }
1571
+ if (!import_fs9.default.existsSync(reportFile)) {
1572
+ process.stderr.write(`[tracegraph] Report file not found: ${reportFile}
1573
+ `);
1574
+ return import_shared_types7.EXIT_CODES.CLI_ERROR;
1575
+ }
1576
+ let report;
1577
+ try {
1578
+ report = JSON.parse(import_fs9.default.readFileSync(reportFile, "utf8"));
1579
+ } catch (err) {
1580
+ process.stderr.write(`[tracegraph] Failed to read report: ${String(err)}
1581
+ `);
1582
+ return import_shared_types7.EXIT_CODES.CLI_ERROR;
1583
+ }
1584
+ const format = options.format ?? "markdown";
1585
+ const rendered = (0, import_ci_reporter.renderReport)(report, {
1586
+ format,
1587
+ projectName: options.projectName
1588
+ });
1589
+ if (options.out) {
1590
+ const outPath = import_path11.default.resolve(cwd, options.out);
1591
+ import_fs9.default.mkdirSync(import_path11.default.dirname(outPath), { recursive: true });
1592
+ import_fs9.default.writeFileSync(outPath, rendered, "utf8");
1593
+ process.stdout.write(`[tracegraph] Report written: ${import_path11.default.relative(cwd, outPath)}
1594
+ `);
1595
+ if (format === "github-step-summary" && process.env["GITHUB_STEP_SUMMARY"]) {
1596
+ import_fs9.default.appendFileSync(process.env["GITHUB_STEP_SUMMARY"], rendered, "utf8");
1597
+ }
1598
+ } else {
1599
+ process.stdout.write(rendered);
1600
+ }
1601
+ return import_shared_types7.EXIT_CODES.SUCCESS;
1602
+ }
1603
+
1604
+ // src/commands/diagnose.ts
1605
+ var import_fs10 = __toESM(require("fs"));
1606
+ var import_path12 = __toESM(require("path"));
1607
+ var import_trace_core5 = require("@tracegraph/trace-core");
1608
+ var import_shared_types8 = require("@tracegraph/shared-types");
1609
+ function diagnoseCommand(options) {
1610
+ const workspaceRoot = process.cwd();
1611
+ const tracegraphDir = import_path12.default.join(workspaceRoot, ".tracegraph");
1612
+ const tracesDir = import_path12.default.join(tracegraphDir, "traces");
1613
+ let traceFile;
1614
+ if (options.trace) {
1615
+ if (import_fs10.default.existsSync(options.trace)) {
1616
+ traceFile = options.trace;
1617
+ } else {
1618
+ const candidate = import_path12.default.join(tracesDir, `${options.trace}.trace.json`);
1619
+ if (import_fs10.default.existsSync(candidate)) traceFile = candidate;
1620
+ }
1621
+ if (!traceFile) {
1622
+ process.stderr.write(`[tracegraph diagnose] Trace not found: ${options.trace}
1623
+ `);
1624
+ return import_shared_types8.EXIT_CODES.CLI_ERROR;
1625
+ }
1626
+ } else {
1627
+ if (!import_fs10.default.existsSync(tracegraphDir)) {
1628
+ noTracesMessage();
1629
+ return import_shared_types8.EXIT_CODES.SUCCESS;
1630
+ }
1631
+ const index = (0, import_trace_core5.readTraceIndex)(tracegraphDir);
1632
+ if (index.traces.length === 0) {
1633
+ noTracesMessage();
1634
+ return import_shared_types8.EXIT_CODES.SUCCESS;
1635
+ }
1636
+ const latest = [...index.traces].sort((a, b) => b.createdAt - a.createdAt)[0];
1637
+ traceFile = import_path12.default.resolve(workspaceRoot, latest.file);
1638
+ }
1639
+ let session;
1640
+ try {
1641
+ session = (0, import_trace_core5.readTrace)(traceFile);
1642
+ } catch (err) {
1643
+ process.stderr.write(`[tracegraph diagnose] Failed to read trace: ${String(err)}
1644
+ `);
1645
+ return import_shared_types8.EXIT_CODES.CLI_ERROR;
1646
+ }
1647
+ if (options.json) {
1648
+ process.stdout.write(JSON.stringify(buildReport(session), null, 2) + "\n");
1649
+ return import_shared_types8.EXIT_CODES.SUCCESS;
1650
+ }
1651
+ printReport(session);
1652
+ return import_shared_types8.EXIT_CODES.SUCCESS;
1653
+ }
1654
+ function buildReport(session) {
1655
+ const cl = session.captureLevel;
1656
+ const level = cl.overall;
1657
+ const eventTypes = new Set(session.events.map((e) => e.type));
1658
+ const captured = [];
1659
+ const notCaptured = [];
1660
+ if (eventTypes.has("http_request") || eventTypes.has("http_response")) {
1661
+ captured.push("HTTP requests and responses");
1662
+ } else {
1663
+ notCaptured.push("HTTP requests and responses (add traceExpress / TraceMiddleware)");
1664
+ }
1665
+ if (eventTypes.has("db_query")) {
1666
+ captured.push("Database queries (SQL, duration, table, operation)");
1667
+ } else {
1668
+ notCaptured.push("Database queries (enable DB::listen / query logging)");
1669
+ }
1670
+ if (eventTypes.has("auth_check") || eventTypes.has("authorization_check")) {
1671
+ captured.push("Authentication and authorisation checks");
1672
+ } else {
1673
+ notCaptured.push("Auth / Gate checks (add auth listener)");
1674
+ }
1675
+ if (eventTypes.has("external_http_call")) {
1676
+ captured.push("Outbound HTTP calls (observed via undici / fetch)");
1677
+ } else {
1678
+ notCaptured.push("Outbound HTTP calls (add @tracegraph/js fetch tracking)");
1679
+ }
1680
+ if (eventTypes.has("function_call") || eventTypes.has("method_call")) {
1681
+ captured.push("Internal function and method calls");
1682
+ } else {
1683
+ notCaptured.push("Internal function calls (use traceFunction() for critical logic)");
1684
+ }
1685
+ if (eventTypes.has("test_run")) {
1686
+ captured.push("Per-test lifecycle (test_file \u2192 test_suite \u2192 test_run events)");
1687
+ } else {
1688
+ notCaptured.push("Per-test isolation (add @tracegraph/vitest or @tracegraph/jest reporter)");
1689
+ }
1690
+ if (eventTypes.has("queue_event")) {
1691
+ captured.push("Queue job dispatch and lifecycle");
1692
+ } else {
1693
+ notCaptured.push("Queue events (enable queue lifecycle hooks)");
1694
+ }
1695
+ const recommendations = [];
1696
+ const isJsLang = session.language === "javascript";
1697
+ const isPhpLang = session.language === "php";
1698
+ if (level < 5 && !eventTypes.has("test_run")) {
1699
+ if (isJsLang) {
1700
+ const fw = session.framework ?? "vitest";
1701
+ const pkg = fw === "jest" ? "@tracegraph/jest" : "@tracegraph/vitest";
1702
+ const flag = fw === "jest" ? `--reporters=default --reporters=${pkg}` : `--reporter=default --reporter=${pkg}`;
1703
+ recommendations.push({
1704
+ rank: 1,
1705
+ title: `Add ${pkg} reporter \u2192 Level 5 (per-test traces + full structure)`,
1706
+ command: `npm install -D ${pkg}
1707
+ Then add to vitest.config.ts: reporters: ['verbose', new TraceGraphReporter()]
1708
+ Or run: tracegraph run -- npx ${fw} ${flag}`,
1709
+ level: 5
1710
+ });
1711
+ } else if (isPhpLang) {
1712
+ recommendations.push({
1713
+ rank: 1,
1714
+ title: "Add TraceGraphPHPUnitExtension \u2192 Level 1 (per-test lifecycle)",
1715
+ command: 'Add <extension class="Tracegraph\\PhpUnit\\TraceGraphPHPUnitExtension"/> to phpunit.xml',
1716
+ level: 1
1717
+ });
1718
+ }
1719
+ }
1720
+ if (level < 2 && isJsLang && !eventTypes.has("function_call")) {
1721
+ recommendations.push({
1722
+ rank: 2,
1723
+ title: "Use traceFunction() for critical business logic \u2192 Level 2",
1724
+ command: `import { traceFunction } from "@tracegraph/js"
1725
+ const traced = traceFunction("InvoiceService.create", createInvoice)`,
1726
+ level: 2
1727
+ });
1728
+ }
1729
+ if (!eventTypes.has("db_query") && isPhpLang) {
1730
+ recommendations.push({
1731
+ rank: 3,
1732
+ title: "Enable DB::listen in TraceServiceProvider for query tracing",
1733
+ command: `// TraceServiceProvider already includes DB::listen when TRACEGRAPH_ENABLED=1
1734
+ // Ensure the provider is registered in config/app.php`,
1735
+ level: 3
1736
+ });
1737
+ }
1738
+ if (!eventTypes.has("auth_check") && !eventTypes.has("authorization_check")) {
1739
+ if (isPhpLang) {
1740
+ recommendations.push({
1741
+ rank: 4,
1742
+ title: "Enable Gate::after hook for authorisation tracing",
1743
+ command: `// TraceServiceProvider registers Gate::after automatically when TRACEGRAPH_ENABLED=1`,
1744
+ level: 4
1745
+ });
1746
+ }
1747
+ }
1748
+ return {
1749
+ traceId: session.traceId,
1750
+ captureLevel: { overall: level, label: cl.label },
1751
+ language: session.language,
1752
+ ...session.framework ? { framework: session.framework } : {},
1753
+ captured,
1754
+ notCaptured,
1755
+ recommendations: recommendations.sort((a, b) => a.rank - b.rank)
1756
+ };
1757
+ }
1758
+ var LINE = "\u2500".repeat(56);
1759
+ function printReport(session) {
1760
+ const report = buildReport(session);
1761
+ const cl = report.captureLevel;
1762
+ const lines = [];
1763
+ lines.push("");
1764
+ lines.push("TraceGraph Capture Report");
1765
+ lines.push(LINE);
1766
+ lines.push(`Trace ID: ${report.traceId}`);
1767
+ lines.push(`Capture level: ${cl.overall} \u2014 ${cl.label}`);
1768
+ lines.push(`Language: ${report.language}`);
1769
+ if (report.framework) lines.push(`Framework: ${report.framework}`);
1770
+ lines.push("");
1771
+ if (report.captured.length > 0) {
1772
+ lines.push("Captured:");
1773
+ for (const item of report.captured) {
1774
+ lines.push(` \u2713 ${item}`);
1775
+ }
1776
+ }
1777
+ if (report.notCaptured.length > 0) {
1778
+ lines.push("Not captured:");
1779
+ for (const item of report.notCaptured) {
1780
+ lines.push(` \u2717 ${item}`);
1781
+ }
1782
+ }
1783
+ if (report.recommendations.length > 0) {
1784
+ lines.push("");
1785
+ lines.push("Recommendations:");
1786
+ let i = 1;
1787
+ for (const rec of report.recommendations) {
1788
+ lines.push(` ${i}. ${rec.title}`);
1789
+ for (const cmdLine of rec.command.split("\n")) {
1790
+ lines.push(` ${cmdLine}`);
1791
+ }
1792
+ i++;
1793
+ }
1794
+ } else {
1795
+ lines.push("");
1796
+ lines.push(" \u2713 Capture level is optimal \u2014 no recommendations.");
1797
+ }
1798
+ lines.push(LINE);
1799
+ lines.push("");
1800
+ process.stdout.write(lines.join("\n"));
1801
+ }
1802
+ function noTracesMessage() {
1803
+ process.stdout.write([
1804
+ "",
1805
+ "TraceGraph Capture Report",
1806
+ LINE,
1807
+ "No traces found in .tracegraph/traces/",
1808
+ "",
1809
+ "Run a command with tracing first:",
1810
+ " tracegraph run -- npm test",
1811
+ " tracegraph run -- npx vitest",
1812
+ LINE,
1813
+ ""
1814
+ ].join("\n"));
1815
+ }
1816
+
1817
+ // src/commands/import-xdebug.ts
1818
+ var import_fs11 = __toESM(require("fs"));
1819
+ var import_path13 = __toESM(require("path"));
1820
+ var import_readline = require("readline");
1821
+ var import_shared_types9 = require("@tracegraph/shared-types");
1822
+ var import_trace_core6 = require("@tracegraph/trace-core");
1823
+ var import_trace_xdebug = require("@tracegraph/trace-xdebug");
1824
+ async function importXdebugCommand(xtFile, options) {
1825
+ const maxEvents = options.maxEvents ?? 1e4;
1826
+ const cwd = process.cwd();
1827
+ const xtPath = import_path13.default.resolve(cwd, xtFile);
1828
+ if (!import_fs11.default.existsSync(xtPath)) {
1829
+ process.stderr.write(`[tracegraph] Error: Xdebug trace file not found: ${xtPath}
1830
+ `);
1831
+ return import_shared_types9.EXIT_CODES.CLI_ERROR;
1832
+ }
1833
+ process.stderr.write(`[tracegraph] Parsing Xdebug trace: ${xtPath}
1834
+ `);
1835
+ const rl = (0, import_readline.createInterface)({
1836
+ input: import_fs11.default.createReadStream(xtPath, "utf8"),
1837
+ crlfDelay: Infinity
1838
+ });
1839
+ const xdebugResult = await (0, import_trace_xdebug.parseXdebugStream)(rl);
1840
+ process.stderr.write(
1841
+ `[tracegraph] Parsed ${xdebugResult.entries.length} Xdebug entries.
1842
+ `
1843
+ );
1844
+ let semanticEvents = [];
1845
+ if (options.semantic) {
1846
+ const semanticPath = import_path13.default.resolve(cwd, options.semantic);
1847
+ if (!import_fs11.default.existsSync(semanticPath)) {
1848
+ process.stderr.write(`[tracegraph] Warning: semantic JSONL not found: ${semanticPath}
1849
+ `);
1850
+ } else {
1851
+ const raw = import_fs11.default.readFileSync(semanticPath, "utf8");
1852
+ for (const line of raw.split("\n")) {
1853
+ const trimmed = line.trim();
1854
+ if (!trimmed) continue;
1855
+ try {
1856
+ const parsed = JSON.parse(trimmed);
1857
+ semanticEvents.push(parsed);
1858
+ } catch {
1859
+ }
1860
+ }
1861
+ process.stderr.write(
1862
+ `[tracegraph] Loaded ${semanticEvents.length} semantic events from JSONL.
1863
+ `
1864
+ );
1865
+ }
1866
+ }
1867
+ let traceStartMs = Date.now();
1868
+ if (xdebugResult.traceStart) {
1869
+ const parsed = Date.parse(xdebugResult.traceStart);
1870
+ if (!isNaN(parsed)) traceStartMs = parsed;
1871
+ } else {
1872
+ traceStartMs = import_fs11.default.statSync(xtPath).mtimeMs;
1873
+ }
1874
+ const traceId = (0, import_trace_core6.createTraceId)();
1875
+ let allEvents;
1876
+ if (semanticEvents.length > 0) {
1877
+ const merged = (0, import_trace_xdebug.mergeXdebugTrace)(semanticEvents, xdebugResult, traceStartMs);
1878
+ allEvents = (0, import_trace_xdebug.mergedTraceToEvents)(merged, traceId, traceStartMs);
1879
+ process.stderr.write(
1880
+ `[tracegraph] Correlation: ${merged.correlationStats.markerMatched} marker-matched, ${merged.correlationStats.timestampMatched} timestamp-matched, ${merged.correlationStats.unmatched} unmatched.
1881
+ `
1882
+ );
1883
+ } else {
1884
+ allEvents = buildEventsFromXdebug(xdebugResult.entries, traceId, traceStartMs);
1885
+ }
1886
+ if (options.include) {
1887
+ const includeFilter = options.include;
1888
+ const before = allEvents.length;
1889
+ allEvents = allEvents.filter((e) => {
1890
+ if (!e.file) return true;
1891
+ return e.file.includes(includeFilter.replace(/\*\*/g, "").replace(/\*/g, ""));
1892
+ });
1893
+ process.stderr.write(
1894
+ `[tracegraph] Include filter "${includeFilter}": ${allEvents.length}/${before} events kept.
1895
+ `
1896
+ );
1897
+ }
1898
+ if (allEvents.length > maxEvents) {
1899
+ process.stderr.write(
1900
+ `[tracegraph] Capping at ${maxEvents} events (${allEvents.length} total).
1901
+ `
1902
+ );
1903
+ allEvents = allEvents.slice(0, maxEvents);
1904
+ }
1905
+ const outDir = options.outDir ? import_path13.default.resolve(cwd, options.outDir) : import_path13.default.join(cwd, ".tracegraph", "traces");
1906
+ if (!import_fs11.default.existsSync(outDir)) {
1907
+ import_fs11.default.mkdirSync(outDir, { recursive: true });
1908
+ }
1909
+ const jsonlPath = import_path13.default.join(outDir, `${traceId}.events.jsonl`);
1910
+ const lines = allEvents.map((e) => JSON.stringify(e)).join("\n") + "\n";
1911
+ import_fs11.default.writeFileSync(jsonlPath, lines, "utf8");
1912
+ const runDir = import_path13.default.dirname(jsonlPath);
1913
+ try {
1914
+ import_fs11.default.writeFileSync(
1915
+ import_path13.default.join(runDir, "capture-level.json"),
1916
+ JSON.stringify({
1917
+ overall: 3,
1918
+ label: "Xdebug import (function call detail)",
1919
+ adapters: {
1920
+ xdebug: {
1921
+ level: 3,
1922
+ mode: "xdebug-import",
1923
+ captured: ["function_call", "return", "file", "line", "timing"]
1924
+ }
1925
+ }
1926
+ }, null, 2) + "\n",
1927
+ "utf8"
1928
+ );
1929
+ } catch {
1930
+ }
1931
+ process.stdout.write(
1932
+ `[tracegraph] Wrote ${allEvents.length} events to ${jsonlPath}
1933
+ `
1934
+ );
1935
+ process.stdout.write(`[tracegraph] Trace ID: ${traceId}
1936
+ `);
1937
+ return import_shared_types9.EXIT_CODES.SUCCESS;
1938
+ }
1939
+ function buildEventsFromXdebug(entries, traceId, traceStartMs) {
1940
+ const events = [];
1941
+ const stack = [];
1942
+ const rootId = (0, import_trace_core6.createEventId)();
1943
+ events.push({
1944
+ schemaVersion: import_shared_types9.SCHEMA_VERSIONS.event,
1945
+ eventId: rootId,
1946
+ traceId,
1947
+ parentEventId: null,
1948
+ type: "function_call",
1949
+ language: "php",
1950
+ framework: "xdebug",
1951
+ name: "xdebug import",
1952
+ startTime: traceStartMs
1953
+ });
1954
+ for (const entry of entries) {
1955
+ if (entry.kind === "marker") continue;
1956
+ while (stack.length > 0 && stack[stack.length - 1].depth >= entry.depth) {
1957
+ stack.pop();
1958
+ }
1959
+ const parentId = stack.length > 0 ? stack[stack.length - 1].eventId : rootId;
1960
+ const absoluteMs = Math.round(traceStartMs + entry.timeIndex * 1e3);
1961
+ if (entry.kind === "entry") {
1962
+ const eventId = (0, import_trace_core6.createEventId)();
1963
+ events.push({
1964
+ schemaVersion: import_shared_types9.SCHEMA_VERSIONS.event,
1965
+ eventId,
1966
+ traceId,
1967
+ parentEventId: parentId,
1968
+ type: "function_call",
1969
+ language: "php",
1970
+ framework: "xdebug",
1971
+ name: entry.fnName,
1972
+ functionName: entry.fnName,
1973
+ startTime: absoluteMs,
1974
+ file: entry.file,
1975
+ metadata: {
1976
+ xdebugDepth: entry.depth,
1977
+ memoryBytes: entry.memoryBytes
1978
+ }
1979
+ });
1980
+ stack.push({ eventId, depth: entry.depth });
1981
+ } else {
1982
+ events.push({
1983
+ schemaVersion: import_shared_types9.SCHEMA_VERSIONS.event,
1984
+ eventId: (0, import_trace_core6.createEventId)(),
1985
+ traceId,
1986
+ parentEventId: parentId,
1987
+ type: "return",
1988
+ language: "php",
1989
+ framework: "xdebug",
1990
+ name: `${entry.fnName} \u2192 return`,
1991
+ startTime: absoluteMs
1992
+ });
1993
+ }
1994
+ }
1995
+ return events;
1996
+ }
1997
+
1998
+ // src/commands/schema.ts
1999
+ var import_fs12 = __toESM(require("fs"));
2000
+ var import_path14 = __toESM(require("path"));
2001
+ var import_shared_types10 = require("@tracegraph/shared-types");
2002
+ function schemaDoctorCommand(options) {
2003
+ const cwd = process.cwd();
2004
+ const tracegraphDir = import_path14.default.join(cwd, ".tracegraph");
2005
+ if (!import_fs12.default.existsSync(tracegraphDir)) {
2006
+ process.stdout.write("[tracegraph] No .tracegraph/ directory found \u2014 nothing to check.\n");
2007
+ return import_shared_types10.EXIT_CODES.SUCCESS;
2008
+ }
2009
+ const reports = [
2010
+ ...scanDir(import_path14.default.join(tracegraphDir, "traces"), "trace", import_shared_types10.SCHEMA_VERSIONS.trace),
2011
+ ...scanDir(import_path14.default.join(tracegraphDir, "baselines"), "baseline", import_shared_types10.SCHEMA_VERSIONS.baseline),
2012
+ ...scanDir(import_path14.default.join(tracegraphDir, "reports"), "report", import_shared_types10.SCHEMA_VERSIONS.report)
2013
+ ];
2014
+ if (options.json) {
2015
+ process.stdout.write(JSON.stringify(reports, null, 2) + "\n");
2016
+ return reports.some((r) => r.status !== "ok") ? import_shared_types10.EXIT_CODES.SCHEMA_MIGRATION : import_shared_types10.EXIT_CODES.SUCCESS;
2017
+ }
2018
+ const ok = reports.filter((r) => r.status === "ok");
2019
+ const mismatched = reports.filter((r) => r.status === "mismatch");
2020
+ const unreadable = reports.filter((r) => r.status === "unreadable");
2021
+ const LINE2 = "\u2500".repeat(56);
2022
+ process.stdout.write("\nTraceGraph Schema Doctor\n");
2023
+ process.stdout.write(LINE2 + "\n");
2024
+ process.stdout.write(`Total artifacts scanned: ${reports.length}
2025
+ `);
2026
+ process.stdout.write(` \u2713 Current: ${ok.length}
2027
+ `);
2028
+ process.stdout.write(` \u2717 Mismatched: ${mismatched.length}
2029
+ `);
2030
+ if (unreadable.length > 0) {
2031
+ process.stdout.write(` \u26A0 Unreadable: ${unreadable.length}
2032
+ `);
2033
+ }
2034
+ if (mismatched.length === 0 && unreadable.length === 0) {
2035
+ process.stdout.write("\nAll artifacts are at the current schema version.\n");
2036
+ process.stdout.write(LINE2 + "\n\n");
2037
+ return import_shared_types10.EXIT_CODES.SUCCESS;
2038
+ }
2039
+ if (mismatched.length > 0) {
2040
+ process.stdout.write("\nMismatched artifacts:\n");
2041
+ for (const r of mismatched) {
2042
+ process.stdout.write(
2043
+ ` ${r.kind.padEnd(10)} ${import_path14.default.basename(r.file)}
2044
+ found: ${r.foundVersion ?? "(none)"}
2045
+ expected: ${r.expectedVersion}
2046
+ `
2047
+ );
2048
+ }
2049
+ process.stdout.write("\nRun the appropriate migration:\n");
2050
+ if (mismatched.some((r) => r.kind === "baseline")) {
2051
+ process.stdout.write(" tracegraph baseline migrate\n");
2052
+ }
2053
+ if (mismatched.some((r) => r.kind === "trace" || r.kind === "report")) {
2054
+ process.stdout.write(" tracegraph clean --all-runs (traces and reports are regenerated by re-running)\n");
2055
+ }
2056
+ }
2057
+ if (unreadable.length > 0) {
2058
+ process.stdout.write("\nUnreadable files (corrupt or empty):\n");
2059
+ for (const r of unreadable) {
2060
+ process.stdout.write(` ${r.kind.padEnd(10)} ${import_path14.default.basename(r.file)}
2061
+ `);
2062
+ }
2063
+ }
2064
+ process.stdout.write(LINE2 + "\n\n");
2065
+ return import_shared_types10.EXIT_CODES.SCHEMA_MIGRATION;
2066
+ }
2067
+ function baselineMigrateCommand(options) {
2068
+ const cwd = process.cwd();
2069
+ const baselinesDir = import_path14.default.join(cwd, ".tracegraph", "baselines");
2070
+ if (!import_fs12.default.existsSync(baselinesDir)) {
2071
+ process.stdout.write("[tracegraph] No baselines directory found \u2014 nothing to migrate.\n");
2072
+ return import_shared_types10.EXIT_CODES.SUCCESS;
2073
+ }
2074
+ const files = import_fs12.default.readdirSync(baselinesDir).filter((f) => f.endsWith(".baseline.json")).map((f) => import_path14.default.join(baselinesDir, f));
2075
+ if (files.length === 0) {
2076
+ process.stdout.write("[tracegraph] No baseline files found.\n");
2077
+ return import_shared_types10.EXIT_CODES.SUCCESS;
2078
+ }
2079
+ let migrated = 0;
2080
+ let skipped = 0;
2081
+ let failed = 0;
2082
+ for (const file of files) {
2083
+ let raw;
2084
+ try {
2085
+ raw = JSON.parse(import_fs12.default.readFileSync(file, "utf8"));
2086
+ } catch {
2087
+ process.stderr.write(` [error] Cannot read ${import_path14.default.basename(file)} \u2014 skipping.
2088
+ `);
2089
+ failed++;
2090
+ continue;
2091
+ }
2092
+ if (raw["schemaVersion"] === import_shared_types10.SCHEMA_VERSIONS.baseline) {
2093
+ skipped++;
2094
+ continue;
2095
+ }
2096
+ const oldVersion = raw["schemaVersion"];
2097
+ if (typeof raw["baselineId"] === "string" && typeof raw["testId"] === "string" && Array.isArray(raw["events"])) {
2098
+ raw["schemaVersion"] = import_shared_types10.SCHEMA_VERSIONS.baseline;
2099
+ if (options.dryRun) {
2100
+ process.stdout.write(
2101
+ ` [dry-run] Would migrate ${import_path14.default.basename(file)}: ${oldVersion ?? "(none)"} \u2192 ${import_shared_types10.SCHEMA_VERSIONS.baseline}
2102
+ `
2103
+ );
2104
+ } else {
2105
+ import_fs12.default.writeFileSync(file, JSON.stringify(raw, null, 2) + "\n", "utf8");
2106
+ process.stdout.write(
2107
+ ` [ok] Migrated ${import_path14.default.basename(file)}: ${oldVersion ?? "(none)"} \u2192 ${import_shared_types10.SCHEMA_VERSIONS.baseline}
2108
+ `
2109
+ );
2110
+ }
2111
+ migrated++;
2112
+ } else {
2113
+ process.stderr.write(
2114
+ ` [warn] ${import_path14.default.basename(file)} schema is incompatible \u2014 cannot auto-migrate.
2115
+ Regenerate with: tracegraph baseline create --all-traces
2116
+ `
2117
+ );
2118
+ failed++;
2119
+ }
2120
+ }
2121
+ process.stdout.write(
2122
+ `
2123
+ [tracegraph] baseline migrate: ${migrated} migrated, ${skipped} already current, ${failed} failed.
2124
+ `
2125
+ );
2126
+ return failed > 0 ? import_shared_types10.EXIT_CODES.SCHEMA_MIGRATION : import_shared_types10.EXIT_CODES.SUCCESS;
2127
+ }
2128
+ function scanDir(dir, kind, expectedVersion) {
2129
+ if (!import_fs12.default.existsSync(dir)) return [];
2130
+ const ext = kind === "trace" ? ".trace.json" : kind === "baseline" ? ".baseline.json" : ".report.json";
2131
+ return import_fs12.default.readdirSync(dir).filter((f) => f.endsWith(ext)).map((f) => {
2132
+ const file = import_path14.default.join(dir, f);
2133
+ try {
2134
+ const data = JSON.parse(import_fs12.default.readFileSync(file, "utf8"));
2135
+ const foundVersion = data["schemaVersion"] ?? null;
2136
+ return {
2137
+ file,
2138
+ kind,
2139
+ foundVersion,
2140
+ expectedVersion,
2141
+ status: foundVersion === expectedVersion ? "ok" : "mismatch"
2142
+ };
2143
+ } catch {
2144
+ return { file, kind, foundVersion: null, expectedVersion, status: "unreadable" };
2145
+ }
2146
+ });
2147
+ }
2148
+
2149
+ // src/commands/scenario.ts
2150
+ var import_fs13 = __toESM(require("fs"));
2151
+ var import_path15 = __toESM(require("path"));
2152
+ var import_shared_types11 = require("@tracegraph/shared-types");
2153
+ var import_scenario_runner = require("@tracegraph/scenario-runner");
2154
+ async function scenarioRunCommand(scenarioFile, options = {}) {
2155
+ const cwd = options.workspaceRoot ?? process.cwd();
2156
+ const absScenario = import_path15.default.resolve(cwd, scenarioFile);
2157
+ process.stderr.write(
2158
+ `[tracegraph] Running scenario: ${import_path15.default.relative(cwd, absScenario)}
2159
+ `
2160
+ );
2161
+ try {
2162
+ const result = await (0, import_scenario_runner.runScenario)(absScenario, { workspaceRoot: cwd });
2163
+ for (const step of result.steps) {
2164
+ const icon = step.status === "passed" ? " \u2713" : step.status === "skipped" ? " \u2013" : " \u2717";
2165
+ const extra = step.error ? ` \u2014 ${step.error}` : step.statusCode ? ` [${step.statusCode}]` : "";
2166
+ process.stderr.write(`${icon} ${step.name}${extra}
2167
+ `);
2168
+ }
2169
+ const statusLabel = result.passed ? "\u2713 passed" : "\u2717 failed";
2170
+ process.stderr.write(
2171
+ `[tracegraph] Scenario "${result.scenarioId}" ${statusLabel} (${result.steps.length} step(s), ${result.durationMs}ms)
2172
+ `
2173
+ );
2174
+ if (result.bundleFile) {
2175
+ process.stderr.write(`[tracegraph] Bundle written: ${result.bundleFile}
2176
+ `);
2177
+ process.stdout.write(
2178
+ JSON.stringify({
2179
+ protocol: "tracegraph.cli.v1",
2180
+ type: "bundle.created",
2181
+ runId: result.runId,
2182
+ timestamp: Date.now(),
2183
+ payload: {
2184
+ file: result.bundleFile,
2185
+ scenarioId: result.scenarioId
2186
+ }
2187
+ }) + "\n"
2188
+ );
2189
+ }
2190
+ return result.passed ? import_shared_types11.EXIT_CODES.SUCCESS : import_shared_types11.EXIT_CODES.COMMAND_FAILURE;
2191
+ } catch (err) {
2192
+ process.stderr.write(`[tracegraph] Error: ${String(err)}
2193
+ `);
2194
+ return import_shared_types11.EXIT_CODES.CLI_ERROR;
2195
+ }
2196
+ }
2197
+ function scenarioValidateCommand(scenarioFile) {
2198
+ const cwd = process.cwd();
2199
+ const abs = import_path15.default.resolve(cwd, scenarioFile);
2200
+ try {
2201
+ const def = (0, import_scenario_runner.loadScenarioDefinition)(abs);
2202
+ const servers = def.servers ?? [];
2203
+ const lines = [
2204
+ `\u2713 ${def.name} (${def.scenarioId})`,
2205
+ ` Steps: ${def.steps.length}`,
2206
+ ` Servers: ${servers.length}`
2207
+ ];
2208
+ if (servers.length > 0) {
2209
+ for (const s of servers) {
2210
+ lines.push(` \u2022 ${s.name} port ${s.port} "${s.command}"`);
2211
+ }
2212
+ }
2213
+ if (def.tags?.length) {
2214
+ lines.push(` Tags: ${def.tags.join(", ")}`);
2215
+ }
2216
+ process.stdout.write(lines.join("\n") + "\n");
2217
+ return import_shared_types11.EXIT_CODES.SUCCESS;
2218
+ } catch (err) {
2219
+ process.stderr.write(`[tracegraph] Invalid scenario: ${String(err)}
2220
+ `);
2221
+ return import_shared_types11.EXIT_CODES.CLI_ERROR;
2222
+ }
2223
+ }
2224
+ function scenarioListCommand() {
2225
+ const cwd = process.cwd();
2226
+ const scenariosDir = import_path15.default.join(cwd, ".tracegraph", "scenarios");
2227
+ if (!import_fs13.default.existsSync(scenariosDir)) {
2228
+ process.stdout.write(
2229
+ "No scenarios directory found.\nCreate .tracegraph/scenarios/ and add *.scenario.json files.\n"
2230
+ );
2231
+ return import_shared_types11.EXIT_CODES.SUCCESS;
2232
+ }
2233
+ const files = import_fs13.default.readdirSync(scenariosDir).filter((f) => f.endsWith(".scenario.json")).sort();
2234
+ if (files.length === 0) {
2235
+ process.stdout.write(
2236
+ "No scenario files found in .tracegraph/scenarios/\n"
2237
+ );
2238
+ return import_shared_types11.EXIT_CODES.SUCCESS;
2239
+ }
2240
+ process.stdout.write(`Scenarios in .tracegraph/scenarios/ (${files.length}):
2241
+
2242
+ `);
2243
+ for (const file of files) {
2244
+ const abs = import_path15.default.join(scenariosDir, file);
2245
+ try {
2246
+ const def = JSON.parse(import_fs13.default.readFileSync(abs, "utf8"));
2247
+ const name = def.name ?? def.scenarioId ?? file;
2248
+ const nSteps = (def.steps ?? []).length;
2249
+ const nSrvs = (def.servers ?? []).length;
2250
+ process.stdout.write(
2251
+ ` ${file}
2252
+ ${name} \u2014 ${nSteps} step(s), ${nSrvs} server(s)
2253
+ `
2254
+ );
2255
+ } catch {
2256
+ process.stdout.write(` ${file} (unreadable)
2257
+ `);
2258
+ }
2259
+ }
2260
+ return import_shared_types11.EXIT_CODES.SUCCESS;
2261
+ }
2262
+
2263
+ // src/commands/coverage.ts
2264
+ var fs14 = __toESM(require("fs"));
2265
+ var path16 = __toESM(require("path"));
2266
+ var import_ai_coverage = require("@tracegraph/ai-coverage");
2267
+ var import_shared_types12 = require("@tracegraph/shared-types");
2268
+ function coverageCommand(options = {}) {
2269
+ const {
2270
+ base = "HEAD~1",
2271
+ head = "HEAD",
2272
+ traces,
2273
+ out,
2274
+ json: printJson = false,
2275
+ failUncovered = false
2276
+ } = options;
2277
+ const cwd = process.cwd();
2278
+ const tracesDir = traces ? path16.resolve(cwd, traces) : path16.join(cwd, ".tracegraph", "traces");
2279
+ let report;
2280
+ try {
2281
+ report = (0, import_ai_coverage.computeCoverage)({
2282
+ baseRef: base,
2283
+ headRef: head,
2284
+ tracesDir,
2285
+ cwd
2286
+ });
2287
+ } catch (err) {
2288
+ process.stderr.write(`[tracegraph coverage] Error: ${String(err)}
2289
+ `);
2290
+ return import_shared_types12.EXIT_CODES.CLI_ERROR;
2291
+ }
2292
+ const reportsDir = path16.join(cwd, ".tracegraph", "reports");
2293
+ const outFile = out ? path16.resolve(cwd, out) : path16.join(reportsDir, `${report.reportId}.coverage.json`);
2294
+ try {
2295
+ fs14.mkdirSync(path16.dirname(outFile), { recursive: true });
2296
+ fs14.writeFileSync(outFile, JSON.stringify(report, null, 2), "utf8");
2297
+ } catch (err) {
2298
+ process.stderr.write(`[tracegraph coverage] Failed to write report: ${String(err)}
2299
+ `);
2300
+ return import_shared_types12.EXIT_CODES.CLI_ERROR;
2301
+ }
2302
+ const { summary } = report;
2303
+ process.stderr.write("\n");
2304
+ process.stderr.write(` tracegraph coverage
2305
+ `);
2306
+ process.stderr.write(` diff: ${report.baseRef}..${report.headRef}
2307
+ `);
2308
+ process.stderr.write(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2309
+ `);
2310
+ process.stderr.write(` Changed functions: ${summary.changedFunctions}
2311
+ `);
2312
+ process.stderr.write(` Covered: ${summary.coveredCount}
2313
+ `);
2314
+ process.stderr.write(` Uncovered: ${summary.uncoveredCount}
2315
+ `);
2316
+ process.stderr.write(` Coverage: ${summary.coveragePercent}%
2317
+ `);
2318
+ if (report.uncovered.length > 0) {
2319
+ process.stderr.write("\n Uncovered changed functions:\n");
2320
+ for (const fn of report.uncovered) {
2321
+ process.stderr.write(` \u2717 ${formatFunction(fn)} (${fn.file}:${fn.startLine})
2322
+ `);
2323
+ }
2324
+ }
2325
+ if (report.covered.length > 0) {
2326
+ process.stderr.write("\n Covered changed functions:\n");
2327
+ for (const entry of report.covered) {
2328
+ process.stderr.write(
2329
+ ` \u2713 ${formatFunction(entry.changed)} \u2014 ${entry.coveredBy.length} trace(s)
2330
+ `
2331
+ );
2332
+ }
2333
+ }
2334
+ process.stderr.write(`
2335
+ Report: ${outFile}
2336
+
2337
+ `);
2338
+ if (printJson) {
2339
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
2340
+ }
2341
+ if (failUncovered && report.uncovered.length > 0) {
2342
+ return import_shared_types12.EXIT_CODES.COMMAND_FAILURE;
2343
+ }
2344
+ return import_shared_types12.EXIT_CODES.SUCCESS;
2345
+ }
2346
+ function formatFunction(fn) {
2347
+ if (fn.className && fn.methodName) {
2348
+ return `${fn.className}.${fn.methodName}()`;
2349
+ }
2350
+ return `${fn.functionName ?? "(unknown)"}()`;
2351
+ }
2352
+
2353
+ // src/commands/pack.ts
2354
+ var fs15 = __toESM(require("fs"));
2355
+ var path17 = __toESM(require("path"));
2356
+ var import_ai_coverage2 = require("@tracegraph/ai-coverage");
2357
+ var import_shared_types13 = require("@tracegraph/shared-types");
2358
+ var ALL_FORMATS = ["cursor", "claude-code", "copilot", "mcp"];
2359
+ var VALID_FORMATS = /* @__PURE__ */ new Set([...ALL_FORMATS, "all"]);
2360
+ function packCommand(options = {}) {
2361
+ const {
2362
+ format: formatArg = "all",
2363
+ report: reportArg,
2364
+ traces: tracesArg,
2365
+ outDir: outDirArg,
2366
+ project: projectName,
2367
+ maxChars = 4e4,
2368
+ dryRun = false
2369
+ } = options;
2370
+ const cwd = process.cwd();
2371
+ if (!VALID_FORMATS.has(formatArg)) {
2372
+ process.stderr.write(
2373
+ `[tracegraph pack] Unknown format "${formatArg}". Valid values: cursor, claude-code, copilot, mcp, all
2374
+ `
2375
+ );
2376
+ return import_shared_types13.EXIT_CODES.CLI_ERROR;
2377
+ }
2378
+ const formats = formatArg === "all" ? ALL_FORMATS : [formatArg];
2379
+ const reportFile = reportArg ? path17.resolve(cwd, reportArg) : findLatestReport(cwd);
2380
+ if (!reportFile) {
2381
+ process.stderr.write(
2382
+ "[tracegraph pack] No report file found. Run `tracegraph compare` first, or supply --report <file>.\n"
2383
+ );
2384
+ return import_shared_types13.EXIT_CODES.CLI_ERROR;
2385
+ }
2386
+ if (!fs15.existsSync(reportFile)) {
2387
+ process.stderr.write(`[tracegraph pack] Report not found: ${reportFile}
2388
+ `);
2389
+ return import_shared_types13.EXIT_CODES.CLI_ERROR;
2390
+ }
2391
+ const traceFiles = resolveTraceFiles2(tracesArg, cwd);
2392
+ const resolvedProjectName = projectName ?? resolveProjectName(cwd);
2393
+ let packs;
2394
+ try {
2395
+ packs = (0, import_ai_coverage2.buildPromptPacks)({
2396
+ formats,
2397
+ report: reportFile,
2398
+ traceFiles,
2399
+ maxContextChars: maxChars,
2400
+ projectName: resolvedProjectName
2401
+ });
2402
+ } catch (err) {
2403
+ process.stderr.write(`[tracegraph pack] Error building packs: ${String(err)}
2404
+ `);
2405
+ return import_shared_types13.EXIT_CODES.CLI_ERROR;
2406
+ }
2407
+ const outBase = outDirArg ? path17.resolve(cwd, outDirArg) : cwd;
2408
+ process.stderr.write("\n");
2409
+ if (dryRun) {
2410
+ process.stderr.write(" tracegraph pack (dry run)\n");
2411
+ } else {
2412
+ process.stderr.write(" tracegraph pack\n");
2413
+ }
2414
+ process.stderr.write(` report: ${reportFile}
2415
+ `);
2416
+ process.stderr.write(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2417
+ `);
2418
+ let errCount = 0;
2419
+ for (const pack of packs) {
2420
+ const destFile = path17.join(outBase, pack.fileName);
2421
+ const rel = path17.relative(cwd, destFile);
2422
+ if (dryRun) {
2423
+ process.stderr.write(` [dry-run] would write: ${rel} (${pack.format})
2424
+ `);
2425
+ continue;
2426
+ }
2427
+ try {
2428
+ fs15.mkdirSync(path17.dirname(destFile), { recursive: true });
2429
+ fs15.writeFileSync(destFile, pack.content, "utf8");
2430
+ process.stderr.write(` \u2713 wrote: ${rel}
2431
+ `);
2432
+ } catch (err) {
2433
+ process.stderr.write(` \u2717 failed: ${rel} \u2014 ${String(err)}
2434
+ `);
2435
+ errCount++;
2436
+ }
2437
+ }
2438
+ process.stderr.write("\n");
2439
+ return errCount > 0 ? import_shared_types13.EXIT_CODES.CLI_ERROR : import_shared_types13.EXIT_CODES.SUCCESS;
2440
+ }
2441
+ function findLatestReport(cwd) {
2442
+ const latestJsonPath = path17.join(cwd, ".tracegraph", "latest.json");
2443
+ if (fs15.existsSync(latestJsonPath)) {
2444
+ try {
2445
+ const latest = JSON.parse(fs15.readFileSync(latestJsonPath, "utf8"));
2446
+ if (latest.latestReportId) {
2447
+ const reportPath = path17.join(
2448
+ cwd,
2449
+ ".tracegraph",
2450
+ "reports",
2451
+ `${latest.latestReportId}.report.json`
2452
+ );
2453
+ if (fs15.existsSync(reportPath)) return reportPath;
2454
+ }
2455
+ } catch {
2456
+ }
2457
+ }
2458
+ const reportsDir = path17.join(cwd, ".tracegraph", "reports");
2459
+ if (!fs15.existsSync(reportsDir)) return null;
2460
+ try {
2461
+ const files = fs15.readdirSync(reportsDir).filter((f) => f.endsWith(".report.json")).map((f) => ({
2462
+ file: path17.join(reportsDir, f),
2463
+ mtime: fs15.statSync(path17.join(reportsDir, f)).mtimeMs
2464
+ })).sort((a, b) => b.mtime - a.mtime);
2465
+ return files[0]?.file ?? null;
2466
+ } catch {
2467
+ return null;
2468
+ }
2469
+ }
2470
+ function resolveTraceFiles2(tracesArg, cwd) {
2471
+ if (!tracesArg) {
2472
+ const tracesDir = path17.join(cwd, ".tracegraph", "traces");
2473
+ if (!fs15.existsSync(tracesDir)) return [];
2474
+ try {
2475
+ return fs15.readdirSync(tracesDir).filter((f) => f.endsWith(".trace.json")).map((f) => path17.join(tracesDir, f));
2476
+ } catch {
2477
+ return [];
2478
+ }
2479
+ }
2480
+ const resolved = path17.resolve(cwd, tracesArg);
2481
+ if (fs15.existsSync(resolved) && fs15.statSync(resolved).isDirectory()) {
2482
+ try {
2483
+ return fs15.readdirSync(resolved).filter((f) => f.endsWith(".trace.json")).map((f) => path17.join(resolved, f));
2484
+ } catch {
2485
+ return [];
2486
+ }
2487
+ }
2488
+ return fs15.existsSync(resolved) ? [resolved] : [];
2489
+ }
2490
+ function resolveProjectName(cwd) {
2491
+ const pkgPath = path17.join(cwd, "package.json");
2492
+ if (fs15.existsSync(pkgPath)) {
2493
+ try {
2494
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
2495
+ if (pkg.name) return pkg.name;
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ return path17.basename(cwd);
2500
+ }
2501
+
2502
+ // src/index.ts
2503
+ var rawArgv = process.argv.slice(2);
2504
+ var ddIdx = rawArgv.indexOf("--");
2505
+ var tgArgv = ddIdx === -1 ? rawArgv : rawArgv.slice(0, ddIdx);
2506
+ var wrappedArgs = ddIdx === -1 ? [] : rawArgv.slice(ddIdx + 1);
2507
+ var program = new import_commander.Command();
2508
+ program.name("tracegraph").description("Capture how code actually behaves during tests, scenarios, and local development runs, then produces trace files, behavior graphs, baselines, diffs, findings, and reports that help code reviewers with with runtime evidence of code changes.").version("0.0.1");
2509
+ program.command("run").description("Run a command with tracing enabled").option("--run-id <id>", "Override the generated run ID").option("--scenario-id <id>", "Tag this run with a scenario/PR correlation ID").action(async (options) => {
2510
+ const code = await runCommand(wrappedArgs, options);
2511
+ process.exit(code);
2512
+ });
2513
+ var baselineCmd = program.command("baseline").description("Manage behaviour baselines");
2514
+ baselineCmd.command("create").description("Create baselines from traces (defaults to latest run)").option("--reason <reason>", "Approval reason (non-interactive mode)").option("--approved-by <name>", "Approver name (defaults to current user)").option("--all", "Overwrite existing baselines").option("--latest-run", "Use traces from the most recent run (default)").option("--run-id <id>", "Use traces from a specific run ID").option("--all-traces", "Use ALL traces in .tracegraph/traces/ (overrides default scope)").option("--only-passed", 'Skip traces whose status is not "passed"').action((options) => {
2515
+ process.exit(baselineCreateCommand(options));
2516
+ });
2517
+ baselineCmd.command("list").description("List all stored baselines").action(() => {
2518
+ process.exit(baselineListCommand());
2519
+ });
2520
+ baselineCmd.command("migrate").description("Migrate baseline files to the current schema version").option("--dry-run", "Show what would be migrated without writing changes").action((options) => {
2521
+ process.exit(baselineMigrateCommand(options));
2522
+ });
2523
+ baselineCmd.command("approve").description("Approve (re-approve) an existing baseline").argument("<baseline-id>", "Baseline ID or test ID").requiredOption("--reason <reason>", "Approval reason").option("--approved-by <name>", "Approver name").action((baselineId, options) => {
2524
+ process.exit(baselineApproveCommand(baselineId, options));
2525
+ });
2526
+ program.command("compare").description("Compare candidate traces against baselines and produce a report").option("--baseline <dir>", "Directory containing baseline files").option("--candidate <file>", "Candidate trace file or directory (default: latest run)").option("--bundle <file>", "TraceBundle JSON file \u2014 compare all traces in the bundle").option("--out <file>", "Output path for the report JSON").option("--latest", "Compare only traces from the most recent run (reads .tracegraph/latest.json)").option("--fail-on-critical", "Exit 3 if any critical findings are open").action((options) => {
2527
+ process.exit(compareCommand(options));
2528
+ });
2529
+ var findingCmd = program.command("finding").description("Manage findings");
2530
+ findingCmd.command("list").description("List all findings from the latest report").option("--report <file>", "Path to a specific report JSON").action((options) => {
2531
+ process.exit(findingListCommand(options.report));
2532
+ });
2533
+ findingCmd.command("approve").description("Approve a finding by fingerprint").argument("<fingerprint>", "Finding fingerprint (16-char hex)").requiredOption("--reason <reason>", "Approval reason").option("--approved-by <name>", "Approver name").option("--expires <ISO-date>", "Approval expiry date (default: 1 year)").action((fingerprint, options) => {
2534
+ process.exit(findingApproveCommand(fingerprint, options));
2535
+ });
2536
+ findingCmd.command("suppress").description("Add a suppression for a finding").argument("<fingerprint>", "Finding fingerprint (16-char hex)").requiredOption("--reason <reason>", "Suppression reason").option("--approved-by <name>", "Approver name").option("--expires <ISO-date>", "Expiry date").option("--requires-evidence <type:name>", "Evidence required for suppression to be active").action((fingerprint, options) => {
2537
+ process.exit(findingSuppressCommand(fingerprint, options));
2538
+ });
2539
+ findingCmd.command("explain").description("Show a detailed explanation of a finding by fingerprint").argument("<fingerprint>", "Finding fingerprint (or unique prefix)").option("--report <file>", "Path to a specific report JSON").option("--json", "Output raw JSON instead of human-readable text").action((fingerprint, options) => {
2540
+ process.exit(findingExplainCommand(fingerprint, options));
2541
+ });
2542
+ program.command("report").description("Render a trace report in the requested format").option("--format <format>", "Output format: markdown | json | github-step-summary", "markdown").option("--input <file>", "Path to a specific .report.json file").option("--out <file>", "Write rendered output to this file instead of stdout").option("--project-name <name>", "Project name for the report header").action((options) => {
2543
+ process.exit(reportCommand(options));
2544
+ });
2545
+ program.command("open").description("Open a trace or report in the browser").option("--html", "Produce a self-contained HTML file").option("--out <path>", "Output path for the HTML file").option("--no-open", "Write the HTML file but do not open a browser").argument("[file]", "Trace JSON file to open").action((file, options) => {
2546
+ if (!file) {
2547
+ process.stderr.write("Usage: tracegraph open --html <trace-file>\n");
2548
+ process.exit(import_shared_types14.EXIT_CODES.CLI_ERROR);
2549
+ }
2550
+ openCommand(file, { out: options.out, noOpen: options.open === false });
2551
+ process.exit(import_shared_types14.EXIT_CODES.SUCCESS);
2552
+ });
2553
+ program.command("init").description("Initialise TraceGraph in the current project").action(() => {
2554
+ initCommand();
2555
+ process.exit(import_shared_types14.EXIT_CODES.SUCCESS);
2556
+ });
2557
+ program.command("diagnose").description("Show what is being captured and how to improve the capture level").option("--trace <traceId>", "Diagnose a specific trace by ID or file path").option("--json", "Output as JSON instead of human-readable text").action((options) => {
2558
+ process.exit(diagnoseCommand(options));
2559
+ });
2560
+ var schemaCmd = program.command("schema").description("Inspect and migrate TraceGraph artifact schemas");
2561
+ schemaCmd.command("doctor").description("Scan local artifacts for schema version mismatches").option("--json", "Output report as JSON instead of human-readable text").action((options) => {
2562
+ process.exit(schemaDoctorCommand(options));
2563
+ });
2564
+ var importCmd = program.command("import").description("Import traces from external tools");
2565
+ importCmd.command("xdebug").description("Import an Xdebug .xt trace file, optionally merging with a Laravel semantic trace").argument("<file>", "Path to the Xdebug .xt trace file").option("--semantic <file>", "Path to a Laravel semantic JSONL trace to merge with").option("--include <pattern>", "Only include functions whose file path matches this pattern").option("--max-events <n>", "Maximum number of events to emit (default: 10000)", parseInt).option("--out-dir <dir>", "Output directory (default: .tracegraph/traces/)").action(async (file, options) => {
2566
+ process.exit(await importXdebugCommand(file, options));
2567
+ });
2568
+ var scenarioCmd = program.command("scenario").description("Run and manage TraceGraph scenarios");
2569
+ scenarioCmd.command("run").description("Execute a scenario definition file end-to-end").argument("<file>", "Path to the .scenario.json file").action(async (file) => {
2570
+ process.exit(await scenarioRunCommand(file));
2571
+ });
2572
+ scenarioCmd.command("validate").description("Validate a scenario file structure without running it").argument("<file>", "Path to the .scenario.json file").action((file) => {
2573
+ process.exit(scenarioValidateCommand(file));
2574
+ });
2575
+ scenarioCmd.command("list").description("List scenario files in .tracegraph/scenarios/").action(() => {
2576
+ process.exit(scenarioListCommand());
2577
+ });
2578
+ program.command("coverage").description("Map changed functions (git diff) to runtime trace coverage").option("--base <ref>", "Git ref to diff from (default: HEAD~1)").option("--head <ref>", "Git ref to diff to (default: HEAD)").option("--traces <dir>", "Directory containing .trace.json files").option("--out <file>", "Write coverage report JSON to this file").option("--json", "Also print the full report JSON to stdout").option("--fail-uncovered", "Exit 1 if any changed functions have no trace coverage").action((options) => {
2579
+ process.exit(coverageCommand(options));
2580
+ });
2581
+ program.command("pack").description("Generate AI context packs (Cursor / Claude Code / Copilot / MCP) from findings").option("--format <fmt>", "cursor | claude-code | copilot | mcp | all (default: all)").option("--report <file>", "Path to a .report.json file (default: latest report)").option("--traces <dir>", "Directory of .trace.json files to include as context").option("--out-dir <dir>", "Output directory for pack files (default: project root)").option("--project <name>", "Project name for pack headers").option("--max-chars <n>", "Max trace context characters per pack", parseInt).option("--dry-run", "Print what would be written without writing files").action((options) => {
2582
+ process.exit(packCommand(options));
2583
+ });
2584
+ program.command("clean").description("Remove local trace run artifacts").option("--older-than <age>", "Remove runs older than this (e.g. 7d, 12h)").option("--keep-last <n>", "Keep the N most recent runs", parseInt).option("--all-runs", "Remove all runs (baselines are never removed)").action((options) => {
2585
+ cleanCommand(options);
2586
+ process.exit(import_shared_types14.EXIT_CODES.SUCCESS);
2587
+ });
2588
+ var storageCmd = program.command("storage").description("Manage local trace storage");
2589
+ storageCmd.command("status").description("Show storage usage for the current project").action(() => {
2590
+ storageStatusCommand();
2591
+ process.exit(import_shared_types14.EXIT_CODES.SUCCESS);
2592
+ });
2593
+ program.parseAsync(["node", "tracegraph", ...tgArgv]).catch((err) => {
2594
+ process.stderr.write(`[tracegraph] Unexpected error: ${String(err)}
2595
+ `);
2596
+ process.exit(import_shared_types14.EXIT_CODES.CLI_ERROR);
2597
+ });