@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.
- package/README.md +314 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +381 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agent.d.ts +107 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +197 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agent.test.d.ts +5 -0
- package/dist/commands/agent.test.d.ts.map +1 -0
- package/dist/commands/agent.test.js +199 -0
- package/dist/commands/agent.test.js.map +1 -0
- package/dist/commands/constraint.d.ts +100 -0
- package/dist/commands/constraint.d.ts.map +1 -0
- package/dist/commands/constraint.js +763 -0
- package/dist/commands/constraint.js.map +1 -0
- package/dist/commands/constraint.test.d.ts +9 -0
- package/dist/commands/constraint.test.d.ts.map +1 -0
- package/dist/commands/constraint.test.js +470 -0
- package/dist/commands/constraint.test.js.map +1 -0
- package/dist/commands/graph.d.ts +99 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +552 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/graph.test.d.ts +2 -0
- package/dist/commands/graph.test.d.ts.map +1 -0
- package/dist/commands/graph.test.js +258 -0
- package/dist/commands/graph.test.js.map +1 -0
- package/dist/commands/impact.d.ts +83 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +319 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/impact.test.d.ts +2 -0
- package/dist/commands/impact.test.d.ts.map +1 -0
- package/dist/commands/impact.test.js +234 -0
- package/dist/commands/impact.test.js.map +1 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +7 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +174 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/integration.test.d.ts +10 -0
- package/dist/commands/integration.test.d.ts.map +1 -0
- package/dist/commands/integration.test.js +456 -0
- package/dist/commands/integration.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +21 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +616 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/mcp.test.d.ts +7 -0
- package/dist/commands/mcp.test.d.ts.map +1 -0
- package/dist/commands/mcp.test.js +132 -0
- package/dist/commands/mcp.test.js.map +1 -0
- package/dist/commands/plan.d.ts +218 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +1307 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/plan.test.d.ts +9 -0
- package/dist/commands/plan.test.d.ts.map +1 -0
- package/dist/commands/plan.test.js +569 -0
- package/dist/commands/plan.test.js.map +1 -0
- package/dist/commands/spec.d.ts +94 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +635 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/spec.test.d.ts +9 -0
- package/dist/commands/spec.test.d.ts.map +1 -0
- package/dist/commands/spec.test.js +407 -0
- package/dist/commands/spec.test.js.map +1 -0
- package/dist/commands/trace.d.ts +157 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +847 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/trace.test.d.ts +9 -0
- package/dist/commands/trace.test.d.ts.map +1 -0
- package/dist/commands/trace.test.js +524 -0
- package/dist/commands/trace.test.js.map +1 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +496 -0
- package/dist/graph.js.map +1 -0
- package/dist/graph.test.d.ts +2 -0
- package/dist/graph.test.d.ts.map +1 -0
- package/dist/graph.test.js +382 -0
- package/dist/graph.test.js.map +1 -0
- package/dist/hash.d.ts +72 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +137 -0
- package/dist/hash.js.map +1 -0
- package/dist/hash.test.d.ts +2 -0
- package/dist/hash.test.d.ts.map +1 -0
- package/dist/hash.test.js +227 -0
- package/dist/hash.test.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +11 -0
- package/dist/index.test.js.map +1 -0
- package/dist/output.d.ts +84 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +175 -0
- package/dist/output.js.map +1 -0
- package/dist/output.test.d.ts +7 -0
- package/dist/output.test.d.ts.map +1 -0
- package/dist/output.test.js +146 -0
- package/dist/output.test.js.map +1 -0
- package/dist/staleness.d.ts +162 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +309 -0
- package/dist/staleness.js.map +1 -0
- package/dist/staleness.test.d.ts +2 -0
- package/dist/staleness.test.d.ts.map +1 -0
- package/dist/staleness.test.js +448 -0
- package/dist/staleness.test.js.map +1 -0
- package/dist/storage.d.ts +267 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +623 -0
- package/dist/storage.js.map +1 -0
- package/dist/storage.test.d.ts +5 -0
- package/dist/storage.test.d.ts.map +1 -0
- package/dist/storage.test.js +434 -0
- package/dist/storage.test.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +232 -0
- package/dist/types.test.js.map +1 -0
- package/dist/watcher.d.ts +139 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +406 -0
- package/dist/watcher.js.map +1 -0
- package/dist/watcher.test.d.ts +5 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +327 -0
- package/dist/watcher.test.js.map +1 -0
- 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
|