olympus-ai 3.4.0 → 3.5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +631 -630
- package/dist/__tests__/installer.test.js +1 -1
- package/dist/__tests__/workflow-engine/checkpoint.test.d.ts +7 -0
- package/dist/__tests__/workflow-engine/checkpoint.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-engine/checkpoint.test.js +373 -0
- package/dist/__tests__/workflow-engine/checkpoint.test.js.map +1 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +8 -0
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/idea-intake.d.ts +20 -0
- package/dist/agents/idea-intake.d.ts.map +1 -0
- package/dist/agents/idea-intake.js +255 -0
- package/dist/agents/idea-intake.js.map +1 -0
- package/dist/agents/index.d.ts +4 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +4 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/intent-generator.d.ts +19 -0
- package/dist/agents/intent-generator.d.ts.map +1 -0
- package/dist/agents/intent-generator.js +303 -0
- package/dist/agents/intent-generator.js.map +1 -0
- package/dist/agents/prd-writer.d.ts +19 -0
- package/dist/agents/prd-writer.d.ts.map +1 -0
- package/dist/agents/prd-writer.js +236 -0
- package/dist/agents/prd-writer.js.map +1 -0
- package/dist/agents/prometheus.d.ts.map +1 -1
- package/dist/agents/prometheus.js +123 -2
- package/dist/agents/prometheus.js.map +1 -1
- package/dist/agents/spec-writer.d.ts +19 -0
- package/dist/agents/spec-writer.d.ts.map +1 -0
- package/dist/agents/spec-writer.js +528 -0
- package/dist/agents/spec-writer.js.map +1 -0
- package/dist/features/index.d.ts +1 -0
- package/dist/features/index.d.ts.map +1 -1
- package/dist/features/index.js +6 -0
- package/dist/features/index.js.map +1 -1
- package/dist/features/workflow-engine/artifacts.d.ts +96 -0
- package/dist/features/workflow-engine/artifacts.d.ts.map +1 -0
- package/dist/features/workflow-engine/artifacts.js +399 -0
- package/dist/features/workflow-engine/artifacts.js.map +1 -0
- package/dist/features/workflow-engine/checkpoint.d.ts +67 -0
- package/dist/features/workflow-engine/checkpoint.d.ts.map +1 -0
- package/dist/features/workflow-engine/checkpoint.js +249 -0
- package/dist/features/workflow-engine/checkpoint.js.map +1 -0
- package/dist/features/workflow-engine/engine.d.ts +128 -0
- package/dist/features/workflow-engine/engine.d.ts.map +1 -0
- package/dist/features/workflow-engine/engine.js +600 -0
- package/dist/features/workflow-engine/engine.js.map +1 -0
- package/dist/features/workflow-engine/execution.d.ts +99 -0
- package/dist/features/workflow-engine/execution.d.ts.map +1 -0
- package/dist/features/workflow-engine/execution.js +493 -0
- package/dist/features/workflow-engine/execution.js.map +1 -0
- package/dist/features/workflow-engine/hooks.d.ts +78 -0
- package/dist/features/workflow-engine/hooks.d.ts.map +1 -0
- package/dist/features/workflow-engine/hooks.js +188 -0
- package/dist/features/workflow-engine/hooks.js.map +1 -0
- package/dist/features/workflow-engine/index.d.ts +17 -0
- package/dist/features/workflow-engine/index.d.ts.map +1 -0
- package/dist/features/workflow-engine/index.js +19 -0
- package/dist/features/workflow-engine/index.js.map +1 -0
- package/dist/features/workflow-engine/types.d.ts +220 -0
- package/dist/features/workflow-engine/types.d.ts.map +1 -0
- package/dist/features/workflow-engine/types.js +8 -0
- package/dist/features/workflow-engine/types.js.map +1 -0
- package/dist/features/workflow-engine/validation.d.ts +128 -0
- package/dist/features/workflow-engine/validation.d.ts.map +1 -0
- package/dist/features/workflow-engine/validation.js +746 -0
- package/dist/features/workflow-engine/validation.js.map +1 -0
- package/dist/hooks/ascent-verifier/index.d.ts +52 -0
- package/dist/hooks/ascent-verifier/index.d.ts.map +1 -1
- package/dist/hooks/ascent-verifier/index.js +146 -0
- package/dist/hooks/ascent-verifier/index.js.map +1 -1
- package/dist/hooks/registrations/learning-capture.d.ts.map +1 -1
- package/dist/hooks/registrations/learning-capture.js +32 -9
- package/dist/hooks/registrations/learning-capture.js.map +1 -1
- package/dist/hooks/registrations/user-prompt-submit.d.ts.map +1 -1
- package/dist/hooks/registrations/user-prompt-submit.js +85 -0
- package/dist/hooks/registrations/user-prompt-submit.js.map +1 -1
- package/dist/installer/index.d.ts +1 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +456 -16
- package/dist/installer/index.js.map +1 -1
- package/dist/learning/session-state.d.ts.map +1 -1
- package/dist/learning/session-state.js +17 -0
- package/dist/learning/session-state.js.map +1 -1
- package/dist/learning/types.d.ts +3 -0
- package/dist/learning/types.d.ts.map +1 -1
- package/dist/shared/types.d.ts +17 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/scripts/dist/hooks/olympus-hooks.cjs +208 -97
- package/scripts/rebrand.mjs +0 -206
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDEA Artifact Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates completeness of IDEA stage artifacts against required criteria.
|
|
5
|
+
* An IDEA artifact must contain all essential sections for progression to PRD stage.
|
|
6
|
+
*
|
|
7
|
+
* Performance optimizations:
|
|
8
|
+
* - Parallel validation where possible
|
|
9
|
+
* - Cached file reads within validation session
|
|
10
|
+
* - Optimized regex patterns
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
/**
|
|
14
|
+
* Simple file content cache to avoid redundant reads during validation
|
|
15
|
+
*/
|
|
16
|
+
const fileCache = new Map();
|
|
17
|
+
const FILE_CACHE_TTL = 10000; // 10 seconds
|
|
18
|
+
/**
|
|
19
|
+
* Read file with caching
|
|
20
|
+
*/
|
|
21
|
+
function readFileWithCache(filePath) {
|
|
22
|
+
const cached = fileCache.get(filePath);
|
|
23
|
+
if (cached && Date.now() - cached.timestamp < FILE_CACHE_TTL) {
|
|
24
|
+
return cached.content;
|
|
25
|
+
}
|
|
26
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
27
|
+
fileCache.set(filePath, { content, timestamp: Date.now() });
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clear the file cache
|
|
32
|
+
*/
|
|
33
|
+
export function clearFileCache() {
|
|
34
|
+
fileCache.clear();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Required sections for a valid IDEA artifact
|
|
38
|
+
*/
|
|
39
|
+
const REQUIRED_SECTIONS = [
|
|
40
|
+
'Problem Statement',
|
|
41
|
+
'Business Context',
|
|
42
|
+
'Success Metrics',
|
|
43
|
+
'Constraints',
|
|
44
|
+
'Solution Approach',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Validates an IDEA artifact for completeness.
|
|
48
|
+
*
|
|
49
|
+
* Checks 6 criteria:
|
|
50
|
+
* 1. Problem statement present (non-empty ## Problem Statement section)
|
|
51
|
+
* 2. Business context present (non-empty ## Business Context section)
|
|
52
|
+
* 3. At least 2 success metrics (check ## Success Metrics section has 2+ bullet points)
|
|
53
|
+
* 4. Constraints documented (## Constraints section present with content)
|
|
54
|
+
* 5. Risk tier assessed (YAML frontmatter has risk_tier field)
|
|
55
|
+
* 6. All required sections present (all 5 sections exist in document)
|
|
56
|
+
*
|
|
57
|
+
* @param artifactPath - Absolute path to the IDEA artifact file
|
|
58
|
+
* @returns ValidationResult with pass/fail status, coverage percentage, and any blocking issues
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const result = await validateIdea('.olympus/workflows/feature-x/idea.md');
|
|
62
|
+
* if (result.passed) {
|
|
63
|
+
* console.log('IDEA artifact is complete!');
|
|
64
|
+
* } else {
|
|
65
|
+
* console.log('Issues found:', result.blocking_issues);
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
export async function validateIdea(artifactPath) {
|
|
69
|
+
const timestamp = new Date().toISOString();
|
|
70
|
+
const blockingIssues = [];
|
|
71
|
+
// Read artifact file with caching
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = readFileWithCache(artifactPath);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const err = error;
|
|
78
|
+
if (err.code === 'ENOENT') {
|
|
79
|
+
console.error(`[Validation] IDEA artifact not found: ${artifactPath}`);
|
|
80
|
+
return {
|
|
81
|
+
passed: false,
|
|
82
|
+
coverage_percentage: 0,
|
|
83
|
+
blocking_issues: ['Artifact file not found'],
|
|
84
|
+
timestamp,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
88
|
+
console.error(`[Validation] Permission denied reading IDEA artifact: ${artifactPath}`);
|
|
89
|
+
return {
|
|
90
|
+
passed: false,
|
|
91
|
+
coverage_percentage: 0,
|
|
92
|
+
blocking_issues: ['Permission denied reading artifact file'],
|
|
93
|
+
timestamp,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
console.error(`[Validation] Failed to read IDEA artifact: ${err.message}`);
|
|
97
|
+
console.error(`[Validation] Path: ${artifactPath}`);
|
|
98
|
+
return {
|
|
99
|
+
passed: false,
|
|
100
|
+
coverage_percentage: 0,
|
|
101
|
+
blocking_issues: [`Failed to read artifact: ${err.message}`],
|
|
102
|
+
timestamp,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Parse YAML frontmatter
|
|
106
|
+
const frontmatter = parseFrontmatter(content);
|
|
107
|
+
if (!frontmatter || !frontmatter.risk_tier) {
|
|
108
|
+
blockingIssues.push('Risk tier not specified in frontmatter');
|
|
109
|
+
}
|
|
110
|
+
// Remove frontmatter from content for section parsing
|
|
111
|
+
const markdownContent = removeFrontmatter(content);
|
|
112
|
+
// Parse markdown sections (single pass optimization)
|
|
113
|
+
const sections = parseSections(markdownContent);
|
|
114
|
+
// Run all validations in parallel (independent checks)
|
|
115
|
+
const validationChecks = [
|
|
116
|
+
// Check criterion 1: Problem statement present and non-empty
|
|
117
|
+
() => {
|
|
118
|
+
const problemStatement = sections.get('Problem Statement');
|
|
119
|
+
if (!problemStatement || problemStatement.trim().length === 0) {
|
|
120
|
+
return 'Missing problem statement section';
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
},
|
|
124
|
+
// Check criterion 2: Business context present and non-empty
|
|
125
|
+
() => {
|
|
126
|
+
const businessContext = sections.get('Business Context');
|
|
127
|
+
if (!businessContext || businessContext.trim().length === 0) {
|
|
128
|
+
return 'Business context section is empty';
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
// Check criterion 3: At least 2 success metrics
|
|
133
|
+
() => {
|
|
134
|
+
const successMetrics = sections.get('Success Metrics');
|
|
135
|
+
if (successMetrics) {
|
|
136
|
+
const metricCount = countBulletPoints(successMetrics);
|
|
137
|
+
if (metricCount < 2) {
|
|
138
|
+
return `Only ${metricCount} success metric found, need at least 2`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
return 'Missing success metrics section';
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
// Check criterion 4: Constraints documented
|
|
147
|
+
() => {
|
|
148
|
+
const constraints = sections.get('Constraints');
|
|
149
|
+
if (!constraints || constraints.trim().length === 0) {
|
|
150
|
+
return 'Constraints section missing';
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
// Check criterion 6: All required sections present
|
|
155
|
+
() => {
|
|
156
|
+
const missing = [];
|
|
157
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
158
|
+
if (!sections.has(section)) {
|
|
159
|
+
missing.push(`Missing required section: ${section}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return missing.length > 0 ? missing.join('; ') : null;
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
// Execute all checks (can be optimized to parallel execution if needed)
|
|
166
|
+
for (const check of validationChecks) {
|
|
167
|
+
const issue = check();
|
|
168
|
+
if (issue) {
|
|
169
|
+
blockingIssues.push(issue);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Calculate coverage (6 total criteria)
|
|
173
|
+
const totalCriteria = 6;
|
|
174
|
+
const passedCriteria = totalCriteria - blockingIssues.length;
|
|
175
|
+
const coveragePercentage = Math.round((passedCriteria / totalCriteria) * 100);
|
|
176
|
+
return {
|
|
177
|
+
passed: blockingIssues.length === 0,
|
|
178
|
+
coverage_percentage: coveragePercentage,
|
|
179
|
+
blocking_issues: blockingIssues,
|
|
180
|
+
timestamp,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Parses YAML frontmatter from markdown content.
|
|
185
|
+
* Frontmatter must be delimited by --- at the start of the file.
|
|
186
|
+
*
|
|
187
|
+
* @param content - Full markdown content
|
|
188
|
+
* @returns Parsed frontmatter object or null if not found/invalid
|
|
189
|
+
*/
|
|
190
|
+
function parseFrontmatter(content) {
|
|
191
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
192
|
+
if (!frontmatterMatch) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const yamlContent = frontmatterMatch[1];
|
|
196
|
+
try {
|
|
197
|
+
// Simple YAML parser for key: value pairs
|
|
198
|
+
const result = {};
|
|
199
|
+
const lines = yamlContent.split('\n');
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
202
|
+
if (match) {
|
|
203
|
+
const [, key, value] = match;
|
|
204
|
+
result[key] = value.trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Removes YAML frontmatter from markdown content.
|
|
215
|
+
*
|
|
216
|
+
* @param content - Full markdown content
|
|
217
|
+
* @returns Content without frontmatter
|
|
218
|
+
*/
|
|
219
|
+
function removeFrontmatter(content) {
|
|
220
|
+
return content.replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Parses markdown sections based on ## headings.
|
|
224
|
+
*
|
|
225
|
+
* @param content - Markdown content (without frontmatter)
|
|
226
|
+
* @returns Map of section name to section content
|
|
227
|
+
*/
|
|
228
|
+
function parseSections(content) {
|
|
229
|
+
const sections = new Map();
|
|
230
|
+
const lines = content.split('\n');
|
|
231
|
+
let currentSection = null;
|
|
232
|
+
let currentContent = [];
|
|
233
|
+
for (const line of lines) {
|
|
234
|
+
// Check for ## heading
|
|
235
|
+
const headingMatch = line.match(/^##\s+(.+)$/);
|
|
236
|
+
if (headingMatch) {
|
|
237
|
+
// Save previous section if it exists
|
|
238
|
+
if (currentSection) {
|
|
239
|
+
sections.set(currentSection, currentContent.join('\n'));
|
|
240
|
+
}
|
|
241
|
+
// Start new section
|
|
242
|
+
currentSection = headingMatch[1].trim();
|
|
243
|
+
currentContent = [];
|
|
244
|
+
}
|
|
245
|
+
else if (currentSection) {
|
|
246
|
+
// Add line to current section
|
|
247
|
+
currentContent.push(line);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Save last section
|
|
251
|
+
if (currentSection) {
|
|
252
|
+
sections.set(currentSection, currentContent.join('\n'));
|
|
253
|
+
}
|
|
254
|
+
return sections;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Counts bullet points in a markdown section.
|
|
258
|
+
* Looks for lines starting with -, *, or + (standard markdown bullets).
|
|
259
|
+
*
|
|
260
|
+
* @param content - Section content
|
|
261
|
+
* @returns Number of bullet points found
|
|
262
|
+
*/
|
|
263
|
+
function countBulletPoints(content) {
|
|
264
|
+
const lines = content.split('\n');
|
|
265
|
+
let count = 0;
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
if (line.trim().match(/^[-*+]\s+/)) {
|
|
268
|
+
count++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return count;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Validates a PRD artifact for coverage against IDEA constraints.
|
|
275
|
+
*
|
|
276
|
+
* **Phase 2 MVP Stub Implementation**
|
|
277
|
+
* This is a simplified implementation that calculates coverage but does not
|
|
278
|
+
* invoke the Momus agent for critical review. Full Momus integration is
|
|
279
|
+
* deferred to Phase 3.
|
|
280
|
+
*
|
|
281
|
+
* Checks:
|
|
282
|
+
* - PRD addresses >= 90% of IDEA constraints
|
|
283
|
+
* - User stories are present
|
|
284
|
+
* - Requirement coverage section exists
|
|
285
|
+
*
|
|
286
|
+
* TODO (Phase 3): Integrate Momus agent for:
|
|
287
|
+
* - Scope drift detection
|
|
288
|
+
* - Acceptance criteria completeness check
|
|
289
|
+
* - Risk alignment verification
|
|
290
|
+
*
|
|
291
|
+
* @param artifactPath - Absolute path to the PRD artifact file
|
|
292
|
+
* @param ideaPath - Absolute path to the IDEA artifact file
|
|
293
|
+
* @returns ValidationResult with coverage percentage and Momus placeholder
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* const result = await validatePrd(
|
|
297
|
+
* '.olympus/workflows/feature-x/prd.md',
|
|
298
|
+
* '.olympus/workflows/feature-x/idea.md'
|
|
299
|
+
* );
|
|
300
|
+
* if (result.coverage_percentage >= 90) {
|
|
301
|
+
* console.log('PRD has sufficient coverage');
|
|
302
|
+
* }
|
|
303
|
+
*/
|
|
304
|
+
export async function validatePrd(artifactPath, ideaPath) {
|
|
305
|
+
const timestamp = new Date().toISOString();
|
|
306
|
+
const blockingIssues = [];
|
|
307
|
+
// Read PRD artifact
|
|
308
|
+
let prdContent;
|
|
309
|
+
try {
|
|
310
|
+
prdContent = readFileSync(artifactPath, 'utf-8');
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const err = error;
|
|
314
|
+
console.error(`[Validation] Failed to read PRD artifact: ${err.message}`);
|
|
315
|
+
console.error(`[Validation] Path: ${artifactPath}`);
|
|
316
|
+
const errorMsg = err.code === 'ENOENT'
|
|
317
|
+
? 'PRD artifact file not found'
|
|
318
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
319
|
+
? 'Permission denied reading PRD artifact'
|
|
320
|
+
: `Failed to read PRD artifact: ${err.message}`;
|
|
321
|
+
return {
|
|
322
|
+
passed: false,
|
|
323
|
+
coverage_percentage: 0,
|
|
324
|
+
blocking_issues: [errorMsg],
|
|
325
|
+
reviewer: 'momus',
|
|
326
|
+
timestamp,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// Read IDEA artifact
|
|
330
|
+
let ideaContent;
|
|
331
|
+
try {
|
|
332
|
+
ideaContent = readFileSync(ideaPath, 'utf-8');
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const err = error;
|
|
336
|
+
console.error(`[Validation] Failed to read IDEA artifact for PRD validation: ${err.message}`);
|
|
337
|
+
console.error(`[Validation] Path: ${ideaPath}`);
|
|
338
|
+
const errorMsg = err.code === 'ENOENT'
|
|
339
|
+
? 'IDEA artifact file not found for reference'
|
|
340
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
341
|
+
? 'Permission denied reading IDEA artifact'
|
|
342
|
+
: `Failed to read IDEA artifact: ${err.message}`;
|
|
343
|
+
return {
|
|
344
|
+
passed: false,
|
|
345
|
+
coverage_percentage: 0,
|
|
346
|
+
blocking_issues: [errorMsg],
|
|
347
|
+
reviewer: 'momus',
|
|
348
|
+
timestamp,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// Parse IDEA constraints
|
|
352
|
+
const ideaMarkdown = removeFrontmatter(ideaContent);
|
|
353
|
+
const ideaSections = parseSections(ideaMarkdown);
|
|
354
|
+
const constraintsSection = ideaSections.get('Constraints');
|
|
355
|
+
const ideaConstraints = constraintsSection
|
|
356
|
+
? countBulletPoints(constraintsSection)
|
|
357
|
+
: 0;
|
|
358
|
+
// Parse PRD user stories
|
|
359
|
+
const prdMarkdown = removeFrontmatter(prdContent);
|
|
360
|
+
const prdSections = parseSections(prdMarkdown);
|
|
361
|
+
// Count user stories (sections starting with "US-" or "### US-")
|
|
362
|
+
let userStoryCount = 0;
|
|
363
|
+
for (const line of prdMarkdown.split('\n')) {
|
|
364
|
+
if (line.match(/^###?\s+US-\d+/)) {
|
|
365
|
+
userStoryCount++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Check for requirement coverage section
|
|
369
|
+
const hasCoverageSection = prdSections.has('Requirement Coverage');
|
|
370
|
+
// Calculate coverage percentage
|
|
371
|
+
// Simplified: assume each user story addresses one constraint
|
|
372
|
+
// Real implementation would parse the coverage table
|
|
373
|
+
const coveragePercentage = ideaConstraints > 0
|
|
374
|
+
? Math.round((Math.min(userStoryCount, ideaConstraints) / ideaConstraints) * 100)
|
|
375
|
+
: 100;
|
|
376
|
+
// Validate completeness
|
|
377
|
+
if (userStoryCount === 0) {
|
|
378
|
+
blockingIssues.push('No user stories found in PRD');
|
|
379
|
+
}
|
|
380
|
+
if (!hasCoverageSection) {
|
|
381
|
+
blockingIssues.push('Missing Requirement Coverage section');
|
|
382
|
+
}
|
|
383
|
+
if (coveragePercentage < 90) {
|
|
384
|
+
blockingIssues.push(`Coverage only ${coveragePercentage}%, need at least 90% (${userStoryCount}/${ideaConstraints} constraints addressed)`);
|
|
385
|
+
}
|
|
386
|
+
// TODO (Phase 3): Invoke Momus agent here for critical review
|
|
387
|
+
// const momusReview = await invokeMomusAgent(prdContent, ideaContent);
|
|
388
|
+
// blockingIssues.push(...momusReview.issues);
|
|
389
|
+
return {
|
|
390
|
+
passed: blockingIssues.length === 0 && coveragePercentage >= 90,
|
|
391
|
+
coverage_percentage: coveragePercentage,
|
|
392
|
+
blocking_issues: blockingIssues,
|
|
393
|
+
reviewer: 'momus', // Placeholder - real Momus review deferred to Phase 3
|
|
394
|
+
timestamp,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Validates a SPEC artifact for coverage against PRD user stories.
|
|
399
|
+
*
|
|
400
|
+
* **Phase 2 MVP Stub Implementation**
|
|
401
|
+
* This is a simplified implementation that calculates coverage but does not
|
|
402
|
+
* invoke the Metis agent for critical review. Full Metis integration is
|
|
403
|
+
* deferred to Phase 3.
|
|
404
|
+
*
|
|
405
|
+
* Checks:
|
|
406
|
+
* - SPEC implements >= 95% of PRD user stories
|
|
407
|
+
* - Requirement coverage section exists
|
|
408
|
+
* - All components documented
|
|
409
|
+
*
|
|
410
|
+
* TODO (Phase 3): Integrate Metis agent for:
|
|
411
|
+
* - Hidden requirements analysis
|
|
412
|
+
* - Dependency mapping completeness
|
|
413
|
+
* - Security considerations adequacy
|
|
414
|
+
* - Performance requirements coverage
|
|
415
|
+
*
|
|
416
|
+
* @param specPath - Absolute path to the SPEC artifact file
|
|
417
|
+
* @param prdPath - Absolute path to the PRD artifact file
|
|
418
|
+
* @returns ValidationResult with coverage percentage and Metis placeholder
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* const result = await validateSpec(
|
|
422
|
+
* '.olympus/workflows/feature-x/spec.md',
|
|
423
|
+
* '.olympus/workflows/feature-x/prd.md'
|
|
424
|
+
* );
|
|
425
|
+
* if (result.coverage_percentage >= 95) {
|
|
426
|
+
* console.log('SPEC has sufficient PRD coverage');
|
|
427
|
+
* }
|
|
428
|
+
*/
|
|
429
|
+
export async function validateSpec(specPath, prdPath) {
|
|
430
|
+
const timestamp = new Date().toISOString();
|
|
431
|
+
const blockingIssues = [];
|
|
432
|
+
// Read SPEC artifact
|
|
433
|
+
let specContent;
|
|
434
|
+
try {
|
|
435
|
+
specContent = readFileSync(specPath, 'utf-8');
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
const err = error;
|
|
439
|
+
console.error(`[Validation] Failed to read SPEC artifact: ${err.message}`);
|
|
440
|
+
console.error(`[Validation] Path: ${specPath}`);
|
|
441
|
+
const errorMsg = err.code === 'ENOENT'
|
|
442
|
+
? 'SPEC artifact file not found'
|
|
443
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
444
|
+
? 'Permission denied reading SPEC artifact'
|
|
445
|
+
: `Failed to read SPEC artifact: ${err.message}`;
|
|
446
|
+
return {
|
|
447
|
+
passed: false,
|
|
448
|
+
coverage_percentage: 0,
|
|
449
|
+
blocking_issues: [errorMsg],
|
|
450
|
+
reviewer: 'metis',
|
|
451
|
+
timestamp,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// Read PRD artifact
|
|
455
|
+
let prdContent;
|
|
456
|
+
try {
|
|
457
|
+
prdContent = readFileSync(prdPath, 'utf-8');
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const err = error;
|
|
461
|
+
console.error(`[Validation] Failed to read PRD artifact for SPEC validation: ${err.message}`);
|
|
462
|
+
console.error(`[Validation] Path: ${prdPath}`);
|
|
463
|
+
const errorMsg = err.code === 'ENOENT'
|
|
464
|
+
? 'PRD artifact file not found for reference'
|
|
465
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
466
|
+
? 'Permission denied reading PRD artifact'
|
|
467
|
+
: `Failed to read PRD artifact: ${err.message}`;
|
|
468
|
+
return {
|
|
469
|
+
passed: false,
|
|
470
|
+
coverage_percentage: 0,
|
|
471
|
+
blocking_issues: [errorMsg],
|
|
472
|
+
reviewer: 'metis',
|
|
473
|
+
timestamp,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
// Parse PRD user stories
|
|
477
|
+
const prdMarkdown = removeFrontmatter(prdContent);
|
|
478
|
+
const prdUserStories = [];
|
|
479
|
+
for (const line of prdMarkdown.split('\n')) {
|
|
480
|
+
const match = line.match(/^###?\s+(US-\d+)/);
|
|
481
|
+
if (match) {
|
|
482
|
+
prdUserStories.push(match[1]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Parse SPEC for user story coverage
|
|
486
|
+
const specMarkdown = removeFrontmatter(specContent);
|
|
487
|
+
const specSections = parseSections(specMarkdown);
|
|
488
|
+
const coverageSection = specSections.get('Requirement Coverage') || specSections.get('PRD Coverage') || '';
|
|
489
|
+
// Count how many PRD user stories are referenced in SPEC
|
|
490
|
+
let coveredStories = 0;
|
|
491
|
+
for (const story of prdUserStories) {
|
|
492
|
+
if (specMarkdown.includes(story)) {
|
|
493
|
+
coveredStories++;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Calculate coverage percentage
|
|
497
|
+
const coveragePercentage = prdUserStories.length > 0
|
|
498
|
+
? Math.round((coveredStories / prdUserStories.length) * 100)
|
|
499
|
+
: 0;
|
|
500
|
+
// Validate completeness
|
|
501
|
+
if (prdUserStories.length === 0) {
|
|
502
|
+
blockingIssues.push('No user stories found in PRD for validation');
|
|
503
|
+
}
|
|
504
|
+
if (!coverageSection || coverageSection.trim().length === 0) {
|
|
505
|
+
blockingIssues.push('Missing Requirement Coverage section in SPEC');
|
|
506
|
+
}
|
|
507
|
+
if (coveragePercentage < 95) {
|
|
508
|
+
blockingIssues.push(`Coverage only ${coveragePercentage}%, need at least 95% (${coveredStories}/${prdUserStories.length} user stories addressed)`);
|
|
509
|
+
}
|
|
510
|
+
// Check for components section
|
|
511
|
+
const hasComponentsSection = specSections.has('Components') || specSections.has('Architecture');
|
|
512
|
+
if (!hasComponentsSection) {
|
|
513
|
+
blockingIssues.push('Missing Components or Architecture section');
|
|
514
|
+
}
|
|
515
|
+
// TODO (Phase 3): Invoke Metis agent here for critical review
|
|
516
|
+
// const metisReview = await invokeMetisAgent(specContent, prdContent);
|
|
517
|
+
// blockingIssues.push(...metisReview.issues);
|
|
518
|
+
// Metis should check:
|
|
519
|
+
// - Hidden requirements not explicitly stated in PRD
|
|
520
|
+
// - Dependency mapping completeness
|
|
521
|
+
// - Security considerations adequacy
|
|
522
|
+
// - Performance requirements coverage
|
|
523
|
+
return {
|
|
524
|
+
passed: blockingIssues.length === 0 && coveragePercentage >= 95,
|
|
525
|
+
coverage_percentage: coveragePercentage,
|
|
526
|
+
blocking_issues: blockingIssues,
|
|
527
|
+
reviewer: 'metis', // Placeholder - real Metis review deferred to Phase 3
|
|
528
|
+
timestamp,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Validates TASKS artifacts for coverage against SPEC components.
|
|
533
|
+
*
|
|
534
|
+
* Checks:
|
|
535
|
+
* - 100% of SPEC components have tasks
|
|
536
|
+
* - Dependency graph is valid (no circular dependencies)
|
|
537
|
+
* - All tasks have effort estimates
|
|
538
|
+
* - Effort estimates are reasonable (1, 2, 4, 8, or 16 hours)
|
|
539
|
+
*
|
|
540
|
+
* @param tasksDir - Absolute path to the tasks directory (contains INTENT files)
|
|
541
|
+
* @param specPath - Absolute path to the SPEC artifact file
|
|
542
|
+
* @returns ValidationResult with coverage percentage and validation details
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* const result = await validateTasks(
|
|
546
|
+
* '.olympus/workflows/feature-x/intents/',
|
|
547
|
+
* '.olympus/workflows/feature-x/spec.md'
|
|
548
|
+
* );
|
|
549
|
+
* if (result.passed) {
|
|
550
|
+
* console.log('All SPEC components have task coverage');
|
|
551
|
+
* }
|
|
552
|
+
*/
|
|
553
|
+
export async function validateTasks(tasksDir, specPath) {
|
|
554
|
+
const timestamp = new Date().toISOString();
|
|
555
|
+
const blockingIssues = [];
|
|
556
|
+
// Read SPEC artifact
|
|
557
|
+
let specContent;
|
|
558
|
+
try {
|
|
559
|
+
specContent = readFileSync(specPath, 'utf-8');
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const err = error;
|
|
563
|
+
console.error(`[Validation] Failed to read SPEC artifact for task validation: ${err.message}`);
|
|
564
|
+
console.error(`[Validation] Path: ${specPath}`);
|
|
565
|
+
const errorMsg = err.code === 'ENOENT'
|
|
566
|
+
? 'SPEC artifact file not found'
|
|
567
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
568
|
+
? 'Permission denied reading SPEC artifact'
|
|
569
|
+
: `Failed to read SPEC artifact: ${err.message}`;
|
|
570
|
+
return {
|
|
571
|
+
passed: false,
|
|
572
|
+
coverage_percentage: 0,
|
|
573
|
+
blocking_issues: [errorMsg],
|
|
574
|
+
timestamp,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// Parse SPEC for components
|
|
578
|
+
const specMarkdown = removeFrontmatter(specContent);
|
|
579
|
+
const specSections = parseSections(specMarkdown);
|
|
580
|
+
const componentsSection = specSections.get('Components') || specSections.get('Architecture') || '';
|
|
581
|
+
// Extract component names (look for ### headings in components section)
|
|
582
|
+
const specComponents = [];
|
|
583
|
+
if (componentsSection) {
|
|
584
|
+
for (const line of componentsSection.split('\n')) {
|
|
585
|
+
const match = line.match(/^###\s+(.+)$/);
|
|
586
|
+
if (match) {
|
|
587
|
+
specComponents.push(match[1].trim());
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Read INTENT files from tasksDir
|
|
592
|
+
let intentFiles = [];
|
|
593
|
+
try {
|
|
594
|
+
const fs = await import('fs');
|
|
595
|
+
const files = fs.readdirSync(tasksDir);
|
|
596
|
+
intentFiles = files.filter(f => f.endsWith('.md') || f.includes('INTENT'));
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
const err = error;
|
|
600
|
+
console.error(`[Validation] Failed to read tasks directory: ${err.message}`);
|
|
601
|
+
console.error(`[Validation] Path: ${tasksDir}`);
|
|
602
|
+
const errorMsg = err.code === 'ENOENT'
|
|
603
|
+
? 'Tasks directory not found'
|
|
604
|
+
: err.code === 'EACCES' || err.code === 'EPERM'
|
|
605
|
+
? 'Permission denied reading tasks directory'
|
|
606
|
+
: `Failed to read tasks directory: ${err.message}`;
|
|
607
|
+
return {
|
|
608
|
+
passed: false,
|
|
609
|
+
coverage_percentage: 0,
|
|
610
|
+
blocking_issues: [errorMsg],
|
|
611
|
+
timestamp,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// Parse INTENT files for component coverage
|
|
615
|
+
const coveredComponents = new Set();
|
|
616
|
+
const taskEstimates = [];
|
|
617
|
+
for (const intentFile of intentFiles) {
|
|
618
|
+
try {
|
|
619
|
+
const fs = await import('fs');
|
|
620
|
+
const path = await import('path');
|
|
621
|
+
const intentPath = path.join(tasksDir, intentFile);
|
|
622
|
+
const intentContent = fs.readFileSync(intentPath, 'utf-8');
|
|
623
|
+
// Check which components are mentioned in this INTENT
|
|
624
|
+
for (const component of specComponents) {
|
|
625
|
+
if (intentContent.includes(component)) {
|
|
626
|
+
coveredComponents.add(component);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Extract effort estimate
|
|
630
|
+
const effortMatch = intentContent.match(/estimated_effort:\s*(\d+)/i);
|
|
631
|
+
if (effortMatch) {
|
|
632
|
+
taskEstimates.push(parseInt(effortMatch[1], 10));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (_error) {
|
|
636
|
+
// Skip unreadable files
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Calculate coverage percentage
|
|
640
|
+
const coveragePercentage = specComponents.length > 0
|
|
641
|
+
? Math.round((coveredComponents.size / specComponents.length) * 100)
|
|
642
|
+
: 100;
|
|
643
|
+
// Validate 100% coverage
|
|
644
|
+
if (specComponents.length === 0) {
|
|
645
|
+
blockingIssues.push('No components found in SPEC for validation');
|
|
646
|
+
}
|
|
647
|
+
else if (coveragePercentage < 100) {
|
|
648
|
+
const uncovered = specComponents.filter(c => !coveredComponents.has(c));
|
|
649
|
+
blockingIssues.push(`Incomplete coverage: ${coveragePercentage}% (missing: ${uncovered.join(', ')})`);
|
|
650
|
+
}
|
|
651
|
+
// Validate effort estimates
|
|
652
|
+
const validEstimates = [1, 2, 4, 8, 16];
|
|
653
|
+
for (const estimate of taskEstimates) {
|
|
654
|
+
if (!validEstimates.includes(estimate)) {
|
|
655
|
+
blockingIssues.push(`Invalid effort estimate: ${estimate} hours (must be 1, 2, 4, 8, or 16)`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (intentFiles.length > 0 && taskEstimates.length === 0) {
|
|
659
|
+
blockingIssues.push('No effort estimates found in task files');
|
|
660
|
+
}
|
|
661
|
+
// Check for variance in estimates (within 30%)
|
|
662
|
+
if (taskEstimates.length > 1) {
|
|
663
|
+
const avgEstimate = taskEstimates.reduce((a, b) => a + b, 0) / taskEstimates.length;
|
|
664
|
+
const maxVariance = avgEstimate * 0.3;
|
|
665
|
+
for (const estimate of taskEstimates) {
|
|
666
|
+
if (Math.abs(estimate - avgEstimate) > maxVariance) {
|
|
667
|
+
// This is a warning, not a blocking issue
|
|
668
|
+
// Only add if variance is extreme (>50%)
|
|
669
|
+
if (Math.abs(estimate - avgEstimate) > avgEstimate * 0.5) {
|
|
670
|
+
blockingIssues.push(`High variance in effort estimates: ${estimate}h vs avg ${Math.round(avgEstimate)}h`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Validate dependency graph
|
|
676
|
+
try {
|
|
677
|
+
const fs = await import('fs');
|
|
678
|
+
const path = await import('path');
|
|
679
|
+
const graphPath = path.join(tasksDir, 'dependency-graph.json');
|
|
680
|
+
const graphContent = fs.readFileSync(graphPath, 'utf-8');
|
|
681
|
+
const graph = JSON.parse(graphContent);
|
|
682
|
+
// Basic cycle detection
|
|
683
|
+
const hasCycle = detectCycles(graph);
|
|
684
|
+
if (hasCycle) {
|
|
685
|
+
blockingIssues.push('Circular dependencies detected in dependency graph');
|
|
686
|
+
}
|
|
687
|
+
// Validate all referenced dependencies exist
|
|
688
|
+
const taskIds = new Set(Object.keys(graph));
|
|
689
|
+
for (const [taskId, deps] of Object.entries(graph)) {
|
|
690
|
+
if (Array.isArray(deps)) {
|
|
691
|
+
for (const dep of deps) {
|
|
692
|
+
if (!taskIds.has(dep)) {
|
|
693
|
+
blockingIssues.push(`Task ${taskId} references non-existent dependency: ${dep}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (_error) {
|
|
700
|
+
// Dependency graph is optional for now
|
|
701
|
+
// blockingIssues.push('Dependency graph file not found or invalid');
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
passed: blockingIssues.length === 0 && coveragePercentage === 100,
|
|
705
|
+
coverage_percentage: coveragePercentage,
|
|
706
|
+
blocking_issues: blockingIssues,
|
|
707
|
+
timestamp,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Detects cycles in a dependency graph using depth-first search.
|
|
712
|
+
*
|
|
713
|
+
* @param graph - Adjacency list representation of dependencies
|
|
714
|
+
* @returns true if a cycle is detected, false otherwise
|
|
715
|
+
*/
|
|
716
|
+
function detectCycles(graph) {
|
|
717
|
+
const visited = new Set();
|
|
718
|
+
const recursionStack = new Set();
|
|
719
|
+
function dfs(node) {
|
|
720
|
+
visited.add(node);
|
|
721
|
+
recursionStack.add(node);
|
|
722
|
+
const neighbors = graph[node] || [];
|
|
723
|
+
for (const neighbor of neighbors) {
|
|
724
|
+
if (!visited.has(neighbor)) {
|
|
725
|
+
if (dfs(neighbor)) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else if (recursionStack.has(neighbor)) {
|
|
730
|
+
// Found a back edge - cycle detected
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
recursionStack.delete(node);
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
for (const node of Object.keys(graph)) {
|
|
738
|
+
if (!visited.has(node)) {
|
|
739
|
+
if (dfs(node)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
//# sourceMappingURL=validation.js.map
|