@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.
Files changed (61) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +22 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +10 -5
  8. package/framework/MORPH.md +31 -14
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/create-umbraco-lp.md +35 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +33 -0
  12. package/framework/commands/morph-archive.md +17 -27
  13. package/framework/evals/rubrics/README.md +9 -8
  14. package/framework/hooks/README.md +2 -0
  15. package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
  16. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  17. package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
  18. package/framework/hooks/claude-code/core/state-sync.js +16 -36
  19. package/framework/hooks/claude-code/core/validate-completion.js +105 -0
  20. package/framework/hooks/shared/state-reader.js +302 -221
  21. package/framework/hooks/shared/state-writer.js +124 -0
  22. package/framework/rules/morph-task-tracking.md +69 -49
  23. package/framework/rules/morph-workflow.md +192 -191
  24. package/framework/skills/README.md +1 -1
  25. package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
  26. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
  27. package/framework/skills/core/morph-apply/SKILL.md +69 -27
  28. package/framework/skills/core/morph-brainstorming/SKILL.md +6 -2
  29. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  30. package/framework/skills/core/morph-implement/SKILL.md +51 -22
  31. package/framework/skills/core/morph-plan/SKILL.md +10 -4
  32. package/framework/skills/core/morph-proposal/SKILL.md +8 -7
  33. package/framework/skills/core/morph-review/SKILL.md +15 -8
  34. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  35. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  36. package/framework/standards/ai-agents/setup.md +258 -244
  37. package/framework/templates/REGISTRY.json +1 -3
  38. package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
  39. package/framework/templates/context/CONTEXT.md +181 -180
  40. package/framework/templates/docs/onboarding.md +122 -118
  41. package/framework/templates/docs/proposal.md +10 -0
  42. package/framework/templates/docs/spec.md +228 -228
  43. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
  44. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
  45. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
  46. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
  47. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
  48. package/package.json +1 -1
  49. package/src/commands/project/init.js +517 -515
  50. package/src/commands/project/update.js +415 -368
  51. package/src/commands/project/worktree.js +212 -210
  52. package/src/commands/state/advance.js +82 -0
  53. package/src/commands/state/approve.js +251 -213
  54. package/src/commands/state/archive.js +111 -0
  55. package/src/commands/validation/validate-feature.js +244 -79
  56. package/src/core/state/state-manager.js +256 -74
  57. package/src/dashboard/scanner/state-scanner.js +35 -8
  58. package/src/lib/generators/context-generator.js +12 -8
  59. package/src/lib/risk-detector.js +2 -2
  60. package/src/utils/file-copier.js +7 -0
  61. 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 design
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: ['Overview', 'Problem', 'Solution', 'Scope'],
27
- optional: ['Agents', 'Stack', 'Risks']
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
- 'Overview', 'Functional Requirements', 'Non-Functional Requirements',
32
- 'Technical Architecture', 'Data Model'
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
- 'Infrastructure', 'Testing Strategy', 'Security',
36
- 'Performance', 'Edge Cases', 'Clarifications'
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: ['Stack', 'Standards', 'Done'],
41
- optional: ['Patterns proibidos', 'Riscos']
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: ['ADR', 'Status', 'Context', 'Decision'],
45
- optional: ['Consequences', 'Alternatives', 'Costs']
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: ['Scores', 'Recommendation'],
49
- optional: ['Blockers', 'Improvements']
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 contentLower = content.toLowerCase();
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 section of sections.required) {
152
- if (contentLower.includes(section.toLowerCase())) {
153
- result.sections.found.push(section);
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.sections.missing.push(section);
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 section of (sections.optional || [])) {
161
- if (contentLower.includes(section.toLowerCase())) {
162
- result.sections.optional.push(section);
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
- * Main validate command
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
- 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
- }
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
- // 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));
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 || 'all',
231
- valid: totalErrors === 0,
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)));