sdd-mcp-server 4.0.0 → 5.0.0
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 +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 +284 -627
- package/dist/application/services/WorkflowValidationService.js.map +1 -1
- 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/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/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 +4 -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 +15 -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,319 @@ 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
|
-
|
|
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
|
+
function metadataValues(section, label) {
|
|
72
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
73
|
+
const expression = new RegExp(`^\\s*\\*\\*${escaped}:\\*\\*\\s*(\\S(?:.*\\S)?)\\s*$`);
|
|
74
|
+
const values = [];
|
|
75
|
+
for (const line of section.body) {
|
|
76
|
+
const match = expression.exec(line);
|
|
77
|
+
if (match)
|
|
78
|
+
values.push(match[1]);
|
|
79
|
+
}
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
function metadata(section, label) {
|
|
83
|
+
return metadataValues(section, label)[0];
|
|
84
|
+
}
|
|
85
|
+
function parseList(value) {
|
|
86
|
+
if (!value || value.trim() === 'none')
|
|
87
|
+
return [];
|
|
88
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
function hasDuplicates(values) {
|
|
91
|
+
return new Set(values).size !== values.length;
|
|
92
|
+
}
|
|
93
|
+
function isProjectRelativePath(value) {
|
|
94
|
+
return value.length > 0
|
|
95
|
+
&& value.length <= 500
|
|
96
|
+
&& !/^[/\\]/.test(value)
|
|
97
|
+
&& !/^[A-Za-z]:/.test(value)
|
|
98
|
+
&& !value.includes('\0')
|
|
99
|
+
&& !value.split(/[\\/]/).includes('..')
|
|
100
|
+
&& value === value.trim();
|
|
101
|
+
}
|
|
102
|
+
function uniqueSections(sections) {
|
|
103
|
+
const byId = new Map();
|
|
104
|
+
for (const section of sections)
|
|
105
|
+
if (!byId.has(section.id))
|
|
106
|
+
byId.set(section.id, section);
|
|
107
|
+
return [...byId.values()];
|
|
108
|
+
}
|
|
109
|
+
function extractRequirementIds(content) {
|
|
110
|
+
const { sections } = parseSections(visibleLines(content), /^###\s+((?:FR|NFR)-\d+):\s*(.+?)\s*$/);
|
|
111
|
+
return uniqueSections(sections).map(({ id }) => id);
|
|
112
|
+
}
|
|
113
|
+
function extractDecisionIds(content) {
|
|
114
|
+
const { sections } = parseSections(visibleLines(content), /^###\s+(D-\d+):\s*(.+?)\s*$/);
|
|
115
|
+
return uniqueSections(sections).map(({ id }) => id);
|
|
116
|
+
}
|
|
117
|
+
function addDuplicateBlockers(blockers, duplicates) {
|
|
118
|
+
for (const id of duplicates)
|
|
119
|
+
blockers.push(blocker('DuplicateId', `Identifier is declared more than once: ${id}`, id));
|
|
120
|
+
}
|
|
121
|
+
function requireMetadata(blockers, section, labels) {
|
|
122
|
+
const values = {};
|
|
123
|
+
for (const label of labels) {
|
|
124
|
+
const matches = metadataValues(section, label);
|
|
125
|
+
values[label] = matches[0];
|
|
126
|
+
if (!values[label])
|
|
127
|
+
blockers.push(blocker('MissingMetadata', `Missing or empty **${label}:** metadata`, section.id));
|
|
128
|
+
if (matches.length > 1)
|
|
129
|
+
blockers.push(blocker('DuplicateMetadata', `Metadata label appears more than once: **${label}:**`, section.id));
|
|
130
|
+
}
|
|
131
|
+
return values;
|
|
132
|
+
}
|
|
133
|
+
function coverageBlockers(actual, expected) {
|
|
134
|
+
return expected
|
|
135
|
+
.filter((id) => !actual.has(id))
|
|
136
|
+
.map((id) => blocker('CoverageMissing', `Required identifier is not covered: ${id}`, id));
|
|
137
|
+
}
|
|
138
|
+
let WorkflowValidationService = class WorkflowValidationService {
|
|
139
|
+
validateRequirements(content) {
|
|
140
|
+
const blockers = [];
|
|
141
|
+
if (content.trim().length === 0)
|
|
142
|
+
blockers.push(blocker('EmptyContent', 'Requirements content must not be empty'));
|
|
143
|
+
const { sections, duplicates } = parseSections(visibleLines(content), /^###\s+((?:FR|NFR)-\d+):\s*(.+?)\s*$/);
|
|
144
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
145
|
+
if (sections.length === 0)
|
|
146
|
+
blockers.push(blocker('RequirementsMissing', 'At least one FR-N requirement section is required'));
|
|
147
|
+
if (!sections.some(({ id }) => id.startsWith('FR-'))) {
|
|
148
|
+
blockers.push(blocker('FunctionalRequirementMissing', 'At least one FR-N requirement section is required'));
|
|
164
149
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Check if design document exists
|
|
171
|
-
const designPath = `${project.path}/.spec/specs/${project.name}/design.md`;
|
|
172
|
-
if (!(await this.fileSystem.exists(designPath))) {
|
|
173
|
-
missingItems.push('design.md file not found');
|
|
150
|
+
for (const section of uniqueSections(sections)) {
|
|
151
|
+
const values = requireMetadata(blockers, section, ['Objective', 'EARS Specification', 'Acceptance Criteria']);
|
|
152
|
+
const ears = values['EARS Specification'];
|
|
153
|
+
if (ears && !/\bSHALL\b/.test(ears)) {
|
|
154
|
+
blockers.push(blocker('EarsShallMissing', 'EARS Specification must contain SHALL', section.id));
|
|
174
155
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (content.trim().length < 200) {
|
|
179
|
-
qualityIssues.push('Design document is too short (less than 200 characters)');
|
|
180
|
-
}
|
|
181
|
-
// Check for key design sections
|
|
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;
|
|
156
|
+
const acceptance = values['Acceptance Criteria'];
|
|
157
|
+
if (acceptance && !/(?:^|\s)\d+\.\s+\S/.test(acceptance)) {
|
|
158
|
+
blockers.push(blocker('AcceptanceCriteriaMissing', 'Acceptance Criteria must contain a numbered item', section.id));
|
|
230
159
|
}
|
|
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
|
-
};
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
this.logger.error('Design validation failed', error, {
|
|
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
|
-
};
|
|
271
160
|
}
|
|
161
|
+
const requirementIds = uniqueSections(sections).map(({ id }) => id);
|
|
162
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), requirementIds };
|
|
272
163
|
}
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
isComplete: false,
|
|
283
|
-
missingItems: ['Project not found'],
|
|
284
|
-
qualityIssues: [],
|
|
285
|
-
readinessScore: 0,
|
|
286
|
-
recommendations: []
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const missingItems = [];
|
|
290
|
-
const qualityIssues = [];
|
|
291
|
-
const recommendations = [];
|
|
292
|
-
let readinessScore = 0;
|
|
293
|
-
try {
|
|
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');
|
|
164
|
+
validateDesign(content, approvedRequirementsContent) {
|
|
165
|
+
const blockers = [];
|
|
166
|
+
const lines = visibleLines(content);
|
|
167
|
+
if (content.trim().length === 0)
|
|
168
|
+
blockers.push(blocker('EmptyContent', 'Design content must not be empty'));
|
|
169
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
170
|
+
if (!lines.some((line) => line === `## ${heading}`)) {
|
|
171
|
+
blockers.push(blocker('MissingHeading', `Missing exact heading: ## ${heading}`, heading));
|
|
298
172
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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;
|
|
173
|
+
}
|
|
174
|
+
const { sections, duplicates } = parseSections(lines, /^###\s+(D-\d+):\s*(.+?)\s*$/);
|
|
175
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
176
|
+
if (sections.length === 0)
|
|
177
|
+
blockers.push(blocker('DesignDecisionMissing', 'At least one D-N decision section is required'));
|
|
178
|
+
const knownRequirements = extractRequirementIds(approvedRequirementsContent);
|
|
179
|
+
const knownSet = new Set(knownRequirements);
|
|
180
|
+
const covered = new Set();
|
|
181
|
+
for (const section of uniqueSections(sections)) {
|
|
182
|
+
const values = requireMetadata(blockers, section, ['Covers', 'Decision', 'Failure behavior', 'Verification']);
|
|
183
|
+
for (const id of parseList(values.Covers)) {
|
|
184
|
+
if (!knownSet.has(id)) {
|
|
185
|
+
blockers.push(blocker('UnknownCoverageId', `Covers references unknown requirement: ${id}`, section.id));
|
|
333
186
|
}
|
|
334
187
|
else {
|
|
335
|
-
|
|
188
|
+
covered.add(id);
|
|
336
189
|
}
|
|
337
190
|
}
|
|
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;
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
recommendations.push('Tasks need review and approval before proceeding to implementation');
|
|
357
|
-
}
|
|
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
191
|
}
|
|
192
|
+
blockers.push(...coverageBlockers(covered, knownRequirements));
|
|
193
|
+
return {
|
|
194
|
+
status: resultStatus(blockers),
|
|
195
|
+
blockers: bounded(blockers),
|
|
196
|
+
decisionIds: uniqueSections(sections).map(({ id }) => id),
|
|
197
|
+
};
|
|
397
198
|
}
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
isConsistent: false,
|
|
408
|
-
inconsistencies: ['Project not found'],
|
|
409
|
-
traceabilityIssues: [],
|
|
410
|
-
coverageGaps: []
|
|
411
|
-
};
|
|
199
|
+
parseTasks(content) {
|
|
200
|
+
const blockers = [];
|
|
201
|
+
const { sections, duplicates } = parseSections(visibleLines(content), /^###\s+(\d+(?:\.\d+)+)\s+(.+?)\s*$/);
|
|
202
|
+
addDuplicateBlockers(blockers, duplicates);
|
|
203
|
+
if (sections.length === 0)
|
|
204
|
+
blockers.push(blocker('TaskMissing', 'At least one N.M task section is required'));
|
|
205
|
+
const unique = uniqueSections(sections);
|
|
206
|
+
if (unique.length > 1_000) {
|
|
207
|
+
blockers.push(blocker('TaskLimitExceeded', 'Tasks must contain at most 1000 unique task sections'));
|
|
412
208
|
}
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
209
|
+
const tasks = {};
|
|
210
|
+
for (const section of unique) {
|
|
211
|
+
if (section.id.length > 50) {
|
|
212
|
+
blockers.push(blocker('InvalidTaskId', 'Task identifiers must contain at most 50 characters', section.id.slice(0, 50)));
|
|
213
|
+
}
|
|
214
|
+
if (section.title.trim().length === 0 || section.title.length > 500) {
|
|
215
|
+
blockers.push(blocker('InvalidTaskTitle', 'Task titles must be non-whitespace and at most 500 characters', section.id.slice(0, 50)));
|
|
216
|
+
}
|
|
217
|
+
const values = requireMetadata(blockers, section, [
|
|
218
|
+
'Covers',
|
|
219
|
+
'Dependencies',
|
|
220
|
+
'TDD',
|
|
221
|
+
'Affected artifacts',
|
|
222
|
+
'Acceptance criteria',
|
|
223
|
+
'Verification',
|
|
224
|
+
]);
|
|
225
|
+
const tdd = values.TDD;
|
|
226
|
+
let tddRequired = false;
|
|
227
|
+
if (tdd === 'required')
|
|
228
|
+
tddRequired = true;
|
|
229
|
+
else if (!tdd || !/^not-applicable — \S(?:.*\S)?$/.test(tdd)) {
|
|
230
|
+
blockers.push(blocker('InvalidTdd', 'TDD must be required or not-applicable — <reason>', section.id));
|
|
231
|
+
}
|
|
232
|
+
const dependencies = parseList(values.Dependencies);
|
|
233
|
+
const plannedArtifacts = parseList(values['Affected artifacts']);
|
|
234
|
+
if (dependencies.includes('none') || plannedArtifacts.includes('none')) {
|
|
235
|
+
blockers.push(blocker('InvalidListValue', 'none must be the only value in an empty metadata list', section.id));
|
|
236
|
+
}
|
|
237
|
+
if (dependencies.length > 100) {
|
|
238
|
+
blockers.push(blocker('ListTooLong', 'Dependencies must contain at most 100 entries', section.id));
|
|
239
|
+
}
|
|
240
|
+
if (plannedArtifacts.length > 100) {
|
|
241
|
+
blockers.push(blocker('ListTooLong', 'Affected artifacts must contain at most 100 entries', section.id));
|
|
242
|
+
}
|
|
243
|
+
if (hasDuplicates(dependencies)) {
|
|
244
|
+
blockers.push(blocker('DuplicateListItem', 'Dependencies must not contain duplicate task IDs', section.id));
|
|
245
|
+
}
|
|
246
|
+
if (hasDuplicates(plannedArtifacts)) {
|
|
247
|
+
blockers.push(blocker('DuplicateListItem', 'Affected artifacts must not contain duplicate paths', section.id));
|
|
248
|
+
}
|
|
249
|
+
for (const artifact of plannedArtifacts) {
|
|
250
|
+
if (!isProjectRelativePath(artifact)) {
|
|
251
|
+
blockers.push(blocker('InvalidArtifactPath', `Affected artifact must be project-relative: ${artifact}`, section.id));
|
|
441
252
|
}
|
|
442
253
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
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
|
|
254
|
+
tasks[section.id] = {
|
|
255
|
+
title: section.title,
|
|
256
|
+
tddRequired,
|
|
257
|
+
dependencies,
|
|
258
|
+
plannedArtifacts,
|
|
490
259
|
};
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
correlationId,
|
|
495
|
-
projectId
|
|
496
|
-
});
|
|
497
|
-
return {
|
|
498
|
-
isConsistent: false,
|
|
499
|
-
inconsistencies: [`Validation error: ${error.message}`],
|
|
500
|
-
traceabilityIssues: [],
|
|
501
|
-
coverageGaps: []
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
async validateWorkflowRollback(projectId, targetPhase) {
|
|
506
|
-
const correlationId = uuidv4();
|
|
507
|
-
this.logger.info('Validating workflow rollback', {
|
|
508
|
-
correlationId,
|
|
509
|
-
projectId,
|
|
510
|
-
targetPhase
|
|
511
|
-
});
|
|
512
|
-
const project = await this.projectRepository.findById(projectId);
|
|
513
|
-
if (!project) {
|
|
514
|
-
return {
|
|
515
|
-
canRollback: false,
|
|
516
|
-
reason: 'Project not found',
|
|
517
|
-
impactedItems: [],
|
|
518
|
-
rollbackActions: []
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
const impactedItems = [];
|
|
522
|
-
const rollbackActions = [];
|
|
523
|
-
try {
|
|
524
|
-
// Check if rollback is valid
|
|
525
|
-
const validation = this.stateMachine.canTransition(project, targetPhase);
|
|
526
|
-
if (!validation.allowed) {
|
|
527
|
-
return {
|
|
528
|
-
canRollback: false,
|
|
529
|
-
reason: validation.reason,
|
|
530
|
-
impactedItems: [],
|
|
531
|
-
rollbackActions: []
|
|
532
|
-
};
|
|
260
|
+
const acceptance = values['Acceptance criteria'];
|
|
261
|
+
if (acceptance && !/(?:^|\s)\d+\.\s+\S/.test(acceptance)) {
|
|
262
|
+
blockers.push(blocker('AcceptanceCriteriaMissing', 'Acceptance criteria must contain a numbered item', section.id));
|
|
533
263
|
}
|
|
534
|
-
// Identify impacted items based on rollback target
|
|
535
|
-
const currentPhase = project.phase;
|
|
536
|
-
switch (currentPhase) {
|
|
537
|
-
case WorkflowPhase.REQUIREMENTS:
|
|
538
|
-
if (targetPhase === WorkflowPhase.INIT) {
|
|
539
|
-
impactedItems.push('Requirements document and approval status');
|
|
540
|
-
rollbackActions.push('Reset requirements approval status');
|
|
541
|
-
rollbackActions.push('Optionally backup requirements.md');
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
case WorkflowPhase.DESIGN:
|
|
545
|
-
if (targetPhase === WorkflowPhase.REQUIREMENTS) {
|
|
546
|
-
impactedItems.push('Design document and approval status');
|
|
547
|
-
rollbackActions.push('Reset design approval status');
|
|
548
|
-
rollbackActions.push('Optionally backup design.md');
|
|
549
|
-
}
|
|
550
|
-
break;
|
|
551
|
-
case WorkflowPhase.TASKS:
|
|
552
|
-
if (targetPhase === WorkflowPhase.DESIGN) {
|
|
553
|
-
impactedItems.push('Tasks document and approval status');
|
|
554
|
-
rollbackActions.push('Reset tasks approval status');
|
|
555
|
-
rollbackActions.push('Optionally backup tasks.md');
|
|
556
|
-
}
|
|
557
|
-
break;
|
|
558
|
-
case WorkflowPhase.IMPLEMENTATION:
|
|
559
|
-
if (targetPhase === WorkflowPhase.TASKS) {
|
|
560
|
-
impactedItems.push('Implementation progress');
|
|
561
|
-
rollbackActions.push('Implementation work may be lost');
|
|
562
|
-
rollbackActions.push('Consider backing up current implementation');
|
|
563
|
-
}
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
// Add general rollback actions
|
|
567
|
-
rollbackActions.push('Update project phase in spec.json');
|
|
568
|
-
rollbackActions.push('Update project metadata timestamps');
|
|
569
|
-
rollbackActions.push('Create audit trail entry');
|
|
570
|
-
this.logger.info('Workflow rollback validation completed', {
|
|
571
|
-
correlationId,
|
|
572
|
-
projectId,
|
|
573
|
-
currentPhase,
|
|
574
|
-
targetPhase,
|
|
575
|
-
impactedCount: impactedItems.length
|
|
576
|
-
});
|
|
577
|
-
return {
|
|
578
|
-
canRollback: true,
|
|
579
|
-
impactedItems,
|
|
580
|
-
rollbackActions
|
|
581
|
-
};
|
|
582
264
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
return {
|
|
590
|
-
canRollback: false,
|
|
591
|
-
reason: `Validation error: ${error.message}`,
|
|
592
|
-
impactedItems: [],
|
|
593
|
-
rollbackActions: []
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
async generateComprehensiveValidationReport(projectId) {
|
|
598
|
-
const project = await this.projectRepository.findById(projectId);
|
|
599
|
-
if (!project) {
|
|
600
|
-
return null;
|
|
265
|
+
const taskIds = new Set(Object.keys(tasks));
|
|
266
|
+
for (const [id, task] of Object.entries(tasks)) {
|
|
267
|
+
for (const dependency of task.dependencies) {
|
|
268
|
+
if (!taskIds.has(dependency))
|
|
269
|
+
blockers.push(blocker('UnknownDependency', `Unknown dependency: ${dependency}`, id));
|
|
270
|
+
}
|
|
601
271
|
}
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
272
|
+
const visiting = new Set();
|
|
273
|
+
const visited = new Set();
|
|
274
|
+
let cycleFound = false;
|
|
275
|
+
const visit = (id) => {
|
|
276
|
+
if (visiting.has(id)) {
|
|
277
|
+
cycleFound = true;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (visited.has(id))
|
|
281
|
+
return;
|
|
282
|
+
visiting.add(id);
|
|
283
|
+
for (const dependency of tasks[id]?.dependencies ?? [])
|
|
284
|
+
if (taskIds.has(dependency))
|
|
285
|
+
visit(dependency);
|
|
286
|
+
visiting.delete(id);
|
|
287
|
+
visited.add(id);
|
|
606
288
|
};
|
|
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');
|
|
289
|
+
for (const id of taskIds)
|
|
290
|
+
visit(id);
|
|
291
|
+
if (cycleFound)
|
|
292
|
+
blockers.push(blocker('DependencyCycle', 'Task dependencies must form an acyclic graph'));
|
|
293
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), tasks };
|
|
294
|
+
}
|
|
295
|
+
validateTasks(content, approvedRequirementsContent, approvedDesignContent) {
|
|
296
|
+
const parsed = this.parseTasks(content);
|
|
297
|
+
const blockers = [...parsed.blockers];
|
|
298
|
+
if (content.trim().length === 0 && !blockers.some(({ code }) => code === 'EmptyContent')) {
|
|
299
|
+
blockers.unshift(blocker('EmptyContent', 'Tasks content must not be empty'));
|
|
637
300
|
}
|
|
638
|
-
|
|
639
|
-
|
|
301
|
+
const known = [...extractRequirementIds(approvedRequirementsContent), ...extractDecisionIds(approvedDesignContent)];
|
|
302
|
+
const knownSet = new Set(known);
|
|
303
|
+
const covered = new Set();
|
|
304
|
+
const lines = visibleLines(content);
|
|
305
|
+
const { sections } = parseSections(lines, /^###\s+(\d+(?:\.\d+)+)\s+(.+?)\s*$/);
|
|
306
|
+
for (const section of uniqueSections(sections)) {
|
|
307
|
+
for (const id of parseList(metadata(section, 'Covers'))) {
|
|
308
|
+
if (!knownSet.has(id))
|
|
309
|
+
blockers.push(blocker('UnknownCoverageId', `Covers references unknown identifier: ${id}`, section.id));
|
|
310
|
+
else
|
|
311
|
+
covered.add(id);
|
|
312
|
+
}
|
|
640
313
|
}
|
|
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
|
-
};
|
|
314
|
+
blockers.push(...coverageBlockers(covered, known));
|
|
315
|
+
return { status: resultStatus(blockers), blockers: bounded(blockers), tasks: parsed.tasks };
|
|
654
316
|
}
|
|
655
317
|
};
|
|
656
318
|
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])
|
|
319
|
+
injectable()
|
|
663
320
|
], WorkflowValidationService);
|
|
664
321
|
export { WorkflowValidationService };
|
|
665
322
|
//# sourceMappingURL=WorkflowValidationService.js.map
|