@rengler33/prov 0.1.1

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.
Files changed (146) hide show
  1. package/README.md +314 -0
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +381 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/agent.d.ts +107 -0
  7. package/dist/commands/agent.d.ts.map +1 -0
  8. package/dist/commands/agent.js +197 -0
  9. package/dist/commands/agent.js.map +1 -0
  10. package/dist/commands/agent.test.d.ts +5 -0
  11. package/dist/commands/agent.test.d.ts.map +1 -0
  12. package/dist/commands/agent.test.js +199 -0
  13. package/dist/commands/agent.test.js.map +1 -0
  14. package/dist/commands/constraint.d.ts +100 -0
  15. package/dist/commands/constraint.d.ts.map +1 -0
  16. package/dist/commands/constraint.js +763 -0
  17. package/dist/commands/constraint.js.map +1 -0
  18. package/dist/commands/constraint.test.d.ts +9 -0
  19. package/dist/commands/constraint.test.d.ts.map +1 -0
  20. package/dist/commands/constraint.test.js +470 -0
  21. package/dist/commands/constraint.test.js.map +1 -0
  22. package/dist/commands/graph.d.ts +99 -0
  23. package/dist/commands/graph.d.ts.map +1 -0
  24. package/dist/commands/graph.js +552 -0
  25. package/dist/commands/graph.js.map +1 -0
  26. package/dist/commands/graph.test.d.ts +2 -0
  27. package/dist/commands/graph.test.d.ts.map +1 -0
  28. package/dist/commands/graph.test.js +258 -0
  29. package/dist/commands/graph.test.js.map +1 -0
  30. package/dist/commands/impact.d.ts +83 -0
  31. package/dist/commands/impact.d.ts.map +1 -0
  32. package/dist/commands/impact.js +319 -0
  33. package/dist/commands/impact.js.map +1 -0
  34. package/dist/commands/impact.test.d.ts +2 -0
  35. package/dist/commands/impact.test.d.ts.map +1 -0
  36. package/dist/commands/impact.test.js +234 -0
  37. package/dist/commands/impact.test.js.map +1 -0
  38. package/dist/commands/init.d.ts +45 -0
  39. package/dist/commands/init.d.ts.map +1 -0
  40. package/dist/commands/init.js +94 -0
  41. package/dist/commands/init.js.map +1 -0
  42. package/dist/commands/init.test.d.ts +7 -0
  43. package/dist/commands/init.test.d.ts.map +1 -0
  44. package/dist/commands/init.test.js +174 -0
  45. package/dist/commands/init.test.js.map +1 -0
  46. package/dist/commands/integration.test.d.ts +10 -0
  47. package/dist/commands/integration.test.d.ts.map +1 -0
  48. package/dist/commands/integration.test.js +456 -0
  49. package/dist/commands/integration.test.js.map +1 -0
  50. package/dist/commands/mcp.d.ts +21 -0
  51. package/dist/commands/mcp.d.ts.map +1 -0
  52. package/dist/commands/mcp.js +616 -0
  53. package/dist/commands/mcp.js.map +1 -0
  54. package/dist/commands/mcp.test.d.ts +7 -0
  55. package/dist/commands/mcp.test.d.ts.map +1 -0
  56. package/dist/commands/mcp.test.js +132 -0
  57. package/dist/commands/mcp.test.js.map +1 -0
  58. package/dist/commands/plan.d.ts +218 -0
  59. package/dist/commands/plan.d.ts.map +1 -0
  60. package/dist/commands/plan.js +1307 -0
  61. package/dist/commands/plan.js.map +1 -0
  62. package/dist/commands/plan.test.d.ts +9 -0
  63. package/dist/commands/plan.test.d.ts.map +1 -0
  64. package/dist/commands/plan.test.js +569 -0
  65. package/dist/commands/plan.test.js.map +1 -0
  66. package/dist/commands/spec.d.ts +94 -0
  67. package/dist/commands/spec.d.ts.map +1 -0
  68. package/dist/commands/spec.js +635 -0
  69. package/dist/commands/spec.js.map +1 -0
  70. package/dist/commands/spec.test.d.ts +9 -0
  71. package/dist/commands/spec.test.d.ts.map +1 -0
  72. package/dist/commands/spec.test.js +407 -0
  73. package/dist/commands/spec.test.js.map +1 -0
  74. package/dist/commands/trace.d.ts +157 -0
  75. package/dist/commands/trace.d.ts.map +1 -0
  76. package/dist/commands/trace.js +847 -0
  77. package/dist/commands/trace.js.map +1 -0
  78. package/dist/commands/trace.test.d.ts +9 -0
  79. package/dist/commands/trace.test.d.ts.map +1 -0
  80. package/dist/commands/trace.test.js +524 -0
  81. package/dist/commands/trace.test.js.map +1 -0
  82. package/dist/graph.d.ts +204 -0
  83. package/dist/graph.d.ts.map +1 -0
  84. package/dist/graph.js +496 -0
  85. package/dist/graph.js.map +1 -0
  86. package/dist/graph.test.d.ts +2 -0
  87. package/dist/graph.test.d.ts.map +1 -0
  88. package/dist/graph.test.js +382 -0
  89. package/dist/graph.test.js.map +1 -0
  90. package/dist/hash.d.ts +72 -0
  91. package/dist/hash.d.ts.map +1 -0
  92. package/dist/hash.js +137 -0
  93. package/dist/hash.js.map +1 -0
  94. package/dist/hash.test.d.ts +2 -0
  95. package/dist/hash.test.d.ts.map +1 -0
  96. package/dist/hash.test.js +227 -0
  97. package/dist/hash.test.js.map +1 -0
  98. package/dist/index.d.ts +18 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +64 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/index.test.d.ts +2 -0
  103. package/dist/index.test.d.ts.map +1 -0
  104. package/dist/index.test.js +11 -0
  105. package/dist/index.test.js.map +1 -0
  106. package/dist/output.d.ts +84 -0
  107. package/dist/output.d.ts.map +1 -0
  108. package/dist/output.js +175 -0
  109. package/dist/output.js.map +1 -0
  110. package/dist/output.test.d.ts +7 -0
  111. package/dist/output.test.d.ts.map +1 -0
  112. package/dist/output.test.js +146 -0
  113. package/dist/output.test.js.map +1 -0
  114. package/dist/staleness.d.ts +162 -0
  115. package/dist/staleness.d.ts.map +1 -0
  116. package/dist/staleness.js +309 -0
  117. package/dist/staleness.js.map +1 -0
  118. package/dist/staleness.test.d.ts +2 -0
  119. package/dist/staleness.test.d.ts.map +1 -0
  120. package/dist/staleness.test.js +448 -0
  121. package/dist/staleness.test.js.map +1 -0
  122. package/dist/storage.d.ts +267 -0
  123. package/dist/storage.d.ts.map +1 -0
  124. package/dist/storage.js +623 -0
  125. package/dist/storage.js.map +1 -0
  126. package/dist/storage.test.d.ts +5 -0
  127. package/dist/storage.test.d.ts.map +1 -0
  128. package/dist/storage.test.js +434 -0
  129. package/dist/storage.test.js.map +1 -0
  130. package/dist/types.d.ts +270 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +12 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/types.test.d.ts +2 -0
  135. package/dist/types.test.d.ts.map +1 -0
  136. package/dist/types.test.js +232 -0
  137. package/dist/types.test.js.map +1 -0
  138. package/dist/watcher.d.ts +139 -0
  139. package/dist/watcher.d.ts.map +1 -0
  140. package/dist/watcher.js +406 -0
  141. package/dist/watcher.js.map +1 -0
  142. package/dist/watcher.test.d.ts +5 -0
  143. package/dist/watcher.test.d.ts.map +1 -0
  144. package/dist/watcher.test.js +327 -0
  145. package/dist/watcher.test.js.map +1 -0
  146. package/package.json +53 -0
@@ -0,0 +1,847 @@
1
+ /**
2
+ * prov trace commands implementation.
3
+ *
4
+ * Commands for managing artifact traces:
5
+ * - trace add: Link an artifact to a plan step
6
+ * - trace show: Show provenance for an artifact
7
+ * - trace why: Show implementations of a requirement
8
+ *
9
+ * @see req:cli:trace-add
10
+ * @see req:cli:trace-show
11
+ * @see req:cli:trace-why
12
+ */
13
+ import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
14
+ import { resolve, relative, join, extname } from 'node:path';
15
+ import { isInitialized, loadGraph, saveGraph } from '../storage.js';
16
+ import { addTraceToGraph } from '../graph.js';
17
+ import { sha256 } from '../hash.js';
18
+ import { output, error, success, resolveFormat } from '../output.js';
19
+ // ============================================================================
20
+ // Validation Helpers
21
+ // ============================================================================
22
+ /**
23
+ * Validate step ID format: step:{plan}:{number}
24
+ */
25
+ function isValidStepId(id) {
26
+ if (typeof id !== 'string')
27
+ return false;
28
+ return /^step:[a-z0-9-]+:\d+$/.test(id);
29
+ }
30
+ /**
31
+ * Validate requirement ID format.
32
+ */
33
+ function isValidRequirementId(id) {
34
+ if (typeof id !== 'string')
35
+ return false;
36
+ return /^req:[a-z0-9-]+(:[a-z0-9-]+)?$/.test(id);
37
+ }
38
+ /**
39
+ * Compute hash for a file's content.
40
+ */
41
+ function computeFileHash(filePath) {
42
+ const content = readFileSync(filePath, 'utf8');
43
+ const fullHash = sha256(content);
44
+ return `sha256:${fullHash.slice(0, 12)}`;
45
+ }
46
+ /**
47
+ * Generate a trace ID from artifact path and step.
48
+ */
49
+ function generateTraceId(artifactPath, stepId) {
50
+ const combined = `${artifactPath}:${stepId}`;
51
+ const fullHash = sha256(combined);
52
+ return `trace:${fullHash.slice(0, 12)}`;
53
+ }
54
+ // ============================================================================
55
+ // trace add Command
56
+ // ============================================================================
57
+ /**
58
+ * Execute the trace add command.
59
+ *
60
+ * Links an artifact (file) to a plan step, recording the current hash
61
+ * for staleness detection.
62
+ *
63
+ * @see req:cli:trace-add
64
+ */
65
+ export function runTraceAdd(globalOpts, artifactPath, options) {
66
+ const projectRoot = globalOpts.dir ?? process.cwd();
67
+ const fmt = resolveFormat({ format: globalOpts.format });
68
+ // Check if prov is initialized
69
+ if (!isInitialized(projectRoot)) {
70
+ const result = {
71
+ success: false,
72
+ error: 'prov is not initialized. Run "prov init" first.',
73
+ };
74
+ if (fmt === 'json') {
75
+ output(result, { format: 'json' });
76
+ }
77
+ else if (fmt === 'yaml') {
78
+ output(result, { format: 'yaml' });
79
+ }
80
+ else {
81
+ error(result.error);
82
+ }
83
+ process.exit(1);
84
+ }
85
+ // Validate step ID
86
+ const stepId = options.to;
87
+ if (!isValidStepId(stepId)) {
88
+ const result = {
89
+ success: false,
90
+ error: `Invalid step ID format: ${stepId} (expected step:{plan}:{number})`,
91
+ };
92
+ if (fmt === 'json') {
93
+ output(result, { format: 'json' });
94
+ }
95
+ else if (fmt === 'yaml') {
96
+ output(result, { format: 'yaml' });
97
+ }
98
+ else {
99
+ error(result.error);
100
+ }
101
+ process.exit(1);
102
+ }
103
+ // Resolve artifact path
104
+ const fullPath = resolve(projectRoot, artifactPath);
105
+ const relPath = relative(projectRoot, fullPath);
106
+ if (!existsSync(fullPath)) {
107
+ const result = {
108
+ success: false,
109
+ error: `Artifact not found: ${artifactPath}`,
110
+ };
111
+ if (fmt === 'json') {
112
+ output(result, { format: 'json' });
113
+ }
114
+ else if (fmt === 'yaml') {
115
+ output(result, { format: 'yaml' });
116
+ }
117
+ else {
118
+ error(result.error);
119
+ }
120
+ process.exit(1);
121
+ }
122
+ // Check if it's a file (not directory)
123
+ const stat = statSync(fullPath);
124
+ if (!stat.isFile()) {
125
+ const result = {
126
+ success: false,
127
+ error: `Path is not a file: ${artifactPath}`,
128
+ };
129
+ if (fmt === 'json') {
130
+ output(result, { format: 'json' });
131
+ }
132
+ else if (fmt === 'yaml') {
133
+ output(result, { format: 'yaml' });
134
+ }
135
+ else {
136
+ error(result.error);
137
+ }
138
+ process.exit(1);
139
+ }
140
+ // Load graph
141
+ const loadResult = loadGraph(projectRoot);
142
+ if (!loadResult.success || loadResult.data === undefined) {
143
+ const result = {
144
+ success: false,
145
+ error: loadResult.error ?? 'Failed to load graph',
146
+ };
147
+ if (fmt === 'json') {
148
+ output(result, { format: 'json' });
149
+ }
150
+ else if (fmt === 'yaml') {
151
+ output(result, { format: 'yaml' });
152
+ }
153
+ else {
154
+ error(result.error);
155
+ }
156
+ process.exit(1);
157
+ }
158
+ const graph = loadResult.data;
159
+ // Verify step exists in graph
160
+ const stepNode = graph.getNode(stepId);
161
+ if (stepNode === undefined || stepNode.type !== 'step') {
162
+ const result = {
163
+ success: false,
164
+ error: `Step not found: ${stepId}`,
165
+ };
166
+ if (fmt === 'json') {
167
+ output(result, { format: 'json' });
168
+ }
169
+ else if (fmt === 'yaml') {
170
+ output(result, { format: 'yaml' });
171
+ }
172
+ else {
173
+ error(result.error);
174
+ }
175
+ process.exit(1);
176
+ }
177
+ // Compute artifact hash
178
+ const artifactHash = computeFileHash(fullPath);
179
+ // Generate trace ID
180
+ const traceId = generateTraceId(relPath, stepId);
181
+ // Check if trace already exists
182
+ const existingNode = graph.getNode(traceId);
183
+ if (existingNode !== undefined) {
184
+ const existingTrace = existingNode.data;
185
+ if (existingTrace.artifactHash === artifactHash) {
186
+ // Same artifact, same hash - no update needed
187
+ const result = {
188
+ success: true,
189
+ traceId,
190
+ artifactPath: relPath,
191
+ stepId,
192
+ artifactHash,
193
+ };
194
+ if (fmt === 'json') {
195
+ output({ ...result, unchanged: true }, { format: 'json' });
196
+ }
197
+ else if (fmt === 'yaml') {
198
+ output({ ...result, unchanged: true }, { format: 'yaml' });
199
+ }
200
+ else {
201
+ success(`Trace ${traceId} is already tracked (unchanged)`);
202
+ }
203
+ return;
204
+ }
205
+ // Different hash - remove old trace and add new one
206
+ graph.removeNode(traceId);
207
+ }
208
+ // Create trace
209
+ const trace = {
210
+ id: traceId,
211
+ artifact: {
212
+ path: relPath,
213
+ },
214
+ step: stepId,
215
+ artifactHash,
216
+ createdAt: new Date().toISOString(),
217
+ };
218
+ // Add trace to graph
219
+ addTraceToGraph(graph, trace);
220
+ // Save graph
221
+ const saveResult = saveGraph(graph, projectRoot);
222
+ if (!saveResult.success) {
223
+ const result = {
224
+ success: false,
225
+ error: saveResult.error ?? 'Failed to save graph',
226
+ };
227
+ if (fmt === 'json') {
228
+ output(result, { format: 'json' });
229
+ }
230
+ else if (fmt === 'yaml') {
231
+ output(result, { format: 'yaml' });
232
+ }
233
+ else {
234
+ error(result.error);
235
+ }
236
+ process.exit(1);
237
+ }
238
+ const result = {
239
+ success: true,
240
+ traceId,
241
+ artifactPath: relPath,
242
+ stepId,
243
+ artifactHash,
244
+ };
245
+ if (fmt === 'json') {
246
+ output(result, { format: 'json' });
247
+ }
248
+ else if (fmt === 'yaml') {
249
+ output(result, { format: 'yaml' });
250
+ }
251
+ else {
252
+ success(`Added trace ${traceId} linking ${relPath} to ${stepId}`);
253
+ }
254
+ }
255
+ // ============================================================================
256
+ // trace show Command
257
+ // ============================================================================
258
+ /**
259
+ * Execute the trace show command.
260
+ *
261
+ * Shows the provenance chain for an artifact: artifact -> step -> decision/requirement -> spec.
262
+ *
263
+ * @see req:cli:trace-show
264
+ */
265
+ export function runTraceShow(globalOpts, artifactPath, _options) {
266
+ const projectRoot = globalOpts.dir ?? process.cwd();
267
+ const fmt = resolveFormat({ format: globalOpts.format });
268
+ // Check if prov is initialized
269
+ if (!isInitialized(projectRoot)) {
270
+ const result = {
271
+ success: false,
272
+ error: 'prov is not initialized. Run "prov init" first.',
273
+ };
274
+ if (fmt === 'json') {
275
+ output(result, { format: 'json' });
276
+ }
277
+ else if (fmt === 'yaml') {
278
+ output(result, { format: 'yaml' });
279
+ }
280
+ else {
281
+ error(result.error);
282
+ }
283
+ process.exit(1);
284
+ }
285
+ // Resolve artifact path
286
+ const fullPath = resolve(projectRoot, artifactPath);
287
+ const relPath = relative(projectRoot, fullPath);
288
+ // Load graph
289
+ const loadResult = loadGraph(projectRoot);
290
+ if (!loadResult.success || loadResult.data === undefined) {
291
+ const result = {
292
+ success: false,
293
+ error: loadResult.error ?? 'Failed to load graph',
294
+ };
295
+ if (fmt === 'json') {
296
+ output(result, { format: 'json' });
297
+ }
298
+ else if (fmt === 'yaml') {
299
+ output(result, { format: 'yaml' });
300
+ }
301
+ else {
302
+ error(result.error);
303
+ }
304
+ process.exit(1);
305
+ }
306
+ const graph = loadResult.data;
307
+ // Find trace for this artifact path
308
+ const artifactNodes = graph.getNodesByType('artifact');
309
+ let foundTrace;
310
+ let foundTraceId;
311
+ for (const node of artifactNodes) {
312
+ const trace = node.data;
313
+ if (trace.artifact.path === relPath) {
314
+ foundTrace = trace;
315
+ foundTraceId = trace.id;
316
+ break;
317
+ }
318
+ }
319
+ if (foundTrace === undefined || foundTraceId === undefined) {
320
+ const result = {
321
+ success: false,
322
+ error: `No trace found for artifact: ${artifactPath}`,
323
+ };
324
+ if (fmt === 'json') {
325
+ output(result, { format: 'json' });
326
+ }
327
+ else if (fmt === 'yaml') {
328
+ output(result, { format: 'yaml' });
329
+ }
330
+ else {
331
+ error(result.error);
332
+ }
333
+ process.exit(1);
334
+ }
335
+ // Check if artifact is stale
336
+ let isStale = false;
337
+ let currentHash;
338
+ if (existsSync(fullPath)) {
339
+ currentHash = computeFileHash(fullPath);
340
+ isStale = currentHash !== foundTrace.artifactHash;
341
+ }
342
+ else {
343
+ isStale = true; // File deleted = stale
344
+ }
345
+ // Build provenance chain
346
+ const provenance = [];
347
+ // Start with artifact
348
+ provenance.push({
349
+ type: 'artifact',
350
+ id: relPath,
351
+ description: `Hash: ${currentHash ?? 'missing'}${isStale ? ' (stale)' : ''}`,
352
+ });
353
+ // Find step
354
+ const stepNode = graph.getNode(foundTrace.step);
355
+ if (stepNode !== undefined && stepNode.type === 'step') {
356
+ const step = stepNode.data;
357
+ provenance.push({
358
+ type: 'step',
359
+ id: step.id,
360
+ description: step.action,
361
+ });
362
+ // Find what the step traces to (decisions and requirements)
363
+ for (const targetId of step.tracesTo) {
364
+ const targetNode = graph.getNode(targetId);
365
+ if (targetNode !== undefined) {
366
+ if (targetNode.type === 'decision') {
367
+ provenance.push({
368
+ type: 'decision',
369
+ id: targetId,
370
+ description: targetNode.data.choice,
371
+ });
372
+ }
373
+ else if (targetNode.type === 'requirement') {
374
+ const req = targetNode.data;
375
+ provenance.push({
376
+ type: 'requirement',
377
+ id: targetId,
378
+ description: req.description,
379
+ });
380
+ // Find parent spec
381
+ const incomingEdges = graph.getIncomingEdges(targetId, 'contains');
382
+ for (const edge of incomingEdges) {
383
+ const specNode = graph.getNode(edge.from);
384
+ if (specNode !== undefined && specNode.type === 'spec') {
385
+ const spec = specNode.data;
386
+ provenance.push({
387
+ type: 'spec',
388
+ id: spec.id,
389
+ description: spec.title,
390
+ });
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ const result = {
398
+ success: true,
399
+ artifact: {
400
+ path: relPath,
401
+ hash: currentHash ?? foundTrace.artifactHash,
402
+ stale: isStale,
403
+ },
404
+ trace: {
405
+ id: foundTraceId,
406
+ stepId: foundTrace.step,
407
+ createdAt: foundTrace.createdAt,
408
+ },
409
+ provenance,
410
+ };
411
+ if (fmt === 'json') {
412
+ output(result, { format: 'json' });
413
+ }
414
+ else if (fmt === 'yaml') {
415
+ output(result, { format: 'yaml' });
416
+ }
417
+ else {
418
+ // Display provenance chain
419
+ process.stdout.write(`\nProvenance for ${relPath}\n`);
420
+ process.stdout.write(`${'─'.repeat(40)}\n`);
421
+ if (isStale) {
422
+ process.stdout.write(`⚠ STALE: Artifact has changed since trace was created\n\n`);
423
+ }
424
+ for (let i = 0; i < provenance.length; i++) {
425
+ const entry = provenance[i];
426
+ const indent = ' '.repeat(i);
427
+ const prefix = i === 0 ? '📄' : i === 1 ? '📋' : entry.type === 'spec' ? '📜' : '→';
428
+ process.stdout.write(`${indent}${prefix} [${entry.type}] ${entry.id}\n`);
429
+ if (entry.description !== undefined) {
430
+ process.stdout.write(`${indent} ${entry.description}\n`);
431
+ }
432
+ }
433
+ process.stdout.write('\n');
434
+ }
435
+ }
436
+ // ============================================================================
437
+ // trace why Command
438
+ // ============================================================================
439
+ /**
440
+ * Execute the trace why command.
441
+ *
442
+ * Shows all artifacts that implement a requirement, traversing:
443
+ * requirement <- step <- artifact
444
+ *
445
+ * @see req:cli:trace-why
446
+ */
447
+ export function runTraceWhy(globalOpts, reqIdStr, _options) {
448
+ const projectRoot = globalOpts.dir ?? process.cwd();
449
+ const fmt = resolveFormat({ format: globalOpts.format });
450
+ // Check if prov is initialized
451
+ if (!isInitialized(projectRoot)) {
452
+ const result = {
453
+ success: false,
454
+ error: 'prov is not initialized. Run "prov init" first.',
455
+ };
456
+ if (fmt === 'json') {
457
+ output(result, { format: 'json' });
458
+ }
459
+ else if (fmt === 'yaml') {
460
+ output(result, { format: 'yaml' });
461
+ }
462
+ else {
463
+ error(result.error);
464
+ }
465
+ process.exit(1);
466
+ }
467
+ // Validate requirement ID
468
+ if (!isValidRequirementId(reqIdStr)) {
469
+ const result = {
470
+ success: false,
471
+ error: `Invalid requirement ID format: ${reqIdStr}`,
472
+ };
473
+ if (fmt === 'json') {
474
+ output(result, { format: 'json' });
475
+ }
476
+ else if (fmt === 'yaml') {
477
+ output(result, { format: 'yaml' });
478
+ }
479
+ else {
480
+ error(result.error);
481
+ }
482
+ process.exit(1);
483
+ }
484
+ const reqId = reqIdStr;
485
+ // Load graph
486
+ const loadResult = loadGraph(projectRoot);
487
+ if (!loadResult.success || loadResult.data === undefined) {
488
+ const result = {
489
+ success: false,
490
+ error: loadResult.error ?? 'Failed to load graph',
491
+ };
492
+ if (fmt === 'json') {
493
+ output(result, { format: 'json' });
494
+ }
495
+ else if (fmt === 'yaml') {
496
+ output(result, { format: 'yaml' });
497
+ }
498
+ else {
499
+ error(result.error);
500
+ }
501
+ process.exit(1);
502
+ }
503
+ const graph = loadResult.data;
504
+ // Find the requirement
505
+ const reqNode = graph.getNode(reqId);
506
+ if (reqNode === undefined || reqNode.type !== 'requirement') {
507
+ const result = {
508
+ success: false,
509
+ error: `Requirement not found: ${reqIdStr}`,
510
+ };
511
+ if (fmt === 'json') {
512
+ output(result, { format: 'json' });
513
+ }
514
+ else if (fmt === 'yaml') {
515
+ output(result, { format: 'yaml' });
516
+ }
517
+ else {
518
+ error(result.error);
519
+ }
520
+ process.exit(1);
521
+ }
522
+ const requirement = reqNode.data;
523
+ // Find all steps that trace to this requirement
524
+ const implementations = [];
525
+ const stepNodes = graph.getNodesByType('step');
526
+ for (const stepNode of stepNodes) {
527
+ const step = stepNode.data;
528
+ // Check if step traces to this requirement
529
+ if (step.tracesTo.includes(reqId)) {
530
+ // Find artifacts that implement this step
531
+ const outgoingEdges = graph.getOutgoingEdges(step.id, 'implements');
532
+ for (const edge of outgoingEdges) {
533
+ const artifactNode = graph.getNode(edge.to);
534
+ if (artifactNode !== undefined && artifactNode.type === 'artifact') {
535
+ const trace = artifactNode.data;
536
+ // Check if artifact is stale
537
+ let isStale = false;
538
+ const fullPath = resolve(projectRoot, trace.artifact.path);
539
+ if (existsSync(fullPath)) {
540
+ const currentHash = computeFileHash(fullPath);
541
+ isStale = currentHash !== trace.artifactHash;
542
+ }
543
+ else {
544
+ isStale = true;
545
+ }
546
+ implementations.push({
547
+ artifactPath: trace.artifact.path,
548
+ stepId: step.id,
549
+ stepAction: step.action,
550
+ traceId: trace.id,
551
+ stale: isStale,
552
+ });
553
+ }
554
+ }
555
+ }
556
+ }
557
+ const result = {
558
+ success: true,
559
+ requirementId: reqId,
560
+ requirement: {
561
+ id: reqId,
562
+ description: requirement.description,
563
+ },
564
+ implementations,
565
+ };
566
+ if (fmt === 'json') {
567
+ output(result, { format: 'json' });
568
+ }
569
+ else if (fmt === 'yaml') {
570
+ output(result, { format: 'yaml' });
571
+ }
572
+ else {
573
+ // Display implementations
574
+ process.stdout.write(`\nImplementations of ${reqId}\n`);
575
+ process.stdout.write(`${'─'.repeat(40)}\n`);
576
+ process.stdout.write(`${requirement.description}\n\n`);
577
+ if (implementations.length === 0) {
578
+ process.stdout.write(`No implementations found.\n`);
579
+ }
580
+ else {
581
+ const columns = [
582
+ { header: 'Artifact', key: 'artifactPath', maxWidth: 40 },
583
+ { header: 'Step', key: 'stepId', minWidth: 20 },
584
+ { header: 'Action', key: 'stepAction', maxWidth: 30 },
585
+ { header: 'Status', key: 'status', minWidth: 8 },
586
+ ];
587
+ const rows = implementations.map((impl) => ({
588
+ artifactPath: impl.artifactPath,
589
+ stepId: impl.stepId,
590
+ stepAction: impl.stepAction,
591
+ status: impl.stale ? 'stale' : 'current',
592
+ }));
593
+ output(rows, { format: 'table', columns });
594
+ }
595
+ process.stdout.write('\n');
596
+ }
597
+ }
598
+ /**
599
+ * Pattern to match prov trace comments.
600
+ * Matches: // prov:step:xxx or # prov:step:xxx
601
+ */
602
+ const TRACE_PATTERN = /(?:\/\/|#)\s*prov:(step:[a-z0-9-]+:\d+)/gi;
603
+ /**
604
+ * File extensions to scan for trace markers.
605
+ */
606
+ const SCANNABLE_EXTENSIONS = new Set([
607
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
608
+ '.py', '.rb', '.go', '.rs', '.java', '.kt',
609
+ '.c', '.cpp', '.h', '.hpp', '.cs',
610
+ '.sh', '.bash', '.zsh',
611
+ '.yaml', '.yml', '.json', '.toml',
612
+ ]);
613
+ /**
614
+ * Scan a single file for prov trace markers.
615
+ */
616
+ function scanFileForTraces(filePath, projectRoot) {
617
+ const traces = [];
618
+ const relPath = relative(projectRoot, filePath);
619
+ try {
620
+ const content = readFileSync(filePath, 'utf8');
621
+ const lines = content.split('\n');
622
+ for (let i = 0; i < lines.length; i++) {
623
+ const line = lines[i];
624
+ let match;
625
+ // Reset regex lastIndex for each line
626
+ TRACE_PATTERN.lastIndex = 0;
627
+ while ((match = TRACE_PATTERN.exec(line)) !== null) {
628
+ const stepId = match[1];
629
+ traces.push({
630
+ file: relPath,
631
+ line: i + 1, // 1-indexed
632
+ stepId,
633
+ comment: line.trim(),
634
+ });
635
+ }
636
+ }
637
+ }
638
+ catch {
639
+ // Skip files we can't read
640
+ }
641
+ return traces;
642
+ }
643
+ /**
644
+ * Recursively find all scannable files in a directory.
645
+ */
646
+ function findScannableFiles(dir, files = []) {
647
+ try {
648
+ const entries = readdirSync(dir);
649
+ for (const entry of entries) {
650
+ // Skip hidden directories and node_modules
651
+ if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist') {
652
+ continue;
653
+ }
654
+ const fullPath = join(dir, entry);
655
+ const stat = statSync(fullPath);
656
+ if (stat.isDirectory()) {
657
+ findScannableFiles(fullPath, files);
658
+ }
659
+ else if (stat.isFile()) {
660
+ const ext = extname(entry).toLowerCase();
661
+ if (SCANNABLE_EXTENSIONS.has(ext)) {
662
+ files.push(fullPath);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ catch {
668
+ // Skip directories we can't read
669
+ }
670
+ return files;
671
+ }
672
+ /**
673
+ * Execute the trace scan command.
674
+ *
675
+ * Scans source files for inline trace markers (// prov:step:xxx)
676
+ * and creates/updates traces automatically.
677
+ *
678
+ * @see req:agent:inline-tracing
679
+ */
680
+ export function runTraceScan(globalOpts, options) {
681
+ const projectRoot = globalOpts.dir ?? process.cwd();
682
+ const fmt = resolveFormat({ format: globalOpts.format });
683
+ // Check if prov is initialized
684
+ if (!isInitialized(projectRoot)) {
685
+ const result = {
686
+ success: false,
687
+ error: 'prov is not initialized. Run "prov init" first.',
688
+ };
689
+ if (fmt === 'json') {
690
+ output(result, { format: 'json' });
691
+ }
692
+ else if (fmt === 'yaml') {
693
+ output(result, { format: 'yaml' });
694
+ }
695
+ else {
696
+ error(result.error);
697
+ }
698
+ process.exit(1);
699
+ }
700
+ // Load graph
701
+ const loadResult = loadGraph(projectRoot);
702
+ if (!loadResult.success || loadResult.data === undefined) {
703
+ const result = {
704
+ success: false,
705
+ error: loadResult.error ?? 'Failed to load graph',
706
+ };
707
+ if (fmt === 'json') {
708
+ output(result, { format: 'json' });
709
+ }
710
+ else if (fmt === 'yaml') {
711
+ output(result, { format: 'yaml' });
712
+ }
713
+ else {
714
+ error(result.error);
715
+ }
716
+ process.exit(1);
717
+ }
718
+ const graph = loadResult.data;
719
+ // Determine directories to scan
720
+ const dirsToScan = options.dirs !== undefined && options.dirs.length > 0
721
+ ? options.dirs.map((d) => resolve(projectRoot, d))
722
+ : [projectRoot];
723
+ // Find all scannable files
724
+ const allFiles = [];
725
+ for (const dir of dirsToScan) {
726
+ if (existsSync(dir)) {
727
+ const files = findScannableFiles(dir);
728
+ allFiles.push(...files);
729
+ }
730
+ }
731
+ // Scan all files for trace markers
732
+ const allTraces = [];
733
+ for (const file of allFiles) {
734
+ const traces = scanFileForTraces(file, projectRoot);
735
+ allTraces.push(...traces);
736
+ }
737
+ // Group traces by file (we'll create one trace per file-step combination)
738
+ const tracesByKey = new Map();
739
+ for (const trace of allTraces) {
740
+ const key = `${trace.file}:${trace.stepId}`;
741
+ // Keep the first occurrence (lowest line number)
742
+ if (!tracesByKey.has(key)) {
743
+ tracesByKey.set(key, trace);
744
+ }
745
+ }
746
+ // Create/update traces
747
+ let tracesAdded = 0;
748
+ let tracesUpdated = 0;
749
+ for (const trace of tracesByKey.values()) {
750
+ const fullPath = resolve(projectRoot, trace.file);
751
+ // Verify step exists
752
+ const stepNode = graph.getNode(trace.stepId);
753
+ if (stepNode === undefined || stepNode.type !== 'step') {
754
+ // Skip traces to non-existent steps
755
+ continue;
756
+ }
757
+ // Compute artifact hash
758
+ const artifactHash = computeFileHash(fullPath);
759
+ // Generate trace ID
760
+ const traceId = generateTraceId(trace.file, trace.stepId);
761
+ // Check if trace already exists
762
+ const existingNode = graph.getNode(traceId);
763
+ if (existingNode !== undefined) {
764
+ const existingTrace = existingNode.data;
765
+ if (existingTrace.artifactHash === artifactHash) {
766
+ // No update needed
767
+ continue;
768
+ }
769
+ // Remove old trace
770
+ graph.removeNode(traceId);
771
+ tracesUpdated++;
772
+ }
773
+ else {
774
+ tracesAdded++;
775
+ }
776
+ // Create new trace
777
+ const newTrace = {
778
+ id: traceId,
779
+ artifact: {
780
+ path: trace.file,
781
+ startLine: trace.line,
782
+ },
783
+ step: trace.stepId,
784
+ artifactHash,
785
+ createdAt: new Date().toISOString(),
786
+ };
787
+ addTraceToGraph(graph, newTrace);
788
+ }
789
+ // Save graph if we made changes
790
+ if (tracesAdded > 0 || tracesUpdated > 0) {
791
+ const saveResult = saveGraph(graph, projectRoot);
792
+ if (!saveResult.success) {
793
+ const result = {
794
+ success: false,
795
+ error: saveResult.error ?? 'Failed to save graph',
796
+ };
797
+ if (fmt === 'json') {
798
+ output(result, { format: 'json' });
799
+ }
800
+ else if (fmt === 'yaml') {
801
+ output(result, { format: 'yaml' });
802
+ }
803
+ else {
804
+ error(result.error);
805
+ }
806
+ process.exit(1);
807
+ }
808
+ }
809
+ const result = {
810
+ success: true,
811
+ scannedFiles: allFiles.length,
812
+ tracesFound: tracesByKey.size,
813
+ tracesAdded,
814
+ tracesUpdated,
815
+ traces: Array.from(tracesByKey.values()),
816
+ };
817
+ if (fmt === 'json') {
818
+ output(result, { format: 'json' });
819
+ }
820
+ else if (fmt === 'yaml') {
821
+ output(result, { format: 'yaml' });
822
+ }
823
+ else {
824
+ process.stdout.write(`\nTrace Scan Results\n`);
825
+ process.stdout.write(`${'─'.repeat(40)}\n`);
826
+ process.stdout.write(`Files scanned: ${allFiles.length}\n`);
827
+ process.stdout.write(`Traces found: ${tracesByKey.size}\n`);
828
+ process.stdout.write(`Traces added: ${tracesAdded}\n`);
829
+ process.stdout.write(`Traces updated: ${tracesUpdated}\n`);
830
+ if (tracesByKey.size > 0) {
831
+ process.stdout.write(`\nFound traces:\n`);
832
+ const columns = [
833
+ { header: 'File', key: 'file', maxWidth: 40 },
834
+ { header: 'Line', key: 'line', minWidth: 6, align: 'right' },
835
+ { header: 'Step', key: 'stepId', minWidth: 20 },
836
+ ];
837
+ const rows = Array.from(tracesByKey.values()).map((t) => ({
838
+ file: t.file,
839
+ line: t.line,
840
+ stepId: t.stepId,
841
+ }));
842
+ output(rows, { format: 'table', columns });
843
+ }
844
+ process.stdout.write('\n');
845
+ }
846
+ }
847
+ //# sourceMappingURL=trace.js.map