@polymorphism-tech/morph-spec 10.1.1 → 10.2.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 (48) hide show
  1. package/README.md +3 -7
  2. package/bin/morph-spec.js +1 -41
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -4
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/agents.json +0 -1
  7. package/framework/commands/morph-archive.md +1 -1
  8. package/framework/commands/morph-preflight.md +3 -4
  9. package/framework/docs/CLI.md +23 -20
  10. package/framework/hooks/README.md +2 -2
  11. package/framework/hooks/claude-code/core/validate-completion.js +0 -35
  12. package/framework/hooks/shared/e2e-check.js +44 -7
  13. package/framework/hooks/shared/heavy-command.js +12 -1
  14. package/framework/hooks/shared/next-step.js +2 -2
  15. package/framework/phases.json +4 -4
  16. package/framework/schemas/config.schema.json +20 -1
  17. package/framework/schemas/feature.schema.json +1 -0
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  20. package/framework/skills/core/morph-apply/SKILL.md +5 -5
  21. package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
  22. package/framework/skills/core/morph-apply/references/review.md +5 -5
  23. package/framework/skills/core/morph-plan/SKILL.md +4 -4
  24. package/framework/skills/core/morph-uiux/SKILL.md +2 -2
  25. package/framework/templates/examples/design-system-examples.md +0 -3
  26. package/package.json +1 -1
  27. package/src/commands/project/doctor.js +0 -1
  28. package/src/commands/state/approve.js +9 -109
  29. package/src/commands/state/report-check.js +44 -2
  30. package/src/commands/validation/verify.js +7 -3
  31. package/src/core/state/state-manager.js +0 -24
  32. package/src/lib/heavy/cleanup.js +8 -2
  33. package/src/lib/plan/critique.js +23 -3
  34. package/src/lib/review-confirmation.js +67 -0
  35. package/src/lib/review-reports.js +1 -0
  36. package/src/lib/risk-detector.js +2 -2
  37. package/src/lib/tasks/test-nodes.js +5 -0
  38. package/src/lib/tasks/test-runner.js +37 -43
  39. package/src/lib/telemetry/telemetry-reader.js +1 -2
  40. package/src/lib/telemetry/verify-event.js +7 -0
  41. package/src/lib/validators/verify-cache.js +63 -0
  42. package/src/lib/validators/verify-runner.js +98 -23
  43. package/src/utils/agents-installer.js +2 -2
  44. package/src/utils/hooks-installer.js +0 -20
  45. package/bin/validate.js +0 -212
  46. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  47. package/src/commands/observability/telemetry.js +0 -60
  48. package/src/commands/validation/validate-feature.js +0 -337
@@ -6,7 +6,6 @@ import { realEffects } from '../lib/installers/effects.js';
6
6
 
7
7
  const HOOKS_VERSION = '3.17.0';
8
8
 
9
- const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
10
9
 
11
10
  // `Edit(path)` only: an Edit rule also covers Write, while a `Write(path)` rule
12
11
  // matches nothing and makes Claude Code warn on every session start.
@@ -195,7 +194,6 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
195
194
  const managed = settings._morph_managed || {};
196
195
  const alreadyCurrent = managed.hooks_version === HOOKS_VERSION;
197
196
 
198
- removeOldDispatch(settings);
199
197
  removeMorphHooks(settings);
200
198
 
201
199
  let installed = 0;
@@ -250,8 +248,6 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
250
248
  const STALE_ALLOW = [
251
249
  'Write(.morph/features/**)',
252
250
  'Write(.morph/context/**)',
253
- 'Bash(morph-spec mutate:*)',
254
- 'Bash(npx morph-spec mutate:*)',
255
251
  ];
256
252
  settings.permissions.allow = settings.permissions.allow.filter(
257
253
  p => !MORPH_ALLOW.includes(p) && !STALE_ALLOW.includes(p)
@@ -319,22 +315,6 @@ export async function installGlobalStatusline(hooksSourceDir, globalClaudeDirOve
319
315
  return { installed: true, globalDir };
320
316
  }
321
317
 
322
- function removeOldDispatch(settings) {
323
- if (!settings.hooks?.PostToolUse) return;
324
-
325
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
326
- if (!Array.isArray(entry.hooks)) return true;
327
- return !entry.hooks.some(h =>
328
- h.command === OLD_DISPATCH_COMMAND ||
329
- (typeof h.command === 'string' && h.command.includes('agent-teams/dispatch.js'))
330
- );
331
- });
332
-
333
- if (settings.hooks.PostToolUse.length === 0) {
334
- delete settings.hooks.PostToolUse;
335
- }
336
- }
337
-
338
318
  function removeMorphHooks(settings) {
339
319
  const morphPathPattern = /framework\/hooks\/claude-code\//;
340
320
 
package/bin/validate.js DELETED
@@ -1,212 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import chalk from 'chalk';
4
- import { getStackProfile } from '../src/lib/stack/stack-profile.js';
5
-
6
- async function buildValidators(stack) {
7
- const validators = {};
8
-
9
- const isDotnet = !stack || ['dotnet', 'blazor', 'aspnet', 'dotnet-blazor'].includes(stack.toLowerCase());
10
- const isNextjs = stack && ['nextjs', 'next', 'next.js'].includes(stack.toLowerCase());
11
-
12
- if (isDotnet) {
13
- const { validatePackages } = await import('../src/lib/validators/packages/package-validator.js');
14
-
15
- validators.packages = {
16
- name: 'Package Compatibility',
17
- description: 'Validates NuGet package versions against .NET compatibility matrix',
18
- run: validatePackages
19
- };
20
- }
21
-
22
- if (isNextjs) {
23
- const { validateNextComponentFiles } = await import('../src/lib/validators/nextjs/next-component-validator.js');
24
- validators['next-components'] = {
25
- name: 'Next.js Components',
26
- description: "Validates 'use client' directives, hook usage, and file naming conventions",
27
- run: (projectPath, opts) => validateNextComponentFiles(projectPath, opts)
28
- };
29
- }
30
-
31
- return validators;
32
- }
33
-
34
- export async function validateCommand(args = []) {
35
- const validatorName = args[0] || 'all';
36
- const options = parseOptions(args);
37
-
38
- const { stack } = getStackProfile();
39
- const VALIDATORS = await buildValidators(stack);
40
-
41
- console.log(chalk.bold.cyan('\n🔍 MORPH-SPEC Validator\n'));
42
- if (stack) {
43
- console.log(chalk.gray(`Stack: ${stack}\n`));
44
- }
45
-
46
- if (validatorName === 'all') {
47
- await runAllValidators(VALIDATORS, options);
48
- } else if (VALIDATORS[validatorName]) {
49
- await runValidator(VALIDATORS, validatorName, options);
50
- } else {
51
- console.error(chalk.red(`❌ Unknown validator: ${validatorName}`));
52
- console.log(chalk.gray('\nAvailable validators:'));
53
- for (const [name, validator] of Object.entries(VALIDATORS)) {
54
- console.log(chalk.gray(` - ${name}: ${validator.description}`));
55
- }
56
- console.log(chalk.gray(' - all: Run all validators'));
57
- process.exit(1);
58
- }
59
- }
60
-
61
- async function runAllValidators(VALIDATORS, options) {
62
- const results = {};
63
- let totalErrors = 0;
64
- let totalWarnings = 0;
65
-
66
- console.log(chalk.gray('Running all validators...\n'));
67
-
68
- for (const [name, validator] of Object.entries(VALIDATORS)) {
69
- console.log(chalk.cyan(`\n▸ ${validator.name}`));
70
- console.log(chalk.gray(` ${validator.description}\n`));
71
-
72
- try {
73
- const result = await validator.run('.', {
74
- verbose: options.verbose,
75
- autoFix: options.autoFix
76
- });
77
-
78
- results[name] = result;
79
-
80
- if (result.errors !== undefined) {
81
- totalErrors += result.errors;
82
- totalWarnings += result.warnings || 0;
83
- } else if (result.results) {
84
- const issues = result.results.flatMap(r => r.issues || []);
85
- totalErrors += issues.filter(i => i.level === 'error').length;
86
- totalWarnings += issues.filter(i => i.level === 'warning').length;
87
- }
88
-
89
- } catch (error) {
90
- console.error(chalk.red(` ❌ Failed: ${error.message}`));
91
- results[name] = { status: 'error', error: error.message };
92
- }
93
- }
94
-
95
- console.log(chalk.bold.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
96
- console.log(chalk.bold('📊 Validation Summary\n'));
97
-
98
- if (totalErrors === 0 && totalWarnings === 0) {
99
- console.log(chalk.green('✅ All validations passed!'));
100
- } else {
101
- if (totalErrors > 0) {
102
- console.log(chalk.red(`❌ ${totalErrors} error(s) found`));
103
- }
104
- if (totalWarnings > 0) {
105
- console.log(chalk.yellow(`⚠️ ${totalWarnings} warning(s) found`));
106
- }
107
- }
108
-
109
- console.log('');
110
-
111
- if (totalErrors > 0 && !options.noFail) {
112
- process.exit(1);
113
- }
114
- }
115
-
116
- async function runValidator(VALIDATORS, name, options) {
117
- const validator = VALIDATORS[name];
118
-
119
- console.log(chalk.cyan(`▸ ${validator.name}`));
120
- console.log(chalk.gray(` ${validator.description}\n`));
121
-
122
- try {
123
- const result = await validator.run('.', {
124
- verbose: true,
125
- autoFix: options.autoFix
126
- });
127
-
128
- if (result.status === 'ok') {
129
- console.log(chalk.green('\n✅ Validation passed!\n'));
130
- } else {
131
- console.log(chalk.yellow('\n⚠️ Validation completed with issues\n'));
132
-
133
- if (result.errors > 0 && !options.noFail) {
134
- process.exit(1);
135
- }
136
- }
137
-
138
- } catch (error) {
139
- console.error(chalk.red(`\n❌ Validation failed: ${error.message}\n`));
140
- if (!options.noFail) {
141
- process.exit(1);
142
- }
143
- }
144
- }
145
-
146
- function parseOptions(args) {
147
- const options = {
148
- verbose: false,
149
- autoFix: false,
150
- noFail: false
151
- };
152
-
153
- for (const arg of args) {
154
- if (arg === '--verbose' || arg === '-v') {
155
- options.verbose = true;
156
- } else if (arg === '--auto-fix' || arg === '--fix') {
157
- options.autoFix = true;
158
- } else if (arg === '--no-fail') {
159
- options.noFail = true;
160
- }
161
- }
162
-
163
- return options;
164
- }
165
-
166
- export function showValidateHelp() {
167
- console.log(`
168
- ${chalk.bold.cyan('morph-spec validate')} - Run project validations
169
-
170
- ${chalk.bold('USAGE')}
171
- morph-spec validate [validator] [options]
172
-
173
- ${chalk.bold('VALIDATORS')}
174
- all Run all validators (default)
175
- packages Validate NuGet package compatibility (.NET 10)
176
-
177
- ${chalk.bold('OPTIONS')}
178
- --verbose, -v Show detailed output
179
- --auto-fix, --fix Auto-fix issues where possible
180
- --no-fail Don't exit with error code
181
-
182
- ${chalk.bold('EXAMPLES')}
183
- morph-spec validate # Run all validators
184
- morph-spec validate packages --fix # Validate packages and auto-fix
185
- morph-spec validate all # Run all validators
186
-
187
- ${chalk.bold('EXIT CODES')}
188
- 0 All validations passed
189
- 1 Validation errors found (unless --no-fail)
190
-
191
- ${chalk.bold('INTEGRATION')}
192
- Add to pre-commit hook:
193
- morph-spec validate --no-fail
194
-
195
- CI/CD pipeline:
196
- morph-spec validate || exit 1
197
- `);
198
- }
199
-
200
- if (import.meta.url === `file://${process.argv[1]}`) {
201
- const args = process.argv.slice(2);
202
-
203
- if (args.includes('--help') || args.includes('-h')) {
204
- showValidateHelp();
205
- process.exit(0);
206
- }
207
-
208
- validateCommand(args).catch(error => {
209
- console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
210
- process.exit(1);
211
- });
212
- }
@@ -1,60 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadTelemetry, aggregateTelemetry } from '../../lib/telemetry/telemetry-reader.js';
3
-
4
- // A rule injects its standards' digests through Claude Code's rules system,
5
- // not the Read tool, so no hook sees them: a low count means "few full-text reads".
6
- const STANDARDS_CAVEAT =
7
- 'Só reads explícitos (via Read) são observáveis; o resumo que uma rule injeta ao tocar arquivo casado por `paths:` não passa por tool call: não observável aqui.';
8
-
9
- function section(title, rows, fmt) {
10
- if (!rows.length) return;
11
- console.log('\n' + chalk.white.bold(title));
12
- for (const r of rows) console.log(' ' + fmt(r));
13
- }
14
-
15
- // Read-only and always exits 0: an empty stream is an answer.
16
- export function telemetryCommand(options = {}) {
17
- const events = loadTelemetry(options.cwd || process.cwd());
18
- const summary = aggregateTelemetry(events, { feature: options.feature });
19
-
20
- if (options.json) {
21
- console.log(JSON.stringify({ ...summary, notes: { standards: STANDARDS_CAVEAT } }));
22
- process.exit(0);
23
- }
24
-
25
- if (summary.total === 0) {
26
- const scope = options.feature ? ` para a feature "${options.feature}"` : '';
27
- console.log(chalk.gray(`Telemetria vazia${scope} — nenhum evento capturado ainda em .morph/logs/events.jsonl.`));
28
- process.exit(0);
29
- }
30
-
31
- const scope = options.feature ? chalk.cyan(` · feature ${options.feature}`) : '';
32
- console.log(chalk.white.bold(`Telemetria do harness — ${summary.total} eventos${scope}`));
33
- if (summary.features.length) {
34
- console.log(chalk.gray(`Features: ${summary.features.join(', ')}`));
35
- }
36
-
37
- section('Skills', summary.skills, s => `${chalk.green(s.name.padEnd(24))} ${s.count}`);
38
- section('Sub-agents despachados', summary.personas, p => `${chalk.magenta(p.name.padEnd(24))} ${p.count}`);
39
- section('Base de conhecimento (standards lidos explicitamente)', summary.standards,
40
- s => `${chalk.yellow(String(s.count).padStart(3))}× ${s.path}`);
41
- if (summary.standards.length || summary.tools.some(t => t.name === 'Read')) {
42
- console.log(chalk.gray(` ⚠ ${STANDARDS_CAVEAT}`));
43
- }
44
- section('Mix de tools', summary.tools, t => `${t.name.padEnd(16)} ${t.count}`);
45
-
46
- const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 10;
47
- const prompts = summary.prompts.slice(-limit);
48
- if (prompts.length) {
49
- console.log('\n' + chalk.white.bold(`Prompts (últimos ${prompts.length})`));
50
- for (const p of prompts) {
51
- const tag = p.source === 'user' ? chalk.blue('user ') : chalk.magenta('agent');
52
- const where = p.feature ? chalk.gray(` [${p.feature}/${p.phase || '-'}]`) : '';
53
- const oneLine = String(p.text || '').replace(/\s+/g, ' ').trim();
54
- const preview = oneLine.length > 120 ? oneLine.slice(0, 117) + '...' : oneLine;
55
- console.log(` ${tag}${where} ${preview}`);
56
- }
57
- }
58
-
59
- process.exit(0);
60
- }
@@ -1,337 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import { parsePlan } from '../../lib/plan/plan-parser.js';
5
- import { planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
6
-
7
- // Matched against section markers (headings and line-start bold labels), never whole-file
8
- // substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
9
- const OUTPUT_SECTIONS = {
10
- 'verifier.md': {
11
- required: [
12
- ['Verdict', 'Veredito'],
13
- ],
14
- recommended: [
15
- ['Bloqueadores', 'Bloqueantes', 'Blockers'],
16
- 'Checks',
17
- ],
18
- optional: [],
19
- },
20
- 'reviewer.md': {
21
- required: [
22
- ['Verdict', 'Veredito'],
23
- ],
24
- recommended: [
25
- ['Bloqueadores', 'Bloqueantes', 'Blockers'],
26
- ],
27
- optional: [],
28
- },
29
- 'security.md': {
30
- required: [
31
- ['Verdict', 'Veredito'],
32
- ],
33
- recommended: [
34
- ['Bloqueadores', 'Bloqueantes', 'Blockers'],
35
- ],
36
- optional: [],
37
- },
38
- };
39
-
40
- const PHASE_OUTPUTS = {
41
- plan: ['plan.md', 'checks.json'],
42
- uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
43
- implement: [],
44
- review: ['verifier.md', 'reviewer.md'],
45
- };
46
-
47
- const PHASE_FOLDERS = {
48
- plan: '2-plan',
49
- uiux: '1-design',
50
- implement: '3-implement',
51
- review: '4-review',
52
- };
53
-
54
- // Line-start bold labels count: reports write `**Verdict:**` as a label, not a heading.
55
- function extractSectionMarkers(content) {
56
- const markers = [];
57
- for (const rawLine of content.split('\n')) {
58
- const line = rawLine.trim();
59
- const heading = /^#{1,6}\s+(.+)$/.exec(line);
60
- if (heading) {
61
- markers.push(heading[1].toLowerCase());
62
- continue;
63
- }
64
- const bold = /^\*\*(.+?)\*\*/.exec(line);
65
- if (bold) {
66
- markers.push(bold[1].toLowerCase());
67
- }
68
- }
69
- return markers;
70
- }
71
-
72
- function validateOutput(featurePath, outputFile) {
73
- const filePath = path.join(featurePath, outputFile);
74
- const result = {
75
- file: outputFile,
76
- exists: false,
77
- sections: { found: [], missing: [], optional: [] },
78
- warnings: [],
79
- errors: [],
80
- lineCount: 0,
81
- };
82
-
83
- if (!fs.existsSync(filePath)) {
84
- result.errors.push(`File not found: ${outputFile}`);
85
- return result;
86
- }
87
-
88
- result.exists = true;
89
- const content = fs.readFileSync(filePath, 'utf8');
90
- result.lineCount = content.split('\n').length;
91
-
92
- if (outputFile === 'plan.md') {
93
- validatePlanOutput(content, featurePath, result);
94
- return result;
95
- }
96
- if (outputFile === 'checks.json') {
97
- validateChecksOutput(content, featurePath, result);
98
- return result;
99
- }
100
-
101
- const sections = OUTPUT_SECTIONS[outputFile];
102
- if (!sections) {
103
- return result;
104
- }
105
-
106
- validateMarkdownSections(content, sections, result);
107
-
108
- if (result.lineCount < 10) {
109
- result.warnings.push(`File is very short (${result.lineCount} lines) — may be incomplete`);
110
- }
111
-
112
- return result;
113
- }
114
-
115
- // `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
116
- function validatePlanOutput(content, planDir, result) {
117
- const plan = parsePlan(content);
118
- const root = path.resolve(planDir, '..', '..', '..', '..');
119
- const feature = path.basename(path.resolve(planDir, '..'));
120
- const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(root, feature), text: content });
121
- result.errors.push(...errors);
122
- result.warnings.push(...warnings);
123
- result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
124
- }
125
-
126
- function validateChecksOutput(content, planDir, result) {
127
- let doc;
128
- try {
129
- doc = JSON.parse(content);
130
- } catch (err) {
131
- result.errors.push(`checks.json is not valid JSON: ${err.message}`);
132
- return;
133
- }
134
- const planPath = path.join(planDir, 'plan.md');
135
- if (!fs.existsSync(planPath)) return;
136
- if (doc?.planHash !== planHash(fs.readFileSync(planPath, 'utf8'))) {
137
- result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
138
- return;
139
- }
140
- result.sections.found.push(`${doc.checks?.length ?? 0} check(s)`);
141
- }
142
-
143
- function validateMarkdownSections(content, sections, result) {
144
- const markers = extractSectionMarkers(content);
145
- const match = (entry) => {
146
- const candidates = Array.isArray(entry) ? entry : [entry];
147
- const hit = candidates.find(c => markers.some(m => m.includes(c.toLowerCase())));
148
- return { hit, label: candidates[0] };
149
- };
150
- const describe = (entry) =>
151
- Array.isArray(entry) ? entry.map(s => `"${s}"`).join(' or ') : `"${entry}"`;
152
-
153
- for (const entry of (sections.required || [])) {
154
- const { hit, label } = match(entry);
155
- if (hit) {
156
- result.sections.found.push(hit);
157
- } else {
158
- result.sections.missing.push(label);
159
- result.errors.push(`Missing required section: ${describe(entry)}`);
160
- }
161
- }
162
-
163
- for (const entry of (sections.recommended || [])) {
164
- const { hit } = match(entry);
165
- if (hit) {
166
- result.sections.found.push(hit);
167
- } else {
168
- result.warnings.push(`Missing recommended section: ${describe(entry)}`);
169
- }
170
- }
171
-
172
- for (const entry of (sections.optional || [])) {
173
- const { hit } = match(entry);
174
- if (hit) result.sections.optional.push(hit);
175
- }
176
- }
177
-
178
- function resolveOutputFiles(featurePath, phase) {
179
- if (phase) {
180
- const files = PHASE_OUTPUTS[phase];
181
- if (!files) return { error: `Unknown phase: ${phase}` };
182
- const phaseFolder = PHASE_FOLDERS[phase];
183
- const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
184
- return { outputFiles: files.map(file => ({ file, dir: searchPath })) };
185
- }
186
-
187
- const outputFiles = [];
188
- const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'checks.json';
189
- const entries = fs.readdirSync(featurePath, { withFileTypes: true });
190
- for (const entry of entries) {
191
- if (entry.isDirectory()) {
192
- const subDir = path.join(featurePath, entry.name);
193
- for (const f of fs.readdirSync(subDir).filter(isOutput)) {
194
- outputFiles.push({ file: f, dir: subDir });
195
- }
196
- } else if (isOutput(entry.name)) {
197
- outputFiles.push({ file: entry.name, dir: featurePath });
198
- }
199
- }
200
- return { outputFiles };
201
- }
202
-
203
- /** Pure core: no console output, no process.exit. */
204
- export function runFeatureValidation(featurePath, options = {}) {
205
- const phase = options.phase || null;
206
- const feature = path.basename(featurePath);
207
-
208
- const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
209
- if (error) {
210
- return {
211
- feature,
212
- phase: phase || 'all',
213
- valid: false,
214
- error,
215
- totalErrors: 1,
216
- totalWarnings: 0,
217
- totalFound: 0,
218
- results: [],
219
- };
220
- }
221
-
222
- const results = outputFiles.map(({ file, dir }) => validateOutput(dir, file));
223
- const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
224
- const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
225
- const totalFound = results.reduce((sum, r) => sum + r.sections.found.length, 0);
226
-
227
- return {
228
- feature,
229
- phase: phase || 'all',
230
- valid: totalErrors === 0,
231
- totalErrors,
232
- totalWarnings,
233
- totalFound,
234
- results,
235
- };
236
- }
237
-
238
- export async function validateFeatureCommand(featureName, options = {}) {
239
- if (!featureName) {
240
- console.error(chalk.red('Error: Feature name required'));
241
- console.error(chalk.gray('Usage: morph-spec validate <feature-name> [--phase <phase>]'));
242
- process.exit(1);
243
- }
244
-
245
- const featurePath = path.join(process.cwd(), '.morph/features', featureName);
246
-
247
- if (!fs.existsSync(featurePath)) {
248
- console.error(chalk.red(`Error: Feature directory not found: ${featurePath}`));
249
- process.exit(1);
250
- }
251
-
252
- const phase = options.phase || null;
253
- if (phase && !PHASE_OUTPUTS[phase]) {
254
- console.error(chalk.red(`Unknown phase: ${phase}`));
255
- console.error(chalk.gray(`Valid phases: ${Object.keys(PHASE_OUTPUTS).join(', ')}`));
256
- process.exit(1);
257
- }
258
-
259
- const validation = runFeatureValidation(featurePath, { phase });
260
- const { results, totalErrors, totalWarnings, totalFound } = validation;
261
- const schemaWarnings = [];
262
-
263
- if (options.json) {
264
- const output = {
265
- feature: featureName,
266
- phase: validation.phase,
267
- valid: validation.valid,
268
- summary: {
269
- files: results.length,
270
- errors: totalErrors,
271
- warnings: totalWarnings + schemaWarnings.length,
272
- sectionsFound: totalFound,
273
- },
274
- results: results.map(r => ({
275
- file: r.file,
276
- exists: r.exists,
277
- lineCount: r.lineCount,
278
- errors: r.errors,
279
- warnings: r.warnings,
280
- sectionsFound: r.sections.found,
281
- sectionsMissing: r.sections.missing,
282
- })),
283
- schemaWarnings,
284
- };
285
- console.log(JSON.stringify(output, null, 2));
286
- return;
287
- }
288
-
289
- console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
290
- console.log(chalk.cyan('│ MORPH-SPEC FEATURE VALIDATOR │'));
291
- console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
292
-
293
- console.log(chalk.white.bold(`Feature: ${featureName}`));
294
- if (phase) console.log(chalk.gray(`Phase: ${phase}`));
295
- console.log('');
296
-
297
- for (const result of results) {
298
- const symbol = result.exists
299
- ? (result.errors.length === 0 ? '✅' : '❌')
300
- : '⬜';
301
- const name = result.exists
302
- ? (result.errors.length === 0 ? chalk.green(result.file) : chalk.red(result.file))
303
- : chalk.gray(result.file);
304
-
305
- console.log(`${symbol} ${name}${result.exists ? chalk.gray(` (${result.lineCount} lines)`) : ''}`);
306
-
307
- if (result.sections.found.length > 0 && (options.verbose || result.errors.length > 0)) {
308
- result.sections.found.forEach(s => {
309
- console.log(chalk.green(` ✓ ${s}`));
310
- });
311
- }
312
-
313
- result.errors.forEach(err => {
314
- console.log(chalk.red(` ✗ ${err}`));
315
- });
316
-
317
- result.warnings.forEach(warn => {
318
- console.log(chalk.yellow(` ⚠ ${warn}`));
319
- });
320
- }
321
-
322
- console.log('');
323
- console.log(chalk.gray('─'.repeat(60)));
324
-
325
- if (totalErrors === 0) {
326
- console.log(chalk.green(`\n✓ VALIDATION PASSED — ${results.filter(r => r.exists).length} files validated`));
327
- if (totalWarnings > 0) {
328
- console.log(chalk.yellow(` ${totalWarnings} warning(s) — review recommended`));
329
- }
330
- } else {
331
- console.log(chalk.red(`\n✗ VALIDATION FAILED — ${totalErrors} error(s), ${totalWarnings} warning(s)`));
332
- console.log(chalk.yellow(' Fix errors before advancing to the next phase'));
333
- }
334
- console.log('');
335
- }
336
-
337
- export default validateFeatureCommand;