@polymorphism-tech/morph-spec 7.3.0 → 7.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.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +22 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +10 -5
- package/framework/MORPH.md +31 -14
- package/framework/agents.json +132 -132
- package/framework/commands/create-umbraco-lp.md +35 -0
- package/framework/commands/migrate-lp-to-umbraco.md +33 -0
- package/framework/commands/morph-archive.md +17 -27
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
- package/framework/hooks/claude-code/core/gate-guard.js +160 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
- package/framework/hooks/claude-code/core/state-sync.js +16 -36
- package/framework/hooks/claude-code/core/validate-completion.js +105 -0
- package/framework/hooks/shared/state-reader.js +302 -221
- package/framework/hooks/shared/state-writer.js +124 -0
- package/framework/rules/morph-task-tracking.md +69 -49
- package/framework/rules/morph-workflow.md +192 -191
- package/framework/skills/README.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
- package/framework/skills/core/morph-apply/SKILL.md +69 -27
- package/framework/skills/core/morph-brainstorming/SKILL.md +6 -2
- package/framework/skills/core/morph-eval/SKILL.md +4 -5
- package/framework/skills/core/morph-implement/SKILL.md +51 -22
- package/framework/skills/core/morph-plan/SKILL.md +10 -4
- package/framework/skills/core/morph-proposal/SKILL.md +8 -7
- package/framework/skills/core/morph-review/SKILL.md +15 -8
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/setup.md +258 -244
- package/framework/templates/REGISTRY.json +1 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
- package/framework/templates/context/CONTEXT.md +181 -180
- package/framework/templates/docs/onboarding.md +122 -118
- package/framework/templates/docs/proposal.md +10 -0
- package/framework/templates/docs/spec.md +228 -228
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
- package/package.json +1 -1
- package/src/commands/project/init.js +517 -515
- package/src/commands/project/update.js +415 -368
- package/src/commands/project/worktree.js +212 -210
- package/src/commands/state/advance.js +82 -0
- package/src/commands/state/approve.js +251 -213
- package/src/commands/state/archive.js +111 -0
- package/src/commands/validation/validate-feature.js +244 -79
- package/src/core/state/state-manager.js +256 -74
- package/src/dashboard/scanner/state-scanner.js +35 -8
- package/src/lib/generators/context-generator.js +12 -8
- package/src/lib/risk-detector.js +2 -2
- package/src/utils/file-copier.js +7 -0
- package/src/utils/hooks-installer.js +527 -516
|
@@ -8,45 +8,116 @@
|
|
|
8
8
|
* morph-spec validate <feature-name> [--phase <phase>] [--json] [--verbose]
|
|
9
9
|
*
|
|
10
10
|
* Example:
|
|
11
|
-
* morph-spec validate user-portal --phase
|
|
11
|
+
* morph-spec validate user-portal --phase plan
|
|
12
12
|
* morph-spec validate user-portal --json
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
-
import { getFeature } from '../../core/state/state-manager.js';
|
|
19
18
|
import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
|
-
* Expected sections per output file
|
|
21
|
+
* Expected sections per output file — realigned with what the core skills
|
|
22
|
+
* actually emit (`morph-brainstorming`, `morph-plan`, `morph-review`) so that
|
|
23
|
+
* the framework's own output passes its own validator.
|
|
24
|
+
*
|
|
25
|
+
* Three tiers:
|
|
26
|
+
* required → ERROR if absent. Strict subset of what the skills always emit.
|
|
27
|
+
* recommended → WARNING if absent. Skills emit these, but a lean doc may skip.
|
|
28
|
+
* optional → silent. Tracked when present, never penalized.
|
|
29
|
+
*
|
|
30
|
+
* Each entry is a string or an array of acceptable strings (any-of). The array
|
|
31
|
+
* form carries bilingual aliases (pt-BR section names the skills emit AND their
|
|
32
|
+
* English equivalents in the canonical templates), e.g. ['Escopo', 'Scope'].
|
|
33
|
+
*
|
|
34
|
+
* Matching is done against *section markers* (headings and line-start bold
|
|
35
|
+
* labels), not whole-file substrings — see `extractSectionMarkers`.
|
|
23
36
|
*/
|
|
24
37
|
const OUTPUT_SECTIONS = {
|
|
38
|
+
// morph-brainstorming emits: Problema, Escopo, Usuários e fluxos críticos,
|
|
39
|
+
// Critérios de aceite, Stack, Riscos e dependências, Compliance de stack.
|
|
25
40
|
'proposal.md': {
|
|
26
|
-
required: [
|
|
27
|
-
|
|
41
|
+
required: [
|
|
42
|
+
['Problema', 'Problem'],
|
|
43
|
+
['Escopo', 'Scope'],
|
|
44
|
+
['Critérios de aceite', 'Critério', 'Aceite', 'Acceptance Criteria', 'Acceptance', 'Success Criteria', 'Success Metrics'],
|
|
45
|
+
],
|
|
46
|
+
recommended: [
|
|
47
|
+
['Riscos', 'Risks'],
|
|
48
|
+
],
|
|
49
|
+
optional: [
|
|
50
|
+
'Stack',
|
|
51
|
+
['Usuários', 'Usuarios', 'Users', 'User Journey', 'User Stories', 'Fluxos'],
|
|
52
|
+
['Dependências', 'Dependencias', 'Dependencies'],
|
|
53
|
+
['Compliance'],
|
|
54
|
+
['Overview', 'Visão geral', 'Visao geral', 'Resumo'],
|
|
55
|
+
],
|
|
28
56
|
},
|
|
57
|
+
// morph-plan (spec.md) emits: Arquitetura da solução, Edge cases,
|
|
58
|
+
// Integrações externas e contratos esperados.
|
|
29
59
|
'spec.md': {
|
|
30
60
|
required: [
|
|
31
|
-
'
|
|
32
|
-
'
|
|
61
|
+
['Arquitetura', 'Architecture'],
|
|
62
|
+
['Integrações', 'Integracoes', 'Integrations', 'Contratos', 'Contracts', 'Data Model', 'Modelo de Dados'],
|
|
63
|
+
],
|
|
64
|
+
recommended: [
|
|
65
|
+
['Edge cases', 'Edge Cases', 'Casos limite', 'Casos Limite', 'Casos de borda'],
|
|
33
66
|
],
|
|
34
67
|
optional: [
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
|
|
68
|
+
['Overview', 'Visão geral', 'Resumo'],
|
|
69
|
+
['Requirements', 'Requisitos'],
|
|
70
|
+
['Security', 'Segurança', 'Seguranca'],
|
|
71
|
+
['Testing', 'Testes', 'Estratégia de Testes'],
|
|
72
|
+
['Performance', 'Desempenho'],
|
|
73
|
+
],
|
|
38
74
|
},
|
|
75
|
+
// morph-plan (mandate.md) emits: Stack, Standards obrigatórios,
|
|
76
|
+
// Patterns proibidos, Checklist de done.
|
|
39
77
|
'mandate.md': {
|
|
40
|
-
required: [
|
|
41
|
-
|
|
78
|
+
required: [
|
|
79
|
+
'Stack',
|
|
80
|
+
['Standards', 'Padrões', 'Padroes'],
|
|
81
|
+
['Done', 'Pronto', 'Checklist', 'Critérios de done', 'Definition of Done'],
|
|
82
|
+
],
|
|
83
|
+
recommended: [
|
|
84
|
+
['Patterns proibidos', 'Padrões proibidos', 'Padroes proibidos', 'Forbidden Patterns', 'Forbidden', 'Proibido'],
|
|
85
|
+
],
|
|
86
|
+
optional: [
|
|
87
|
+
['Riscos', 'Risks'],
|
|
88
|
+
['Acesso a dados', 'Data Access', 'Repository', 'Repositório'],
|
|
89
|
+
],
|
|
42
90
|
},
|
|
91
|
+
// decisions.md follows the ADR convention: an `### ADR-NNN` heading with
|
|
92
|
+
// `**Status:** / **Context:** / **Decision:**` line-start bold labels.
|
|
43
93
|
'decisions.md': {
|
|
44
|
-
required: [
|
|
45
|
-
|
|
94
|
+
required: [
|
|
95
|
+
'ADR',
|
|
96
|
+
'Status',
|
|
97
|
+
['Contexto', 'Context'],
|
|
98
|
+
['Decisão', 'Decisao', 'Decision'],
|
|
99
|
+
],
|
|
100
|
+
recommended: [
|
|
101
|
+
['Consequências', 'Consequencias', 'Consequences', 'Implicações', 'Trade-offs', 'Trade-off'],
|
|
102
|
+
],
|
|
103
|
+
optional: [
|
|
104
|
+
['Alternativas', 'Alternatives'],
|
|
105
|
+
['Custos', 'Costs'],
|
|
106
|
+
],
|
|
46
107
|
},
|
|
108
|
+
// morph-review emits: Scores por dimensão, Bloqueantes, Melhorias,
|
|
109
|
+
// Recomendação final.
|
|
47
110
|
'review-report.md': {
|
|
48
|
-
required: [
|
|
49
|
-
|
|
111
|
+
required: [
|
|
112
|
+
['Scores', 'Score', 'Pontuação', 'Pontuacao', 'Scoring'],
|
|
113
|
+
['Recomendação', 'Recomendacao', 'Recommendation'],
|
|
114
|
+
],
|
|
115
|
+
recommended: [
|
|
116
|
+
['Bloqueantes', 'Bloqueante', 'Blockers'],
|
|
117
|
+
],
|
|
118
|
+
optional: [
|
|
119
|
+
['Melhorias', 'Improvements', 'Recomendadas'],
|
|
120
|
+
],
|
|
50
121
|
},
|
|
51
122
|
};
|
|
52
123
|
|
|
@@ -72,6 +143,38 @@ const PHASE_FOLDERS = {
|
|
|
72
143
|
review: '4-review',
|
|
73
144
|
};
|
|
74
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Extract section markers from markdown content.
|
|
148
|
+
*
|
|
149
|
+
* A marker is a line that structurally introduces a section:
|
|
150
|
+
* - a markdown heading → `## Arquitetura da solução`
|
|
151
|
+
* - a line-start bold label → `**Status:** Aceito`
|
|
152
|
+
*
|
|
153
|
+
* Bold labels are included on purpose (deviation from heading-only): the ADR
|
|
154
|
+
* convention used in `decisions.md` — and in the spec.md ADR block — writes
|
|
155
|
+
* Status/Context/Decision as line-start bold labels, not headings. They are
|
|
156
|
+
* structural, not prose, so this keeps ADRs valid without reintroducing the
|
|
157
|
+
* accidental-prose-match problem that whole-file substring matching had.
|
|
158
|
+
*
|
|
159
|
+
* Returns lowercased marker texts.
|
|
160
|
+
*/
|
|
161
|
+
function extractSectionMarkers(content) {
|
|
162
|
+
const markers = [];
|
|
163
|
+
for (const rawLine of content.split('\n')) {
|
|
164
|
+
const line = rawLine.trim();
|
|
165
|
+
const heading = /^#{1,6}\s+(.+)$/.exec(line);
|
|
166
|
+
if (heading) {
|
|
167
|
+
markers.push(heading[1].toLowerCase());
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const bold = /^\*\*(.+?)\*\*/.exec(line);
|
|
171
|
+
if (bold) {
|
|
172
|
+
markers.push(bold[1].toLowerCase());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return markers;
|
|
176
|
+
}
|
|
177
|
+
|
|
75
178
|
/**
|
|
76
179
|
* Validate a single output file
|
|
77
180
|
*/
|
|
@@ -83,7 +186,7 @@ function validateOutput(featurePath, outputFile) {
|
|
|
83
186
|
sections: { found: [], missing: [], optional: [] },
|
|
84
187
|
warnings: [],
|
|
85
188
|
errors: [],
|
|
86
|
-
lineCount: 0
|
|
189
|
+
lineCount: 0,
|
|
87
190
|
};
|
|
88
191
|
|
|
89
192
|
if (!fs.existsSync(filePath)) {
|
|
@@ -143,29 +246,130 @@ function validateTasksJsonOutput(content, result) {
|
|
|
143
246
|
}
|
|
144
247
|
|
|
145
248
|
/**
|
|
146
|
-
* Validate markdown sections
|
|
249
|
+
* Validate markdown sections against section markers (headings + bold labels).
|
|
250
|
+
*
|
|
251
|
+
* Each `entry` is a string or array of acceptable aliases (any-of). A section
|
|
252
|
+
* is present when at least one alias is a substring of at least one marker.
|
|
253
|
+
*
|
|
254
|
+
* required → missing produces an ERROR
|
|
255
|
+
* recommended → missing produces a WARNING
|
|
256
|
+
* optional → tracked when present, never penalized
|
|
147
257
|
*/
|
|
148
258
|
function validateMarkdownSections(content, sections, result) {
|
|
149
|
-
const
|
|
259
|
+
const markers = extractSectionMarkers(content);
|
|
260
|
+
const match = (entry) => {
|
|
261
|
+
const candidates = Array.isArray(entry) ? entry : [entry];
|
|
262
|
+
const hit = candidates.find(c => markers.some(m => m.includes(c.toLowerCase())));
|
|
263
|
+
return { hit, label: candidates[0] };
|
|
264
|
+
};
|
|
265
|
+
const describe = (entry) =>
|
|
266
|
+
Array.isArray(entry) ? entry.map(s => `"${s}"`).join(' or ') : `"${entry}"`;
|
|
267
|
+
|
|
268
|
+
for (const entry of (sections.required || [])) {
|
|
269
|
+
const { hit, label } = match(entry);
|
|
270
|
+
if (hit) {
|
|
271
|
+
result.sections.found.push(hit);
|
|
272
|
+
} else {
|
|
273
|
+
result.sections.missing.push(label);
|
|
274
|
+
result.errors.push(`Missing required section: ${describe(entry)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
150
277
|
|
|
151
|
-
for (const
|
|
152
|
-
|
|
153
|
-
|
|
278
|
+
for (const entry of (sections.recommended || [])) {
|
|
279
|
+
const { hit } = match(entry);
|
|
280
|
+
if (hit) {
|
|
281
|
+
result.sections.found.push(hit);
|
|
154
282
|
} else {
|
|
155
|
-
result.
|
|
156
|
-
result.errors.push(`Missing required section: "${section}"`);
|
|
283
|
+
result.warnings.push(`Missing recommended section: ${describe(entry)}`);
|
|
157
284
|
}
|
|
158
285
|
}
|
|
159
286
|
|
|
160
|
-
for (const
|
|
161
|
-
|
|
162
|
-
|
|
287
|
+
for (const entry of (sections.optional || [])) {
|
|
288
|
+
const { hit } = match(entry);
|
|
289
|
+
if (hit) result.sections.optional.push(hit);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the list of `{ file, dir }` outputs to validate.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} featurePath Absolute path to the feature directory.
|
|
297
|
+
* @param {string|null} phase Phase key, or null to validate everything.
|
|
298
|
+
* @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
|
|
299
|
+
*/
|
|
300
|
+
function resolveOutputFiles(featurePath, phase) {
|
|
301
|
+
if (phase) {
|
|
302
|
+
const files = PHASE_OUTPUTS[phase];
|
|
303
|
+
if (!files) return { error: `Unknown phase: ${phase}` };
|
|
304
|
+
const phaseFolder = PHASE_FOLDERS[phase];
|
|
305
|
+
const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
|
|
306
|
+
return { outputFiles: files.map(file => ({ file, dir: searchPath })) };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate every existing output across the feature root + phase folders.
|
|
310
|
+
const outputFiles = [];
|
|
311
|
+
const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'tasks.json';
|
|
312
|
+
const entries = fs.readdirSync(featurePath, { withFileTypes: true });
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
if (entry.isDirectory()) {
|
|
315
|
+
const subDir = path.join(featurePath, entry.name);
|
|
316
|
+
for (const f of fs.readdirSync(subDir).filter(isOutput)) {
|
|
317
|
+
outputFiles.push({ file: f, dir: subDir });
|
|
318
|
+
}
|
|
319
|
+
} else if (isOutput(entry.name)) {
|
|
320
|
+
outputFiles.push({ file: entry.name, dir: featurePath });
|
|
163
321
|
}
|
|
164
322
|
}
|
|
323
|
+
return { outputFiles };
|
|
165
324
|
}
|
|
166
325
|
|
|
167
326
|
/**
|
|
168
|
-
*
|
|
327
|
+
* Pure validation core — no console output, no process.exit.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} featurePath Absolute path to the feature directory.
|
|
330
|
+
* @param {{ phase?: string }} [options]
|
|
331
|
+
* @returns {{
|
|
332
|
+
* feature: string, phase: string, valid: boolean, error?: string,
|
|
333
|
+
* totalErrors: number, totalWarnings: number, totalFound: number,
|
|
334
|
+
* results: object[]
|
|
335
|
+
* }}
|
|
336
|
+
*/
|
|
337
|
+
export function runFeatureValidation(featurePath, options = {}) {
|
|
338
|
+
const phase = options.phase || null;
|
|
339
|
+
const feature = path.basename(featurePath);
|
|
340
|
+
|
|
341
|
+
const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
|
|
342
|
+
if (error) {
|
|
343
|
+
return {
|
|
344
|
+
feature,
|
|
345
|
+
phase: phase || 'all',
|
|
346
|
+
valid: false,
|
|
347
|
+
error,
|
|
348
|
+
totalErrors: 1,
|
|
349
|
+
totalWarnings: 0,
|
|
350
|
+
totalFound: 0,
|
|
351
|
+
results: [],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const results = outputFiles.map(({ file, dir }) => validateOutput(dir, file));
|
|
356
|
+
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
357
|
+
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
358
|
+
const totalFound = results.reduce((sum, r) => sum + r.sections.found.length, 0);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
feature,
|
|
362
|
+
phase: phase || 'all',
|
|
363
|
+
valid: totalErrors === 0,
|
|
364
|
+
totalErrors,
|
|
365
|
+
totalWarnings,
|
|
366
|
+
totalFound,
|
|
367
|
+
results,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Main validate command (CLI wrapper around `runFeatureValidation`)
|
|
169
373
|
*/
|
|
170
374
|
export async function validateFeatureCommand(featureName, options = {}) {
|
|
171
375
|
if (!featureName) {
|
|
@@ -181,59 +385,28 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
181
385
|
process.exit(1);
|
|
182
386
|
}
|
|
183
387
|
|
|
184
|
-
// Determine which outputs to validate
|
|
185
388
|
const phase = options.phase || null;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
}
|
|
389
|
+
if (phase && !PHASE_OUTPUTS[phase]) {
|
|
390
|
+
console.error(chalk.red(`Unknown phase: ${phase}`));
|
|
391
|
+
console.error(chalk.gray(`Valid phases: ${Object.keys(PHASE_OUTPUTS).join(', ')}`));
|
|
392
|
+
process.exit(1);
|
|
209
393
|
}
|
|
210
394
|
|
|
211
|
-
|
|
212
|
-
const
|
|
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));
|
|
395
|
+
const validation = runFeatureValidation(featurePath, { phase });
|
|
396
|
+
const { results, totalErrors, totalWarnings, totalFound } = validation;
|
|
219
397
|
const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
|
|
220
398
|
|
|
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
399
|
// JSON output mode
|
|
227
400
|
if (options.json) {
|
|
228
401
|
const output = {
|
|
229
402
|
feature: featureName,
|
|
230
|
-
phase: phase
|
|
231
|
-
valid:
|
|
403
|
+
phase: validation.phase,
|
|
404
|
+
valid: validation.valid,
|
|
232
405
|
summary: {
|
|
233
406
|
files: results.length,
|
|
234
407
|
errors: totalErrors,
|
|
235
|
-
warnings: totalWarnings,
|
|
236
|
-
sectionsFound: totalFound
|
|
408
|
+
warnings: totalWarnings + schemaWarnings.length,
|
|
409
|
+
sectionsFound: totalFound,
|
|
237
410
|
},
|
|
238
411
|
results: results.map(r => ({
|
|
239
412
|
file: r.file,
|
|
@@ -242,9 +415,9 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
242
415
|
errors: r.errors,
|
|
243
416
|
warnings: r.warnings,
|
|
244
417
|
sectionsFound: r.sections.found,
|
|
245
|
-
sectionsMissing: r.sections.missing
|
|
418
|
+
sectionsMissing: r.sections.missing,
|
|
246
419
|
})),
|
|
247
|
-
schemaWarnings
|
|
420
|
+
schemaWarnings,
|
|
248
421
|
};
|
|
249
422
|
console.log(JSON.stringify(output, null, 2));
|
|
250
423
|
return;
|
|
@@ -288,14 +461,6 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
288
461
|
});
|
|
289
462
|
}
|
|
290
463
|
|
|
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
464
|
// Summary
|
|
300
465
|
console.log('');
|
|
301
466
|
console.log(chalk.gray('─'.repeat(60)));
|