@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.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +18 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +9 -4
  7. package/framework/MORPH.md +4 -4
  8. package/framework/commands/create-umbraco-lp.md +35 -0
  9. package/framework/commands/migrate-lp-to-umbraco.md +33 -0
  10. package/framework/commands/morph-archive.md +17 -27
  11. package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
  12. package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
  13. package/framework/hooks/claude-code/core/state-sync.js +16 -48
  14. package/framework/hooks/shared/state-reader.js +74 -8
  15. package/framework/hooks/shared/state-writer.js +124 -0
  16. package/framework/rules/morph-task-tracking.md +69 -49
  17. package/framework/rules/morph-workflow.md +192 -191
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
  20. package/framework/skills/core/morph-apply/SKILL.md +15 -15
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -5
  22. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  23. package/framework/skills/core/morph-implement/SKILL.md +5 -4
  24. package/framework/skills/core/morph-plan/SKILL.md +6 -6
  25. package/framework/skills/core/morph-proposal/SKILL.md +8 -7
  26. package/framework/skills/core/morph-review/SKILL.md +3 -3
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/templates/REGISTRY.json +1 -3
  30. package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
  31. package/framework/templates/context/CONTEXT.md +181 -180
  32. package/framework/templates/docs/onboarding.md +122 -118
  33. package/framework/templates/docs/proposal.md +10 -0
  34. package/framework/templates/docs/spec.md +228 -228
  35. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
  36. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
  37. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
  38. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
  39. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
  40. package/package.json +1 -1
  41. package/src/commands/project/update.js +415 -368
  42. package/src/commands/project/worktree.js +212 -210
  43. package/src/commands/state/advance.js +82 -0
  44. package/src/commands/state/archive.js +111 -0
  45. package/src/commands/validation/validate-feature.js +229 -105
  46. package/src/core/state/state-manager.js +238 -66
  47. package/src/dashboard/scanner/state-scanner.js +35 -8
  48. package/src/lib/generators/context-generator.js +12 -8
  49. 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 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.
23
24
  *
24
- * Each section entry can be a string (single substring to match) or an array
25
- * of acceptable substrings (any-of match). This lets pt-BR and English
26
- * coexist — e.g. "Problem" matches both "Problem" and "Problema", but
27
- * "Scope" needs to be paired with "Escopo" explicitly.
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
- * The matcher (`validateMarkdownSections`) does case-insensitive substring
30
- * matching against the whole file content.
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
- 'Overview',
36
- 'Problem', // also matches Problema
37
- ['Solution', 'Solução'],
38
- ['Scope', 'Escopo']
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
- 'Overview',
45
- ['Functional Requirements', 'Requisitos Funcionais'],
46
- ['Non-Functional Requirements', 'Requisitos Não-Funcionais', 'Requisitos Nao-Funcionais'],
47
- ['Technical Architecture', 'Arquitetura'],
48
- ['Data Model', 'Modelo de Dados', 'Contratos']
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
- ['Infrastructure', 'Infraestrutura'],
52
- ['Testing Strategy', 'Estratégia de Testes', 'Testes'],
53
- ['Security', 'Segurança'],
54
- 'Performance',
55
- ['Edge Cases', 'Casos Limite'],
56
- ['Clarifications', 'Esclarecimentos']
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: ['Stack', 'Standards', ['Done', 'Pronto']],
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
- ['Patterns proibidos', 'Padrões proibidos', 'Forbidden Patterns'],
63
- ['Riscos', 'Risks']
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: ['ADR', 'Status', ['Context', 'Contexto'], ['Decision', 'Decisão']],
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
- ['Consequences', 'Consequências', 'Implicações'],
70
- ['Alternatives', 'Alternativas'],
71
- ['Costs', 'Custos']
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: [['Scores', 'Pontuação', 'Scoring'], ['Recommendation', 'Recomendação']],
76
- optional: [['Blockers', 'Bloqueantes'], ['Improvements', 'Melhorias']]
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 `section` entry can be:
176
- * - a string → must appear as a substring (case-insensitive)
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
- * The any-of form supports bilingual content (e.g. ['Scope', 'Escopo']).
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 contentLower = content.toLowerCase();
183
- const matches = (entry) => {
259
+ const markers = extractSectionMarkers(content);
260
+ const match = (entry) => {
184
261
  const candidates = Array.isArray(entry) ? entry : [entry];
185
- const hit = candidates.find(c => contentLower.includes(c.toLowerCase()));
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 } = matches(entry);
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
- const acceptable = Array.isArray(entry)
196
- ? entry.map(s => `"${s}"`).join(' or ')
197
- : `"${entry}"`;
198
- result.errors.push(`Missing required section: ${acceptable}`);
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 } = matches(entry);
288
+ const { hit } = match(entry);
204
289
  if (hit) result.sections.optional.push(hit);
205
290
  }
206
291
  }
207
292
 
208
293
  /**
209
- * Main validate command
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
- let outputFiles;
228
-
229
- if (phase) {
230
- outputFiles = PHASE_OUTPUTS[phase];
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
- // Resolve phase folder for path lookup
253
- const phaseFolder = phase ? PHASE_FOLDERS[phase] : null;
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 || 'all',
272
- valid: totalErrors === 0,
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)));