@polymorphism-tech/morph-spec 7.1.1 → 7.3.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 (66) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +4 -2
  6. package/framework/MORPH.md +3 -3
  7. package/framework/commands/morph-apply.md +1 -1
  8. package/framework/commands/morph-proposal.md +1 -1
  9. package/framework/hooks/README.md +1 -1
  10. package/framework/hooks/claude-code/core/protect-spec-files.js +193 -72
  11. package/framework/hooks/claude-code/core/validate-completion.js +136 -136
  12. package/framework/hooks/shared/phase-utils.js +10 -0
  13. package/framework/phases.json +1 -1
  14. package/framework/rules/morph-workflow.md +16 -9
  15. package/framework/skills/README.md +1 -1
  16. package/framework/skills/core/morph-apply/SKILL.md +1 -0
  17. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -0
  18. package/framework/skills/core/morph-eval/SKILL.md +1 -0
  19. package/framework/skills/core/morph-implement/SKILL.md +7 -0
  20. package/framework/skills/core/morph-plan/SKILL.md +1 -0
  21. package/framework/skills/core/morph-proposal/SKILL.md +1 -0
  22. package/framework/skills/core/morph-review/SKILL.md +1 -0
  23. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -0
  24. package/framework/skills/core/morph-standards/SKILL.md +1 -0
  25. package/framework/templates/README.md +708 -709
  26. package/framework/templates/REGISTRY.json +1 -20
  27. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  28. package/framework/templates/docs/proposal.md +171 -171
  29. package/framework/templates/feature/recap.md +145 -145
  30. package/package.json +1 -1
  31. package/src/commands/project/doctor.js +941 -946
  32. package/src/commands/project/status.js +373 -376
  33. package/src/commands/validation/validate-feature.js +315 -287
  34. package/src/core/paths/output-schema.js +18 -1
  35. package/src/core/state/state-manager.js +900 -934
  36. package/src/dashboard/app/App.jsx +29 -31
  37. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  38. package/src/dashboard/scanner/index.js +20 -21
  39. package/src/lib/context/context-builder.js +3 -3
  40. package/src/lib/generators/context-generator.js +2 -3
  41. package/src/lib/generators/mandate-generator.js +48 -92
  42. package/src/lib/generators/metadata-extractor.js +385 -386
  43. package/src/lib/generators/recap-generator.js +285 -285
  44. package/src/lib/validators/content/content-validator.js +89 -286
  45. package/src/lib/validators/content/index.js +0 -1
  46. package/framework/templates/feature/tasks.md +0 -199
  47. package/framework/workflows/README.md +0 -1041
  48. package/framework/workflows/configs/fast-track.json +0 -46
  49. package/framework/workflows/configs/full.json +0 -64
  50. package/framework/workflows/configs/quick.json +0 -32
  51. package/framework/workflows/configs/standard.json +0 -57
  52. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  53. package/framework/workflows/docs/design-impl.md +0 -37
  54. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  55. package/framework/workflows/docs/fast-track.md +0 -29
  56. package/framework/workflows/docs/full-morph.md +0 -69
  57. package/framework/workflows/docs/standard.md +0 -49
  58. package/framework/workflows/docs/ui-refresh.md +0 -39
  59. package/src/commands/state/index.js +0 -7
  60. package/src/core/workflows/workflow-detector.js +0 -586
  61. package/src/core/workflows/workflow-router.js +0 -204
  62. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  63. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  64. package/src/lib/phase-chain/phase-validator.js +0 -359
  65. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  66. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,287 +1,315 @@
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
+ const OUTPUT_SECTIONS = {
25
+ 'proposal.md': {
26
+ required: ['Overview', 'Problem', 'Solution', 'Scope'],
27
+ optional: ['Agents', 'Stack', 'Risks']
28
+ },
29
+ 'spec.md': {
30
+ required: [
31
+ 'Overview', 'Functional Requirements', 'Non-Functional Requirements',
32
+ 'Technical Architecture', 'Data Model'
33
+ ],
34
+ optional: [
35
+ 'Infrastructure', 'Testing Strategy', 'Security',
36
+ 'Performance', 'Edge Cases', 'Clarifications'
37
+ ]
38
+ },
39
+ 'mandate.md': {
40
+ required: ['Stack', 'Standards', 'Done'],
41
+ optional: ['Patterns proibidos', 'Riscos']
42
+ },
43
+ 'decisions.md': {
44
+ required: ['ADR', 'Status', 'Context', 'Decision'],
45
+ optional: ['Consequences', 'Alternatives', 'Costs']
46
+ },
47
+ 'review-report.md': {
48
+ required: ['Scores', 'Recommendation'],
49
+ optional: ['Blockers', 'Improvements']
50
+ },
51
+ };
52
+
53
+ /**
54
+ * Phase-to-output mapping (v5 NLH)
55
+ */
56
+ const PHASE_OUTPUTS = {
57
+ proposal: ['proposal.md'],
58
+ uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
59
+ plan: ['spec.md', 'mandate.md', 'tasks.json'],
60
+ implement: ['recap.md'],
61
+ review: ['review-report.md'],
62
+ };
63
+
64
+ /**
65
+ * Phase-to-folder mapping (v5 filesystem convention)
66
+ */
67
+ const PHASE_FOLDERS = {
68
+ proposal: '0-proposal',
69
+ uiux: '1-design',
70
+ plan: '2-plan',
71
+ implement: '3-implement',
72
+ review: '4-review',
73
+ };
74
+
75
+ /**
76
+ * Validate a single output file
77
+ */
78
+ function validateOutput(featurePath, outputFile) {
79
+ const filePath = path.join(featurePath, outputFile);
80
+ const result = {
81
+ file: outputFile,
82
+ exists: false,
83
+ sections: { found: [], missing: [], optional: [] },
84
+ warnings: [],
85
+ errors: [],
86
+ lineCount: 0
87
+ };
88
+
89
+ if (!fs.existsSync(filePath)) {
90
+ result.errors.push(`File not found: ${outputFile}`);
91
+ return result;
92
+ }
93
+
94
+ result.exists = true;
95
+ const content = fs.readFileSync(filePath, 'utf8');
96
+ result.lineCount = content.split('\n').length;
97
+
98
+ // tasks.json: structural validation against the schema, not markdown sections.
99
+ if (outputFile === 'tasks.json') {
100
+ validateTasksJsonOutput(content, result);
101
+ return result;
102
+ }
103
+
104
+ // Skip section checks for files without defined sections
105
+ const sections = OUTPUT_SECTIONS[outputFile];
106
+ if (!sections) {
107
+ return result;
108
+ }
109
+
110
+ // v5: only markdown outputs — contracts moved into spec.md
111
+ validateMarkdownSections(content, sections, result);
112
+
113
+ // Size warnings
114
+ if (result.lineCount < 10) {
115
+ result.warnings.push(`File is very short (${result.lineCount} lines) — may be incomplete`);
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Validate tasks.json structurally: parseable JSON + conforms to the schema +
123
+ * has at least one task.
124
+ */
125
+ function validateTasksJsonOutput(content, result) {
126
+ let data;
127
+ try {
128
+ data = JSON.parse(content);
129
+ } catch (err) {
130
+ result.errors.push(`tasks.json is not valid JSON: ${err.message}`);
131
+ return;
132
+ }
133
+ const { valid, errors } = validateTasksJson(data);
134
+ if (!valid) {
135
+ for (const e of errors) result.errors.push(e);
136
+ return;
137
+ }
138
+ if (data.tasks.length === 0) {
139
+ result.errors.push('tasks.json has no task entries');
140
+ return;
141
+ }
142
+ result.sections.found.push(`${data.tasks.length} task(s)`);
143
+ }
144
+
145
+ /**
146
+ * Validate markdown sections
147
+ */
148
+ function validateMarkdownSections(content, sections, result) {
149
+ const contentLower = content.toLowerCase();
150
+
151
+ for (const section of sections.required) {
152
+ if (contentLower.includes(section.toLowerCase())) {
153
+ result.sections.found.push(section);
154
+ } else {
155
+ result.sections.missing.push(section);
156
+ result.errors.push(`Missing required section: "${section}"`);
157
+ }
158
+ }
159
+
160
+ for (const section of (sections.optional || [])) {
161
+ if (contentLower.includes(section.toLowerCase())) {
162
+ result.sections.optional.push(section);
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Main validate command
169
+ */
170
+ export async function validateFeatureCommand(featureName, options = {}) {
171
+ if (!featureName) {
172
+ console.error(chalk.red('Error: Feature name required'));
173
+ console.error(chalk.gray('Usage: morph-spec validate <feature-name> [--phase <phase>]'));
174
+ process.exit(1);
175
+ }
176
+
177
+ const featurePath = path.join(process.cwd(), '.morph/features', featureName);
178
+
179
+ if (!fs.existsSync(featurePath)) {
180
+ console.error(chalk.red(`Error: Feature directory not found: ${featurePath}`));
181
+ process.exit(1);
182
+ }
183
+
184
+ // Determine which outputs to validate
185
+ const phase = options.phase || null;
186
+ let outputFiles;
187
+
188
+ if (phase) {
189
+ outputFiles = PHASE_OUTPUTS[phase];
190
+ if (!outputFiles) {
191
+ console.error(chalk.red(`Unknown phase: ${phase}`));
192
+ console.error(chalk.gray(`Valid phases: ${Object.keys(PHASE_OUTPUTS).join(', ')}`));
193
+ process.exit(1);
194
+ }
195
+ } else {
196
+ // Validate all existing outputs across all phase folders
197
+ outputFiles = [];
198
+ const entries = fs.readdirSync(featurePath, { withFileTypes: true });
199
+ const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'tasks.json';
200
+ for (const entry of entries) {
201
+ if (entry.isDirectory()) {
202
+ const subDir = path.join(featurePath, entry.name);
203
+ const files = fs.readdirSync(subDir).filter(isOutput);
204
+ outputFiles.push(...files.map(f => ({ file: f, dir: subDir })));
205
+ } else if (isOutput(entry.name)) {
206
+ outputFiles.push({ file: entry.name, dir: featurePath });
207
+ }
208
+ }
209
+ }
210
+
211
+ // Resolve phase folder for path lookup
212
+ const phaseFolder = phase ? PHASE_FOLDERS[phase] : null;
213
+ const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
214
+
215
+ // Run validation
216
+ const results = phase
217
+ ? outputFiles.map(file => validateOutput(searchPath, file))
218
+ : outputFiles.map(({ file, dir }) => validateOutput(dir, file));
219
+ const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
220
+
221
+ // Aggregate results
222
+ const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
223
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0) + schemaWarnings.length;
224
+ const totalFound = results.reduce((sum, r) => sum + r.sections.found.length, 0);
225
+
226
+ // JSON output mode
227
+ if (options.json) {
228
+ const output = {
229
+ feature: featureName,
230
+ phase: phase || 'all',
231
+ valid: totalErrors === 0,
232
+ summary: {
233
+ files: results.length,
234
+ errors: totalErrors,
235
+ warnings: totalWarnings,
236
+ sectionsFound: totalFound
237
+ },
238
+ results: results.map(r => ({
239
+ file: r.file,
240
+ exists: r.exists,
241
+ lineCount: r.lineCount,
242
+ errors: r.errors,
243
+ warnings: r.warnings,
244
+ sectionsFound: r.sections.found,
245
+ sectionsMissing: r.sections.missing
246
+ })),
247
+ schemaWarnings
248
+ };
249
+ console.log(JSON.stringify(output, null, 2));
250
+ return;
251
+ }
252
+
253
+ // Visual output
254
+ console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
255
+ console.log(chalk.cyan('│ MORPH-SPEC FEATURE VALIDATOR │'));
256
+ console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
257
+
258
+ console.log(chalk.white.bold(`Feature: ${featureName}`));
259
+ if (phase) console.log(chalk.gray(`Phase: ${phase}`));
260
+ console.log('');
261
+
262
+ // File-by-file results
263
+ for (const result of results) {
264
+ const symbol = result.exists
265
+ ? (result.errors.length === 0 ? '✅' : '❌')
266
+ : '⬜';
267
+ const name = result.exists
268
+ ? (result.errors.length === 0 ? chalk.green(result.file) : chalk.red(result.file))
269
+ : chalk.gray(result.file);
270
+
271
+ console.log(`${symbol} ${name}${result.exists ? chalk.gray(` (${result.lineCount} lines)`) : ''}`);
272
+
273
+ // Sections found
274
+ if (result.sections.found.length > 0 && (options.verbose || result.errors.length > 0)) {
275
+ result.sections.found.forEach(s => {
276
+ console.log(chalk.green(` ✓ ${s}`));
277
+ });
278
+ }
279
+
280
+ // Errors
281
+ result.errors.forEach(err => {
282
+ console.log(chalk.red(` ✗ ${err}`));
283
+ });
284
+
285
+ // Warnings
286
+ result.warnings.forEach(warn => {
287
+ console.log(chalk.yellow(` ⚠ ${warn}`));
288
+ });
289
+ }
290
+
291
+ // Schema cross-validation
292
+ if (schemaWarnings.length > 0) {
293
+ console.log(chalk.yellow('\nSchema Cross-Validation:'));
294
+ schemaWarnings.forEach(w => {
295
+ console.log(chalk.yellow(` ⚠ ${w}`));
296
+ });
297
+ }
298
+
299
+ // Summary
300
+ console.log('');
301
+ console.log(chalk.gray('─'.repeat(60)));
302
+
303
+ if (totalErrors === 0) {
304
+ console.log(chalk.green(`\n✓ VALIDATION PASSED — ${results.filter(r => r.exists).length} files validated`));
305
+ if (totalWarnings > 0) {
306
+ console.log(chalk.yellow(` ${totalWarnings} warning(s) — review recommended`));
307
+ }
308
+ } else {
309
+ console.log(chalk.red(`\n✗ VALIDATION FAILED — ${totalErrors} error(s), ${totalWarnings} warning(s)`));
310
+ console.log(chalk.yellow(' Fix errors before advancing to the next phase'));
311
+ }
312
+ console.log('');
313
+ }
314
+
315
+ export default validateFeatureCommand;
@@ -20,6 +20,11 @@ import { join } from 'path';
20
20
  * phase - The morph-spec phase that produces this output
21
21
  * protected - Whether the file requires an approval gate before editing
22
22
  * approvalGate - (optional) Which gate protects this file
23
+ * mutableFields- (optional) For protected JSON files: top-level array-item
24
+ * field names that MAY still change after the gate is approved.
25
+ * Lets the implement phase update task progress without
26
+ * unfreezing the plan contract. When present, the protect
27
+ * hook does a semantic diff instead of a blanket block.
23
28
  */
24
29
  export const OUTPUT_SCHEMA = {
25
30
  // 0-proposal/
@@ -36,7 +41,7 @@ export const OUTPUT_SCHEMA = {
36
41
  // 2-plan/
37
42
  spec: { filename: 'spec.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
38
43
  mandate: { filename: 'mandate.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
39
- tasks: { filename: 'tasks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
44
+ tasks: { filename: 'tasks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan', mutableFields: ['status', 'outputs', 'notes'] },
40
45
  decisions: { filename: 'decisions.md', phaseDir: '2-plan', phase: 'plan', protected: false },
41
46
 
42
47
  // 3-implement/
@@ -119,6 +124,18 @@ export const PROTECTED_SPEC_FILES = Object.fromEntries(
119
124
  .map(([, entry]) => [entry.filename, entry.approvalGate])
120
125
  );
121
126
 
127
+ /**
128
+ * For protected files that carry mutable progress fields: filename → the
129
+ * top-level array-item fields that may change after the gate is approved.
130
+ * The protect-spec-files hook uses this to allow progress updates (e.g.
131
+ * task status) while still freezing the plan contract.
132
+ */
133
+ export const MUTABLE_SPEC_FIELDS = Object.fromEntries(
134
+ Object.entries(OUTPUT_SCHEMA)
135
+ .filter(([, entry]) => entry.protected && entry.approvalGate && entry.mutableFields)
136
+ .map(([, entry]) => [entry.filename, entry.mutableFields])
137
+ );
138
+
122
139
  // ============================================================================
123
140
  // Backwards-compat shim (will be removed in v5.1)
124
141
  // ============================================================================