specsmd 0.0.0-dev.5 → 0.0.0-dev.51
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 +10 -2
- package/flows/aidlc/commands/construction-agent.md +5 -1
- package/flows/aidlc/commands/inception-agent.md +4 -0
- package/flows/aidlc/commands/master-agent.md +4 -0
- package/flows/aidlc/commands/operations-agent.md +4 -0
- package/flows/aidlc/memory-bank.yaml +2 -1
- package/{scripts → flows/aidlc/scripts}/artifact-validator.js +3 -3
- package/{scripts → flows/aidlc/scripts}/bolt-complete.js +35 -4
- package/{scripts → flows/aidlc/scripts}/status-integrity.js +4 -4
- package/flows/aidlc/skills/construction/bolt-list.md +1 -1
- package/flows/aidlc/skills/construction/bolt-start.md +2 -2
- package/flows/aidlc/skills/construction/bolt-status.md +1 -1
- package/flows/aidlc/skills/construction/prototype-apply.md +305 -0
- package/flows/aidlc/skills/inception/bolt-plan.md +15 -2
- package/flows/aidlc/skills/inception/vibe-to-spec.md +406 -0
- package/flows/aidlc/skills/master/analyze-context.md +1 -1
- package/flows/aidlc/templates/construction/bolt-template.md +22 -1
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt.md +73 -11
- package/flows/aidlc/templates/construction/bolt-types/simple-construction-bolt.md +5 -0
- package/flows/aidlc/templates/standards/decision-index-template.md +32 -0
- package/flows/fire/README.md +19 -0
- package/flows/fire/agents/builder/agent.md +209 -0
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +221 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.ts +806 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.ts +575 -0
- package/flows/fire/agents/builder/skills/run-plan/SKILL.md +287 -0
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +94 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/SKILL.md +140 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/scripts/render-walkthrough.ts +755 -0
- package/flows/fire/agents/orchestrator/agent.md +113 -0
- package/flows/fire/agents/orchestrator/skills/project-init/SKILL.md +141 -0
- package/flows/fire/agents/orchestrator/skills/route/SKILL.md +123 -0
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +99 -0
- package/flows/fire/agents/planner/agent.md +122 -0
- package/flows/fire/agents/planner/skills/design-doc-generate/SKILL.md +212 -0
- package/flows/fire/agents/planner/skills/intent-capture/SKILL.md +155 -0
- package/flows/fire/agents/planner/skills/work-item-decompose/SKILL.md +193 -0
- package/flows/fire/commands/fire-builder.md +56 -0
- package/flows/fire/commands/fire-planner.md +48 -0
- package/flows/fire/commands/fire.md +46 -0
- package/flows/fire/memory-bank.yaml +154 -0
- package/flows/fire/quick-start.md +130 -0
- package/flows/simple/README.md +190 -0
- package/flows/simple/agents/agent.md +404 -0
- package/flows/simple/commands/agent.md +60 -0
- package/flows/simple/context-config.yaml +34 -0
- package/flows/simple/memory-bank.yaml +66 -0
- package/flows/simple/quick-start.md +231 -0
- package/flows/simple/skills/design.md +96 -0
- package/flows/simple/skills/execute.md +190 -0
- package/flows/simple/skills/requirements.md +94 -0
- package/flows/simple/skills/tasks.md +136 -0
- package/flows/simple/templates/design-template.md +138 -0
- package/flows/simple/templates/requirements-template.md +85 -0
- package/flows/simple/templates/tasks-template.md +104 -0
- package/lib/analytics/tracker.js +6 -2
- package/lib/constants.js +10 -7
- package/lib/installer.js +3 -14
- package/lib/installers/KiroInstaller.js +55 -0
- package/lib/installers/OpenCodeInstaller.js +9 -1
- package/lib/installers/ToolInstaller.js +4 -1
- package/lib/installers/WindsurfInstaller.js +0 -54
- package/package.json +3 -52
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render walkthrough from run data
|
|
3
|
+
*
|
|
4
|
+
* Generates walkthrough.md from run log and implementation data
|
|
5
|
+
* with comprehensive validation and human/LLM-readable error messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, existsSync, statSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface WalkthroughData {
|
|
16
|
+
runId: string;
|
|
17
|
+
workItemId: string;
|
|
18
|
+
workItemTitle: string;
|
|
19
|
+
intentId: string;
|
|
20
|
+
mode: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
filesCreated: Array<{ path: string; purpose: string }>;
|
|
23
|
+
filesModified: Array<{ path: string; changes: string }>;
|
|
24
|
+
implementationDetails: Array<{ title: string; content: string }>;
|
|
25
|
+
decisions: Array<{ decision: string; choice: string; rationale: string }>;
|
|
26
|
+
verificationSteps: Array<{
|
|
27
|
+
title: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
description: string;
|
|
30
|
+
expected?: string;
|
|
31
|
+
}>;
|
|
32
|
+
testsAdded: number;
|
|
33
|
+
coverage: number;
|
|
34
|
+
testStatus: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RenderWalkthroughResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
walkthroughPath: string;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Error Handling Utilities
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a standardized FIRE error with context, issue, and guidance.
|
|
49
|
+
*
|
|
50
|
+
* Format: "FIRE Error: {context} - {issue}. {guidance}"
|
|
51
|
+
*
|
|
52
|
+
* This format is designed to be:
|
|
53
|
+
* - Human readable: Clear what went wrong
|
|
54
|
+
* - LLM readable: Structured for AI agents to parse and act on
|
|
55
|
+
* - Actionable: Includes guidance on how to fix the issue
|
|
56
|
+
*/
|
|
57
|
+
function createFIREError(context: string, issue: string, guidance: string): Error {
|
|
58
|
+
const message = `FIRE Error: ${context} - ${issue}. ${guidance}`;
|
|
59
|
+
const error = new Error(message);
|
|
60
|
+
error.name = 'FIREError';
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Maps Node.js filesystem error codes to human-readable messages.
|
|
66
|
+
*/
|
|
67
|
+
function mapNodeErrorToMessage(err: NodeJS.ErrnoException): string {
|
|
68
|
+
const errorMap: Record<string, string> = {
|
|
69
|
+
ENOENT: 'File or directory does not exist',
|
|
70
|
+
EACCES: 'Permission denied - check file/folder permissions',
|
|
71
|
+
ENOSPC: 'Disk is full - free up space and try again',
|
|
72
|
+
EROFS: 'Read-only file system - cannot write to this location',
|
|
73
|
+
ENAMETOOLONG: 'File path is too long - use a shorter path',
|
|
74
|
+
EMFILE: 'Too many open files - close some files and try again',
|
|
75
|
+
EEXIST: 'File already exists',
|
|
76
|
+
EISDIR: 'Expected a file but found a directory',
|
|
77
|
+
ENOTDIR: 'Expected a directory but found a file',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return errorMap[err.code ?? ''] ?? `System error: ${err.message}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Validation Utilities
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates that a value is a non-empty string.
|
|
89
|
+
* Returns the trimmed string or throws a clear error.
|
|
90
|
+
*/
|
|
91
|
+
function validateRequiredString(
|
|
92
|
+
value: unknown,
|
|
93
|
+
fieldName: string,
|
|
94
|
+
context: string
|
|
95
|
+
): string {
|
|
96
|
+
if (value === undefined || value === null) {
|
|
97
|
+
throw createFIREError(
|
|
98
|
+
context,
|
|
99
|
+
`required field '${fieldName}' is missing`,
|
|
100
|
+
`Ensure '${fieldName}' is provided in the walkthrough data.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value !== 'string') {
|
|
105
|
+
throw createFIREError(
|
|
106
|
+
context,
|
|
107
|
+
`field '${fieldName}' must be a string, but got ${typeof value}`,
|
|
108
|
+
`Ensure '${fieldName}' is a string value, not ${typeof value}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (trimmed.length === 0) {
|
|
114
|
+
throw createFIREError(
|
|
115
|
+
context,
|
|
116
|
+
`required field '${fieldName}' is empty`,
|
|
117
|
+
`Ensure '${fieldName}' contains a non-empty value.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validates that a value is an array.
|
|
126
|
+
* Returns the array or an empty array if null/undefined, with a warning.
|
|
127
|
+
*/
|
|
128
|
+
function validateArray<T>(
|
|
129
|
+
value: unknown,
|
|
130
|
+
fieldName: string,
|
|
131
|
+
context: string,
|
|
132
|
+
warnings: string[]
|
|
133
|
+
): T[] {
|
|
134
|
+
if (value === undefined || value === null) {
|
|
135
|
+
warnings.push(
|
|
136
|
+
`Warning: '${fieldName}' was not provided, defaulting to empty array.`
|
|
137
|
+
);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!Array.isArray(value)) {
|
|
142
|
+
throw createFIREError(
|
|
143
|
+
context,
|
|
144
|
+
`field '${fieldName}' must be an array, but got ${typeof value}`,
|
|
145
|
+
`Ensure '${fieldName}' is an array. Current value type: ${typeof value}.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value as T[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validates and coerces a number with bounds checking.
|
|
154
|
+
* Returns the number or a default if invalid.
|
|
155
|
+
*/
|
|
156
|
+
function validateNumber(
|
|
157
|
+
value: unknown,
|
|
158
|
+
fieldName: string,
|
|
159
|
+
defaultValue: number,
|
|
160
|
+
warnings: string[],
|
|
161
|
+
min?: number,
|
|
162
|
+
max?: number
|
|
163
|
+
): number {
|
|
164
|
+
if (value === undefined || value === null) {
|
|
165
|
+
warnings.push(
|
|
166
|
+
`Warning: '${fieldName}' was not provided, defaulting to ${defaultValue}.`
|
|
167
|
+
);
|
|
168
|
+
return defaultValue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
172
|
+
|
|
173
|
+
if (isNaN(num)) {
|
|
174
|
+
warnings.push(
|
|
175
|
+
`Warning: '${fieldName}' value '${value}' is not a valid number, defaulting to ${defaultValue}.`
|
|
176
|
+
);
|
|
177
|
+
return defaultValue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (min !== undefined && num < min) {
|
|
181
|
+
warnings.push(
|
|
182
|
+
`Warning: '${fieldName}' value ${num} is below minimum ${min}, clamping to ${min}.`
|
|
183
|
+
);
|
|
184
|
+
return min;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (max !== undefined && num > max) {
|
|
188
|
+
warnings.push(
|
|
189
|
+
`Warning: '${fieldName}' value ${num} exceeds maximum ${max}, clamping to ${max}.`
|
|
190
|
+
);
|
|
191
|
+
return max;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return num;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Validates that a path exists and is a directory.
|
|
199
|
+
*/
|
|
200
|
+
function validateDirectoryExists(
|
|
201
|
+
path: string,
|
|
202
|
+
pathDescription: string,
|
|
203
|
+
context: string,
|
|
204
|
+
guidance: string
|
|
205
|
+
): void {
|
|
206
|
+
if (!existsSync(path)) {
|
|
207
|
+
throw createFIREError(
|
|
208
|
+
context,
|
|
209
|
+
`${pathDescription} not found at '${path}'`,
|
|
210
|
+
guidance
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const stat = statSync(path);
|
|
216
|
+
if (!stat.isDirectory()) {
|
|
217
|
+
throw createFIREError(
|
|
218
|
+
context,
|
|
219
|
+
`${pathDescription} exists but is not a directory at '${path}'`,
|
|
220
|
+
`Expected a directory but found a file. ${guidance}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if ((err as Error).name === 'FIREError') {
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
const nodeErr = err as NodeJS.ErrnoException;
|
|
228
|
+
throw createFIREError(
|
|
229
|
+
context,
|
|
230
|
+
`cannot access ${pathDescription} at '${path}'`,
|
|
231
|
+
mapNodeErrorToMessage(nodeErr)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Markdown Utilities
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Escapes special characters in markdown table cells.
|
|
242
|
+
* Handles pipes, newlines, and ensures content doesn't break table formatting.
|
|
243
|
+
*/
|
|
244
|
+
function escapeTableCell(content: string | undefined | null): string {
|
|
245
|
+
if (content === undefined || content === null) {
|
|
246
|
+
return '';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return String(content)
|
|
250
|
+
.replace(/\|/g, '\\|') // Escape pipe characters
|
|
251
|
+
.replace(/\n/g, ' ') // Replace newlines with spaces
|
|
252
|
+
.replace(/\r/g, '') // Remove carriage returns
|
|
253
|
+
.trim();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Escapes backticks in inline code spans.
|
|
258
|
+
*/
|
|
259
|
+
function escapeInlineCode(content: string | undefined | null): string {
|
|
260
|
+
if (content === undefined || content === null) {
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If content contains backticks, we need to use double backticks
|
|
265
|
+
const str = String(content);
|
|
266
|
+
if (str.includes('`')) {
|
|
267
|
+
return str.replace(/`/g, '\\`');
|
|
268
|
+
}
|
|
269
|
+
return str;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Array Item Validators
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
interface FileEntry {
|
|
277
|
+
path: string;
|
|
278
|
+
purpose?: string;
|
|
279
|
+
changes?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface DecisionEntry {
|
|
283
|
+
decision: string;
|
|
284
|
+
choice: string;
|
|
285
|
+
rationale: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface ImplementationDetail {
|
|
289
|
+
title: string;
|
|
290
|
+
content: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface VerificationStep {
|
|
294
|
+
title: string;
|
|
295
|
+
command?: string;
|
|
296
|
+
description: string;
|
|
297
|
+
expected?: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validateFileEntry(
|
|
301
|
+
entry: unknown,
|
|
302
|
+
index: number,
|
|
303
|
+
fieldName: string,
|
|
304
|
+
warnings: string[]
|
|
305
|
+
): FileEntry {
|
|
306
|
+
if (!entry || typeof entry !== 'object') {
|
|
307
|
+
warnings.push(
|
|
308
|
+
`Warning: Invalid entry at index ${index} in '${fieldName}' - expected object, got ${typeof entry}. Skipping.`
|
|
309
|
+
);
|
|
310
|
+
return { path: '(invalid entry)' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const obj = entry as Record<string, unknown>;
|
|
314
|
+
const path =
|
|
315
|
+
typeof obj.path === 'string' && obj.path.trim()
|
|
316
|
+
? obj.path.trim()
|
|
317
|
+
: '(missing path)';
|
|
318
|
+
|
|
319
|
+
if (path === '(missing path)') {
|
|
320
|
+
warnings.push(
|
|
321
|
+
`Warning: Entry at index ${index} in '${fieldName}' is missing 'path' property.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
path,
|
|
327
|
+
purpose: typeof obj.purpose === 'string' ? obj.purpose : undefined,
|
|
328
|
+
changes: typeof obj.changes === 'string' ? obj.changes : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateDecisionEntry(
|
|
333
|
+
entry: unknown,
|
|
334
|
+
index: number,
|
|
335
|
+
warnings: string[]
|
|
336
|
+
): DecisionEntry {
|
|
337
|
+
if (!entry || typeof entry !== 'object') {
|
|
338
|
+
warnings.push(
|
|
339
|
+
`Warning: Invalid decision at index ${index} - expected object, got ${typeof entry}. Using placeholder.`
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
decision: '(invalid)',
|
|
343
|
+
choice: '(invalid)',
|
|
344
|
+
rationale: '(invalid)',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const obj = entry as Record<string, unknown>;
|
|
349
|
+
return {
|
|
350
|
+
decision:
|
|
351
|
+
typeof obj.decision === 'string' ? obj.decision : '(missing decision)',
|
|
352
|
+
choice: typeof obj.choice === 'string' ? obj.choice : '(missing choice)',
|
|
353
|
+
rationale:
|
|
354
|
+
typeof obj.rationale === 'string' ? obj.rationale : '(missing rationale)',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function validateImplementationDetail(
|
|
359
|
+
entry: unknown,
|
|
360
|
+
index: number,
|
|
361
|
+
warnings: string[]
|
|
362
|
+
): ImplementationDetail {
|
|
363
|
+
if (!entry || typeof entry !== 'object') {
|
|
364
|
+
warnings.push(
|
|
365
|
+
`Warning: Invalid implementation detail at index ${index} - expected object. Using placeholder.`
|
|
366
|
+
);
|
|
367
|
+
return { title: '(invalid)', content: '(invalid)' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const obj = entry as Record<string, unknown>;
|
|
371
|
+
return {
|
|
372
|
+
title: typeof obj.title === 'string' ? obj.title : `Detail ${index + 1}`,
|
|
373
|
+
content:
|
|
374
|
+
typeof obj.content === 'string' ? obj.content : '(no content provided)',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function validateVerificationStep(
|
|
379
|
+
entry: unknown,
|
|
380
|
+
index: number,
|
|
381
|
+
warnings: string[]
|
|
382
|
+
): VerificationStep {
|
|
383
|
+
if (!entry || typeof entry !== 'object') {
|
|
384
|
+
warnings.push(
|
|
385
|
+
`Warning: Invalid verification step at index ${index} - expected object. Using placeholder.`
|
|
386
|
+
);
|
|
387
|
+
return { title: '(invalid)', description: '(invalid)' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const obj = entry as Record<string, unknown>;
|
|
391
|
+
return {
|
|
392
|
+
title: typeof obj.title === 'string' ? obj.title : `Step ${index + 1}`,
|
|
393
|
+
command: typeof obj.command === 'string' ? obj.command : undefined,
|
|
394
|
+
description:
|
|
395
|
+
typeof obj.description === 'string'
|
|
396
|
+
? obj.description
|
|
397
|
+
: '(no description)',
|
|
398
|
+
expected: typeof obj.expected === 'string' ? obj.expected : undefined,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Main Function
|
|
404
|
+
// ============================================================================
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Renders a walkthrough document from run data.
|
|
408
|
+
*
|
|
409
|
+
* @param rootPath - The project root path containing .specs-fire directory
|
|
410
|
+
* @param data - The walkthrough data to render
|
|
411
|
+
* @returns Result object with success status, file path, and any warnings
|
|
412
|
+
*
|
|
413
|
+
* @throws {Error} FIREError with clear message if validation fails or write fails
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* const result = renderWalkthrough('/path/to/project', {
|
|
418
|
+
* runId: 'run-001',
|
|
419
|
+
* workItemId: 'WI-001',
|
|
420
|
+
* // ... other fields
|
|
421
|
+
* });
|
|
422
|
+
*
|
|
423
|
+
* if (result.warnings.length > 0) {
|
|
424
|
+
* console.log('Warnings:', result.warnings);
|
|
425
|
+
* }
|
|
426
|
+
* console.log('Walkthrough written to:', result.walkthroughPath);
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
export function renderWalkthrough(
|
|
430
|
+
rootPath: string,
|
|
431
|
+
data: WalkthroughData
|
|
432
|
+
): RenderWalkthroughResult {
|
|
433
|
+
const context = 'Cannot generate walkthrough';
|
|
434
|
+
const warnings: string[] = [];
|
|
435
|
+
|
|
436
|
+
// =========================================================================
|
|
437
|
+
// Input Validation: rootPath
|
|
438
|
+
// =========================================================================
|
|
439
|
+
|
|
440
|
+
if (rootPath === undefined || rootPath === null) {
|
|
441
|
+
throw createFIREError(
|
|
442
|
+
context,
|
|
443
|
+
"rootPath parameter is required but was not provided",
|
|
444
|
+
"Call renderWalkthrough with a valid project root path as the first argument."
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (typeof rootPath !== 'string') {
|
|
449
|
+
throw createFIREError(
|
|
450
|
+
context,
|
|
451
|
+
`rootPath must be a string, but got ${typeof rootPath}`,
|
|
452
|
+
"Ensure rootPath is a string containing the absolute path to the project root."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const trimmedRootPath = rootPath.trim();
|
|
457
|
+
if (trimmedRootPath.length === 0) {
|
|
458
|
+
throw createFIREError(
|
|
459
|
+
context,
|
|
460
|
+
"rootPath is an empty string",
|
|
461
|
+
"Provide a non-empty path to the project root directory."
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Validate rootPath exists
|
|
466
|
+
validateDirectoryExists(
|
|
467
|
+
trimmedRootPath,
|
|
468
|
+
'project root',
|
|
469
|
+
context,
|
|
470
|
+
"Ensure the project root path exists and is accessible."
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// =========================================================================
|
|
474
|
+
// Input Validation: data object
|
|
475
|
+
// =========================================================================
|
|
476
|
+
|
|
477
|
+
if (data === undefined || data === null) {
|
|
478
|
+
throw createFIREError(
|
|
479
|
+
context,
|
|
480
|
+
"data parameter is required but was not provided",
|
|
481
|
+
"Call renderWalkthrough with walkthrough data as the second argument."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
486
|
+
throw createFIREError(
|
|
487
|
+
context,
|
|
488
|
+
`data must be an object, but got ${Array.isArray(data) ? 'array' : typeof data}`,
|
|
489
|
+
"Ensure data is a WalkthroughData object with required fields: runId, workItemId, workItemTitle, intentId, mode, summary."
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// =========================================================================
|
|
494
|
+
// Validate Required String Fields
|
|
495
|
+
// =========================================================================
|
|
496
|
+
|
|
497
|
+
const runId = validateRequiredString(data.runId, 'runId', context);
|
|
498
|
+
const workItemId = validateRequiredString(data.workItemId, 'workItemId', context);
|
|
499
|
+
const workItemTitle = validateRequiredString(data.workItemTitle, 'workItemTitle', context);
|
|
500
|
+
const intentId = validateRequiredString(data.intentId, 'intentId', context);
|
|
501
|
+
const mode = validateRequiredString(data.mode, 'mode', context);
|
|
502
|
+
const summary = validateRequiredString(data.summary, 'summary', context);
|
|
503
|
+
|
|
504
|
+
// testStatus can have a default
|
|
505
|
+
let testStatus: string;
|
|
506
|
+
if (
|
|
507
|
+
data.testStatus === undefined ||
|
|
508
|
+
data.testStatus === null ||
|
|
509
|
+
(typeof data.testStatus === 'string' && data.testStatus.trim() === '')
|
|
510
|
+
) {
|
|
511
|
+
warnings.push("Warning: 'testStatus' was not provided, defaulting to 'unknown'.");
|
|
512
|
+
testStatus = 'unknown';
|
|
513
|
+
} else {
|
|
514
|
+
testStatus = String(data.testStatus);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// =========================================================================
|
|
518
|
+
// Validate Array Fields
|
|
519
|
+
// =========================================================================
|
|
520
|
+
|
|
521
|
+
const rawFilesCreated = validateArray<unknown>(
|
|
522
|
+
data.filesCreated,
|
|
523
|
+
'filesCreated',
|
|
524
|
+
context,
|
|
525
|
+
warnings
|
|
526
|
+
);
|
|
527
|
+
const rawFilesModified = validateArray<unknown>(
|
|
528
|
+
data.filesModified,
|
|
529
|
+
'filesModified',
|
|
530
|
+
context,
|
|
531
|
+
warnings
|
|
532
|
+
);
|
|
533
|
+
const rawImplementationDetails = validateArray<unknown>(
|
|
534
|
+
data.implementationDetails,
|
|
535
|
+
'implementationDetails',
|
|
536
|
+
context,
|
|
537
|
+
warnings
|
|
538
|
+
);
|
|
539
|
+
const rawDecisions = validateArray<unknown>(
|
|
540
|
+
data.decisions,
|
|
541
|
+
'decisions',
|
|
542
|
+
context,
|
|
543
|
+
warnings
|
|
544
|
+
);
|
|
545
|
+
const rawVerificationSteps = validateArray<unknown>(
|
|
546
|
+
data.verificationSteps,
|
|
547
|
+
'verificationSteps',
|
|
548
|
+
context,
|
|
549
|
+
warnings
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// =========================================================================
|
|
553
|
+
// Validate Numeric Fields
|
|
554
|
+
// =========================================================================
|
|
555
|
+
|
|
556
|
+
const testsAdded = validateNumber(data.testsAdded, 'testsAdded', 0, warnings, 0);
|
|
557
|
+
const coverage = validateNumber(data.coverage, 'coverage', 0, warnings, 0, 100);
|
|
558
|
+
|
|
559
|
+
// =========================================================================
|
|
560
|
+
// Validate Run Folder Exists
|
|
561
|
+
// =========================================================================
|
|
562
|
+
|
|
563
|
+
const specsFirePath = join(trimmedRootPath, '.specs-fire');
|
|
564
|
+
validateDirectoryExists(
|
|
565
|
+
specsFirePath,
|
|
566
|
+
'.specs-fire directory',
|
|
567
|
+
context,
|
|
568
|
+
"Initialize FIRE in this project first. The .specs-fire directory should exist at the project root."
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const runsPath = join(specsFirePath, 'runs');
|
|
572
|
+
validateDirectoryExists(
|
|
573
|
+
runsPath,
|
|
574
|
+
'runs directory',
|
|
575
|
+
context,
|
|
576
|
+
"The runs directory should exist within .specs-fire. This is created when FIRE is initialized."
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const runPath = join(runsPath, runId);
|
|
580
|
+
validateDirectoryExists(
|
|
581
|
+
runPath,
|
|
582
|
+
`run folder for '${runId}'`,
|
|
583
|
+
context,
|
|
584
|
+
`Ensure the run '${runId}' was initialized with 'run-start' before generating walkthrough. The run folder should exist at: ${runPath}`
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const walkthroughPath = join(runPath, 'walkthrough.md');
|
|
588
|
+
|
|
589
|
+
// =========================================================================
|
|
590
|
+
// Process and Validate Array Items
|
|
591
|
+
// =========================================================================
|
|
592
|
+
|
|
593
|
+
const filesCreated = rawFilesCreated.map((entry, i) =>
|
|
594
|
+
validateFileEntry(entry, i, 'filesCreated', warnings)
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const filesModified = rawFilesModified.map((entry, i) =>
|
|
598
|
+
validateFileEntry(entry, i, 'filesModified', warnings)
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const implementationDetails = rawImplementationDetails.map((entry, i) =>
|
|
602
|
+
validateImplementationDetail(entry, i, warnings)
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const decisions = rawDecisions.map((entry, i) =>
|
|
606
|
+
validateDecisionEntry(entry, i, warnings)
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const verificationSteps = rawVerificationSteps.map((entry, i) =>
|
|
610
|
+
validateVerificationStep(entry, i, warnings)
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// =========================================================================
|
|
614
|
+
// Build Markdown Content
|
|
615
|
+
// =========================================================================
|
|
616
|
+
|
|
617
|
+
const timestamp = new Date().toISOString();
|
|
618
|
+
|
|
619
|
+
// Build files created table
|
|
620
|
+
const filesCreatedTable =
|
|
621
|
+
filesCreated.length > 0
|
|
622
|
+
? filesCreated
|
|
623
|
+
.map(
|
|
624
|
+
(f) =>
|
|
625
|
+
`| \`${escapeInlineCode(f.path)}\` | ${escapeTableCell(f.purpose ?? '')} |`
|
|
626
|
+
)
|
|
627
|
+
.join('\n')
|
|
628
|
+
: '| (none) | |';
|
|
629
|
+
|
|
630
|
+
// Build files modified table
|
|
631
|
+
const filesModifiedTable =
|
|
632
|
+
filesModified.length > 0
|
|
633
|
+
? filesModified
|
|
634
|
+
.map(
|
|
635
|
+
(f) =>
|
|
636
|
+
`| \`${escapeInlineCode(f.path)}\` | ${escapeTableCell(f.changes ?? '')} |`
|
|
637
|
+
)
|
|
638
|
+
.join('\n')
|
|
639
|
+
: '| (none) | |';
|
|
640
|
+
|
|
641
|
+
// Build implementation details sections
|
|
642
|
+
const implementationDetailsSection =
|
|
643
|
+
implementationDetails.length > 0
|
|
644
|
+
? implementationDetails
|
|
645
|
+
.map((d, i) => `### ${i + 1}. ${escapeTableCell(d.title)}\n\n${d.content}`)
|
|
646
|
+
.join('\n\n')
|
|
647
|
+
: '(No implementation details provided)';
|
|
648
|
+
|
|
649
|
+
// Build decisions table
|
|
650
|
+
const decisionsTable =
|
|
651
|
+
decisions.length > 0
|
|
652
|
+
? decisions
|
|
653
|
+
.map(
|
|
654
|
+
(d) =>
|
|
655
|
+
`| ${escapeTableCell(d.decision)} | ${escapeTableCell(d.choice)} | ${escapeTableCell(d.rationale)} |`
|
|
656
|
+
)
|
|
657
|
+
.join('\n')
|
|
658
|
+
: '| (none) | | |';
|
|
659
|
+
|
|
660
|
+
// Build verification steps
|
|
661
|
+
const verificationStepsSection =
|
|
662
|
+
verificationSteps.length > 0
|
|
663
|
+
? verificationSteps
|
|
664
|
+
.map((s, i) => {
|
|
665
|
+
let step = `${i + 1}. **${escapeTableCell(s.title)}**\n`;
|
|
666
|
+
if (s.command) {
|
|
667
|
+
step += ` \`\`\`bash\n ${s.command}\n \`\`\`\n`;
|
|
668
|
+
}
|
|
669
|
+
step += ` ${s.description}\n`;
|
|
670
|
+
if (s.expected) {
|
|
671
|
+
step += ` Expected: ${s.expected}\n`;
|
|
672
|
+
}
|
|
673
|
+
return step;
|
|
674
|
+
})
|
|
675
|
+
.join('\n')
|
|
676
|
+
: '(No verification steps provided)';
|
|
677
|
+
|
|
678
|
+
// Assemble the full walkthrough document
|
|
679
|
+
const walkthrough = `---
|
|
680
|
+
run: ${runId}
|
|
681
|
+
work_item: ${workItemId}
|
|
682
|
+
intent: ${intentId}
|
|
683
|
+
generated: ${timestamp}
|
|
684
|
+
mode: ${mode}
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
# Implementation Walkthrough: ${workItemTitle}
|
|
688
|
+
|
|
689
|
+
## Summary
|
|
690
|
+
|
|
691
|
+
${summary}
|
|
692
|
+
|
|
693
|
+
## Files Changed
|
|
694
|
+
|
|
695
|
+
### Created
|
|
696
|
+
|
|
697
|
+
| File | Purpose |
|
|
698
|
+
|------|---------|
|
|
699
|
+
${filesCreatedTable}
|
|
700
|
+
|
|
701
|
+
### Modified
|
|
702
|
+
|
|
703
|
+
| File | Changes |
|
|
704
|
+
|------|---------|
|
|
705
|
+
${filesModifiedTable}
|
|
706
|
+
|
|
707
|
+
## Key Implementation Details
|
|
708
|
+
|
|
709
|
+
${implementationDetailsSection}
|
|
710
|
+
|
|
711
|
+
## Decisions Made
|
|
712
|
+
|
|
713
|
+
| Decision | Choice | Rationale |
|
|
714
|
+
|----------|--------|-----------|
|
|
715
|
+
${decisionsTable}
|
|
716
|
+
|
|
717
|
+
## How to Verify
|
|
718
|
+
|
|
719
|
+
${verificationStepsSection}
|
|
720
|
+
|
|
721
|
+
## Test Coverage
|
|
722
|
+
|
|
723
|
+
- Tests added: ${testsAdded}
|
|
724
|
+
- Coverage: ${coverage}%
|
|
725
|
+
- Status: ${testStatus}
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
*Generated by FIRE Run ${runId}*
|
|
729
|
+
`;
|
|
730
|
+
|
|
731
|
+
// =========================================================================
|
|
732
|
+
// Write File with Error Handling
|
|
733
|
+
// =========================================================================
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
writeFileSync(walkthroughPath, walkthrough, 'utf8');
|
|
737
|
+
} catch (err) {
|
|
738
|
+
const nodeErr = err as NodeJS.ErrnoException;
|
|
739
|
+
throw createFIREError(
|
|
740
|
+
'Failed to write walkthrough',
|
|
741
|
+
`could not write to '${walkthroughPath}'`,
|
|
742
|
+
`${mapNodeErrorToMessage(nodeErr)} Verify you have write permissions and sufficient disk space.`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// =========================================================================
|
|
747
|
+
// Return Success Result
|
|
748
|
+
// =========================================================================
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
walkthroughPath,
|
|
753
|
+
warnings,
|
|
754
|
+
};
|
|
755
|
+
}
|