@polymorphism-tech/morph-spec 10.0.0 → 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.
- package/CLAUDE.md +2 -0
- package/README.md +3 -7
- package/bin/morph-spec.js +1 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +27 -38
- package/docs/QUICKSTART.md +31 -191
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +2 -2
- package/framework/agents.json +0 -1
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +6 -7
- package/framework/docs/CLI.md +24 -21
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/next-step.js +4 -4
- package/framework/mcp/registry.json +4 -4
- package/framework/phases.json +4 -4
- package/framework/rules/ai-agents-standards.md +1 -1
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- package/framework/schemas/config.schema.json +20 -1
- package/framework/schemas/feature.schema.json +1 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +8 -7
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/review.md +5 -5
- package/framework/skills/core/morph-plan/SKILL.md +22 -16
- package/framework/skills/core/morph-plan/references/critique.md +11 -8
- package/framework/skills/core/morph-plan/references/interview.md +18 -6
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/standards/STANDARDS.json +20 -24
- package/framework/standards/STANDARDS.meta.json +12 -19
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -0
- package/framework/standards/architecture/stack-baseline.md +26 -3
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +10 -9
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/docs/plan.md +4 -1
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +1 -1
- package/src/commands/state/gate-check.js +2 -2
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +7 -3
- package/src/core/state/state-manager.js +0 -24
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/plan/checks.js +8 -1
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +98 -23
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
|
@@ -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;
|