@polymorphism-tech/morph-spec 7.4.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/README.md +2 -2
- package/bin/morph-spec.js +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +9 -4
- package/framework/MORPH.md +4 -4
- 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/hooks/claude-code/core/enforce-phase-writes.js +115 -90
- package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
- package/framework/hooks/claude-code/core/state-sync.js +16 -48
- package/framework/hooks/shared/state-reader.js +74 -8
- 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/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 +15 -15
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -5
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +5 -4
- package/framework/skills/core/morph-plan/SKILL.md +6 -6
- package/framework/skills/core/morph-proposal/SKILL.md +8 -7
- package/framework/skills/core/morph-review/SKILL.md +3 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- 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/update.js +415 -368
- package/src/commands/project/worktree.js +212 -210
- package/src/commands/state/advance.js +82 -0
- package/src/commands/state/archive.js +111 -0
- package/src/commands/validation/validate-feature.js +229 -105
- package/src/core/state/state-manager.js +238 -66
- package/src/dashboard/scanner/state-scanner.js +35 -8
- package/src/lib/generators/context-generator.js +12 -8
- package/src/utils/file-copier.js +7 -0
|
@@ -8,72 +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.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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.
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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`.
|
|
31
36
|
*/
|
|
32
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.
|
|
33
40
|
'proposal.md': {
|
|
34
41
|
required: [
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
['
|
|
38
|
-
|
|
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'],
|
|
39
55
|
],
|
|
40
|
-
optional: ['Agents', 'Stack', ['Risks', 'Riscos']]
|
|
41
56
|
},
|
|
57
|
+
// morph-plan (spec.md) emits: Arquitetura da solução, Edge cases,
|
|
58
|
+
// Integrações externas e contratos esperados.
|
|
42
59
|
'spec.md': {
|
|
43
60
|
required: [
|
|
44
|
-
'
|
|
45
|
-
['
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
['
|
|
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'],
|
|
49
66
|
],
|
|
50
67
|
optional: [
|
|
51
|
-
['
|
|
52
|
-
['
|
|
53
|
-
['Security', 'Segurança'],
|
|
54
|
-
'
|
|
55
|
-
['
|
|
56
|
-
|
|
57
|
-
]
|
|
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
|
+
],
|
|
58
74
|
},
|
|
75
|
+
// morph-plan (mandate.md) emits: Stack, Standards obrigatórios,
|
|
76
|
+
// Patterns proibidos, Checklist de done.
|
|
59
77
|
'mandate.md': {
|
|
60
|
-
required: [
|
|
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
|
+
],
|
|
61
86
|
optional: [
|
|
62
|
-
['
|
|
63
|
-
['
|
|
64
|
-
]
|
|
87
|
+
['Riscos', 'Risks'],
|
|
88
|
+
['Acesso a dados', 'Data Access', 'Repository', 'Repositório'],
|
|
89
|
+
],
|
|
65
90
|
},
|
|
91
|
+
// decisions.md follows the ADR convention: an `### ADR-NNN` heading with
|
|
92
|
+
// `**Status:** / **Context:** / **Decision:**` line-start bold labels.
|
|
66
93
|
'decisions.md': {
|
|
67
|
-
required: [
|
|
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
|
+
],
|
|
68
103
|
optional: [
|
|
69
|
-
['
|
|
70
|
-
['
|
|
71
|
-
|
|
72
|
-
]
|
|
104
|
+
['Alternativas', 'Alternatives'],
|
|
105
|
+
['Custos', 'Costs'],
|
|
106
|
+
],
|
|
73
107
|
},
|
|
108
|
+
// morph-review emits: Scores por dimensão, Bloqueantes, Melhorias,
|
|
109
|
+
// Recomendação final.
|
|
74
110
|
'review-report.md': {
|
|
75
|
-
required: [
|
|
76
|
-
|
|
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
|
+
],
|
|
77
121
|
},
|
|
78
122
|
};
|
|
79
123
|
|
|
@@ -99,6 +143,38 @@ const PHASE_FOLDERS = {
|
|
|
99
143
|
review: '4-review',
|
|
100
144
|
};
|
|
101
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
|
+
|
|
102
178
|
/**
|
|
103
179
|
* Validate a single output file
|
|
104
180
|
*/
|
|
@@ -110,7 +186,7 @@ function validateOutput(featurePath, outputFile) {
|
|
|
110
186
|
sections: { found: [], missing: [], optional: [] },
|
|
111
187
|
warnings: [],
|
|
112
188
|
errors: [],
|
|
113
|
-
lineCount: 0
|
|
189
|
+
lineCount: 0,
|
|
114
190
|
};
|
|
115
191
|
|
|
116
192
|
if (!fs.existsSync(filePath)) {
|
|
@@ -170,43 +246,130 @@ function validateTasksJsonOutput(content, result) {
|
|
|
170
246
|
}
|
|
171
247
|
|
|
172
248
|
/**
|
|
173
|
-
* Validate markdown sections.
|
|
249
|
+
* Validate markdown sections against section markers (headings + bold labels).
|
|
174
250
|
*
|
|
175
|
-
* Each `
|
|
176
|
-
*
|
|
177
|
-
* - an array → any-of: at least one of the strings must appear
|
|
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.
|
|
178
253
|
*
|
|
179
|
-
*
|
|
254
|
+
* required → missing produces an ERROR
|
|
255
|
+
* recommended → missing produces a WARNING
|
|
256
|
+
* optional → tracked when present, never penalized
|
|
180
257
|
*/
|
|
181
258
|
function validateMarkdownSections(content, sections, result) {
|
|
182
|
-
const
|
|
183
|
-
const
|
|
259
|
+
const markers = extractSectionMarkers(content);
|
|
260
|
+
const match = (entry) => {
|
|
184
261
|
const candidates = Array.isArray(entry) ? entry : [entry];
|
|
185
|
-
const hit = candidates.find(c =>
|
|
262
|
+
const hit = candidates.find(c => markers.some(m => m.includes(c.toLowerCase())));
|
|
186
263
|
return { hit, label: candidates[0] };
|
|
187
264
|
};
|
|
265
|
+
const describe = (entry) =>
|
|
266
|
+
Array.isArray(entry) ? entry.map(s => `"${s}"`).join(' or ') : `"${entry}"`;
|
|
188
267
|
|
|
189
|
-
for (const entry of sections.required) {
|
|
190
|
-
const { hit, label } =
|
|
268
|
+
for (const entry of (sections.required || [])) {
|
|
269
|
+
const { hit, label } = match(entry);
|
|
191
270
|
if (hit) {
|
|
192
271
|
result.sections.found.push(hit);
|
|
193
272
|
} else {
|
|
194
273
|
result.sections.missing.push(label);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
274
|
+
result.errors.push(`Missing required section: ${describe(entry)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const entry of (sections.recommended || [])) {
|
|
279
|
+
const { hit } = match(entry);
|
|
280
|
+
if (hit) {
|
|
281
|
+
result.sections.found.push(hit);
|
|
282
|
+
} else {
|
|
283
|
+
result.warnings.push(`Missing recommended section: ${describe(entry)}`);
|
|
199
284
|
}
|
|
200
285
|
}
|
|
201
286
|
|
|
202
287
|
for (const entry of (sections.optional || [])) {
|
|
203
|
-
const { hit } =
|
|
288
|
+
const { hit } = match(entry);
|
|
204
289
|
if (hit) result.sections.optional.push(hit);
|
|
205
290
|
}
|
|
206
291
|
}
|
|
207
292
|
|
|
208
293
|
/**
|
|
209
|
-
*
|
|
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 });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { outputFiles };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
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`)
|
|
210
373
|
*/
|
|
211
374
|
export async function validateFeatureCommand(featureName, options = {}) {
|
|
212
375
|
if (!featureName) {
|
|
@@ -222,59 +385,28 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
222
385
|
process.exit(1);
|
|
223
386
|
}
|
|
224
387
|
|
|
225
|
-
// Determine which outputs to validate
|
|
226
388
|
const phase = options.phase || null;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
}
|
|
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);
|
|
250
393
|
}
|
|
251
394
|
|
|
252
|
-
|
|
253
|
-
const
|
|
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));
|
|
395
|
+
const validation = runFeatureValidation(featurePath, { phase });
|
|
396
|
+
const { results, totalErrors, totalWarnings, totalFound } = validation;
|
|
260
397
|
const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
|
|
261
398
|
|
|
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
399
|
// JSON output mode
|
|
268
400
|
if (options.json) {
|
|
269
401
|
const output = {
|
|
270
402
|
feature: featureName,
|
|
271
|
-
phase: phase
|
|
272
|
-
valid:
|
|
403
|
+
phase: validation.phase,
|
|
404
|
+
valid: validation.valid,
|
|
273
405
|
summary: {
|
|
274
406
|
files: results.length,
|
|
275
407
|
errors: totalErrors,
|
|
276
|
-
warnings: totalWarnings,
|
|
277
|
-
sectionsFound: totalFound
|
|
408
|
+
warnings: totalWarnings + schemaWarnings.length,
|
|
409
|
+
sectionsFound: totalFound,
|
|
278
410
|
},
|
|
279
411
|
results: results.map(r => ({
|
|
280
412
|
file: r.file,
|
|
@@ -283,9 +415,9 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
283
415
|
errors: r.errors,
|
|
284
416
|
warnings: r.warnings,
|
|
285
417
|
sectionsFound: r.sections.found,
|
|
286
|
-
sectionsMissing: r.sections.missing
|
|
418
|
+
sectionsMissing: r.sections.missing,
|
|
287
419
|
})),
|
|
288
|
-
schemaWarnings
|
|
420
|
+
schemaWarnings,
|
|
289
421
|
};
|
|
290
422
|
console.log(JSON.stringify(output, null, 2));
|
|
291
423
|
return;
|
|
@@ -329,14 +461,6 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
329
461
|
});
|
|
330
462
|
}
|
|
331
463
|
|
|
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
464
|
// Summary
|
|
341
465
|
console.log('');
|
|
342
466
|
console.log(chalk.gray('─'.repeat(60)));
|