specweave 0.22.11 ā 0.22.12
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/CLAUDE.md +77 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +42 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/initial-increment-generator.d.ts +26 -0
- package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/initial-increment-generator.js +240 -0
- package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -0
- package/dist/src/generators/spec/spec-parser.d.ts +79 -0
- package/dist/src/generators/spec/spec-parser.d.ts.map +1 -0
- package/dist/src/generators/spec/spec-parser.js +185 -0
- package/dist/src/generators/spec/spec-parser.js.map +1 -0
- package/dist/src/generators/spec/task-parser.d.ts +95 -0
- package/dist/src/generators/spec/task-parser.d.ts.map +1 -0
- package/dist/src/generators/spec/task-parser.js +301 -0
- package/dist/src/generators/spec/task-parser.js.map +1 -0
- package/dist/src/validators/ac-coverage-validator.d.ts +106 -0
- package/dist/src/validators/ac-coverage-validator.d.ts.map +1 -0
- package/dist/src/validators/ac-coverage-validator.js +250 -0
- package/dist/src/validators/ac-coverage-validator.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-done.md +58 -0
- package/plugins/specweave/commands/specweave-validate.md +36 -6
- package/plugins/specweave/lib/hooks/sync-living-docs.js +21 -0
- package/plugins/specweave/lib/hooks/sync-us-tasks.js +290 -0
- package/src/templates/AGENTS.md.template +16 -0
- package/src/templates/CLAUDE.md.template +18 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Parser with US-Task Linkage Support
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.md files to extract task metadata including new US linkage fields:
|
|
5
|
+
* - userStory: US-ID this task implements (e.g., "US-001")
|
|
6
|
+
* - satisfiesACs: List of AC-IDs this task satisfies (e.g., ["AC-US1-01", "AC-US1-02"])
|
|
7
|
+
*
|
|
8
|
+
* Supports hierarchical task structure grouped by User Story.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
/**
|
|
12
|
+
* Parse tasks.md and extract all tasks with US linkage
|
|
13
|
+
*
|
|
14
|
+
* @param tasksPath - Path to tasks.md file
|
|
15
|
+
* @returns Map of User Story ID ā Tasks
|
|
16
|
+
* @throws Error if tasks.md cannot be read or is malformed
|
|
17
|
+
*/
|
|
18
|
+
export function parseTasksWithUSLinks(tasksPath) {
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(tasksPath, 'utf-8');
|
|
21
|
+
const tasks = {};
|
|
22
|
+
// Split content into lines for line number tracking
|
|
23
|
+
const lines = content.split('\n');
|
|
24
|
+
// Regex patterns for task parsing
|
|
25
|
+
const taskHeaderRegex = /^###\s+(T-\d{3}):\s*(.+)$/;
|
|
26
|
+
const userStoryRegex = /^\*\*User Story\*\*:\s*(US-\d{3})/;
|
|
27
|
+
const satisfiesACsRegex = /^\*\*Satisfies ACs\*\*:\s*(AC-US\d+-\d{2}(?:,\s*AC-US\d+-\d{2})*)/;
|
|
28
|
+
const statusRegex = /^\*\*Status\*\*:\s*\[([x ])\]\s*(\w+)/;
|
|
29
|
+
const priorityRegex = /^\*\*Priority\*\*:\s*(.+)/;
|
|
30
|
+
const estimatedEffortRegex = /^\*\*Estimated Effort\*\*:\s*(.+)/;
|
|
31
|
+
const dependenciesRegex = /^\*\*Dependencies\*\*:\s*(.+)/;
|
|
32
|
+
let currentTask = null;
|
|
33
|
+
let currentDescription = [];
|
|
34
|
+
let currentSection = 'none';
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
const lineNumber = i + 1;
|
|
38
|
+
// Check for task header (### T-XXX: Title)
|
|
39
|
+
const taskHeaderMatch = line.match(taskHeaderRegex);
|
|
40
|
+
if (taskHeaderMatch) {
|
|
41
|
+
// Save previous task if exists
|
|
42
|
+
if (currentTask) {
|
|
43
|
+
saveTask(tasks, currentTask, currentDescription);
|
|
44
|
+
}
|
|
45
|
+
// Start new task
|
|
46
|
+
currentTask = {
|
|
47
|
+
id: taskHeaderMatch[1],
|
|
48
|
+
title: taskHeaderMatch[2],
|
|
49
|
+
status: 'pending',
|
|
50
|
+
lineNumber
|
|
51
|
+
};
|
|
52
|
+
currentDescription = [];
|
|
53
|
+
currentSection = 'none';
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Skip if no current task
|
|
57
|
+
if (!currentTask)
|
|
58
|
+
continue;
|
|
59
|
+
// Parse task fields
|
|
60
|
+
const userStoryMatch = line.match(userStoryRegex);
|
|
61
|
+
if (userStoryMatch) {
|
|
62
|
+
currentTask.userStory = userStoryMatch[1];
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const satisfiesACsMatch = line.match(satisfiesACsRegex);
|
|
66
|
+
if (satisfiesACsMatch) {
|
|
67
|
+
// Split comma-separated AC-IDs and trim whitespace
|
|
68
|
+
currentTask.satisfiesACs = satisfiesACsMatch[1]
|
|
69
|
+
.split(',')
|
|
70
|
+
.map(ac => ac.trim());
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const statusMatch = line.match(statusRegex);
|
|
74
|
+
if (statusMatch) {
|
|
75
|
+
const checkbox = statusMatch[1];
|
|
76
|
+
const statusText = statusMatch[2].toLowerCase();
|
|
77
|
+
// Map checkbox and status text to TaskStatus
|
|
78
|
+
if (checkbox === 'x') {
|
|
79
|
+
currentTask.status = 'completed';
|
|
80
|
+
}
|
|
81
|
+
else if (statusText.includes('progress')) {
|
|
82
|
+
currentTask.status = 'in_progress';
|
|
83
|
+
}
|
|
84
|
+
else if (statusText.includes('transfer')) {
|
|
85
|
+
currentTask.status = 'transferred';
|
|
86
|
+
}
|
|
87
|
+
else if (statusText.includes('cancel')) {
|
|
88
|
+
currentTask.status = 'canceled';
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
currentTask.status = 'pending';
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const priorityMatch = line.match(priorityRegex);
|
|
96
|
+
if (priorityMatch) {
|
|
97
|
+
currentTask.priority = priorityMatch[1];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const estimatedEffortMatch = line.match(estimatedEffortRegex);
|
|
101
|
+
if (estimatedEffortMatch) {
|
|
102
|
+
currentTask.estimatedEffort = estimatedEffortMatch[1];
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const dependenciesMatch = line.match(dependenciesRegex);
|
|
106
|
+
if (dependenciesMatch) {
|
|
107
|
+
// Parse dependencies (T-001, T-002, etc.)
|
|
108
|
+
currentTask.dependencies = dependenciesMatch[1]
|
|
109
|
+
.split(',')
|
|
110
|
+
.map(dep => dep.trim())
|
|
111
|
+
.filter(dep => dep.match(/^T-\d{3}$/));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Track sections for description parsing
|
|
115
|
+
if (line.startsWith('**Description**:')) {
|
|
116
|
+
currentSection = 'description';
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith('**Implementation Steps**:')) {
|
|
120
|
+
currentSection = 'implementation';
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (line.startsWith('**Test Plan**:')) {
|
|
124
|
+
currentSection = 'test';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (line.startsWith('**Files Affected**:')) {
|
|
128
|
+
currentSection = 'files';
|
|
129
|
+
currentTask.filesAffected = [];
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Collect description lines
|
|
133
|
+
if (currentSection === 'description' && line.trim() && !line.startsWith('**')) {
|
|
134
|
+
currentDescription.push(line.trim());
|
|
135
|
+
}
|
|
136
|
+
// Collect files affected
|
|
137
|
+
if (currentSection === 'files' && line.trim().startsWith('- `')) {
|
|
138
|
+
const filePath = line.trim().replace(/^- `(.+)`/, '$1');
|
|
139
|
+
if (currentTask.filesAffected) {
|
|
140
|
+
currentTask.filesAffected.push(filePath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Save last task
|
|
145
|
+
if (currentTask) {
|
|
146
|
+
saveTask(tasks, currentTask, currentDescription);
|
|
147
|
+
}
|
|
148
|
+
return tasks;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw new Error(`Failed to parse tasks.md at ${tasksPath}: ${error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Helper: Save task to tasks map, grouped by User Story
|
|
156
|
+
*/
|
|
157
|
+
function saveTask(tasks, task, description) {
|
|
158
|
+
// Set description
|
|
159
|
+
if (description.length > 0) {
|
|
160
|
+
task.description = description.join(' ');
|
|
161
|
+
}
|
|
162
|
+
// Group by User Story (or "unassigned" if no userStory field)
|
|
163
|
+
const usId = task.userStory || 'unassigned';
|
|
164
|
+
if (!tasks[usId]) {
|
|
165
|
+
tasks[usId] = [];
|
|
166
|
+
}
|
|
167
|
+
tasks[usId].push(task);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Validate task US and AC linkage
|
|
171
|
+
*
|
|
172
|
+
* @param task - Task to validate
|
|
173
|
+
* @param validUSIds - List of valid US-IDs from spec.md
|
|
174
|
+
* @param validACIds - List of valid AC-IDs from spec.md
|
|
175
|
+
* @returns Array of validation errors (empty if valid)
|
|
176
|
+
*/
|
|
177
|
+
export function validateTaskLinkage(task, validUSIds, validACIds) {
|
|
178
|
+
const errors = [];
|
|
179
|
+
// Validate userStory field
|
|
180
|
+
if (task.userStory) {
|
|
181
|
+
// Check format (US-XXX)
|
|
182
|
+
if (!task.userStory.match(/^US-\d{3}$/)) {
|
|
183
|
+
errors.push({
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
field: 'userStory',
|
|
186
|
+
value: task.userStory,
|
|
187
|
+
message: `Invalid US-ID format: "${task.userStory}" (expected format: US-001)`,
|
|
188
|
+
suggestedFix: 'Use format: US-XXX where XXX is a 3-digit number'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Check if US exists in spec.md
|
|
192
|
+
else if (!validUSIds.includes(task.userStory)) {
|
|
193
|
+
errors.push({
|
|
194
|
+
taskId: task.id,
|
|
195
|
+
field: 'userStory',
|
|
196
|
+
value: task.userStory,
|
|
197
|
+
message: `User Story ${task.userStory} not found in spec.md`,
|
|
198
|
+
suggestedFix: `Valid User Stories: ${validUSIds.join(', ')}`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Validate satisfiesACs field
|
|
203
|
+
if (task.satisfiesACs && task.satisfiesACs.length > 0) {
|
|
204
|
+
for (const acId of task.satisfiesACs) {
|
|
205
|
+
// Check format (AC-USXX-YY)
|
|
206
|
+
if (!acId.match(/^AC-US\d+-\d{2}$/)) {
|
|
207
|
+
errors.push({
|
|
208
|
+
taskId: task.id,
|
|
209
|
+
field: 'satisfiesACs',
|
|
210
|
+
value: acId,
|
|
211
|
+
message: `Invalid AC-ID format: "${acId}" (expected format: AC-US1-01)`,
|
|
212
|
+
suggestedFix: 'Use format: AC-USXX-YY where XX is US number, YY is AC number'
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Check if AC exists in spec.md
|
|
217
|
+
if (!validACIds.includes(acId)) {
|
|
218
|
+
errors.push({
|
|
219
|
+
taskId: task.id,
|
|
220
|
+
field: 'satisfiesACs',
|
|
221
|
+
value: acId,
|
|
222
|
+
message: `Acceptance Criteria ${acId} not found in spec.md`,
|
|
223
|
+
suggestedFix: `Check spec.md for valid AC-IDs in this User Story`
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Check if AC belongs to correct User Story
|
|
228
|
+
if (task.userStory) {
|
|
229
|
+
const acUSNumber = extractUSNumberFromACId(acId);
|
|
230
|
+
const taskUSNumber = extractUSNumber(task.userStory);
|
|
231
|
+
if (acUSNumber !== taskUSNumber) {
|
|
232
|
+
errors.push({
|
|
233
|
+
taskId: task.id,
|
|
234
|
+
field: 'satisfiesACs',
|
|
235
|
+
value: acId,
|
|
236
|
+
message: `AC ${acId} belongs to US-${String(acUSNumber).padStart(3, '0')}, but task is linked to ${task.userStory}`,
|
|
237
|
+
suggestedFix: `Either link task to US-${String(acUSNumber).padStart(3, '0')} or use different AC-ID`
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Extract US number from AC-ID (AC-US1-01 ā 1)
|
|
247
|
+
*/
|
|
248
|
+
function extractUSNumberFromACId(acId) {
|
|
249
|
+
const match = acId.match(/^AC-US(\d+)-\d{2}$/);
|
|
250
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Extract US number from US-ID (US-001 ā 1)
|
|
254
|
+
*/
|
|
255
|
+
function extractUSNumber(usId) {
|
|
256
|
+
const match = usId.match(/^US-(\d{3})$/);
|
|
257
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get all tasks (flattened, not grouped by US)
|
|
261
|
+
*
|
|
262
|
+
* @param tasksByUS - Tasks grouped by User Story
|
|
263
|
+
* @returns Array of all tasks
|
|
264
|
+
*/
|
|
265
|
+
export function getAllTasks(tasksByUS) {
|
|
266
|
+
return Object.values(tasksByUS).flat();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Count tasks by status
|
|
270
|
+
*
|
|
271
|
+
* @param tasksByUS - Tasks grouped by User Story
|
|
272
|
+
* @returns Map of status ā count
|
|
273
|
+
*/
|
|
274
|
+
export function countTasksByStatus(tasksByUS) {
|
|
275
|
+
const counts = {
|
|
276
|
+
pending: 0,
|
|
277
|
+
in_progress: 0,
|
|
278
|
+
completed: 0,
|
|
279
|
+
transferred: 0,
|
|
280
|
+
canceled: 0
|
|
281
|
+
};
|
|
282
|
+
const allTasks = getAllTasks(tasksByUS);
|
|
283
|
+
allTasks.forEach(task => {
|
|
284
|
+
counts[task.status]++;
|
|
285
|
+
});
|
|
286
|
+
return counts;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Calculate completion percentage
|
|
290
|
+
*
|
|
291
|
+
* @param tasksByUS - Tasks grouped by User Story
|
|
292
|
+
* @returns Completion percentage (0-100)
|
|
293
|
+
*/
|
|
294
|
+
export function calculateCompletionPercentage(tasksByUS) {
|
|
295
|
+
const allTasks = getAllTasks(tasksByUS);
|
|
296
|
+
if (allTasks.length === 0)
|
|
297
|
+
return 0;
|
|
298
|
+
const completedTasks = allTasks.filter(t => t.status === 'completed').length;
|
|
299
|
+
return Math.round((completedTasks / allTasks.length) * 100);
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=task-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-parser.js","sourceRoot":"","sources":["../../../../src/generators/spec/task-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AC Coverage Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates Acceptance Criteria coverage by tasks:
|
|
5
|
+
* - Detects uncovered ACs (ACs with no implementing tasks)
|
|
6
|
+
* - Detects orphan tasks (tasks with no AC linkage)
|
|
7
|
+
* - Builds traceability matrix (AC ā Task mapping)
|
|
8
|
+
* - Calculates coverage metrics
|
|
9
|
+
*
|
|
10
|
+
* Used by `/specweave:validate` and `/specweave:done` commands to ensure
|
|
11
|
+
* all acceptance criteria are covered before increment closure.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* AC Coverage Report
|
|
15
|
+
*/
|
|
16
|
+
export interface ACCoverageReport {
|
|
17
|
+
/** Total number of Acceptance Criteria in spec.md */
|
|
18
|
+
totalACs: number;
|
|
19
|
+
/** Number of ACs covered by at least one task */
|
|
20
|
+
coveredACs: number;
|
|
21
|
+
/** AC-IDs with no implementing tasks */
|
|
22
|
+
uncoveredACs: string[];
|
|
23
|
+
/** Task IDs with no satisfiesACs field or invalid AC-IDs */
|
|
24
|
+
orphanTasks: string[];
|
|
25
|
+
/** Coverage percentage (0-100) */
|
|
26
|
+
coveragePercentage: number;
|
|
27
|
+
/** Traceability matrix: AC-ID ā Task IDs */
|
|
28
|
+
acToTasksMap: Map<string, string[]>;
|
|
29
|
+
/** Reverse mapping: Task ID ā AC-IDs */
|
|
30
|
+
taskToACsMap: Map<string, string[]>;
|
|
31
|
+
/** Per-User Story coverage breakdown */
|
|
32
|
+
coverageByUS: Map<string, USCoverageStats>;
|
|
33
|
+
/** Validation timestamp */
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Coverage statistics per User Story
|
|
38
|
+
*/
|
|
39
|
+
export interface USCoverageStats {
|
|
40
|
+
/** User Story ID */
|
|
41
|
+
usId: string;
|
|
42
|
+
/** User Story title */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Total ACs for this US */
|
|
45
|
+
totalACs: number;
|
|
46
|
+
/** Covered ACs for this US */
|
|
47
|
+
coveredACs: number;
|
|
48
|
+
/** Coverage percentage for this US */
|
|
49
|
+
coveragePercentage: number;
|
|
50
|
+
/** Uncovered AC-IDs for this US */
|
|
51
|
+
uncoveredACs: string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validation options
|
|
55
|
+
*/
|
|
56
|
+
export interface ValidationOptions {
|
|
57
|
+
/** Allow orphan tasks (default: false) */
|
|
58
|
+
allowOrphans?: boolean;
|
|
59
|
+
/** Minimum coverage percentage required (default: 100) */
|
|
60
|
+
minCoveragePercentage?: number;
|
|
61
|
+
/** Logger for output messages */
|
|
62
|
+
logger?: {
|
|
63
|
+
log: (msg: string) => void;
|
|
64
|
+
error: (msg: string, err?: Error) => void;
|
|
65
|
+
warn: (msg: string) => void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate AC coverage for an increment
|
|
70
|
+
*
|
|
71
|
+
* @param incrementPath - Path to increment directory (e.g., .specweave/increments/0047-us-task-linkage)
|
|
72
|
+
* @param options - Validation options
|
|
73
|
+
* @returns AC Coverage Report
|
|
74
|
+
* @throws Error if spec.md or tasks.md cannot be read
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateACCoverage(incrementPath: string, options?: ValidationOptions): ACCoverageReport;
|
|
77
|
+
/**
|
|
78
|
+
* Check if coverage report passes validation
|
|
79
|
+
*
|
|
80
|
+
* @param report - Coverage report
|
|
81
|
+
* @param options - Validation options
|
|
82
|
+
* @returns True if validation passes, false otherwise
|
|
83
|
+
*/
|
|
84
|
+
export declare function isCoveragePassing(report: ACCoverageReport, options?: ValidationOptions): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Print detailed coverage report to console
|
|
87
|
+
*
|
|
88
|
+
* @param report - Coverage report
|
|
89
|
+
* @param options - Validation options
|
|
90
|
+
*/
|
|
91
|
+
export declare function printCoverageReport(report: ACCoverageReport, options?: ValidationOptions): void;
|
|
92
|
+
/**
|
|
93
|
+
* Export coverage report to JSON file
|
|
94
|
+
*
|
|
95
|
+
* @param report - Coverage report
|
|
96
|
+
* @param outputPath - Path to output JSON file
|
|
97
|
+
*/
|
|
98
|
+
export declare function exportCoverageReportJSON(report: ACCoverageReport, outputPath: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Get recommended actions based on coverage report
|
|
101
|
+
*
|
|
102
|
+
* @param report - Coverage report
|
|
103
|
+
* @returns Array of recommended actions
|
|
104
|
+
*/
|
|
105
|
+
export declare function getRecommendedActions(report: ACCoverageReport): string[];
|
|
106
|
+
//# sourceMappingURL=ac-coverage-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ac-coverage-validator.d.ts","sourceRoot":"","sources":["../../../src/validators/ac-coverage-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAYH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IAEjB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IAEnB,wCAAwC;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IAEvB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,EAAE,CAAC;IAEtB,kCAAkC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAE3B,4CAA4C;IAC5C,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEpC,wCAAwC;IACxC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEpC,wCAAwC;IACxC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAE3C,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IAEb,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IAEd,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IAEjB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IAEnB,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAE3B,mCAAmC;IACnC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,0DAA0D;IAC1D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B,iCAAiC;IACjC,MAAM,CAAC,EAAE;QACP,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC3B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;QAC1C,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;KAC7B,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAwGlB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAeT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,iBAAsB,GAC9B,IAAI,CAqEN;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CA4BxE"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AC Coverage Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates Acceptance Criteria coverage by tasks:
|
|
5
|
+
* - Detects uncovered ACs (ACs with no implementing tasks)
|
|
6
|
+
* - Detects orphan tasks (tasks with no AC linkage)
|
|
7
|
+
* - Builds traceability matrix (AC ā Task mapping)
|
|
8
|
+
* - Calculates coverage metrics
|
|
9
|
+
*
|
|
10
|
+
* Used by `/specweave:validate` and `/specweave:done` commands to ensure
|
|
11
|
+
* all acceptance criteria are covered before increment closure.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { parseSpecMd } from '../generators/spec/spec-parser.js';
|
|
16
|
+
import { parseTasksWithUSLinks, getAllTasks } from '../generators/spec/task-parser.js';
|
|
17
|
+
/**
|
|
18
|
+
* Validate AC coverage for an increment
|
|
19
|
+
*
|
|
20
|
+
* @param incrementPath - Path to increment directory (e.g., .specweave/increments/0047-us-task-linkage)
|
|
21
|
+
* @param options - Validation options
|
|
22
|
+
* @returns AC Coverage Report
|
|
23
|
+
* @throws Error if spec.md or tasks.md cannot be read
|
|
24
|
+
*/
|
|
25
|
+
export function validateACCoverage(incrementPath, options = {}) {
|
|
26
|
+
const logger = options.logger ?? console;
|
|
27
|
+
// Validate paths
|
|
28
|
+
const specPath = path.join(incrementPath, 'spec.md');
|
|
29
|
+
const tasksPath = path.join(incrementPath, 'tasks.md');
|
|
30
|
+
if (!existsSync(specPath)) {
|
|
31
|
+
throw new Error(`spec.md not found at ${specPath}`);
|
|
32
|
+
}
|
|
33
|
+
if (!existsSync(tasksPath)) {
|
|
34
|
+
throw new Error(`tasks.md not found at ${tasksPath}`);
|
|
35
|
+
}
|
|
36
|
+
// Parse spec.md to get all ACs
|
|
37
|
+
logger.log('š Parsing spec.md to extract Acceptance Criteria...');
|
|
38
|
+
const specMetadata = parseSpecMd(specPath);
|
|
39
|
+
logger.log(` Found ${specMetadata.allACIds.length} Acceptance Criteria across ${specMetadata.userStories.length} User Stories`);
|
|
40
|
+
// Parse tasks.md to get all tasks
|
|
41
|
+
logger.log('š Parsing tasks.md to extract tasks...');
|
|
42
|
+
const tasksByUS = parseTasksWithUSLinks(tasksPath);
|
|
43
|
+
const allTasks = getAllTasks(tasksByUS);
|
|
44
|
+
logger.log(` Found ${allTasks.length} tasks`);
|
|
45
|
+
// Build traceability matrices
|
|
46
|
+
logger.log('š Building traceability matrix...');
|
|
47
|
+
const acToTasksMap = new Map();
|
|
48
|
+
const taskToACsMap = new Map();
|
|
49
|
+
const orphanTasks = [];
|
|
50
|
+
// Initialize AC map with empty arrays
|
|
51
|
+
specMetadata.allACIds.forEach(acId => {
|
|
52
|
+
acToTasksMap.set(acId, []);
|
|
53
|
+
});
|
|
54
|
+
// Populate mappings from tasks
|
|
55
|
+
allTasks.forEach(task => {
|
|
56
|
+
if (!task.satisfiesACs || task.satisfiesACs.length === 0) {
|
|
57
|
+
// Task has no AC linkage - mark as orphan
|
|
58
|
+
orphanTasks.push(task.id);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Store task ā ACs mapping
|
|
62
|
+
taskToACsMap.set(task.id, task.satisfiesACs);
|
|
63
|
+
// Store AC ā tasks mapping
|
|
64
|
+
task.satisfiesACs.forEach(acId => {
|
|
65
|
+
// Only count if AC exists in spec.md (valid AC-ID)
|
|
66
|
+
if (acToTasksMap.has(acId)) {
|
|
67
|
+
const tasks = acToTasksMap.get(acId);
|
|
68
|
+
tasks.push(task.id);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
// Calculate coverage
|
|
73
|
+
const uncoveredACs = [];
|
|
74
|
+
acToTasksMap.forEach((tasks, acId) => {
|
|
75
|
+
if (tasks.length === 0) {
|
|
76
|
+
uncoveredACs.push(acId);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const coveredACs = specMetadata.allACIds.length - uncoveredACs.length;
|
|
80
|
+
const coveragePercentage = specMetadata.allACIds.length > 0
|
|
81
|
+
? Math.round((coveredACs / specMetadata.allACIds.length) * 100)
|
|
82
|
+
: 100;
|
|
83
|
+
// Calculate per-US coverage
|
|
84
|
+
const coverageByUS = new Map();
|
|
85
|
+
specMetadata.userStories.forEach(us => {
|
|
86
|
+
const usACs = us.acceptanceCriteria;
|
|
87
|
+
const usUncoveredACs = usACs.filter(acId => uncoveredACs.includes(acId));
|
|
88
|
+
const usCoveredACs = usACs.length - usUncoveredACs.length;
|
|
89
|
+
const usCoveragePercentage = usACs.length > 0
|
|
90
|
+
? Math.round((usCoveredACs / usACs.length) * 100)
|
|
91
|
+
: 100;
|
|
92
|
+
coverageByUS.set(us.id, {
|
|
93
|
+
usId: us.id,
|
|
94
|
+
title: us.title,
|
|
95
|
+
totalACs: usACs.length,
|
|
96
|
+
coveredACs: usCoveredACs,
|
|
97
|
+
coveragePercentage: usCoveragePercentage,
|
|
98
|
+
uncoveredACs: usUncoveredACs
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
logger.log(` Coverage: ${coveredACs}/${specMetadata.allACIds.length} ACs (${coveragePercentage}%)`);
|
|
102
|
+
logger.log(` Orphan tasks: ${orphanTasks.length}`);
|
|
103
|
+
return {
|
|
104
|
+
totalACs: specMetadata.allACIds.length,
|
|
105
|
+
coveredACs,
|
|
106
|
+
uncoveredACs,
|
|
107
|
+
orphanTasks,
|
|
108
|
+
coveragePercentage,
|
|
109
|
+
acToTasksMap,
|
|
110
|
+
taskToACsMap,
|
|
111
|
+
coverageByUS,
|
|
112
|
+
timestamp: new Date().toISOString()
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if coverage report passes validation
|
|
117
|
+
*
|
|
118
|
+
* @param report - Coverage report
|
|
119
|
+
* @param options - Validation options
|
|
120
|
+
* @returns True if validation passes, false otherwise
|
|
121
|
+
*/
|
|
122
|
+
export function isCoveragePassing(report, options = {}) {
|
|
123
|
+
const minCoverage = options.minCoveragePercentage ?? 100;
|
|
124
|
+
const allowOrphans = options.allowOrphans ?? false;
|
|
125
|
+
// Check coverage percentage
|
|
126
|
+
if (report.coveragePercentage < minCoverage) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Check orphan tasks
|
|
130
|
+
if (!allowOrphans && report.orphanTasks.length > 0) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Print detailed coverage report to console
|
|
137
|
+
*
|
|
138
|
+
* @param report - Coverage report
|
|
139
|
+
* @param options - Validation options
|
|
140
|
+
*/
|
|
141
|
+
export function printCoverageReport(report, options = {}) {
|
|
142
|
+
const logger = options.logger ?? console;
|
|
143
|
+
logger.log('\n' + '='.repeat(80));
|
|
144
|
+
logger.log('š Acceptance Criteria Coverage Report');
|
|
145
|
+
logger.log('='.repeat(80));
|
|
146
|
+
// Summary
|
|
147
|
+
logger.log('\nš Summary:');
|
|
148
|
+
logger.log(` Total ACs: ${report.totalACs}`);
|
|
149
|
+
logger.log(` Covered ACs: ${report.coveredACs}`);
|
|
150
|
+
logger.log(` Uncovered ACs: ${report.uncoveredACs.length}`);
|
|
151
|
+
logger.log(` Coverage: ${report.coveragePercentage}%`);
|
|
152
|
+
logger.log(` Orphan Tasks: ${report.orphanTasks.length}`);
|
|
153
|
+
// Overall status
|
|
154
|
+
const passing = isCoveragePassing(report, options);
|
|
155
|
+
if (passing) {
|
|
156
|
+
logger.log('\nā
VALIDATION PASSED');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
logger.log('\nā VALIDATION FAILED');
|
|
160
|
+
}
|
|
161
|
+
// Per-User Story breakdown
|
|
162
|
+
if (report.coverageByUS.size > 0) {
|
|
163
|
+
logger.log('\nš Coverage by User Story:');
|
|
164
|
+
report.coverageByUS.forEach(stats => {
|
|
165
|
+
const icon = stats.coveragePercentage === 100 ? 'ā
' : 'ā ļø';
|
|
166
|
+
logger.log(` ${icon} ${stats.usId}: ${stats.coveredACs}/${stats.totalACs} ACs (${stats.coveragePercentage}%)`);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Uncovered ACs
|
|
170
|
+
if (report.uncoveredACs.length > 0) {
|
|
171
|
+
logger.log('\nā ļø Uncovered Acceptance Criteria:');
|
|
172
|
+
report.coverageByUS.forEach(stats => {
|
|
173
|
+
if (stats.uncoveredACs.length > 0) {
|
|
174
|
+
logger.log(` ${stats.usId} - ${stats.title}:`);
|
|
175
|
+
stats.uncoveredACs.forEach(acId => {
|
|
176
|
+
logger.log(` - ${acId}`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
logger.log('\nš” Suggestion: Create tasks that satisfy these ACs or mark them as not applicable');
|
|
181
|
+
}
|
|
182
|
+
// Orphan tasks
|
|
183
|
+
if (report.orphanTasks.length > 0) {
|
|
184
|
+
logger.log('\nā ļø Orphan Tasks (no AC linkage):');
|
|
185
|
+
report.orphanTasks.forEach(taskId => {
|
|
186
|
+
logger.log(` - ${taskId}`);
|
|
187
|
+
});
|
|
188
|
+
logger.log('\nš” Suggestion: Add **Satisfies ACs** field to these tasks');
|
|
189
|
+
}
|
|
190
|
+
// Traceability matrix (only show if requested via debug mode)
|
|
191
|
+
if (process.env.DEBUG) {
|
|
192
|
+
logger.log('\nš Traceability Matrix:');
|
|
193
|
+
report.acToTasksMap.forEach((tasks, acId) => {
|
|
194
|
+
if (tasks.length > 0) {
|
|
195
|
+
logger.log(` ${acId} ā ${tasks.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
logger.log('\n' + '='.repeat(80));
|
|
200
|
+
logger.log(`Generated: ${report.timestamp}\n`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Export coverage report to JSON file
|
|
204
|
+
*
|
|
205
|
+
* @param report - Coverage report
|
|
206
|
+
* @param outputPath - Path to output JSON file
|
|
207
|
+
*/
|
|
208
|
+
export async function exportCoverageReportJSON(report, outputPath) {
|
|
209
|
+
const fs = await import('fs-extra');
|
|
210
|
+
// Convert Maps to plain objects for JSON serialization
|
|
211
|
+
const serializable = {
|
|
212
|
+
totalACs: report.totalACs,
|
|
213
|
+
coveredACs: report.coveredACs,
|
|
214
|
+
uncoveredACs: report.uncoveredACs,
|
|
215
|
+
orphanTasks: report.orphanTasks,
|
|
216
|
+
coveragePercentage: report.coveragePercentage,
|
|
217
|
+
acToTasksMap: Object.fromEntries(report.acToTasksMap),
|
|
218
|
+
taskToACsMap: Object.fromEntries(report.taskToACsMap),
|
|
219
|
+
coverageByUS: Object.fromEntries(report.coverageByUS),
|
|
220
|
+
timestamp: report.timestamp
|
|
221
|
+
};
|
|
222
|
+
await fs.writeJSON(outputPath, serializable, { spaces: 2 });
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get recommended actions based on coverage report
|
|
226
|
+
*
|
|
227
|
+
* @param report - Coverage report
|
|
228
|
+
* @returns Array of recommended actions
|
|
229
|
+
*/
|
|
230
|
+
export function getRecommendedActions(report) {
|
|
231
|
+
const actions = [];
|
|
232
|
+
if (report.uncoveredACs.length > 0) {
|
|
233
|
+
actions.push(`Create tasks to satisfy ${report.uncoveredACs.length} uncovered AC(s): ${report.uncoveredACs.slice(0, 3).join(', ')}${report.uncoveredACs.length > 3 ? '...' : ''}`);
|
|
234
|
+
}
|
|
235
|
+
if (report.orphanTasks.length > 0) {
|
|
236
|
+
actions.push(`Add **Satisfies ACs** field to ${report.orphanTasks.length} orphan task(s): ${report.orphanTasks.slice(0, 3).join(', ')}${report.orphanTasks.length > 3 ? '...' : ''}`);
|
|
237
|
+
}
|
|
238
|
+
if (report.coveragePercentage < 100) {
|
|
239
|
+
const gap = 100 - report.coveragePercentage;
|
|
240
|
+
actions.push(`Increase coverage by ${gap}% to reach 100% target`);
|
|
241
|
+
}
|
|
242
|
+
// Check for User Stories with low coverage
|
|
243
|
+
report.coverageByUS.forEach(stats => {
|
|
244
|
+
if (stats.coveragePercentage < 80) {
|
|
245
|
+
actions.push(`User Story ${stats.usId} has low coverage (${stats.coveragePercentage}%) - prioritize tasks for this US`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return actions;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=ac-coverage-validator.js.map
|