sdd-mcp-server 4.0.0 → 5.0.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/CHANGELOG.md +884 -0
- package/README.md +18 -35
- package/dist/adapters/cli/SDDToolAdapter.d.ts +2 -5
- package/dist/adapters/cli/SDDToolAdapter.js +48 -92
- package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
- package/dist/application/services/ContextCompactionService.d.ts +10 -3
- package/dist/application/services/ContextCompactionService.js +139 -35
- package/dist/application/services/ContextCompactionService.js.map +1 -1
- package/dist/application/services/ProjectService.js +3 -3
- package/dist/application/services/ProjectService.js.map +1 -1
- package/dist/application/services/SpecPathResolver.js +1 -1
- package/dist/application/services/SpecPathResolver.js.map +1 -1
- package/dist/application/services/WorkflowEngineService.d.ts +160 -50
- package/dist/application/services/WorkflowEngineService.js +1404 -429
- package/dist/application/services/WorkflowEngineService.js.map +1 -1
- package/dist/application/services/WorkflowErrors.d.ts +16 -0
- package/dist/application/services/WorkflowErrors.js +53 -0
- package/dist/application/services/WorkflowErrors.js.map +1 -0
- package/dist/application/services/WorkflowValidationService.d.ts +25 -46
- package/dist/application/services/WorkflowValidationService.js +308 -627
- package/dist/application/services/WorkflowValidationService.js.map +1 -1
- package/dist/cli/install-skills.js +0 -0
- package/dist/cli/install-skills.js.map +1 -1
- package/dist/cli/install-target.d.ts +4 -1
- package/dist/cli/install-target.js +4 -0
- package/dist/cli/install-target.js.map +1 -1
- package/dist/cli/sdd-mcp-cli.js +0 -0
- package/dist/cli/tool-support/claude-code.js +7 -3
- package/dist/cli/tool-support/claude-code.js.map +1 -1
- package/dist/cli/tool-support/codex.js +6 -2
- package/dist/cli/tool-support/codex.js.map +1 -1
- package/dist/cli/tool-support/mcp-registration.d.ts +22 -0
- package/dist/cli/tool-support/mcp-registration.js +275 -0
- package/dist/cli/tool-support/mcp-registration.js.map +1 -0
- package/dist/cli/tool-support/omp.js +6 -2
- package/dist/cli/tool-support/omp.js.map +1 -1
- package/dist/cli/tool-support/root-guidance.js +2 -2
- package/dist/cli/tool-support/root-guidance.js.map +1 -1
- package/dist/cli/tool-support/target-installer.d.ts +2 -2
- package/dist/cli/tool-support/target-installer.js +9 -3
- package/dist/cli/tool-support/target-installer.js.map +1 -1
- package/dist/cli/utils/preserving-writer.d.ts +35 -1
- package/dist/cli/utils/preserving-writer.js +479 -108
- package/dist/cli/utils/preserving-writer.js.map +1 -1
- package/dist/domain/types.d.ts +52 -7
- package/dist/domain/types.js +5 -4
- package/dist/domain/types.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/infrastructure/mcp/MCPServer.js +13 -13
- package/dist/infrastructure/mcp/MCPServer.js.map +1 -1
- package/dist/infrastructure/mcp/ToolRegistry.d.ts +5 -1
- package/dist/infrastructure/mcp/ToolRegistry.js +11 -4
- package/dist/infrastructure/mcp/ToolRegistry.js.map +1 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.js +76 -90
- package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -1
- package/dist/infrastructure/schemas/project.schema.d.ts +2 -2
- package/dist/infrastructure/schemas/project.schema.js +2 -2
- package/dist/infrastructure/schemas/project.schema.js.map +1 -1
- package/dist/shared/version.d.ts +3 -0
- package/dist/shared/version.js +4 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/utils/atomicWrite.js +20 -5
- package/dist/utils/atomicWrite.js.map +1 -1
- package/dist/utils/withFilesystemLock.d.ts +22 -0
- package/dist/utils/withFilesystemLock.js +219 -0
- package/dist/utils/withFilesystemLock.js.map +1 -0
- package/package.json +5 -2
- package/skills/sdd-design/REFERENCE.md +16 -0
- package/skills/sdd-design/SKILL.md +22 -13
- package/skills/sdd-implement/REFERENCE.md +4 -0
- package/skills/sdd-implement/SKILL.md +20 -16
- package/skills/sdd-requirements/REFERENCE.md +17 -7
- package/skills/sdd-requirements/SKILL.md +30 -22
- package/skills/sdd-tasks/REFERENCE.md +9 -9
- package/skills/sdd-tasks/SKILL.md +27 -15
- package/templates/CLAUDE.md +6 -12
- package/templates/codex-AGENTS.md +7 -9
|
@@ -4,662 +4,343 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
isComplete: false,
|
|
41
|
-
missingItems: ['Project not found'],
|
|
42
|
-
qualityIssues: [],
|
|
43
|
-
readinessScore: 0,
|
|
44
|
-
recommendations: []
|
|
45
|
-
};
|
|
7
|
+
import { injectable } from 'inversify';
|
|
8
|
+
const REQUIRED_DESIGN_HEADINGS = [
|
|
9
|
+
'Requirements Traceability',
|
|
10
|
+
'Architecture and Data Flow',
|
|
11
|
+
'Components and Interfaces',
|
|
12
|
+
'Failure Handling',
|
|
13
|
+
'Verification',
|
|
14
|
+
];
|
|
15
|
+
function resultStatus(blockers) {
|
|
16
|
+
return blockers.length === 0 ? 'passed' : 'failed';
|
|
17
|
+
}
|
|
18
|
+
function bounded(blockers) {
|
|
19
|
+
return blockers.slice(0, 100);
|
|
20
|
+
}
|
|
21
|
+
function blocker(code, message, reference) {
|
|
22
|
+
const boundedMessage = message.slice(0, 2_000);
|
|
23
|
+
return reference === undefined
|
|
24
|
+
? { code: code.slice(0, 100), message: boundedMessage }
|
|
25
|
+
: { code: code.slice(0, 100), message: boundedMessage, reference: reference.slice(0, 200) };
|
|
26
|
+
}
|
|
27
|
+
/** Removes fenced regions while preserving line boundaries for deterministic parsing. */
|
|
28
|
+
function visibleLines(content) {
|
|
29
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n');
|
|
30
|
+
let fence;
|
|
31
|
+
return lines.map((line) => {
|
|
32
|
+
const match = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
33
|
+
if (!fence) {
|
|
34
|
+
if (match) {
|
|
35
|
+
fence = { marker: match[1][0], length: match[1].length };
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
return line;
|
|
46
39
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
// Check for EARS format patterns
|
|
64
|
-
const earsPatterns = ['WHEN', 'WHERE', 'IF', 'THEN', 'SHALL'];
|
|
65
|
-
const hasEarsFormat = earsPatterns.some(pattern => content.includes(pattern));
|
|
66
|
-
if (!hasEarsFormat) {
|
|
67
|
-
qualityIssues.push('Requirements do not follow EARS format (missing WHEN/WHERE/IF...THEN/SHALL patterns)');
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
readinessScore += 30;
|
|
71
|
-
}
|
|
72
|
-
// Check for acceptance criteria
|
|
73
|
-
if (content.includes('Acceptance Criteria') || content.includes('acceptance criteria')) {
|
|
74
|
-
readinessScore += 20;
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
missingItems.push('Acceptance criteria sections');
|
|
78
|
-
}
|
|
79
|
-
// Check for requirement numbering
|
|
80
|
-
const requirementCount = (content.match(/### Requirement \d+:/g) || []).length;
|
|
81
|
-
if (requirementCount === 0) {
|
|
82
|
-
missingItems.push('Numbered requirement sections');
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
readinessScore += 20;
|
|
86
|
-
}
|
|
87
|
-
if (requirementCount >= 3) {
|
|
88
|
-
readinessScore += 10; // Bonus for having multiple requirements
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Check approval status
|
|
92
|
-
if (project.metadata.approvals.requirements.generated) {
|
|
93
|
-
readinessScore += 10;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
missingItems.push('Requirements generation flag');
|
|
97
|
-
}
|
|
98
|
-
if (project.metadata.approvals.requirements.approved) {
|
|
99
|
-
readinessScore += 10;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
recommendations.push('Requirements need review and approval before proceeding to design');
|
|
103
|
-
}
|
|
104
|
-
// Generate recommendations
|
|
105
|
-
if (missingItems.length > 0) {
|
|
106
|
-
recommendations.push('Complete missing requirements items before approval');
|
|
107
|
-
}
|
|
108
|
-
if (qualityIssues.length > 0) {
|
|
109
|
-
recommendations.push('Address quality issues to improve requirements clarity');
|
|
110
|
-
}
|
|
111
|
-
if (readinessScore >= 80) {
|
|
112
|
-
recommendations.push('Requirements are ready for design phase');
|
|
113
|
-
}
|
|
114
|
-
else if (readinessScore >= 60) {
|
|
115
|
-
recommendations.push('Requirements need minor improvements before design');
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
recommendations.push('Requirements need significant work before design phase');
|
|
119
|
-
}
|
|
120
|
-
this.logger.info('Requirements validation completed', {
|
|
121
|
-
correlationId,
|
|
122
|
-
projectId,
|
|
123
|
-
readinessScore,
|
|
124
|
-
missingCount: missingItems.length,
|
|
125
|
-
qualityIssueCount: qualityIssues.length
|
|
126
|
-
});
|
|
127
|
-
return {
|
|
128
|
-
isComplete: missingItems.length === 0 && qualityIssues.length === 0,
|
|
129
|
-
missingItems,
|
|
130
|
-
qualityIssues,
|
|
131
|
-
readinessScore: Math.min(readinessScore, 100),
|
|
132
|
-
recommendations
|
|
133
|
-
};
|
|
40
|
+
const close = new RegExp(`^\\s*${fence.marker}{${fence.length},}\\s*$`);
|
|
41
|
+
if (close.test(line))
|
|
42
|
+
fence = undefined;
|
|
43
|
+
return '';
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function parseSections(lines, heading) {
|
|
47
|
+
const sections = [];
|
|
48
|
+
let current;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const headingMatch = heading.exec(line);
|
|
51
|
+
if (headingMatch) {
|
|
52
|
+
current = { id: headingMatch[1], title: headingMatch[2].trim(), body: [] };
|
|
53
|
+
sections.push(current);
|
|
54
|
+
continue;
|
|
134
55
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
projectId
|
|
139
|
-
});
|
|
140
|
-
return {
|
|
141
|
-
isComplete: false,
|
|
142
|
-
missingItems: [`Validation error: ${error.message}`],
|
|
143
|
-
qualityIssues: [],
|
|
144
|
-
readinessScore: 0,
|
|
145
|
-
recommendations: ['Fix validation errors and try again']
|
|
146
|
-
};
|
|
56
|
+
if (/^#{1,3}(?:\s|$)/.test(line)) {
|
|
57
|
+
current = undefined;
|
|
58
|
+
continue;
|
|
147
59
|
}
|
|
60
|
+
current?.body.push(line);
|
|
148
61
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const designSections = [
|
|
183
|
-
'Architecture',
|
|
184
|
-
'Components',
|
|
185
|
-
'Interface',
|
|
186
|
-
'Data Model',
|
|
187
|
-
'Technology'
|
|
188
|
-
];
|
|
189
|
-
let foundSections = 0;
|
|
190
|
-
for (const section of designSections) {
|
|
191
|
-
if (content.toLowerCase().includes(section.toLowerCase())) {
|
|
192
|
-
foundSections++;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
readinessScore += (foundSections / designSections.length) * 40;
|
|
196
|
-
if (foundSections < 3) {
|
|
197
|
-
missingItems.push(`Design missing key sections (found ${foundSections}/${designSections.length})`);
|
|
198
|
-
}
|
|
199
|
-
// Check for architectural diagrams or descriptions
|
|
200
|
-
if (content.includes('```') || content.includes('diagram') || content.includes('architecture')) {
|
|
201
|
-
readinessScore += 20;
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
qualityIssues.push('Design lacks architectural diagrams or detailed descriptions');
|
|
205
|
-
}
|
|
206
|
-
// Check for technology decisions
|
|
207
|
-
if (content.toLowerCase().includes('technology') || content.toLowerCase().includes('stack')) {
|
|
208
|
-
readinessScore += 15;
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
missingItems.push('Technology stack decisions');
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Validate requirements are approved first
|
|
215
|
-
if (!project.metadata.approvals.requirements.approved) {
|
|
216
|
-
missingItems.push('Requirements must be approved before design approval');
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
readinessScore += 15;
|
|
220
|
-
}
|
|
221
|
-
// Check approval status
|
|
222
|
-
if (project.metadata.approvals.design.generated) {
|
|
223
|
-
readinessScore += 5;
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
missingItems.push('Design generation flag');
|
|
227
|
-
}
|
|
228
|
-
if (project.metadata.approvals.design.approved) {
|
|
229
|
-
readinessScore += 5;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
recommendations.push('Design needs review and approval before proceeding to tasks');
|
|
233
|
-
}
|
|
234
|
-
// Generate recommendations
|
|
235
|
-
if (readinessScore >= 80) {
|
|
236
|
-
recommendations.push('Design is ready for tasks phase');
|
|
237
|
-
}
|
|
238
|
-
else if (readinessScore >= 60) {
|
|
239
|
-
recommendations.push('Design needs minor improvements before tasks');
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
recommendations.push('Design needs significant work before tasks phase');
|
|
243
|
-
}
|
|
244
|
-
this.logger.info('Design validation completed', {
|
|
245
|
-
correlationId,
|
|
246
|
-
projectId,
|
|
247
|
-
readinessScore,
|
|
248
|
-
missingCount: missingItems.length,
|
|
249
|
-
qualityIssueCount: qualityIssues.length
|
|
250
|
-
});
|
|
251
|
-
return {
|
|
252
|
-
isComplete: missingItems.length === 0 && qualityIssues.length === 0,
|
|
253
|
-
missingItems,
|
|
254
|
-
qualityIssues,
|
|
255
|
-
readinessScore: Math.min(readinessScore, 100),
|
|
256
|
-
recommendations
|
|
257
|
-
};
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const duplicates = [];
|
|
64
|
+
for (const section of sections) {
|
|
65
|
+
if (seen.has(section.id))
|
|
66
|
+
duplicates.push(section.id);
|
|
67
|
+
seen.add(section.id);
|
|
68
|
+
}
|
|
69
|
+
return { sections, duplicates: [...new Set(duplicates)] };
|
|
70
|
+
}
|
|
71
|
+
const ANY_METADATA_LABEL = /^\s*\*\*[^*]+:\*\*/;
|
|
72
|
+
/**
|
|
73
|
+
* A metadata value may sit inline after the label or, per the documented
|
|
74
|
+
* requirements shape (skills/sdd-requirements/REFERENCE.md), on the lines
|
|
75
|
+
* that follow a label with nothing after it (e.g. `**Acceptance Criteria:**`
|
|
76
|
+
* followed by a numbered list). Collect both forms.
|
|
77
|
+
*/
|
|
78
|
+
function metadataValues(section, label) {
|
|
79
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
80
|
+
const start = new RegExp(`^\\s*\\*\\*${escaped}:\\*\\*\\s*(.*)$`);
|
|
81
|
+
const body = section.body;
|
|
82
|
+
const values = [];
|
|
83
|
+
for (let i = 0; i < body.length; i++) {
|
|
84
|
+
const match = start.exec(body[i]);
|
|
85
|
+
if (!match)
|
|
86
|
+
continue;
|
|
87
|
+
const collected = [];
|
|
88
|
+
const inline = match[1].trim();
|
|
89
|
+
if (inline)
|
|
90
|
+
collected.push(inline);
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
while (j < body.length && !ANY_METADATA_LABEL.test(body[j])) {
|
|
93
|
+
collected.push(body[j]);
|
|
94
|
+
j++;
|
|
258
95
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
correlationId,
|
|
262
|
-
projectId
|
|
263
|
-
});
|
|
264
|
-
return {
|
|
265
|
-
isComplete: false,
|
|
266
|
-
missingItems: [`Validation error: ${error.message}`],
|
|
267
|
-
qualityIssues: [],
|
|
268
|
-
readinessScore: 0,
|
|
269
|
-
recommendations: ['Fix validation errors and try again']
|
|
270
|
-
};
|
|
96
|
+
while (collected.length > 0 && collected[collected.length - 1] === '') {
|
|
97
|
+
collected.pop();
|
|
271
98
|
}
|
|
99
|
+
const value = collected.join('\n').trim();
|
|
100
|
+
if (value)
|
|
101
|
+
values.push(value);
|
|
102
|
+
i = j - 1;
|
|
272
103
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
104
|
+
return values;
|
|
105
|
+
}
|
|
106
|
+
function metadata(section, label) {
|
|
107
|
+
return metadataValues(section, label)[0];
|
|
108
|
+
}
|
|
109
|
+
function parseList(value) {
|
|
110
|
+
if (!value || value.trim() === 'none')
|
|
111
|
+
return [];
|
|
112
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
function hasDuplicates(values) {
|
|
115
|
+
return new Set(values).size !== values.length;
|
|
116
|
+
}
|
|
117
|
+
function isProjectRelativePath(value) {
|
|
118
|
+
return value.length > 0
|
|
119
|
+
&& value.length <= 500
|
|
120
|
+
&& !/^[/\\]/.test(value)
|
|
121
|
+
&& !/^[A-Za-z]:/.test(value)
|
|
122
|
+
&& !value.includes('\0')
|
|
123
|
+
&& !value.split(/[\\/]/).includes('..')
|
|
124
|
+
&& value === value.trim();
|
|
125
|
+
}
|
|
126
|
+
function uniqueSections(sections) {
|
|
127
|
+
const byId = new Map();
|
|
128
|
+
for (const section of sections)
|
|
129
|
+
if (!byId.has(section.id))
|
|
130
|
+
byId.set(section.id, section);
|
|
131
|
+
return [...byId.values()];
|
|
132
|
+
}
|
|
133
|
+
function extractRequirementIds(content) {
|
|
134
|
+
const { sections } = parseSections(visibleLines(content), /^###\s+((?:FR|NFR)-\d+):\s*(.+?)\s*$/);
|
|
135
|
+
return uniqueSections(sections).map(({ id }) => id);
|
|
136
|
+
}
|
|
137
|
+
function extractDecisionIds(content) {
|
|
138
|
+
const { sections } = parseSections(visibleLines(content), /^###\s+(D-\d+):\s*(.+?)\s*$/);
|
|
139
|
+
return uniqueSections(sections).map(({ id }) => id);
|
|
140
|
+
}
|
|
141
|
+
function addDuplicateBlockers(blockers, duplicates) {
|
|
142
|
+
for (const id of duplicates)
|
|
143
|
+
blockers.push(blocker('DuplicateId', `Identifier is declared more than once: ${id}`, id));
|
|
144
|
+
}
|
|
145
|
+
function requireMetadata(blockers, section, labels) {
|
|
146
|
+
const values = {};
|
|
147
|
+
for (const label of labels) {
|
|
148
|
+
const matches = metadataValues(section, label);
|
|
149
|
+
values[label] = matches[0];
|
|
150
|
+
if (!values[label])
|
|
151
|
+
blockers.push(blocker('MissingMetadata', `Missing or empty **${label}:** metadata`, section.id));
|
|
152
|
+
if (matches.length > 1)
|
|
153
|
+
blockers.push(blocker('DuplicateMetadata', `Metadata label appears more than once: **${label}:**`, section.id));
|
|
154
|
+
}
|
|
155
|
+
return values;
|
|
156
|
+
}
|
|
157
|
+
function coverageBlockers(actual, expected) {
|
|
158
|
+
return expected
|
|
159
|
+
.filter((id) => !actual.has(id))
|
|
160
|
+
.map((id) => blocker('CoverageMissing', `Required identifier is not covered: ${id}`, id));
|
|
161
|
+
}
|
|
162
|
+
let WorkflowValidationService = class WorkflowValidationService {
|
|
163
|
+
validateRequirements(content) {
|
|
164
|
+
const blockers = [];
|
|
165
|
+
if (content.trim().length === 0)
|
|
166
|
+
blockers.push(blocker('EmptyContent', 'Requirements content must not be empty'));
|
|
167
|
+
const { sections, duplicates } = parseSections(visibleLines(content), /^###\s+((?:FR|NFR)-\d+):\s*(.+?)\s*$/);
|
|
168
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
169
|
+
if (sections.length === 0)
|
|
170
|
+
blockers.push(blocker('RequirementsMissing', 'At least one FR-N requirement section is required'));
|
|
171
|
+
if (!sections.some(({ id }) => id.startsWith('FR-'))) {
|
|
172
|
+
blockers.push(blocker('FunctionalRequirementMissing', 'At least one FR-N requirement section is required'));
|
|
288
173
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// Check if tasks document exists
|
|
295
|
-
const tasksPath = `${project.path}/.spec/specs/${project.name}/tasks.md`;
|
|
296
|
-
if (!(await this.fileSystem.exists(tasksPath))) {
|
|
297
|
-
missingItems.push('tasks.md file not found');
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
const content = await this.fileSystem.readFile(tasksPath);
|
|
301
|
-
// Basic content validation
|
|
302
|
-
if (content.trim().length < 150) {
|
|
303
|
-
qualityIssues.push('Tasks document is too short (less than 150 characters)');
|
|
304
|
-
}
|
|
305
|
-
// Count tasks
|
|
306
|
-
const taskMatches = content.match(/- \[ \] \d+\./g) || [];
|
|
307
|
-
const taskCount = taskMatches.length;
|
|
308
|
-
if (taskCount === 0) {
|
|
309
|
-
missingItems.push('No tasks found in checkbox format');
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
readinessScore += Math.min(taskCount * 5, 30); // Up to 30 points for tasks
|
|
313
|
-
}
|
|
314
|
-
// Check for requirement references
|
|
315
|
-
const requirementRefs = (content.match(/_Requirements: /g) || []).length;
|
|
316
|
-
if (requirementRefs === 0) {
|
|
317
|
-
qualityIssues.push('Tasks missing requirement traceability references');
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
readinessScore += 20;
|
|
321
|
-
}
|
|
322
|
-
// Check for subtasks
|
|
323
|
-
const subtaskMatches = content.match(/ - [A-Z]/g) || [];
|
|
324
|
-
if (subtaskMatches.length === 0) {
|
|
325
|
-
qualityIssues.push('Tasks missing detailed subtasks');
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
readinessScore += 15;
|
|
329
|
-
}
|
|
330
|
-
// Check task categories/organization
|
|
331
|
-
if (content.includes('# Implementation Plan') || content.includes('## ')) {
|
|
332
|
-
readinessScore += 10;
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
qualityIssues.push('Tasks lack proper organization/categorization');
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Validate design is approved first
|
|
339
|
-
if (!project.metadata.approvals.design.approved) {
|
|
340
|
-
missingItems.push('Design must be approved before task approval');
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
readinessScore += 15;
|
|
344
|
-
}
|
|
345
|
-
// Check approval status
|
|
346
|
-
if (project.metadata.approvals.tasks.generated) {
|
|
347
|
-
readinessScore += 5;
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
missingItems.push('Tasks generation flag');
|
|
351
|
-
}
|
|
352
|
-
if (project.metadata.approvals.tasks.approved) {
|
|
353
|
-
readinessScore += 5;
|
|
174
|
+
for (const section of uniqueSections(sections)) {
|
|
175
|
+
const values = requireMetadata(blockers, section, ['Objective', 'EARS Specification', 'Acceptance Criteria']);
|
|
176
|
+
const ears = values['EARS Specification'];
|
|
177
|
+
if (ears && !/\bSHALL\b/.test(ears)) {
|
|
178
|
+
blockers.push(blocker('EarsShallMissing', 'EARS Specification must contain SHALL', section.id));
|
|
354
179
|
}
|
|
355
|
-
|
|
356
|
-
|
|
180
|
+
const acceptance = values['Acceptance Criteria'];
|
|
181
|
+
if (acceptance && !/(?:^|\s)\d+\.\s+\S/.test(acceptance)) {
|
|
182
|
+
blockers.push(blocker('AcceptanceCriteriaMissing', 'Acceptance Criteria must contain a numbered item', section.id));
|
|
357
183
|
}
|
|
358
|
-
// Generate recommendations
|
|
359
|
-
if (readinessScore >= 80) {
|
|
360
|
-
recommendations.push('Tasks are ready for implementation phase');
|
|
361
|
-
recommendations.push('Begin implementing tasks in order of priority');
|
|
362
|
-
}
|
|
363
|
-
else if (readinessScore >= 60) {
|
|
364
|
-
recommendations.push('Tasks need minor improvements before implementation');
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
recommendations.push('Tasks need significant work before implementation phase');
|
|
368
|
-
}
|
|
369
|
-
this.logger.info('Tasks validation completed', {
|
|
370
|
-
correlationId,
|
|
371
|
-
projectId,
|
|
372
|
-
readinessScore,
|
|
373
|
-
missingCount: missingItems.length,
|
|
374
|
-
qualityIssueCount: qualityIssues.length
|
|
375
|
-
});
|
|
376
|
-
return {
|
|
377
|
-
isComplete: missingItems.length === 0 && qualityIssues.length === 0,
|
|
378
|
-
missingItems,
|
|
379
|
-
qualityIssues,
|
|
380
|
-
readinessScore: Math.min(readinessScore, 100),
|
|
381
|
-
recommendations
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
this.logger.error('Tasks validation failed', error, {
|
|
386
|
-
correlationId,
|
|
387
|
-
projectId
|
|
388
|
-
});
|
|
389
|
-
return {
|
|
390
|
-
isComplete: false,
|
|
391
|
-
missingItems: [`Validation error: ${error.message}`],
|
|
392
|
-
qualityIssues: [],
|
|
393
|
-
readinessScore: 0,
|
|
394
|
-
recommendations: ['Fix validation errors and try again']
|
|
395
|
-
};
|
|
396
184
|
}
|
|
185
|
+
const requirementIds = uniqueSections(sections).map(({ id }) => id);
|
|
186
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), requirementIds };
|
|
397
187
|
}
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
isConsistent: false,
|
|
408
|
-
inconsistencies: ['Project not found'],
|
|
409
|
-
traceabilityIssues: [],
|
|
410
|
-
coverageGaps: []
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const inconsistencies = [];
|
|
414
|
-
const traceabilityIssues = [];
|
|
415
|
-
const coverageGaps = [];
|
|
416
|
-
try {
|
|
417
|
-
const basePath = `${project.path}/.spec/specs/${project.name}`;
|
|
418
|
-
// Read all phase documents
|
|
419
|
-
const documents = {};
|
|
420
|
-
const files = ['requirements.md', 'design.md', 'tasks.md'];
|
|
421
|
-
for (const file of files) {
|
|
422
|
-
const filePath = `${basePath}/${file}`;
|
|
423
|
-
if (await this.fileSystem.exists(filePath)) {
|
|
424
|
-
documents[file] = await this.fileSystem.readFile(filePath);
|
|
425
|
-
}
|
|
188
|
+
validateDesign(content, approvedRequirementsContent) {
|
|
189
|
+
const blockers = [];
|
|
190
|
+
const lines = visibleLines(content);
|
|
191
|
+
if (content.trim().length === 0)
|
|
192
|
+
blockers.push(blocker('EmptyContent', 'Design content must not be empty'));
|
|
193
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
194
|
+
if (!lines.some((line) => line === `## ${heading}`)) {
|
|
195
|
+
blockers.push(blocker('MissingHeading', `Missing exact heading: ## ${heading}`, heading));
|
|
426
196
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
197
|
+
}
|
|
198
|
+
const { sections, duplicates } = parseSections(lines, /^###\s+(D-\d+):\s*(.+?)\s*$/);
|
|
199
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
200
|
+
if (sections.length === 0)
|
|
201
|
+
blockers.push(blocker('DesignDecisionMissing', 'At least one D-N decision section is required'));
|
|
202
|
+
const knownRequirements = extractRequirementIds(approvedRequirementsContent);
|
|
203
|
+
const knownSet = new Set(knownRequirements);
|
|
204
|
+
const covered = new Set();
|
|
205
|
+
for (const section of uniqueSections(sections)) {
|
|
206
|
+
const values = requireMetadata(blockers, section, ['Covers', 'Decision', 'Failure behavior', 'Verification']);
|
|
207
|
+
for (const id of parseList(values.Covers)) {
|
|
208
|
+
if (!knownSet.has(id)) {
|
|
209
|
+
blockers.push(blocker('UnknownCoverageId', `Covers references unknown requirement: ${id}`, section.id));
|
|
441
210
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (documents['design.md'] && documents['tasks.md']) {
|
|
445
|
-
const design = documents['design.md'];
|
|
446
|
-
const tasks = documents['tasks.md'];
|
|
447
|
-
// Look for component mentions in design
|
|
448
|
-
const componentMatches = design.match(/## (.+Component|.+Service|.+Manager)/g) || [];
|
|
449
|
-
const components = componentMatches.map(match => match.replace('## ', '').trim());
|
|
450
|
-
// Check if tasks cover all components
|
|
451
|
-
for (const component of components) {
|
|
452
|
-
const keywords = component.split(' ').filter(word => word.length > 3);
|
|
453
|
-
const hasCoverage = keywords.some(keyword => tasks.toLowerCase().includes(keyword.toLowerCase()));
|
|
454
|
-
if (!hasCoverage) {
|
|
455
|
-
coverageGaps.push(`Tasks may not cover design component: "${component}"`);
|
|
456
|
-
}
|
|
211
|
+
else {
|
|
212
|
+
covered.add(id);
|
|
457
213
|
}
|
|
458
214
|
}
|
|
459
|
-
// Validate phase consistency with project metadata
|
|
460
|
-
const { phase, metadata } = project;
|
|
461
|
-
if (phase === WorkflowPhase.REQUIREMENTS && !metadata.approvals.requirements.generated) {
|
|
462
|
-
inconsistencies.push('Project in requirements phase but requirements not marked as generated');
|
|
463
|
-
}
|
|
464
|
-
if (phase === WorkflowPhase.DESIGN && !metadata.approvals.design.generated) {
|
|
465
|
-
inconsistencies.push('Project in design phase but design not marked as generated');
|
|
466
|
-
}
|
|
467
|
-
if (phase === WorkflowPhase.TASKS && !metadata.approvals.tasks.generated) {
|
|
468
|
-
inconsistencies.push('Project in tasks phase but tasks not marked as generated');
|
|
469
|
-
}
|
|
470
|
-
// Check for orphaned approvals (approved but not generated)
|
|
471
|
-
Object.entries(metadata.approvals).forEach(([phaseName, approval]) => {
|
|
472
|
-
if (approval.approved && !approval.generated) {
|
|
473
|
-
inconsistencies.push(`${phaseName} marked as approved but not generated`);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
this.logger.info('Cross-phase validation completed', {
|
|
477
|
-
correlationId,
|
|
478
|
-
projectId,
|
|
479
|
-
inconsistencyCount: inconsistencies.length,
|
|
480
|
-
traceabilityIssueCount: traceabilityIssues.length,
|
|
481
|
-
coverageGapCount: coverageGaps.length
|
|
482
|
-
});
|
|
483
|
-
return {
|
|
484
|
-
isConsistent: inconsistencies.length === 0 &&
|
|
485
|
-
traceabilityIssues.length === 0 &&
|
|
486
|
-
coverageGaps.length === 0,
|
|
487
|
-
inconsistencies,
|
|
488
|
-
traceabilityIssues,
|
|
489
|
-
coverageGaps
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
catch (error) {
|
|
493
|
-
this.logger.error('Cross-phase validation failed', error, {
|
|
494
|
-
correlationId,
|
|
495
|
-
projectId
|
|
496
|
-
});
|
|
497
|
-
return {
|
|
498
|
-
isConsistent: false,
|
|
499
|
-
inconsistencies: [`Validation error: ${error.message}`],
|
|
500
|
-
traceabilityIssues: [],
|
|
501
|
-
coverageGaps: []
|
|
502
|
-
};
|
|
503
215
|
}
|
|
216
|
+
blockers.push(...coverageBlockers(covered, knownRequirements));
|
|
217
|
+
return {
|
|
218
|
+
status: resultStatus(blockers),
|
|
219
|
+
blockers: bounded(blockers),
|
|
220
|
+
decisionIds: uniqueSections(sections).map(({ id }) => id),
|
|
221
|
+
};
|
|
504
222
|
}
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return {
|
|
515
|
-
canRollback: false,
|
|
516
|
-
reason: 'Project not found',
|
|
517
|
-
impactedItems: [],
|
|
518
|
-
rollbackActions: []
|
|
519
|
-
};
|
|
223
|
+
parseTasks(content) {
|
|
224
|
+
const blockers = [];
|
|
225
|
+
const { sections, duplicates } = parseSections(visibleLines(content), /^###\s+(\d+(?:\.\d+)+)\s+(.+?)\s*$/);
|
|
226
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
227
|
+
if (sections.length === 0)
|
|
228
|
+
blockers.push(blocker('TaskMissing', 'At least one N.M task section is required'));
|
|
229
|
+
const unique = uniqueSections(sections);
|
|
230
|
+
if (unique.length > 1_000) {
|
|
231
|
+
blockers.push(blocker('TaskLimitExceeded', 'Tasks must contain at most 1000 unique task sections'));
|
|
520
232
|
}
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
233
|
+
const tasks = {};
|
|
234
|
+
for (const section of unique) {
|
|
235
|
+
if (section.id.length > 50) {
|
|
236
|
+
blockers.push(blocker('InvalidTaskId', 'Task identifiers must contain at most 50 characters', section.id.slice(0, 50)));
|
|
237
|
+
}
|
|
238
|
+
if (section.title.trim().length === 0 || section.title.length > 500) {
|
|
239
|
+
blockers.push(blocker('InvalidTaskTitle', 'Task titles must be non-whitespace and at most 500 characters', section.id.slice(0, 50)));
|
|
240
|
+
}
|
|
241
|
+
const values = requireMetadata(blockers, section, [
|
|
242
|
+
'Covers',
|
|
243
|
+
'Dependencies',
|
|
244
|
+
'TDD',
|
|
245
|
+
'Affected artifacts',
|
|
246
|
+
'Acceptance criteria',
|
|
247
|
+
'Verification',
|
|
248
|
+
]);
|
|
249
|
+
const tdd = values.TDD;
|
|
250
|
+
let tddRequired = false;
|
|
251
|
+
if (tdd === 'required')
|
|
252
|
+
tddRequired = true;
|
|
253
|
+
else if (!tdd || !/^not-applicable — \S(?:.*\S)?$/.test(tdd)) {
|
|
254
|
+
blockers.push(blocker('InvalidTdd', 'TDD must be required or not-applicable — <reason>', section.id));
|
|
255
|
+
}
|
|
256
|
+
const dependencies = parseList(values.Dependencies);
|
|
257
|
+
const plannedArtifacts = parseList(values['Affected artifacts']);
|
|
258
|
+
if (dependencies.includes('none') || plannedArtifacts.includes('none')) {
|
|
259
|
+
blockers.push(blocker('InvalidListValue', 'none must be the only value in an empty metadata list', section.id));
|
|
260
|
+
}
|
|
261
|
+
if (dependencies.length > 100) {
|
|
262
|
+
blockers.push(blocker('ListTooLong', 'Dependencies must contain at most 100 entries', section.id));
|
|
263
|
+
}
|
|
264
|
+
if (plannedArtifacts.length > 100) {
|
|
265
|
+
blockers.push(blocker('ListTooLong', 'Affected artifacts must contain at most 100 entries', section.id));
|
|
266
|
+
}
|
|
267
|
+
if (hasDuplicates(dependencies)) {
|
|
268
|
+
blockers.push(blocker('DuplicateListItem', 'Dependencies must not contain duplicate task IDs', section.id));
|
|
269
|
+
}
|
|
270
|
+
if (hasDuplicates(plannedArtifacts)) {
|
|
271
|
+
blockers.push(blocker('DuplicateListItem', 'Affected artifacts must not contain duplicate paths', section.id));
|
|
272
|
+
}
|
|
273
|
+
for (const artifact of plannedArtifacts) {
|
|
274
|
+
if (!isProjectRelativePath(artifact)) {
|
|
275
|
+
blockers.push(blocker('InvalidArtifactPath', `Affected artifact must be project-relative: ${artifact}`, section.id));
|
|
276
|
+
}
|
|
565
277
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
correlationId,
|
|
572
|
-
projectId,
|
|
573
|
-
currentPhase,
|
|
574
|
-
targetPhase,
|
|
575
|
-
impactedCount: impactedItems.length
|
|
576
|
-
});
|
|
577
|
-
return {
|
|
578
|
-
canRollback: true,
|
|
579
|
-
impactedItems,
|
|
580
|
-
rollbackActions
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
catch (error) {
|
|
584
|
-
this.logger.error('Workflow rollback validation failed', error, {
|
|
585
|
-
correlationId,
|
|
586
|
-
projectId,
|
|
587
|
-
targetPhase
|
|
588
|
-
});
|
|
589
|
-
return {
|
|
590
|
-
canRollback: false,
|
|
591
|
-
reason: `Validation error: ${error.message}`,
|
|
592
|
-
impactedItems: [],
|
|
593
|
-
rollbackActions: []
|
|
278
|
+
tasks[section.id] = {
|
|
279
|
+
title: section.title,
|
|
280
|
+
tddRequired,
|
|
281
|
+
dependencies,
|
|
282
|
+
plannedArtifacts,
|
|
594
283
|
};
|
|
284
|
+
const acceptance = values['Acceptance criteria'];
|
|
285
|
+
if (acceptance && !/(?:^|\s)\d+\.\s+\S/.test(acceptance)) {
|
|
286
|
+
blockers.push(blocker('AcceptanceCriteriaMissing', 'Acceptance criteria must contain a numbered item', section.id));
|
|
287
|
+
}
|
|
595
288
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
289
|
+
const taskIds = new Set(Object.keys(tasks));
|
|
290
|
+
for (const [id, task] of Object.entries(tasks)) {
|
|
291
|
+
for (const dependency of task.dependencies) {
|
|
292
|
+
if (!taskIds.has(dependency))
|
|
293
|
+
blockers.push(blocker('UnknownDependency', `Unknown dependency: ${dependency}`, id));
|
|
294
|
+
}
|
|
601
295
|
}
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
296
|
+
const visiting = new Set();
|
|
297
|
+
const visited = new Set();
|
|
298
|
+
let cycleFound = false;
|
|
299
|
+
const visit = (id) => {
|
|
300
|
+
if (visiting.has(id)) {
|
|
301
|
+
cycleFound = true;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (visited.has(id))
|
|
305
|
+
return;
|
|
306
|
+
visiting.add(id);
|
|
307
|
+
for (const dependency of tasks[id]?.dependencies ?? [])
|
|
308
|
+
if (taskIds.has(dependency))
|
|
309
|
+
visit(dependency);
|
|
310
|
+
visiting.delete(id);
|
|
311
|
+
visited.add(id);
|
|
606
312
|
};
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
phases.tasks.isComplete &&
|
|
619
|
-
crossPhase.isConsistent;
|
|
620
|
-
// Check if can progress
|
|
621
|
-
const nextPhase = this.stateMachine.getNextPhase(project.phase);
|
|
622
|
-
const canProgress = nextPhase ?
|
|
623
|
-
this.stateMachine.canTransition(project, nextPhase).allowed : false;
|
|
624
|
-
// Collect recommendations
|
|
625
|
-
const recommendations = [
|
|
626
|
-
...phases.requirements.recommendations,
|
|
627
|
-
...phases.design.recommendations,
|
|
628
|
-
...phases.tasks.recommendations
|
|
629
|
-
];
|
|
630
|
-
// Generate next actions
|
|
631
|
-
const nextActions = [];
|
|
632
|
-
if (!canProgress && nextPhase) {
|
|
633
|
-
nextActions.push(`Complete requirements for ${nextPhase} phase progression`);
|
|
634
|
-
}
|
|
635
|
-
if (crossPhase.inconsistencies.length > 0) {
|
|
636
|
-
nextActions.push('Resolve cross-phase consistency issues');
|
|
313
|
+
for (const id of taskIds)
|
|
314
|
+
visit(id);
|
|
315
|
+
if (cycleFound)
|
|
316
|
+
blockers.push(blocker('DependencyCycle', 'Task dependencies must form an acyclic graph'));
|
|
317
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), tasks };
|
|
318
|
+
}
|
|
319
|
+
validateTasks(content, approvedRequirementsContent, approvedDesignContent) {
|
|
320
|
+
const parsed = this.parseTasks(content);
|
|
321
|
+
const blockers = [...parsed.blockers];
|
|
322
|
+
if (content.trim().length === 0 && !blockers.some(({ code }) => code === 'EmptyContent')) {
|
|
323
|
+
blockers.unshift(blocker('EmptyContent', 'Tasks content must not be empty'));
|
|
637
324
|
}
|
|
638
|
-
|
|
639
|
-
|
|
325
|
+
const known = [...extractRequirementIds(approvedRequirementsContent), ...extractDecisionIds(approvedDesignContent)];
|
|
326
|
+
const knownSet = new Set(known);
|
|
327
|
+
const covered = new Set();
|
|
328
|
+
const lines = visibleLines(content);
|
|
329
|
+
const { sections } = parseSections(lines, /^###\s+(\d+(?:\.\d+)+)\s+(.+?)\s*$/);
|
|
330
|
+
for (const section of uniqueSections(sections)) {
|
|
331
|
+
for (const id of parseList(metadata(section, 'Covers'))) {
|
|
332
|
+
if (!knownSet.has(id))
|
|
333
|
+
blockers.push(blocker('UnknownCoverageId', `Covers references unknown identifier: ${id}`, section.id));
|
|
334
|
+
else
|
|
335
|
+
covered.add(id);
|
|
336
|
+
}
|
|
640
337
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
overall: {
|
|
644
|
-
isValid,
|
|
645
|
-
readinessScore: overallScore,
|
|
646
|
-
phase: project.phase,
|
|
647
|
-
canProgress
|
|
648
|
-
},
|
|
649
|
-
phases,
|
|
650
|
-
crossPhase,
|
|
651
|
-
recommendations: [...new Set(recommendations)], // Remove duplicates
|
|
652
|
-
nextActions
|
|
653
|
-
};
|
|
338
|
+
blockers.push(...coverageBlockers(covered, known));
|
|
339
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), tasks: parsed.tasks };
|
|
654
340
|
}
|
|
655
341
|
};
|
|
656
342
|
WorkflowValidationService = __decorate([
|
|
657
|
-
injectable()
|
|
658
|
-
__param(0, inject(TYPES.ProjectRepository)),
|
|
659
|
-
__param(1, inject(TYPES.FileSystemPort)),
|
|
660
|
-
__param(2, inject(TYPES.ValidationPort)),
|
|
661
|
-
__param(3, inject(TYPES.LoggerPort)),
|
|
662
|
-
__metadata("design:paramtypes", [Object, Object, Object, Object])
|
|
343
|
+
injectable()
|
|
663
344
|
], WorkflowValidationService);
|
|
664
345
|
export { WorkflowValidationService };
|
|
665
346
|
//# sourceMappingURL=WorkflowValidationService.js.map
|