@polymorphism-tech/morph-spec 8.2.0 → 8.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 (54) hide show
  1. package/README.md +70 -99
  2. package/bin/morph-spec.js +13 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +76 -135
  5. package/docs/QUICKSTART.md +35 -51
  6. package/framework/CLAUDE.md +3 -2
  7. package/framework/MORPH.md +20 -2
  8. package/framework/agents.json +3 -1
  9. package/framework/hooks/README.md +3 -0
  10. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -1
  11. package/framework/hooks/claude-code/core/telemetry-log.js +102 -0
  12. package/framework/hooks/shared/build-check.js +8 -1
  13. package/framework/hooks/shared/payload-utils.js +16 -2
  14. package/framework/hooks/shared/phase-utils.js +2 -0
  15. package/framework/hooks/shared/telemetry-logger.js +75 -0
  16. package/framework/rules/ai-agents-standards.md +1 -0
  17. package/framework/schemas/feature.schema.json +0 -5
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  20. package/framework/skills/core/morph-apply/SKILL.md +12 -7
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  23. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  24. package/framework/skills/core/morph-implement/SKILL.md +22 -10
  25. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  26. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  27. package/framework/skills/core/morph-review/SKILL.md +19 -12
  28. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  29. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  30. package/framework/standards/STANDARDS.json +43 -1
  31. package/framework/standards/ai-agents/observability-patterns.md +156 -0
  32. package/framework/standards/ai-agents/production.md +3 -47
  33. package/framework/templates/REGISTRY.json +0 -93
  34. package/package.json +1 -1
  35. package/src/commands/observability/telemetry.js +75 -0
  36. package/src/commands/project/worktree.js +150 -36
  37. package/src/commands/state/score.js +13 -2
  38. package/src/commands/tasks/dag.js +11 -3
  39. package/src/core/paths/output-schema.js +28 -0
  40. package/src/core/state/state-manager.js +0 -128
  41. package/src/dashboard/app/pages/LiveState.jsx +0 -43
  42. package/src/lib/detectors/design-system-detector.js +0 -43
  43. package/src/lib/generators/index.js +0 -1
  44. package/src/lib/stack/stack-profile.js +40 -9
  45. package/src/lib/tasks/tasks-json-parser.js +21 -0
  46. package/src/lib/tasks/test-runner.js +26 -4
  47. package/src/lib/telemetry/telemetry-reader.js +103 -0
  48. package/src/lib/validators/validation-runner.js +13 -74
  49. package/src/utils/hooks-installer.js +27 -3
  50. package/framework/templates/context/CONTEXT-FEATURE.md +0 -269
  51. package/framework/templates/context/CONTEXT.md +0 -181
  52. package/src/lib/agents/metrics-aggregator.js +0 -56
  53. package/src/lib/agents/metrics-calculator.js +0 -125
  54. package/src/lib/generators/metadata-extractor.js +0 -385
@@ -1,385 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs';
2
-
3
- /**
4
- * Metadata Extractor - Parse markdown documents into structured JSON
5
- *
6
- * Generates quick summaries for LLM to parse, reducing token usage
7
- * by 60-80% compared to reading full markdown files.
8
- */
9
-
10
- /**
11
- * Extract a field value from markdown content (table or colon format)
12
- * @param {string} content - Markdown content
13
- * @param {string} fieldName - Field name to extract
14
- * @returns {string|null} Field value or null
15
- */
16
- function extractField(content, fieldName) {
17
- // Try table row format first: | FieldName | Value |
18
- const tableRegex = new RegExp(`\\|\\s*${fieldName}\\s*\\|\\s*(.+?)\\s*\\|`, 'i');
19
- const tableMatch = content.match(tableRegex);
20
- if (tableMatch) {
21
- return tableMatch[1].trim();
22
- }
23
-
24
- // Try colon format: FieldName: Value
25
- const colonRegex = new RegExp(`^${fieldName}\\s*:\\s*(.+)$`, 'im');
26
- const colonMatch = content.match(colonRegex);
27
- return colonMatch ? colonMatch[1].trim() : null;
28
- }
29
-
30
- /**
31
- * Extract first paragraph from a section
32
- * @param {string} content - Markdown content
33
- * @param {string} sectionHeader - Section header (e.g., "## Overview")
34
- * @returns {string|null} First paragraph or null
35
- */
36
- function extractFirstParagraph(content, sectionHeader) {
37
- const regex = new RegExp(`${sectionHeader}\\s+([^\\n]+(?:\\n(?!##)[^\\n]+)*)`, 'i');
38
- const match = content.match(regex);
39
-
40
- if (!match) return null;
41
-
42
- // Get first paragraph (up to first blank line or next section)
43
- const text = match[1].trim();
44
- const firstPara = text.split('\n\n')[0];
45
- return firstPara.substring(0, 300); // Limit to 300 chars
46
- }
47
-
48
- /**
49
- * Extract bullet points from a section
50
- * @param {string} content - Markdown content
51
- * @param {string} sectionHeader - Section header
52
- * @returns {string[]} Array of bullet points
53
- */
54
- function extractBulletPoints(content, sectionHeader) {
55
- const regex = new RegExp(`${sectionHeader}([\\s\\S]*?)(?=\\n## |$)`, 'i');
56
- const match = content.match(regex);
57
-
58
- if (!match) return [];
59
-
60
- const sectionContent = match[1];
61
- const bullets = [];
62
-
63
- // Match lines starting with -, *, or numbered list
64
- const bulletRegex = /^[\s]*[-*][\s]+(.+)$/gm;
65
- let bulletMatch;
66
-
67
- while ((bulletMatch = bulletRegex.exec(sectionContent)) !== null) {
68
- bullets.push(bulletMatch[1].trim());
69
- }
70
-
71
- return bullets.slice(0, 10); // Limit to 10 items
72
- }
73
-
74
- /**
75
- * Extract table or list from a section
76
- * @param {string} content - Markdown content
77
- * @param {string} sectionHeader - Section header
78
- * @returns {string[]} Array of items
79
- */
80
- function extractTableOrList(content, sectionHeader) {
81
- const items = extractBulletPoints(content, sectionHeader);
82
-
83
- if (items.length > 0) return items;
84
-
85
- // Try extracting table rows
86
- const regex = new RegExp(`${sectionHeader}([\\s\\S]*?)(?=\\n## |$)`, 'i');
87
- const match = content.match(regex);
88
-
89
- if (!match) return [];
90
-
91
- const sectionContent = match[1];
92
- const tableRows = [];
93
-
94
- // Match table rows (| col1 | col2 |)
95
- const rowRegex = /^\|(.+)\|$/gm;
96
- let rowMatch;
97
-
98
- while ((rowMatch = rowRegex.exec(sectionContent)) !== null) {
99
- const row = rowMatch[1].trim();
100
- // Skip header separator rows
101
- if (!row.match(/^[-:|\s]+$/)) {
102
- tableRows.push(row);
103
- }
104
- }
105
-
106
- return tableRows.slice(0, 10); // Limit to 10 rows
107
- }
108
-
109
- /**
110
- * Auto-generate tags from content
111
- * @param {string} content - Markdown content
112
- * @returns {string[]} Array of tags
113
- */
114
- function autoGenerateTags(content) {
115
- const tags = new Set();
116
- const lowerContent = content.toLowerCase();
117
-
118
- // Technology tags
119
- const techKeywords = {
120
- 'react': 'react',
121
- 'nextjs': 'nextjs',
122
- 'next.js': 'nextjs',
123
- 'azure': 'azure',
124
- 'sql': 'database',
125
- 'entity framework': 'ef-core',
126
- 'hangfire': 'background-jobs',
127
- 'signalr': 'realtime',
128
- 'docker': 'container',
129
- 'bicep': 'infrastructure-as-code',
130
- 'api': 'api',
131
- 'rest': 'rest-api',
132
- 'authentication': 'auth',
133
- 'authorization': 'auth'
134
- };
135
-
136
- Object.entries(techKeywords).forEach(([keyword, tag]) => {
137
- if (lowerContent.includes(keyword)) {
138
- tags.add(tag);
139
- }
140
- });
141
-
142
- return Array.from(tags).slice(0, 8); // Limit to 8 tags
143
- }
144
-
145
- /**
146
- * Extract spec.md metadata
147
- * @param {string} specPath - Path to spec.md file
148
- * @returns {Object} Spec metadata
149
- */
150
- export function extractSpecMetadata(specPath) {
151
- if (!existsSync(specPath)) {
152
- return {
153
- error: 'Spec file not found',
154
- path: specPath
155
- };
156
- }
157
-
158
- try {
159
- const content = readFileSync(specPath, 'utf8');
160
-
161
- return {
162
- id: extractField(content, 'ID') || extractField(content, 'Feature'),
163
- status: extractField(content, 'Status'),
164
- complexity: extractField(content, 'Complexity'),
165
- estimatedCost: extractField(content, 'Estimated Cost') || extractField(content, 'Cost'),
166
-
167
- summary: {
168
- problem: extractFirstParagraph(content, '## Overview') ||
169
- extractFirstParagraph(content, '## Problem Statement'),
170
- solution: extractFirstParagraph(content, '## Proposed Solution') ||
171
- extractFirstParagraph(content, '## Solution'),
172
- tags: autoGenerateTags(content)
173
- },
174
-
175
- requirements: extractBulletPoints(content, '## Requirements').slice(0, 5),
176
- dataModel: extractTableOrList(content, '## Data Model').slice(0, 5),
177
- apiContracts: extractTableOrList(content, '## API Contracts').slice(0, 5),
178
-
179
- stats: {
180
- length: content.length,
181
- sections: (content.match(/^## /gm) || []).length,
182
- codeBlocks: (content.match(/```/g) || []).length / 2
183
- },
184
-
185
- generatedAt: new Date().toISOString(),
186
- source: specPath
187
- };
188
- } catch (error) {
189
- return {
190
- error: error.message,
191
- path: specPath
192
- };
193
- }
194
- }
195
-
196
- /**
197
- * Extract proposal.md metadata
198
- * @param {string} proposalPath - Path to proposal.md file
199
- * @returns {Object} Proposal metadata
200
- */
201
- export function extractProposalMetadata(proposalPath) {
202
- if (!existsSync(proposalPath)) {
203
- return {
204
- error: 'Proposal file not found',
205
- path: proposalPath
206
- };
207
- }
208
-
209
- try {
210
- const content = readFileSync(proposalPath, 'utf8');
211
-
212
- return {
213
- id: extractField(content, 'ID') || extractField(content, 'Feature'),
214
- status: extractField(content, 'Status'),
215
-
216
- summary: {
217
- problem: extractFirstParagraph(content, '## Problem Statement'),
218
- solution: extractFirstParagraph(content, '## Proposed Solution'),
219
- tags: autoGenerateTags(content)
220
- },
221
-
222
- successMetrics: extractBulletPoints(content, '## Success Metrics').slice(0, 5),
223
-
224
- generatedAt: new Date().toISOString(),
225
- source: proposalPath
226
- };
227
- } catch (error) {
228
- return {
229
- error: error.message,
230
- path: proposalPath
231
- };
232
- }
233
- }
234
-
235
- /**
236
- * Extract decisions.md metadata (ADRs)
237
- * @param {string} decisionsPath - Path to decisions.md file
238
- * @returns {Object} Decisions metadata
239
- */
240
- export function extractDecisionsMetadata(decisionsPath) {
241
- if (!existsSync(decisionsPath)) {
242
- return {
243
- error: 'Decisions file not found',
244
- path: decisionsPath
245
- };
246
- }
247
-
248
- try {
249
- const content = readFileSync(decisionsPath, 'utf8');
250
- const decisions = [];
251
-
252
- // Match ADR pattern: ### ADR-001: Title
253
- const adrRegex = /### (ADR-\d+): (.+?)\n([\s\S]*?)(?=### ADR-|\n## |$)/g;
254
- let match;
255
-
256
- while ((match = adrRegex.exec(content)) !== null) {
257
- const [, id, title, body] = match;
258
-
259
- decisions.push({
260
- id,
261
- title: title.trim(),
262
- summary: body.trim().substring(0, 200), // First 200 chars
263
- fullPath: `${decisionsPath}#${id.toLowerCase()}`
264
- });
265
- }
266
-
267
- return {
268
- decisions,
269
- total: decisions.length,
270
- generatedAt: new Date().toISOString(),
271
- source: decisionsPath
272
- };
273
- } catch (error) {
274
- return {
275
- error: error.message,
276
- path: decisionsPath
277
- };
278
- }
279
- }
280
-
281
- /**
282
- * Extract tasks.json metadata (summary only)
283
- * @param {string} tasksPath - Path to tasks.json file
284
- * @returns {Object} Tasks metadata
285
- */
286
- export function extractTasksMetadata(tasksPath) {
287
- if (!existsSync(tasksPath)) {
288
- return {
289
- error: 'Tasks file not found',
290
- path: tasksPath
291
- };
292
- }
293
-
294
- try {
295
- const content = readFileSync(tasksPath, 'utf8');
296
- const tasks = JSON.parse(content);
297
-
298
- const regularTasks = tasks.tasks?.filter(t => t.id?.startsWith('T')) || [];
299
- const checkpoints = tasks.tasks?.filter(t => t.id?.startsWith('CHECKPOINT')) || [];
300
-
301
- return {
302
- total: tasks.tasks?.length || 0,
303
- regularTasks: regularTasks.length,
304
- checkpoints: checkpoints.length,
305
-
306
- categories: tasks.categories || {},
307
-
308
- byStatus: {
309
- pending: regularTasks.filter(t => t.status === 'pending').length,
310
- inProgress: regularTasks.filter(t => t.status === 'in_progress').length,
311
- completed: regularTasks.filter(t => t.status === 'completed').length,
312
- blocked: regularTasks.filter(t => t.status === 'blocked').length
313
- },
314
-
315
- estimatedTime: regularTasks.reduce((sum, t) => sum + (t.estimatedMinutes || 0), 0),
316
-
317
- nextTask: regularTasks.find(t => t.status === 'pending' &&
318
- (!t.dependencies || t.dependencies.length === 0)),
319
-
320
- generatedAt: new Date().toISOString(),
321
- source: tasksPath
322
- };
323
- } catch (error) {
324
- return {
325
- error: error.message,
326
- path: tasksPath
327
- };
328
- }
329
- }
330
-
331
- /**
332
- * Extract comprehensive feature metadata
333
- * @param {Object} featureState - Feature state from state.json
334
- * @returns {Object} Complete feature metadata
335
- */
336
- export function extractFeatureMetadata(featureState) {
337
- const metadata = {
338
- version: "1.0.0",
339
- feature: featureState.name || 'unknown',
340
- status: featureState.status,
341
- phase: featureState.phase,
342
-
343
- agents: featureState.activeAgents || [],
344
- checkpoints: featureState.checkpoints || [],
345
-
346
- outputs: {},
347
- quickLinks: {}
348
- };
349
-
350
- // Extract metadata from each output
351
- if (featureState.outputs?.proposal?.created) {
352
- metadata.outputs.proposal = extractProposalMetadata(featureState.outputs.proposal.path);
353
- metadata.quickLinks.proposal = featureState.outputs.proposal.path;
354
- }
355
-
356
- if (featureState.outputs?.spec?.created) {
357
- metadata.outputs.spec = extractSpecMetadata(featureState.outputs.spec.path);
358
- metadata.quickLinks.spec = featureState.outputs.spec.path;
359
- }
360
-
361
- if (featureState.outputs?.decisions?.created) {
362
- metadata.outputs.decisions = extractDecisionsMetadata(featureState.outputs.decisions.path);
363
- metadata.quickLinks.decisions = featureState.outputs.decisions.path;
364
- }
365
-
366
- if (featureState.outputs?.tasks?.created) {
367
- metadata.outputs.tasks = extractTasksMetadata(featureState.outputs.tasks.path);
368
- metadata.quickLinks.tasks = featureState.outputs.tasks.path;
369
- }
370
-
371
- // Calculate progress
372
- if (featureState.tasks) {
373
- metadata.progress = {
374
- total: featureState.tasks.total || 0,
375
- completed: featureState.tasks.completed || 0,
376
- percentage: featureState.tasks.total > 0
377
- ? Math.round((featureState.tasks.completed / featureState.tasks.total) * 100)
378
- : 0
379
- };
380
- }
381
-
382
- metadata.generatedAt = new Date().toISOString();
383
-
384
- return metadata;
385
- }