@polymorphism-tech/morph-spec 7.2.0 → 7.4.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.
Files changed (74) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-proposal.md +1 -1
  12. package/framework/evals/rubrics/README.md +9 -8
  13. package/framework/hooks/README.md +3 -1
  14. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  16. package/framework/hooks/claude-code/core/validate-completion.js +241 -136
  17. package/framework/hooks/shared/state-reader.js +236 -221
  18. package/framework/phases.json +1 -1
  19. package/framework/skills/README.md +2 -2
  20. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  23. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  24. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +13 -6
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/ai-agents/setup.md +258 -244
  30. package/framework/templates/README.md +708 -709
  31. package/framework/templates/REGISTRY.json +1 -20
  32. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  33. package/framework/templates/docs/proposal.md +171 -171
  34. package/framework/templates/feature/recap.md +145 -145
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +941 -946
  37. package/src/commands/project/init.js +517 -515
  38. package/src/commands/project/status.js +373 -376
  39. package/src/commands/state/approve.js +251 -213
  40. package/src/commands/validation/validate-feature.js +356 -287
  41. package/src/core/state/state-manager.js +910 -934
  42. package/src/dashboard/app/App.jsx +29 -31
  43. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  44. package/src/dashboard/scanner/index.js +20 -21
  45. package/src/lib/context/context-builder.js +3 -3
  46. package/src/lib/generators/context-generator.js +2 -3
  47. package/src/lib/generators/mandate-generator.js +48 -92
  48. package/src/lib/generators/metadata-extractor.js +385 -386
  49. package/src/lib/generators/recap-generator.js +285 -285
  50. package/src/lib/risk-detector.js +2 -2
  51. package/src/lib/validators/content/content-validator.js +89 -286
  52. package/src/lib/validators/content/index.js +0 -1
  53. package/src/utils/hooks-installer.js +527 -516
  54. package/framework/templates/feature/tasks.md +0 -199
  55. package/framework/workflows/README.md +0 -1041
  56. package/framework/workflows/configs/fast-track.json +0 -46
  57. package/framework/workflows/configs/full.json +0 -64
  58. package/framework/workflows/configs/quick.json +0 -32
  59. package/framework/workflows/configs/standard.json +0 -57
  60. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  61. package/framework/workflows/docs/design-impl.md +0 -37
  62. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  63. package/framework/workflows/docs/fast-track.md +0 -29
  64. package/framework/workflows/docs/full-morph.md +0 -69
  65. package/framework/workflows/docs/standard.md +0 -49
  66. package/framework/workflows/docs/ui-refresh.md +0 -39
  67. package/src/commands/state/index.js +0 -7
  68. package/src/core/workflows/workflow-detector.js +0 -586
  69. package/src/core/workflows/workflow-router.js +0 -204
  70. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  71. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  72. package/src/lib/phase-chain/phase-validator.js +0 -359
  73. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  74. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,287 +1,356 @@
1
- /**
2
- * MORPH-SPEC Feature Validation Command
3
- *
4
- * Content-aware validation of feature outputs at each phase.
5
- * Checks section presence, schema consistency, and contract correctness.
6
- *
7
- * Usage:
8
- * morph-spec validate <feature-name> [--phase <phase>] [--json] [--verbose]
9
- *
10
- * Example:
11
- * morph-spec validate user-portal --phase design
12
- * morph-spec validate user-portal --json
13
- */
14
-
15
- import fs from 'fs';
16
- import path from 'path';
17
- import chalk from 'chalk';
18
- import { getFeature } from '../../core/state/state-manager.js';
19
-
20
- /**
21
- * Expected sections per output file
22
- */
23
- const OUTPUT_SECTIONS = {
24
- 'proposal.md': {
25
- required: ['Overview', 'Problem', 'Solution', 'Scope'],
26
- optional: ['Agents', 'Stack', 'Risks']
27
- },
28
- 'spec.md': {
29
- required: [
30
- 'Overview', 'Functional Requirements', 'Non-Functional Requirements',
31
- 'Technical Architecture', 'Data Model'
32
- ],
33
- optional: [
34
- 'Infrastructure', 'Testing Strategy', 'Security',
35
- 'Performance', 'Edge Cases', 'Clarifications'
36
- ]
37
- },
38
- 'mandate.md': {
39
- required: ['Stack', 'Standards', 'Done'],
40
- optional: ['Patterns proibidos', 'Riscos']
41
- },
42
- 'decisions.md': {
43
- required: ['ADR', 'Status', 'Context', 'Decision'],
44
- optional: ['Consequences', 'Alternatives', 'Costs']
45
- },
46
- 'tasks.md': {
47
- required: ['T001', 'category', 'dependencies'],
48
- optional: ['CHECKPOINT', 'estimatedMinutes']
49
- },
50
- 'review-report.md': {
51
- required: ['Scores', 'Recommendation'],
52
- optional: ['Blockers', 'Improvements']
53
- },
54
- };
55
-
56
- /**
57
- * Phase-to-output mapping (v5 NLH)
58
- */
59
- const PHASE_OUTPUTS = {
60
- proposal: ['proposal.md'],
61
- uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
62
- plan: ['spec.md', 'mandate.md', 'tasks.md'],
63
- implement: ['recap.md'],
64
- review: ['review-report.md'],
65
- };
66
-
67
- /**
68
- * Phase-to-folder mapping (v5 filesystem convention)
69
- */
70
- const PHASE_FOLDERS = {
71
- proposal: '0-proposal',
72
- uiux: '1-design',
73
- plan: '2-plan',
74
- implement: '3-implement',
75
- review: '4-review',
76
- };
77
-
78
- /**
79
- * Validate a single output file
80
- */
81
- function validateOutput(featurePath, outputFile) {
82
- const filePath = path.join(featurePath, outputFile);
83
- const result = {
84
- file: outputFile,
85
- exists: false,
86
- sections: { found: [], missing: [], optional: [] },
87
- warnings: [],
88
- errors: [],
89
- lineCount: 0
90
- };
91
-
92
- if (!fs.existsSync(filePath)) {
93
- result.errors.push(`File not found: ${outputFile}`);
94
- return result;
95
- }
96
-
97
- result.exists = true;
98
- const content = fs.readFileSync(filePath, 'utf8');
99
- result.lineCount = content.split('\n').length;
100
-
101
- // Skip section checks for files without defined sections
102
- const sections = OUTPUT_SECTIONS[outputFile];
103
- if (!sections) {
104
- return result;
105
- }
106
-
107
- // v5: only markdown outputs — contracts moved into spec.md
108
- validateMarkdownSections(content, sections, result);
109
-
110
- // Size warnings
111
- if (result.lineCount < 10) {
112
- result.warnings.push(`File is very short (${result.lineCount} lines) — may be incomplete`);
113
- }
114
-
115
- return result;
116
- }
117
-
118
- /**
119
- * Validate markdown sections
120
- */
121
- function validateMarkdownSections(content, sections, result) {
122
- const contentLower = content.toLowerCase();
123
-
124
- for (const section of sections.required) {
125
- if (contentLower.includes(section.toLowerCase())) {
126
- result.sections.found.push(section);
127
- } else {
128
- result.sections.missing.push(section);
129
- result.errors.push(`Missing required section: "${section}"`);
130
- }
131
- }
132
-
133
- for (const section of (sections.optional || [])) {
134
- if (contentLower.includes(section.toLowerCase())) {
135
- result.sections.optional.push(section);
136
- }
137
- }
138
- }
139
-
140
- /**
141
- * Main validate command
142
- */
143
- export async function validateFeatureCommand(featureName, options = {}) {
144
- if (!featureName) {
145
- console.error(chalk.red('Error: Feature name required'));
146
- console.error(chalk.gray('Usage: morph-spec validate <feature-name> [--phase <phase>]'));
147
- process.exit(1);
148
- }
149
-
150
- const featurePath = path.join(process.cwd(), '.morph/features', featureName);
151
-
152
- if (!fs.existsSync(featurePath)) {
153
- console.error(chalk.red(`Error: Feature directory not found: ${featurePath}`));
154
- process.exit(1);
155
- }
156
-
157
- // Determine which outputs to validate
158
- const phase = options.phase || null;
159
- let outputFiles;
160
-
161
- if (phase) {
162
- outputFiles = PHASE_OUTPUTS[phase];
163
- if (!outputFiles) {
164
- console.error(chalk.red(`Unknown phase: ${phase}`));
165
- console.error(chalk.gray(`Valid phases: ${Object.keys(PHASE_OUTPUTS).join(', ')}`));
166
- process.exit(1);
167
- }
168
- } else {
169
- // Validate all existing outputs across all phase folders
170
- outputFiles = [];
171
- const entries = fs.readdirSync(featurePath, { withFileTypes: true });
172
- for (const entry of entries) {
173
- if (entry.isDirectory()) {
174
- const subDir = path.join(featurePath, entry.name);
175
- const files = fs.readdirSync(subDir).filter(f => f.endsWith('.md') || f.endsWith('.cs'));
176
- outputFiles.push(...files.map(f => ({ file: f, dir: subDir })));
177
- } else if (entry.name.endsWith('.md') || entry.name.endsWith('.cs')) {
178
- outputFiles.push({ file: entry.name, dir: featurePath });
179
- }
180
- }
181
- }
182
-
183
- // Resolve phase folder for path lookup
184
- const phaseFolder = phase ? PHASE_FOLDERS[phase] : null;
185
- const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
186
-
187
- // Run validation
188
- const results = phase
189
- ? outputFiles.map(file => validateOutput(searchPath, file))
190
- : outputFiles.map(({ file, dir }) => validateOutput(dir, file));
191
- const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
192
-
193
- // Aggregate results
194
- const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
195
- const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0) + schemaWarnings.length;
196
- const totalFound = results.reduce((sum, r) => sum + r.sections.found.length, 0);
197
-
198
- // JSON output mode
199
- if (options.json) {
200
- const output = {
201
- feature: featureName,
202
- phase: phase || 'all',
203
- valid: totalErrors === 0,
204
- summary: {
205
- files: results.length,
206
- errors: totalErrors,
207
- warnings: totalWarnings,
208
- sectionsFound: totalFound
209
- },
210
- results: results.map(r => ({
211
- file: r.file,
212
- exists: r.exists,
213
- lineCount: r.lineCount,
214
- errors: r.errors,
215
- warnings: r.warnings,
216
- sectionsFound: r.sections.found,
217
- sectionsMissing: r.sections.missing
218
- })),
219
- schemaWarnings
220
- };
221
- console.log(JSON.stringify(output, null, 2));
222
- return;
223
- }
224
-
225
- // Visual output
226
- console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
227
- console.log(chalk.cyan('│ MORPH-SPEC FEATURE VALIDATOR │'));
228
- console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
229
-
230
- console.log(chalk.white.bold(`Feature: ${featureName}`));
231
- if (phase) console.log(chalk.gray(`Phase: ${phase}`));
232
- console.log('');
233
-
234
- // File-by-file results
235
- for (const result of results) {
236
- const symbol = result.exists
237
- ? (result.errors.length === 0 ? '✅' : '❌')
238
- : '⬜';
239
- const name = result.exists
240
- ? (result.errors.length === 0 ? chalk.green(result.file) : chalk.red(result.file))
241
- : chalk.gray(result.file);
242
-
243
- console.log(`${symbol} ${name}${result.exists ? chalk.gray(` (${result.lineCount} lines)`) : ''}`);
244
-
245
- // Sections found
246
- if (result.sections.found.length > 0 && (options.verbose || result.errors.length > 0)) {
247
- result.sections.found.forEach(s => {
248
- console.log(chalk.green(` ✓ ${s}`));
249
- });
250
- }
251
-
252
- // Errors
253
- result.errors.forEach(err => {
254
- console.log(chalk.red(` ✗ ${err}`));
255
- });
256
-
257
- // Warnings
258
- result.warnings.forEach(warn => {
259
- console.log(chalk.yellow(` ⚠ ${warn}`));
260
- });
261
- }
262
-
263
- // Schema cross-validation
264
- if (schemaWarnings.length > 0) {
265
- console.log(chalk.yellow('\nSchema Cross-Validation:'));
266
- schemaWarnings.forEach(w => {
267
- console.log(chalk.yellow(` ⚠ ${w}`));
268
- });
269
- }
270
-
271
- // Summary
272
- console.log('');
273
- console.log(chalk.gray('─'.repeat(60)));
274
-
275
- if (totalErrors === 0) {
276
- console.log(chalk.green(`\n✓ VALIDATION PASSED — ${results.filter(r => r.exists).length} files validated`));
277
- if (totalWarnings > 0) {
278
- console.log(chalk.yellow(` ${totalWarnings} warning(s) — review recommended`));
279
- }
280
- } else {
281
- console.log(chalk.red(`\n✗ VALIDATION FAILED — ${totalErrors} error(s), ${totalWarnings} warning(s)`));
282
- console.log(chalk.yellow(' Fix errors before advancing to the next phase'));
283
- }
284
- console.log('');
285
- }
286
-
287
- export default validateFeatureCommand;
1
+ /**
2
+ * MORPH-SPEC Feature Validation Command
3
+ *
4
+ * Content-aware validation of feature outputs at each phase.
5
+ * Checks section presence, schema consistency, and contract correctness.
6
+ *
7
+ * Usage:
8
+ * morph-spec validate <feature-name> [--phase <phase>] [--json] [--verbose]
9
+ *
10
+ * Example:
11
+ * morph-spec validate user-portal --phase design
12
+ * morph-spec validate user-portal --json
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import chalk from 'chalk';
18
+ import { getFeature } from '../../core/state/state-manager.js';
19
+ import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
20
+
21
+ /**
22
+ * Expected sections per output file.
23
+ *
24
+ * Each section entry can be a string (single substring to match) or an array
25
+ * of acceptable substrings (any-of match). This lets pt-BR and English
26
+ * coexist — e.g. "Problem" matches both "Problem" and "Problema", but
27
+ * "Scope" needs to be paired with "Escopo" explicitly.
28
+ *
29
+ * The matcher (`validateMarkdownSections`) does case-insensitive substring
30
+ * matching against the whole file content.
31
+ */
32
+ const OUTPUT_SECTIONS = {
33
+ 'proposal.md': {
34
+ required: [
35
+ 'Overview',
36
+ 'Problem', // also matches Problema
37
+ ['Solution', 'Solução'],
38
+ ['Scope', 'Escopo']
39
+ ],
40
+ optional: ['Agents', 'Stack', ['Risks', 'Riscos']]
41
+ },
42
+ 'spec.md': {
43
+ required: [
44
+ 'Overview',
45
+ ['Functional Requirements', 'Requisitos Funcionais'],
46
+ ['Non-Functional Requirements', 'Requisitos Não-Funcionais', 'Requisitos Nao-Funcionais'],
47
+ ['Technical Architecture', 'Arquitetura'],
48
+ ['Data Model', 'Modelo de Dados', 'Contratos']
49
+ ],
50
+ optional: [
51
+ ['Infrastructure', 'Infraestrutura'],
52
+ ['Testing Strategy', 'Estratégia de Testes', 'Testes'],
53
+ ['Security', 'Segurança'],
54
+ 'Performance',
55
+ ['Edge Cases', 'Casos Limite'],
56
+ ['Clarifications', 'Esclarecimentos']
57
+ ]
58
+ },
59
+ 'mandate.md': {
60
+ required: ['Stack', 'Standards', ['Done', 'Pronto']],
61
+ optional: [
62
+ ['Patterns proibidos', 'Padrões proibidos', 'Forbidden Patterns'],
63
+ ['Riscos', 'Risks']
64
+ ]
65
+ },
66
+ 'decisions.md': {
67
+ required: ['ADR', 'Status', ['Context', 'Contexto'], ['Decision', 'Decisão']],
68
+ optional: [
69
+ ['Consequences', 'Consequências', 'Implicações'],
70
+ ['Alternatives', 'Alternativas'],
71
+ ['Costs', 'Custos']
72
+ ]
73
+ },
74
+ 'review-report.md': {
75
+ required: [['Scores', 'Pontuação', 'Scoring'], ['Recommendation', 'Recomendação']],
76
+ optional: [['Blockers', 'Bloqueantes'], ['Improvements', 'Melhorias']]
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Phase-to-output mapping (v5 NLH)
82
+ */
83
+ const PHASE_OUTPUTS = {
84
+ proposal: ['proposal.md'],
85
+ uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
86
+ plan: ['spec.md', 'mandate.md', 'tasks.json'],
87
+ implement: ['recap.md'],
88
+ review: ['review-report.md'],
89
+ };
90
+
91
+ /**
92
+ * Phase-to-folder mapping (v5 filesystem convention)
93
+ */
94
+ const PHASE_FOLDERS = {
95
+ proposal: '0-proposal',
96
+ uiux: '1-design',
97
+ plan: '2-plan',
98
+ implement: '3-implement',
99
+ review: '4-review',
100
+ };
101
+
102
+ /**
103
+ * Validate a single output file
104
+ */
105
+ function validateOutput(featurePath, outputFile) {
106
+ const filePath = path.join(featurePath, outputFile);
107
+ const result = {
108
+ file: outputFile,
109
+ exists: false,
110
+ sections: { found: [], missing: [], optional: [] },
111
+ warnings: [],
112
+ errors: [],
113
+ lineCount: 0
114
+ };
115
+
116
+ if (!fs.existsSync(filePath)) {
117
+ result.errors.push(`File not found: ${outputFile}`);
118
+ return result;
119
+ }
120
+
121
+ result.exists = true;
122
+ const content = fs.readFileSync(filePath, 'utf8');
123
+ result.lineCount = content.split('\n').length;
124
+
125
+ // tasks.json: structural validation against the schema, not markdown sections.
126
+ if (outputFile === 'tasks.json') {
127
+ validateTasksJsonOutput(content, result);
128
+ return result;
129
+ }
130
+
131
+ // Skip section checks for files without defined sections
132
+ const sections = OUTPUT_SECTIONS[outputFile];
133
+ if (!sections) {
134
+ return result;
135
+ }
136
+
137
+ // v5: only markdown outputs — contracts moved into spec.md
138
+ validateMarkdownSections(content, sections, result);
139
+
140
+ // Size warnings
141
+ if (result.lineCount < 10) {
142
+ result.warnings.push(`File is very short (${result.lineCount} lines) — may be incomplete`);
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ /**
149
+ * Validate tasks.json structurally: parseable JSON + conforms to the schema +
150
+ * has at least one task.
151
+ */
152
+ function validateTasksJsonOutput(content, result) {
153
+ let data;
154
+ try {
155
+ data = JSON.parse(content);
156
+ } catch (err) {
157
+ result.errors.push(`tasks.json is not valid JSON: ${err.message}`);
158
+ return;
159
+ }
160
+ const { valid, errors } = validateTasksJson(data);
161
+ if (!valid) {
162
+ for (const e of errors) result.errors.push(e);
163
+ return;
164
+ }
165
+ if (data.tasks.length === 0) {
166
+ result.errors.push('tasks.json has no task entries');
167
+ return;
168
+ }
169
+ result.sections.found.push(`${data.tasks.length} task(s)`);
170
+ }
171
+
172
+ /**
173
+ * Validate markdown sections.
174
+ *
175
+ * Each `section` entry can be:
176
+ * - a string → must appear as a substring (case-insensitive)
177
+ * - an array → any-of: at least one of the strings must appear
178
+ *
179
+ * The any-of form supports bilingual content (e.g. ['Scope', 'Escopo']).
180
+ */
181
+ function validateMarkdownSections(content, sections, result) {
182
+ const contentLower = content.toLowerCase();
183
+ const matches = (entry) => {
184
+ const candidates = Array.isArray(entry) ? entry : [entry];
185
+ const hit = candidates.find(c => contentLower.includes(c.toLowerCase()));
186
+ return { hit, label: candidates[0] };
187
+ };
188
+
189
+ for (const entry of sections.required) {
190
+ const { hit, label } = matches(entry);
191
+ if (hit) {
192
+ result.sections.found.push(hit);
193
+ } else {
194
+ result.sections.missing.push(label);
195
+ const acceptable = Array.isArray(entry)
196
+ ? entry.map(s => `"${s}"`).join(' or ')
197
+ : `"${entry}"`;
198
+ result.errors.push(`Missing required section: ${acceptable}`);
199
+ }
200
+ }
201
+
202
+ for (const entry of (sections.optional || [])) {
203
+ const { hit } = matches(entry);
204
+ if (hit) result.sections.optional.push(hit);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Main validate command
210
+ */
211
+ export async function validateFeatureCommand(featureName, options = {}) {
212
+ if (!featureName) {
213
+ console.error(chalk.red('Error: Feature name required'));
214
+ console.error(chalk.gray('Usage: morph-spec validate <feature-name> [--phase <phase>]'));
215
+ process.exit(1);
216
+ }
217
+
218
+ const featurePath = path.join(process.cwd(), '.morph/features', featureName);
219
+
220
+ if (!fs.existsSync(featurePath)) {
221
+ console.error(chalk.red(`Error: Feature directory not found: ${featurePath}`));
222
+ process.exit(1);
223
+ }
224
+
225
+ // Determine which outputs to validate
226
+ const phase = options.phase || null;
227
+ let outputFiles;
228
+
229
+ if (phase) {
230
+ outputFiles = PHASE_OUTPUTS[phase];
231
+ if (!outputFiles) {
232
+ console.error(chalk.red(`Unknown phase: ${phase}`));
233
+ console.error(chalk.gray(`Valid phases: ${Object.keys(PHASE_OUTPUTS).join(', ')}`));
234
+ process.exit(1);
235
+ }
236
+ } else {
237
+ // Validate all existing outputs across all phase folders
238
+ outputFiles = [];
239
+ const entries = fs.readdirSync(featurePath, { withFileTypes: true });
240
+ const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'tasks.json';
241
+ for (const entry of entries) {
242
+ if (entry.isDirectory()) {
243
+ const subDir = path.join(featurePath, entry.name);
244
+ const files = fs.readdirSync(subDir).filter(isOutput);
245
+ outputFiles.push(...files.map(f => ({ file: f, dir: subDir })));
246
+ } else if (isOutput(entry.name)) {
247
+ outputFiles.push({ file: entry.name, dir: featurePath });
248
+ }
249
+ }
250
+ }
251
+
252
+ // Resolve phase folder for path lookup
253
+ const phaseFolder = phase ? PHASE_FOLDERS[phase] : null;
254
+ const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
255
+
256
+ // Run validation
257
+ const results = phase
258
+ ? outputFiles.map(file => validateOutput(searchPath, file))
259
+ : outputFiles.map(({ file, dir }) => validateOutput(dir, file));
260
+ const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
261
+
262
+ // Aggregate results
263
+ const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
264
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0) + schemaWarnings.length;
265
+ const totalFound = results.reduce((sum, r) => sum + r.sections.found.length, 0);
266
+
267
+ // JSON output mode
268
+ if (options.json) {
269
+ const output = {
270
+ feature: featureName,
271
+ phase: phase || 'all',
272
+ valid: totalErrors === 0,
273
+ summary: {
274
+ files: results.length,
275
+ errors: totalErrors,
276
+ warnings: totalWarnings,
277
+ sectionsFound: totalFound
278
+ },
279
+ results: results.map(r => ({
280
+ file: r.file,
281
+ exists: r.exists,
282
+ lineCount: r.lineCount,
283
+ errors: r.errors,
284
+ warnings: r.warnings,
285
+ sectionsFound: r.sections.found,
286
+ sectionsMissing: r.sections.missing
287
+ })),
288
+ schemaWarnings
289
+ };
290
+ console.log(JSON.stringify(output, null, 2));
291
+ return;
292
+ }
293
+
294
+ // Visual output
295
+ console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
296
+ console.log(chalk.cyan('│ MORPH-SPEC FEATURE VALIDATOR │'));
297
+ console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
298
+
299
+ console.log(chalk.white.bold(`Feature: ${featureName}`));
300
+ if (phase) console.log(chalk.gray(`Phase: ${phase}`));
301
+ console.log('');
302
+
303
+ // File-by-file results
304
+ for (const result of results) {
305
+ const symbol = result.exists
306
+ ? (result.errors.length === 0 ? '✅' : '❌')
307
+ : '⬜';
308
+ const name = result.exists
309
+ ? (result.errors.length === 0 ? chalk.green(result.file) : chalk.red(result.file))
310
+ : chalk.gray(result.file);
311
+
312
+ console.log(`${symbol} ${name}${result.exists ? chalk.gray(` (${result.lineCount} lines)`) : ''}`);
313
+
314
+ // Sections found
315
+ if (result.sections.found.length > 0 && (options.verbose || result.errors.length > 0)) {
316
+ result.sections.found.forEach(s => {
317
+ console.log(chalk.green(` ✓ ${s}`));
318
+ });
319
+ }
320
+
321
+ // Errors
322
+ result.errors.forEach(err => {
323
+ console.log(chalk.red(` ✗ ${err}`));
324
+ });
325
+
326
+ // Warnings
327
+ result.warnings.forEach(warn => {
328
+ console.log(chalk.yellow(` ⚠ ${warn}`));
329
+ });
330
+ }
331
+
332
+ // Schema cross-validation
333
+ if (schemaWarnings.length > 0) {
334
+ console.log(chalk.yellow('\nSchema Cross-Validation:'));
335
+ schemaWarnings.forEach(w => {
336
+ console.log(chalk.yellow(` ⚠ ${w}`));
337
+ });
338
+ }
339
+
340
+ // Summary
341
+ console.log('');
342
+ console.log(chalk.gray('─'.repeat(60)));
343
+
344
+ if (totalErrors === 0) {
345
+ console.log(chalk.green(`\n✓ VALIDATION PASSED — ${results.filter(r => r.exists).length} files validated`));
346
+ if (totalWarnings > 0) {
347
+ console.log(chalk.yellow(` ${totalWarnings} warning(s) — review recommended`));
348
+ }
349
+ } else {
350
+ console.log(chalk.red(`\n✗ VALIDATION FAILED — ${totalErrors} error(s), ${totalWarnings} warning(s)`));
351
+ console.log(chalk.yellow(' Fix errors before advancing to the next phase'));
352
+ }
353
+ console.log('');
354
+ }
355
+
356
+ export default validateFeatureCommand;