@polymorphism-tech/morph-spec 7.2.0 → 7.3.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/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +1 -1
  6. package/framework/commands/morph-apply.md +1 -1
  7. package/framework/commands/morph-proposal.md +1 -1
  8. package/framework/hooks/README.md +1 -1
  9. package/framework/hooks/claude-code/core/validate-completion.js +136 -136
  10. package/framework/phases.json +1 -1
  11. package/framework/skills/README.md +1 -1
  12. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  13. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  14. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +1 -1
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/templates/README.md +708 -709
  22. package/framework/templates/REGISTRY.json +1 -20
  23. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  24. package/framework/templates/docs/proposal.md +171 -171
  25. package/framework/templates/feature/recap.md +145 -145
  26. package/package.json +1 -1
  27. package/src/commands/project/doctor.js +941 -946
  28. package/src/commands/project/status.js +373 -376
  29. package/src/commands/validation/validate-feature.js +315 -287
  30. package/src/core/state/state-manager.js +900 -934
  31. package/src/dashboard/app/App.jsx +29 -31
  32. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  33. package/src/dashboard/scanner/index.js +20 -21
  34. package/src/lib/context/context-builder.js +3 -3
  35. package/src/lib/generators/context-generator.js +2 -3
  36. package/src/lib/generators/mandate-generator.js +48 -92
  37. package/src/lib/generators/metadata-extractor.js +385 -386
  38. package/src/lib/generators/recap-generator.js +285 -285
  39. package/src/lib/validators/content/content-validator.js +89 -286
  40. package/src/lib/validators/content/index.js +0 -1
  41. package/framework/templates/feature/tasks.md +0 -199
  42. package/framework/workflows/README.md +0 -1041
  43. package/framework/workflows/configs/fast-track.json +0 -46
  44. package/framework/workflows/configs/full.json +0 -64
  45. package/framework/workflows/configs/quick.json +0 -32
  46. package/framework/workflows/configs/standard.json +0 -57
  47. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  48. package/framework/workflows/docs/design-impl.md +0 -37
  49. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  50. package/framework/workflows/docs/fast-track.md +0 -29
  51. package/framework/workflows/docs/full-morph.md +0 -69
  52. package/framework/workflows/docs/standard.md +0 -49
  53. package/framework/workflows/docs/ui-refresh.md +0 -39
  54. package/src/commands/state/index.js +0 -7
  55. package/src/core/workflows/workflow-detector.js +0 -586
  56. package/src/core/workflows/workflow-router.js +0 -204
  57. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  58. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  59. package/src/lib/phase-chain/phase-validator.js +0 -359
  60. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  61. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,285 +1,285 @@
1
- /**
2
- * Recap Generator
3
- *
4
- * Auto-generates recap.md from real project data: state.json, contract compliance,
5
- * validation results, and decisions. Replaces manual template filling.
6
- *
7
- * MORPH-SPEC 5.0 - Recap Generator
8
- */
9
-
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
11
- import { join, dirname } from 'path';
12
- import chalk from 'chalk';
13
- import { loadState, markOutput } from '../../core/state/state-manager.js';
14
- import { runValidation } from '../validators/validation-runner.js';
15
-
16
- /**
17
- * Generate recap.md for a feature
18
- *
19
- * @param {string} projectPath - Project root path
20
- * @param {string} featureName - Feature name
21
- * @param {Object} options - Options
22
- * @returns {string} Generated recap content
23
- */
24
- export async function generateRecap(projectPath, featureName, options = {}) {
25
- const state = loadState(false);
26
- if (!state || !state.features[featureName]) {
27
- throw new Error(`Feature '${featureName}' not found in state.json`);
28
- }
29
-
30
- const feature = state.features[featureName];
31
- const outputsPath = join(projectPath, '.morph/features', featureName);
32
-
33
- // Gather data
34
- const tasksSummary = getTasksSummary(feature);
35
- const agentsSummary = getAgentsSummary(feature);
36
- const checkpointsSummary = getCheckpointsSummary(feature);
37
- // Decision constraints support (stub - decision-constraint-loader removed as orphaned code)
38
- const decisions = { decisions: [] };
39
- const validationResult = await runValidation(projectPath, featureName, { verbose: false });
40
-
41
- // Contract coverage
42
- let contractCoverage = null;
43
- if (validationResult.issues) {
44
- const contractIssues = validationResult.issues.filter(i => i.message?.includes('Interface') || i.message?.includes('contracts'));
45
- if (contractIssues.length > 0 || existsSync(join(outputsPath, 'contracts.cs'))) {
46
- // Extract coverage from validation result
47
- contractCoverage = extractContractCoverage(validationResult);
48
- }
49
- }
50
-
51
- // Generate markdown
52
- const recap = buildRecapMarkdown({
53
- featureName,
54
- feature,
55
- tasksSummary,
56
- agentsSummary,
57
- checkpointsSummary,
58
- decisions: decisions.decisions,
59
- validationResult,
60
- contractCoverage
61
- });
62
-
63
- // Write to file
64
- const recapPath = join(outputsPath, 'recap.md');
65
- if (!existsSync(dirname(recapPath))) {
66
- mkdirSync(dirname(recapPath), { recursive: true });
67
- }
68
- writeFileSync(recapPath, recap, 'utf-8');
69
-
70
- // Auto-mark recap output as created in state
71
- try {
72
- await markOutput(featureName, 'recap');
73
- } catch {
74
- // Non-fatal: state may not exist in all contexts
75
- }
76
-
77
- if (!options.quiet) {
78
- console.log(chalk.green(`\n✅ Recap generated: ${recapPath}`));
79
- }
80
-
81
- return recap;
82
- }
83
-
84
- function getTasksSummary(feature) {
85
- // v2: feature.tasks is an array of task objects
86
- if (Array.isArray(feature.tasks)) {
87
- const tasks = feature.tasks;
88
- const completed = tasks.filter(t => t.status === 'completed');
89
- const pending = tasks.filter(t => t.status === 'pending');
90
- const inProgress = tasks.filter(t => t.status === 'in_progress');
91
- return {
92
- total: tasks.length,
93
- completed: completed.length,
94
- pending: pending.length,
95
- inProgress: inProgress.length,
96
- percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
97
- items: tasks
98
- };
99
- }
100
-
101
- // v3-v4 legacy: feature.tasks is a counter object {total, completed, inProgress, pending}
102
- const counters = feature.tasks || {};
103
- const items = [];
104
- const total = counters.total || items.length || 0;
105
- const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
106
- const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
107
- const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
108
-
109
- return {
110
- total,
111
- completed: completedCount,
112
- pending: pendingCount,
113
- inProgress: inProgressCount,
114
- percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
115
- items
116
- };
117
- }
118
-
119
- function getAgentsSummary(feature) {
120
- return feature.activeAgents || [];
121
- }
122
-
123
- function getCheckpointsSummary(feature) {
124
- return (feature.checkpoints || []).map(cp => ({
125
- timestamp: cp.timestamp,
126
- note: cp.note,
127
- tasks: cp.tasksCompleted || []
128
- }));
129
- }
130
-
131
- function extractContractCoverage(validationResult) {
132
- // Look for contract compliance data in validation issues
133
- const interfaceErrors = validationResult.issues.filter(i =>
134
- i.message?.includes('Interface') && i.level === 'error'
135
- );
136
- const methodErrors = validationResult.issues.filter(i =>
137
- i.message?.includes('Method') && i.message?.includes('missing') && i.level === 'error'
138
- );
139
-
140
- return {
141
- missingInterfaces: interfaceErrors.length,
142
- missingMethods: methodErrors.length,
143
- hasIssues: interfaceErrors.length > 0 || methodErrors.length > 0
144
- };
145
- }
146
-
147
- function buildRecapMarkdown(data) {
148
- const { featureName, feature, tasksSummary, agentsSummary, checkpointsSummary, decisions, validationResult, contractCoverage } = data;
149
- const now = new Date().toISOString().split('T')[0];
150
-
151
- let md = `# Recap: ${featureName}\n\n`;
152
- md += `> Generated: ${now} | Phase: ${feature.phase} | Status: ${feature.status}\n\n`;
153
- md += `---\n\n`;
154
-
155
- // Progress
156
- md += `## Progress\n\n`;
157
- md += `| Metric | Value |\n|--------|-------|\n`;
158
- md += `| Tasks completed | ${tasksSummary.completed}/${tasksSummary.total} (${tasksSummary.percentage}%) |\n`;
159
- md += `| Tasks pending | ${tasksSummary.pending} |\n`;
160
- md += `| Tasks in progress | ${tasksSummary.inProgress} |\n`;
161
- md += `| Active agents | ${agentsSummary.length} |\n`;
162
- md += `| Checkpoints | ${checkpointsSummary.length} |\n\n`;
163
-
164
- // Validation Results
165
- md += `## Validation Results\n\n`;
166
- if (validationResult.passed) {
167
- md += `**Status: PASSED** (${validationResult.warnings} warnings)\n\n`;
168
- } else {
169
- md += `**Status: FAILED** (${validationResult.errors} errors, ${validationResult.warnings} warnings)\n\n`;
170
- if (validationResult.issues.length > 0) {
171
- md += `| Level | Issue |\n|-------|-------|\n`;
172
- for (const issue of validationResult.issues.slice(0, 15)) {
173
- md += `| ${issue.level} | ${issue.message} |\n`;
174
- }
175
- md += `\n`;
176
- }
177
- md += `> **Note:** Errors above are from morph-spec static validators (may include false positives).\n`;
178
- md += `> Verify actual build status with the commands in the section below.\n\n`;
179
- }
180
-
181
- // Score Breakdown (when rubric scoring is available)
182
- if (validationResult.scores) {
183
- const scores = validationResult.scores;
184
- md += `### Quality Score: ${scores.score}/10\n\n`;
185
- md += `| Category | Score | Weight | Status |\n|----------|-------|--------|--------|\n`;
186
- const approveThreshold = scores.approveThreshold ?? 8.0;
187
- for (const [category, data] of Object.entries(scores.breakdown)) {
188
- if (category === '_unmapped') continue;
189
- const pct = Math.round(data.weight * 100);
190
- const status = data.score >= approveThreshold ? 'Pass' : data.score >= 6 ? 'Needs work' : 'Critical';
191
- md += `| ${category} | ${Math.round(data.score * 10) / 10}/10 | ${pct}% | ${status} |\n`;
192
- }
193
- md += `\n`;
194
- }
195
-
196
- // Suggested Quality Verification
197
- const agents = agentsSummary;
198
- const hasNextjs = agents.some(a => ['nextjs-expert', 'css-specialist', 'ui-designer'].includes(a));
199
- const hasDotnet = agents.some(a => ['dotnet-senior', 'ef-modeler'].includes(a));
200
- if (hasNextjs || hasDotnet) {
201
- md += `## Suggested Quality Verification\n\n`;
202
- if (hasNextjs) {
203
- md += `**Next.js / TypeScript:**\n\`\`\`bash\nnpx tsc --noEmit\nnpx eslint . --max-warnings 0\n\`\`\`\n\n`;
204
- }
205
- if (hasDotnet) {
206
- md += `**.NET:**\n\`\`\`bash\ndotnet build\ndotnet test\n\`\`\`\n\n`;
207
- }
208
- }
209
-
210
- // Contract Coverage
211
- if (contractCoverage) {
212
- md += `## Contract Compliance\n\n`;
213
- if (contractCoverage.hasIssues) {
214
- md += `- Missing interfaces: ${contractCoverage.missingInterfaces}\n`;
215
- md += `- Missing methods: ${contractCoverage.missingMethods}\n\n`;
216
- } else {
217
- md += `All contracts implemented.\n\n`;
218
- }
219
- }
220
-
221
- // Decisions
222
- if (decisions.length > 0) {
223
- md += `## Decisions Applied\n\n`;
224
- for (const d of decisions) {
225
- if (d.title) {
226
- md += `- **${d.title}**: ${d.decision.slice(0, 100)}${d.decision.length > 100 ? '...' : ''}\n`;
227
- }
228
- }
229
- md += `\n`;
230
- }
231
-
232
- // Agents
233
- if (agentsSummary.length > 0) {
234
- md += `## Active Agents\n\n`;
235
- md += agentsSummary.map(a => `- ${a}`).join('\n');
236
- md += `\n\n`;
237
- }
238
-
239
- // Checkpoints
240
- if (checkpointsSummary.length > 0) {
241
- md += `## Checkpoints\n\n`;
242
- for (const cp of checkpointsSummary) {
243
- md += `- ${cp.timestamp}: ${cp.note}\n`;
244
- }
245
- md += `\n`;
246
- }
247
-
248
- // Tasks Detail
249
- if (tasksSummary.items.length > 0) {
250
- const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
251
- md += `## Tasks\n\n`;
252
- if (anyNotes) {
253
- md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
254
- for (const task of tasksSummary.items) {
255
- const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
256
- const noteCount = task.notes?.length || 0;
257
- const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
258
- md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
259
- }
260
- } else {
261
- md += `| ID | Title | Status |\n|----|-------|--------|\n`;
262
- for (const task of tasksSummary.items) {
263
- const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
264
- md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
265
- }
266
- }
267
- md += `\n`;
268
- }
269
-
270
- // Deviations from Plan
271
- const deviations = tasksSummary.items
272
- .filter(t => t.notes?.some(n => n.type === 'deviation'))
273
- .flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
274
- if (deviations.length > 0) {
275
- md += `## Deviations from Plan\n\n`;
276
- for (const d of deviations) {
277
- md += `- **${d.taskId}**: ${d.text}\n`;
278
- }
279
- md += `\n`;
280
- }
281
-
282
- md += `---\n\n*Generated by MORPH-SPEC Recap Generator*\n`;
283
-
284
- return md;
285
- }
1
+ /**
2
+ * Recap Generator
3
+ *
4
+ * Auto-generates recap.md from real project data: state.json, contract compliance,
5
+ * validation results, and decisions. Replaces manual template filling.
6
+ *
7
+ * MORPH-SPEC 5.0 - Recap Generator
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
11
+ import { join, dirname } from 'path';
12
+ import chalk from 'chalk';
13
+ import { loadState, markOutput } from '../../core/state/state-manager.js';
14
+ import { runValidation } from '../validators/validation-runner.js';
15
+
16
+ /**
17
+ * Generate recap.md for a feature
18
+ *
19
+ * @param {string} projectPath - Project root path
20
+ * @param {string} featureName - Feature name
21
+ * @param {Object} options - Options
22
+ * @returns {string} Generated recap content
23
+ */
24
+ export async function generateRecap(projectPath, featureName, options = {}) {
25
+ const state = loadState(false);
26
+ if (!state || !state.features[featureName]) {
27
+ throw new Error(`Feature '${featureName}' not found in state.json`);
28
+ }
29
+
30
+ const feature = state.features[featureName];
31
+ const outputsPath = join(projectPath, '.morph/features', featureName);
32
+
33
+ // Gather data
34
+ const tasksSummary = getTasksSummary(feature);
35
+ const agentsSummary = getAgentsSummary(feature);
36
+ const checkpointsSummary = getCheckpointsSummary(feature);
37
+ // Decision constraints support (stub - decision-constraint-loader removed as orphaned code)
38
+ const decisions = { decisions: [] };
39
+ const validationResult = await runValidation(projectPath, featureName, { verbose: false });
40
+
41
+ // Contract coverage — derived from validation issues. v5 keeps contracts
42
+ // inside spec.md, so there is no separate contracts file to probe.
43
+ let contractCoverage = null;
44
+ if (validationResult.issues) {
45
+ const contractIssues = validationResult.issues.filter(i => i.message?.includes('Interface') || i.message?.includes('contracts'));
46
+ if (contractIssues.length > 0) {
47
+ contractCoverage = extractContractCoverage(validationResult);
48
+ }
49
+ }
50
+
51
+ // Generate markdown
52
+ const recap = buildRecapMarkdown({
53
+ featureName,
54
+ feature,
55
+ tasksSummary,
56
+ agentsSummary,
57
+ checkpointsSummary,
58
+ decisions: decisions.decisions,
59
+ validationResult,
60
+ contractCoverage
61
+ });
62
+
63
+ // Write to file
64
+ const recapPath = join(outputsPath, 'recap.md');
65
+ if (!existsSync(dirname(recapPath))) {
66
+ mkdirSync(dirname(recapPath), { recursive: true });
67
+ }
68
+ writeFileSync(recapPath, recap, 'utf-8');
69
+
70
+ // Auto-mark recap output as created in state
71
+ try {
72
+ await markOutput(featureName, 'recap');
73
+ } catch {
74
+ // Non-fatal: state may not exist in all contexts
75
+ }
76
+
77
+ if (!options.quiet) {
78
+ console.log(chalk.green(`\n✅ Recap generated: ${recapPath}`));
79
+ }
80
+
81
+ return recap;
82
+ }
83
+
84
+ function getTasksSummary(feature) {
85
+ // v2: feature.tasks is an array of task objects
86
+ if (Array.isArray(feature.tasks)) {
87
+ const tasks = feature.tasks;
88
+ const completed = tasks.filter(t => t.status === 'completed');
89
+ const pending = tasks.filter(t => t.status === 'pending');
90
+ const inProgress = tasks.filter(t => t.status === 'in_progress');
91
+ return {
92
+ total: tasks.length,
93
+ completed: completed.length,
94
+ pending: pending.length,
95
+ inProgress: inProgress.length,
96
+ percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
97
+ items: tasks
98
+ };
99
+ }
100
+
101
+ // v3-v4 legacy: feature.tasks is a counter object {total, completed, inProgress, pending}
102
+ const counters = feature.tasks || {};
103
+ const items = [];
104
+ const total = counters.total || items.length || 0;
105
+ const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
106
+ const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
107
+ const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
108
+
109
+ return {
110
+ total,
111
+ completed: completedCount,
112
+ pending: pendingCount,
113
+ inProgress: inProgressCount,
114
+ percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
115
+ items
116
+ };
117
+ }
118
+
119
+ function getAgentsSummary(feature) {
120
+ return feature.activeAgents || [];
121
+ }
122
+
123
+ function getCheckpointsSummary(feature) {
124
+ return (feature.checkpoints || []).map(cp => ({
125
+ timestamp: cp.timestamp,
126
+ note: cp.note,
127
+ tasks: cp.tasksCompleted || []
128
+ }));
129
+ }
130
+
131
+ function extractContractCoverage(validationResult) {
132
+ // Look for contract compliance data in validation issues
133
+ const interfaceErrors = validationResult.issues.filter(i =>
134
+ i.message?.includes('Interface') && i.level === 'error'
135
+ );
136
+ const methodErrors = validationResult.issues.filter(i =>
137
+ i.message?.includes('Method') && i.message?.includes('missing') && i.level === 'error'
138
+ );
139
+
140
+ return {
141
+ missingInterfaces: interfaceErrors.length,
142
+ missingMethods: methodErrors.length,
143
+ hasIssues: interfaceErrors.length > 0 || methodErrors.length > 0
144
+ };
145
+ }
146
+
147
+ function buildRecapMarkdown(data) {
148
+ const { featureName, feature, tasksSummary, agentsSummary, checkpointsSummary, decisions, validationResult, contractCoverage } = data;
149
+ const now = new Date().toISOString().split('T')[0];
150
+
151
+ let md = `# Recap: ${featureName}\n\n`;
152
+ md += `> Generated: ${now} | Phase: ${feature.phase} | Status: ${feature.status}\n\n`;
153
+ md += `---\n\n`;
154
+
155
+ // Progress
156
+ md += `## Progress\n\n`;
157
+ md += `| Metric | Value |\n|--------|-------|\n`;
158
+ md += `| Tasks completed | ${tasksSummary.completed}/${tasksSummary.total} (${tasksSummary.percentage}%) |\n`;
159
+ md += `| Tasks pending | ${tasksSummary.pending} |\n`;
160
+ md += `| Tasks in progress | ${tasksSummary.inProgress} |\n`;
161
+ md += `| Active agents | ${agentsSummary.length} |\n`;
162
+ md += `| Checkpoints | ${checkpointsSummary.length} |\n\n`;
163
+
164
+ // Validation Results
165
+ md += `## Validation Results\n\n`;
166
+ if (validationResult.passed) {
167
+ md += `**Status: PASSED** (${validationResult.warnings} warnings)\n\n`;
168
+ } else {
169
+ md += `**Status: FAILED** (${validationResult.errors} errors, ${validationResult.warnings} warnings)\n\n`;
170
+ if (validationResult.issues.length > 0) {
171
+ md += `| Level | Issue |\n|-------|-------|\n`;
172
+ for (const issue of validationResult.issues.slice(0, 15)) {
173
+ md += `| ${issue.level} | ${issue.message} |\n`;
174
+ }
175
+ md += `\n`;
176
+ }
177
+ md += `> **Note:** Errors above are from morph-spec static validators (may include false positives).\n`;
178
+ md += `> Verify actual build status with the commands in the section below.\n\n`;
179
+ }
180
+
181
+ // Score Breakdown (when rubric scoring is available)
182
+ if (validationResult.scores) {
183
+ const scores = validationResult.scores;
184
+ md += `### Quality Score: ${scores.score}/10\n\n`;
185
+ md += `| Category | Score | Weight | Status |\n|----------|-------|--------|--------|\n`;
186
+ const approveThreshold = scores.approveThreshold ?? 8.0;
187
+ for (const [category, data] of Object.entries(scores.breakdown)) {
188
+ if (category === '_unmapped') continue;
189
+ const pct = Math.round(data.weight * 100);
190
+ const status = data.score >= approveThreshold ? 'Pass' : data.score >= 6 ? 'Needs work' : 'Critical';
191
+ md += `| ${category} | ${Math.round(data.score * 10) / 10}/10 | ${pct}% | ${status} |\n`;
192
+ }
193
+ md += `\n`;
194
+ }
195
+
196
+ // Suggested Quality Verification
197
+ const agents = agentsSummary;
198
+ const hasNextjs = agents.some(a => ['nextjs-expert', 'css-specialist', 'ui-designer'].includes(a));
199
+ const hasDotnet = agents.some(a => ['dotnet-senior', 'ef-modeler'].includes(a));
200
+ if (hasNextjs || hasDotnet) {
201
+ md += `## Suggested Quality Verification\n\n`;
202
+ if (hasNextjs) {
203
+ md += `**Next.js / TypeScript:**\n\`\`\`bash\nnpx tsc --noEmit\nnpx eslint . --max-warnings 0\n\`\`\`\n\n`;
204
+ }
205
+ if (hasDotnet) {
206
+ md += `**.NET:**\n\`\`\`bash\ndotnet build\ndotnet test\n\`\`\`\n\n`;
207
+ }
208
+ }
209
+
210
+ // Contract Coverage
211
+ if (contractCoverage) {
212
+ md += `## Contract Compliance\n\n`;
213
+ if (contractCoverage.hasIssues) {
214
+ md += `- Missing interfaces: ${contractCoverage.missingInterfaces}\n`;
215
+ md += `- Missing methods: ${contractCoverage.missingMethods}\n\n`;
216
+ } else {
217
+ md += `All contracts implemented.\n\n`;
218
+ }
219
+ }
220
+
221
+ // Decisions
222
+ if (decisions.length > 0) {
223
+ md += `## Decisions Applied\n\n`;
224
+ for (const d of decisions) {
225
+ if (d.title) {
226
+ md += `- **${d.title}**: ${d.decision.slice(0, 100)}${d.decision.length > 100 ? '...' : ''}\n`;
227
+ }
228
+ }
229
+ md += `\n`;
230
+ }
231
+
232
+ // Agents
233
+ if (agentsSummary.length > 0) {
234
+ md += `## Active Agents\n\n`;
235
+ md += agentsSummary.map(a => `- ${a}`).join('\n');
236
+ md += `\n\n`;
237
+ }
238
+
239
+ // Checkpoints
240
+ if (checkpointsSummary.length > 0) {
241
+ md += `## Checkpoints\n\n`;
242
+ for (const cp of checkpointsSummary) {
243
+ md += `- ${cp.timestamp}: ${cp.note}\n`;
244
+ }
245
+ md += `\n`;
246
+ }
247
+
248
+ // Tasks Detail
249
+ if (tasksSummary.items.length > 0) {
250
+ const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
251
+ md += `## Tasks\n\n`;
252
+ if (anyNotes) {
253
+ md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
254
+ for (const task of tasksSummary.items) {
255
+ const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
256
+ const noteCount = task.notes?.length || 0;
257
+ const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
258
+ md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
259
+ }
260
+ } else {
261
+ md += `| ID | Title | Status |\n|----|-------|--------|\n`;
262
+ for (const task of tasksSummary.items) {
263
+ const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
264
+ md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
265
+ }
266
+ }
267
+ md += `\n`;
268
+ }
269
+
270
+ // Deviations from Plan
271
+ const deviations = tasksSummary.items
272
+ .filter(t => t.notes?.some(n => n.type === 'deviation'))
273
+ .flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
274
+ if (deviations.length > 0) {
275
+ md += `## Deviations from Plan\n\n`;
276
+ for (const d of deviations) {
277
+ md += `- **${d.taskId}**: ${d.text}\n`;
278
+ }
279
+ md += `\n`;
280
+ }
281
+
282
+ md += `---\n\n*Generated by MORPH-SPEC Recap Generator*\n`;
283
+
284
+ return md;
285
+ }