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.
Files changed (77) hide show
  1. package/CHANGELOG.md +884 -0
  2. package/README.md +18 -35
  3. package/dist/adapters/cli/SDDToolAdapter.d.ts +2 -5
  4. package/dist/adapters/cli/SDDToolAdapter.js +48 -92
  5. package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
  6. package/dist/application/services/ContextCompactionService.d.ts +10 -3
  7. package/dist/application/services/ContextCompactionService.js +139 -35
  8. package/dist/application/services/ContextCompactionService.js.map +1 -1
  9. package/dist/application/services/ProjectService.js +3 -3
  10. package/dist/application/services/ProjectService.js.map +1 -1
  11. package/dist/application/services/SpecPathResolver.js +1 -1
  12. package/dist/application/services/SpecPathResolver.js.map +1 -1
  13. package/dist/application/services/WorkflowEngineService.d.ts +160 -50
  14. package/dist/application/services/WorkflowEngineService.js +1404 -429
  15. package/dist/application/services/WorkflowEngineService.js.map +1 -1
  16. package/dist/application/services/WorkflowErrors.d.ts +16 -0
  17. package/dist/application/services/WorkflowErrors.js +53 -0
  18. package/dist/application/services/WorkflowErrors.js.map +1 -0
  19. package/dist/application/services/WorkflowValidationService.d.ts +25 -46
  20. package/dist/application/services/WorkflowValidationService.js +308 -627
  21. package/dist/application/services/WorkflowValidationService.js.map +1 -1
  22. package/dist/cli/install-skills.js +0 -0
  23. package/dist/cli/install-skills.js.map +1 -1
  24. package/dist/cli/install-target.d.ts +4 -1
  25. package/dist/cli/install-target.js +4 -0
  26. package/dist/cli/install-target.js.map +1 -1
  27. package/dist/cli/sdd-mcp-cli.js +0 -0
  28. package/dist/cli/tool-support/claude-code.js +7 -3
  29. package/dist/cli/tool-support/claude-code.js.map +1 -1
  30. package/dist/cli/tool-support/codex.js +6 -2
  31. package/dist/cli/tool-support/codex.js.map +1 -1
  32. package/dist/cli/tool-support/mcp-registration.d.ts +22 -0
  33. package/dist/cli/tool-support/mcp-registration.js +275 -0
  34. package/dist/cli/tool-support/mcp-registration.js.map +1 -0
  35. package/dist/cli/tool-support/omp.js +6 -2
  36. package/dist/cli/tool-support/omp.js.map +1 -1
  37. package/dist/cli/tool-support/root-guidance.js +2 -2
  38. package/dist/cli/tool-support/root-guidance.js.map +1 -1
  39. package/dist/cli/tool-support/target-installer.d.ts +2 -2
  40. package/dist/cli/tool-support/target-installer.js +9 -3
  41. package/dist/cli/tool-support/target-installer.js.map +1 -1
  42. package/dist/cli/utils/preserving-writer.d.ts +35 -1
  43. package/dist/cli/utils/preserving-writer.js +479 -108
  44. package/dist/cli/utils/preserving-writer.js.map +1 -1
  45. package/dist/domain/types.d.ts +52 -7
  46. package/dist/domain/types.js +5 -4
  47. package/dist/domain/types.js.map +1 -1
  48. package/dist/index.js +0 -0
  49. package/dist/infrastructure/mcp/MCPServer.js +13 -13
  50. package/dist/infrastructure/mcp/MCPServer.js.map +1 -1
  51. package/dist/infrastructure/mcp/ToolRegistry.d.ts +5 -1
  52. package/dist/infrastructure/mcp/ToolRegistry.js +11 -4
  53. package/dist/infrastructure/mcp/ToolRegistry.js.map +1 -1
  54. package/dist/infrastructure/mcp/sddToolDefinitions.js +76 -90
  55. package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -1
  56. package/dist/infrastructure/schemas/project.schema.d.ts +2 -2
  57. package/dist/infrastructure/schemas/project.schema.js +2 -2
  58. package/dist/infrastructure/schemas/project.schema.js.map +1 -1
  59. package/dist/shared/version.d.ts +3 -0
  60. package/dist/shared/version.js +4 -0
  61. package/dist/shared/version.js.map +1 -0
  62. package/dist/utils/atomicWrite.js +20 -5
  63. package/dist/utils/atomicWrite.js.map +1 -1
  64. package/dist/utils/withFilesystemLock.d.ts +22 -0
  65. package/dist/utils/withFilesystemLock.js +219 -0
  66. package/dist/utils/withFilesystemLock.js.map +1 -0
  67. package/package.json +5 -2
  68. package/skills/sdd-design/REFERENCE.md +16 -0
  69. package/skills/sdd-design/SKILL.md +22 -13
  70. package/skills/sdd-implement/REFERENCE.md +4 -0
  71. package/skills/sdd-implement/SKILL.md +20 -16
  72. package/skills/sdd-requirements/REFERENCE.md +17 -7
  73. package/skills/sdd-requirements/SKILL.md +30 -22
  74. package/skills/sdd-tasks/REFERENCE.md +9 -9
  75. package/skills/sdd-tasks/SKILL.md +27 -15
  76. package/templates/CLAUDE.md +6 -12
  77. 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
- var __metadata = (this && this.__metadata) || function (k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- };
10
- var __param = (this && this.__param) || function (paramIndex, decorator) {
11
- return function (target, key) { decorator(target, key, paramIndex); }
12
- };
13
- import { injectable, inject } from 'inversify';
14
- import { v4 as uuidv4 } from 'uuid';
15
- import { TYPES } from '../../infrastructure/di/types.js';
16
- import { WorkflowPhase } from '../../domain/types.js';
17
- import { WorkflowStateMachine } from '../../domain/workflow/WorkflowStateMachine.js';
18
- let WorkflowValidationService = class WorkflowValidationService {
19
- projectRepository;
20
- fileSystem;
21
- validation;
22
- logger;
23
- stateMachine;
24
- constructor(projectRepository, fileSystem, validation, logger) {
25
- this.projectRepository = projectRepository;
26
- this.fileSystem = fileSystem;
27
- this.validation = validation;
28
- this.logger = logger;
29
- this.stateMachine = new WorkflowStateMachine();
30
- }
31
- async validateRequirementsCompletion(projectId) {
32
- const correlationId = uuidv4();
33
- this.logger.info('Validating requirements completion', {
34
- correlationId,
35
- projectId
36
- });
37
- const project = await this.projectRepository.findById(projectId);
38
- if (!project) {
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 missingItems = [];
48
- const qualityIssues = [];
49
- const recommendations = [];
50
- let readinessScore = 0;
51
- try {
52
- // Check if requirements document exists
53
- const requirementsPath = `${project.path}/.spec/specs/${project.name}/requirements.md`;
54
- if (!(await this.fileSystem.exists(requirementsPath))) {
55
- missingItems.push('requirements.md file not found');
56
- }
57
- else {
58
- const content = await this.fileSystem.readFile(requirementsPath);
59
- // Basic content validation
60
- if (content.trim().length < 100) {
61
- qualityIssues.push('Requirements document is too short (less than 100 characters)');
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
- catch (error) {
136
- this.logger.error('Requirements validation failed', error, {
137
- correlationId,
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
- async validateDesignApproval(projectId) {
150
- const correlationId = uuidv4();
151
- this.logger.info('Validating design approval readiness', {
152
- correlationId,
153
- projectId
154
- });
155
- const project = await this.projectRepository.findById(projectId);
156
- if (!project) {
157
- return {
158
- isComplete: false,
159
- missingItems: ['Project not found'],
160
- qualityIssues: [],
161
- readinessScore: 0,
162
- recommendations: []
163
- };
164
- }
165
- const missingItems = [];
166
- const qualityIssues = [];
167
- const recommendations = [];
168
- let readinessScore = 0;
169
- try {
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');
174
- }
175
- else {
176
- const content = await this.fileSystem.readFile(designPath);
177
- // Basic content validation
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;
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
- 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
- };
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
- async validateTaskApproval(projectId) {
274
- const correlationId = uuidv4();
275
- this.logger.info('Validating task approval readiness', {
276
- correlationId,
277
- projectId
278
- });
279
- const project = await this.projectRepository.findById(projectId);
280
- if (!project) {
281
- return {
282
- isComplete: false,
283
- missingItems: ['Project not found'],
284
- qualityIssues: [],
285
- readinessScore: 0,
286
- recommendations: []
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 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');
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
- else {
356
- recommendations.push('Tasks need review and approval before proceeding to implementation');
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
- async validateCrossPhaseConsistency(projectId) {
399
- const correlationId = uuidv4();
400
- this.logger.info('Validating cross-phase consistency', {
401
- correlationId,
402
- projectId
403
- });
404
- const project = await this.projectRepository.findById(projectId);
405
- if (!project) {
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
- // Validate Requirements -> Design traceability
428
- if (documents['requirements.md'] && documents['design.md']) {
429
- const requirements = documents['requirements.md'];
430
- const design = documents['design.md'];
431
- // Extract requirement titles
432
- const reqMatches = requirements.match(/### Requirement \d+: (.+)/g) || [];
433
- const requirementTitles = reqMatches.map(match => match.replace(/### Requirement \d+: /, '').trim());
434
- // Check if design addresses all requirements
435
- for (const reqTitle of requirementTitles) {
436
- const keywords = reqTitle.split(' ').filter(word => word.length > 3);
437
- const hasReference = keywords.some(keyword => design.toLowerCase().includes(keyword.toLowerCase()));
438
- if (!hasReference) {
439
- traceabilityIssues.push(`Design may not address requirement: "${reqTitle}"`);
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
- // Validate Design -> Tasks traceability
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
- 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
- };
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 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
- };
533
- }
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;
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
- // 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
- }
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
- async generateComprehensiveValidationReport(projectId) {
598
- const project = await this.projectRepository.findById(projectId);
599
- if (!project) {
600
- return null;
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 phases = {
603
- requirements: await this.validateRequirementsCompletion(projectId),
604
- design: await this.validateDesignApproval(projectId),
605
- tasks: await this.validateTaskApproval(projectId)
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 crossPhase = await this.validateCrossPhaseConsistency(projectId);
608
- // Calculate overall readiness
609
- const phaseScores = [
610
- phases.requirements.readinessScore,
611
- phases.design.readinessScore,
612
- phases.tasks.readinessScore
613
- ];
614
- const overallScore = Math.round(phaseScores.reduce((sum, score) => sum + score, 0) / 3);
615
- // Determine overall validity
616
- const isValid = phases.requirements.isComplete &&
617
- phases.design.isComplete &&
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
- if (isValid && canProgress) {
639
- nextActions.push('Project is ready for phase progression');
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
- return {
642
- project,
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