@polymorphism-tech/morph-spec 4.10.9 → 4.11.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 (145) hide show
  1. package/CLAUDE.md +3 -3
  2. package/LICENSE +71 -71
  3. package/README.md +5 -11
  4. package/bin/morph-spec.js +23 -1
  5. package/bin/task-manager.js +5 -5
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +1 -1
  8. package/docs/QUICKSTART.md +2 -8
  9. package/framework/agents/README.md +14 -14
  10. package/framework/agents/architecture/standards-architect.md +159 -159
  11. package/framework/agents/frontend/css-specialist.md +1 -1
  12. package/framework/agents/infrastructure/azure-architect.md +149 -149
  13. package/framework/agents/infrastructure/azure-deploy-specialist.md +4 -4
  14. package/framework/agents/infrastructure/infra-architect.md +48 -48
  15. package/framework/commands/morph-apply.md +18 -18
  16. package/framework/commands/morph-preflight.md +3 -3
  17. package/framework/commands/morph-proposal.md +15 -15
  18. package/framework/commands/morph-status.md +3 -3
  19. package/framework/commands/morph-troubleshoot.md +1 -1
  20. package/framework/hooks/README.md +204 -204
  21. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +2 -2
  22. package/framework/hooks/claude-code/session-start/inject-morph-context.js +1 -1
  23. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +3 -3
  24. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +2 -2
  25. package/framework/hooks/git/pre-commit/agents.sh +1 -1
  26. package/framework/hooks/shared/phase-utils.js +139 -139
  27. package/framework/rules/csharp-standards.md +13 -13
  28. package/framework/rules/frontend-standards.md +13 -13
  29. package/framework/rules/infrastructure-standards.md +16 -16
  30. package/framework/rules/testing-standards.md +14 -14
  31. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +8 -8
  32. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +3 -3
  33. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  34. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  35. package/framework/skills/level-0-meta/morph-init/SKILL.md +11 -11
  36. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +3 -3
  37. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +1 -1
  38. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +4 -4
  39. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +25 -25
  40. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +8 -8
  41. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +7 -7
  42. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +4 -4
  43. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +23 -23
  44. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +1 -1
  45. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +38 -0
  46. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +29 -29
  47. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +4 -4
  48. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +9 -9
  49. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +17 -17
  50. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +14 -0
  51. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +12 -12
  52. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +16 -16
  53. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +5 -5
  54. package/framework/standards/STANDARDS.json +2338 -2041
  55. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  56. package/framework/standards/infrastructure/supabase/supabase-custom-jwt.md +206 -0
  57. package/framework/templates/.idea/morph-templates.xml +92 -0
  58. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  59. package/framework/templates/README.md +814 -814
  60. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  61. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  62. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  63. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  64. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  65. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  66. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  67. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  68. package/framework/templates/code/dotnet/test.cs +239 -239
  69. package/framework/templates/code/sql/rls-policy.sql +57 -57
  70. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  71. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  72. package/framework/templates/code/typescript/contracts.ts +168 -168
  73. package/framework/templates/context/CONTEXT.md +181 -181
  74. package/framework/templates/docs/proposal.md +182 -182
  75. package/framework/templates/docs/user-stories.md +34 -34
  76. package/framework/templates/examples/design-system-examples.md +357 -357
  77. package/framework/templates/examples/spec-examples.md +90 -90
  78. package/framework/templates/feature/decisions.md +187 -187
  79. package/framework/templates/feature/recap.md +146 -146
  80. package/framework/templates/feature/tasks.md +199 -199
  81. package/framework/templates/infrastructure/azure/README.md +286 -286
  82. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  83. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  84. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  85. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  86. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  87. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  88. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  89. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  90. package/framework/workflows/README.md +18 -18
  91. package/framework/workflows/docs/enforcement-pipeline.md +1 -1
  92. package/framework/workflows/docs/full-morph.md +2 -2
  93. package/package.json +1 -1
  94. package/src/commands/agents/dispatch-agents.js +17 -0
  95. package/src/commands/agents/index.js +13 -13
  96. package/src/commands/agents/project-metrics.js +74 -0
  97. package/src/commands/project/doctor.js +3 -4
  98. package/src/commands/project/install-plugin-cmd.js +28 -28
  99. package/src/commands/project/setup-infra-cmd.js +12 -12
  100. package/src/commands/project/status.js +8 -1
  101. package/src/commands/scope/escalate.js +5 -3
  102. package/src/commands/standards/context-budget.js +89 -0
  103. package/src/commands/tasks/index.js +4 -4
  104. package/src/commands/templates/index.js +5 -5
  105. package/src/commands/utils/index.js +3 -3
  106. package/src/commands/validation/index.js +4 -4
  107. package/src/commands/validation/validate-feature.js +32 -3
  108. package/src/core/state/phase-state-machine.js +217 -217
  109. package/src/lib/agents/metrics-aggregator.js +56 -0
  110. package/src/lib/generators/design-system-generator.js +298 -298
  111. package/src/lib/generators/index.js +10 -10
  112. package/src/lib/generators/metadata-extractor.js +387 -387
  113. package/src/lib/stack-filter.js +65 -65
  114. package/src/lib/standards/digest-builder.js +57 -3
  115. package/src/lib/standards/index.js +7 -7
  116. package/src/lib/standards/standards-context-injector.js +302 -302
  117. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  118. package/src/lib/validators/architecture/index.js +7 -7
  119. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  120. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  121. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  122. package/src/lib/validators/blazor/index.js +9 -9
  123. package/src/lib/validators/content/content-validator.js +287 -287
  124. package/src/lib/validators/content/index.js +7 -7
  125. package/src/lib/validators/contracts/index.js +7 -7
  126. package/src/lib/validators/css/css-validator.js +352 -352
  127. package/src/lib/validators/css/index.js +7 -7
  128. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  129. package/src/lib/validators/design-system/index.js +7 -7
  130. package/src/lib/validators/nextjs/index.js +6 -6
  131. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  132. package/src/lib/validators/packages/index.js +7 -7
  133. package/src/lib/validators/packages/package-validator.js +360 -360
  134. package/src/lib/validators/shared/index.js +12 -12
  135. package/src/lib/validators/shared/issue-counter.js +18 -18
  136. package/src/lib/validators/shared/result-formatter.js +124 -124
  137. package/src/lib/validators/ui/index.js +7 -7
  138. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  139. package/src/scripts/global-install.js +112 -58
  140. package/src/scripts/install-plugin.js +126 -126
  141. package/src/utils/claude-md-injector.js +90 -90
  142. package/src/utils/file-copier.js +200 -200
  143. package/src/utils/logger.js +32 -32
  144. package/src/utils/skills-installer.js +178 -178
  145. package/src/utils/version-checker.js +169 -175
@@ -1,387 +1,387 @@
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
- 'blazor': 'blazor',
121
- 'react': 'react',
122
- 'nextjs': 'nextjs',
123
- 'next.js': 'nextjs',
124
- 'azure': 'azure',
125
- 'sql': 'database',
126
- 'entity framework': 'ef-core',
127
- 'hangfire': 'background-jobs',
128
- 'signalr': 'realtime',
129
- 'docker': 'container',
130
- 'bicep': 'infrastructure-as-code',
131
- 'api': 'api',
132
- 'rest': 'rest-api',
133
- 'authentication': 'auth',
134
- 'authorization': 'auth'
135
- };
136
-
137
- Object.entries(techKeywords).forEach(([keyword, tag]) => {
138
- if (lowerContent.includes(keyword)) {
139
- tags.add(tag);
140
- }
141
- });
142
-
143
- return Array.from(tags).slice(0, 8); // Limit to 8 tags
144
- }
145
-
146
- /**
147
- * Extract spec.md metadata
148
- * @param {string} specPath - Path to spec.md file
149
- * @returns {Object} Spec metadata
150
- */
151
- export function extractSpecMetadata(specPath) {
152
- if (!existsSync(specPath)) {
153
- return {
154
- error: 'Spec file not found',
155
- path: specPath
156
- };
157
- }
158
-
159
- try {
160
- const content = readFileSync(specPath, 'utf8');
161
-
162
- return {
163
- id: extractField(content, 'ID') || extractField(content, 'Feature'),
164
- status: extractField(content, 'Status'),
165
- complexity: extractField(content, 'Complexity'),
166
- estimatedCost: extractField(content, 'Estimated Cost') || extractField(content, 'Cost'),
167
-
168
- summary: {
169
- problem: extractFirstParagraph(content, '## Overview') ||
170
- extractFirstParagraph(content, '## Problem Statement'),
171
- solution: extractFirstParagraph(content, '## Proposed Solution') ||
172
- extractFirstParagraph(content, '## Solution'),
173
- tags: autoGenerateTags(content)
174
- },
175
-
176
- requirements: extractBulletPoints(content, '## Requirements').slice(0, 5),
177
- dataModel: extractTableOrList(content, '## Data Model').slice(0, 5),
178
- apiContracts: extractTableOrList(content, '## API Contracts').slice(0, 5),
179
-
180
- stats: {
181
- length: content.length,
182
- sections: (content.match(/^## /gm) || []).length,
183
- codeBlocks: (content.match(/```/g) || []).length / 2
184
- },
185
-
186
- generatedAt: new Date().toISOString(),
187
- source: specPath
188
- };
189
- } catch (error) {
190
- return {
191
- error: error.message,
192
- path: specPath
193
- };
194
- }
195
- }
196
-
197
- /**
198
- * Extract proposal.md metadata
199
- * @param {string} proposalPath - Path to proposal.md file
200
- * @returns {Object} Proposal metadata
201
- */
202
- export function extractProposalMetadata(proposalPath) {
203
- if (!existsSync(proposalPath)) {
204
- return {
205
- error: 'Proposal file not found',
206
- path: proposalPath
207
- };
208
- }
209
-
210
- try {
211
- const content = readFileSync(proposalPath, 'utf8');
212
-
213
- return {
214
- id: extractField(content, 'ID') || extractField(content, 'Feature'),
215
- status: extractField(content, 'Status'),
216
-
217
- summary: {
218
- problem: extractFirstParagraph(content, '## Problem Statement'),
219
- solution: extractFirstParagraph(content, '## Proposed Solution'),
220
- tags: autoGenerateTags(content)
221
- },
222
-
223
- successMetrics: extractBulletPoints(content, '## Success Metrics').slice(0, 5),
224
-
225
- generatedAt: new Date().toISOString(),
226
- source: proposalPath
227
- };
228
- } catch (error) {
229
- return {
230
- error: error.message,
231
- path: proposalPath
232
- };
233
- }
234
- }
235
-
236
- /**
237
- * Extract decisions.md metadata (ADRs)
238
- * @param {string} decisionsPath - Path to decisions.md file
239
- * @returns {Object} Decisions metadata
240
- */
241
- export function extractDecisionsMetadata(decisionsPath) {
242
- if (!existsSync(decisionsPath)) {
243
- return {
244
- error: 'Decisions file not found',
245
- path: decisionsPath
246
- };
247
- }
248
-
249
- try {
250
- const content = readFileSync(decisionsPath, 'utf8');
251
- const decisions = [];
252
-
253
- // Match ADR pattern: ### ADR-001: Title
254
- const adrRegex = /### (ADR-\d+): (.+?)\n([\s\S]*?)(?=### ADR-|\n## |$)/g;
255
- let match;
256
-
257
- while ((match = adrRegex.exec(content)) !== null) {
258
- const [, id, title, body] = match;
259
-
260
- decisions.push({
261
- id,
262
- title: title.trim(),
263
- summary: body.trim().substring(0, 200), // First 200 chars
264
- fullPath: `${decisionsPath}#${id.toLowerCase()}`
265
- });
266
- }
267
-
268
- return {
269
- decisions,
270
- total: decisions.length,
271
- generatedAt: new Date().toISOString(),
272
- source: decisionsPath
273
- };
274
- } catch (error) {
275
- return {
276
- error: error.message,
277
- path: decisionsPath
278
- };
279
- }
280
- }
281
-
282
- /**
283
- * Extract tasks.json metadata (summary only)
284
- * @param {string} tasksPath - Path to tasks.json file
285
- * @returns {Object} Tasks metadata
286
- */
287
- export function extractTasksMetadata(tasksPath) {
288
- if (!existsSync(tasksPath)) {
289
- return {
290
- error: 'Tasks file not found',
291
- path: tasksPath
292
- };
293
- }
294
-
295
- try {
296
- const content = readFileSync(tasksPath, 'utf8');
297
- const tasks = JSON.parse(content);
298
-
299
- const regularTasks = tasks.tasks?.filter(t => t.id?.startsWith('T')) || [];
300
- const checkpoints = tasks.tasks?.filter(t => t.id?.startsWith('CHECKPOINT')) || [];
301
-
302
- return {
303
- total: tasks.tasks?.length || 0,
304
- regularTasks: regularTasks.length,
305
- checkpoints: checkpoints.length,
306
-
307
- categories: tasks.categories || {},
308
-
309
- byStatus: {
310
- pending: regularTasks.filter(t => t.status === 'pending').length,
311
- inProgress: regularTasks.filter(t => t.status === 'in_progress').length,
312
- done: regularTasks.filter(t => t.status === 'done').length,
313
- blocked: regularTasks.filter(t => t.status === 'blocked').length
314
- },
315
-
316
- estimatedTime: regularTasks.reduce((sum, t) => sum + (t.estimatedMinutes || 0), 0),
317
-
318
- nextTask: regularTasks.find(t => t.status === 'pending' &&
319
- (!t.dependencies || t.dependencies.length === 0)),
320
-
321
- generatedAt: new Date().toISOString(),
322
- source: tasksPath
323
- };
324
- } catch (error) {
325
- return {
326
- error: error.message,
327
- path: tasksPath
328
- };
329
- }
330
- }
331
-
332
- /**
333
- * Extract comprehensive feature metadata
334
- * @param {Object} featureState - Feature state from state.json
335
- * @returns {Object} Complete feature metadata
336
- */
337
- export function extractFeatureMetadata(featureState) {
338
- const metadata = {
339
- version: "1.0.0",
340
- feature: featureState.name || 'unknown',
341
- status: featureState.status,
342
- phase: featureState.phase,
343
- workflow: featureState.workflow,
344
-
345
- agents: featureState.activeAgents || [],
346
- checkpoints: featureState.checkpoints || [],
347
-
348
- outputs: {},
349
- quickLinks: {}
350
- };
351
-
352
- // Extract metadata from each output
353
- if (featureState.outputs?.proposal?.created) {
354
- metadata.outputs.proposal = extractProposalMetadata(featureState.outputs.proposal.path);
355
- metadata.quickLinks.proposal = featureState.outputs.proposal.path;
356
- }
357
-
358
- if (featureState.outputs?.spec?.created) {
359
- metadata.outputs.spec = extractSpecMetadata(featureState.outputs.spec.path);
360
- metadata.quickLinks.spec = featureState.outputs.spec.path;
361
- }
362
-
363
- if (featureState.outputs?.decisions?.created) {
364
- metadata.outputs.decisions = extractDecisionsMetadata(featureState.outputs.decisions.path);
365
- metadata.quickLinks.decisions = featureState.outputs.decisions.path;
366
- }
367
-
368
- if (featureState.outputs?.tasks?.created) {
369
- metadata.outputs.tasks = extractTasksMetadata(featureState.outputs.tasks.path);
370
- metadata.quickLinks.tasks = featureState.outputs.tasks.path;
371
- }
372
-
373
- // Calculate progress
374
- if (featureState.tasks) {
375
- metadata.progress = {
376
- total: featureState.tasks.total || 0,
377
- completed: featureState.tasks.completed || 0,
378
- percentage: featureState.tasks.total > 0
379
- ? Math.round((featureState.tasks.completed / featureState.tasks.total) * 100)
380
- : 0
381
- };
382
- }
383
-
384
- metadata.generatedAt = new Date().toISOString();
385
-
386
- return metadata;
387
- }
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
+ 'blazor': 'blazor',
121
+ 'react': 'react',
122
+ 'nextjs': 'nextjs',
123
+ 'next.js': 'nextjs',
124
+ 'azure': 'azure',
125
+ 'sql': 'database',
126
+ 'entity framework': 'ef-core',
127
+ 'hangfire': 'background-jobs',
128
+ 'signalr': 'realtime',
129
+ 'docker': 'container',
130
+ 'bicep': 'infrastructure-as-code',
131
+ 'api': 'api',
132
+ 'rest': 'rest-api',
133
+ 'authentication': 'auth',
134
+ 'authorization': 'auth'
135
+ };
136
+
137
+ Object.entries(techKeywords).forEach(([keyword, tag]) => {
138
+ if (lowerContent.includes(keyword)) {
139
+ tags.add(tag);
140
+ }
141
+ });
142
+
143
+ return Array.from(tags).slice(0, 8); // Limit to 8 tags
144
+ }
145
+
146
+ /**
147
+ * Extract spec.md metadata
148
+ * @param {string} specPath - Path to spec.md file
149
+ * @returns {Object} Spec metadata
150
+ */
151
+ export function extractSpecMetadata(specPath) {
152
+ if (!existsSync(specPath)) {
153
+ return {
154
+ error: 'Spec file not found',
155
+ path: specPath
156
+ };
157
+ }
158
+
159
+ try {
160
+ const content = readFileSync(specPath, 'utf8');
161
+
162
+ return {
163
+ id: extractField(content, 'ID') || extractField(content, 'Feature'),
164
+ status: extractField(content, 'Status'),
165
+ complexity: extractField(content, 'Complexity'),
166
+ estimatedCost: extractField(content, 'Estimated Cost') || extractField(content, 'Cost'),
167
+
168
+ summary: {
169
+ problem: extractFirstParagraph(content, '## Overview') ||
170
+ extractFirstParagraph(content, '## Problem Statement'),
171
+ solution: extractFirstParagraph(content, '## Proposed Solution') ||
172
+ extractFirstParagraph(content, '## Solution'),
173
+ tags: autoGenerateTags(content)
174
+ },
175
+
176
+ requirements: extractBulletPoints(content, '## Requirements').slice(0, 5),
177
+ dataModel: extractTableOrList(content, '## Data Model').slice(0, 5),
178
+ apiContracts: extractTableOrList(content, '## API Contracts').slice(0, 5),
179
+
180
+ stats: {
181
+ length: content.length,
182
+ sections: (content.match(/^## /gm) || []).length,
183
+ codeBlocks: (content.match(/```/g) || []).length / 2
184
+ },
185
+
186
+ generatedAt: new Date().toISOString(),
187
+ source: specPath
188
+ };
189
+ } catch (error) {
190
+ return {
191
+ error: error.message,
192
+ path: specPath
193
+ };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Extract proposal.md metadata
199
+ * @param {string} proposalPath - Path to proposal.md file
200
+ * @returns {Object} Proposal metadata
201
+ */
202
+ export function extractProposalMetadata(proposalPath) {
203
+ if (!existsSync(proposalPath)) {
204
+ return {
205
+ error: 'Proposal file not found',
206
+ path: proposalPath
207
+ };
208
+ }
209
+
210
+ try {
211
+ const content = readFileSync(proposalPath, 'utf8');
212
+
213
+ return {
214
+ id: extractField(content, 'ID') || extractField(content, 'Feature'),
215
+ status: extractField(content, 'Status'),
216
+
217
+ summary: {
218
+ problem: extractFirstParagraph(content, '## Problem Statement'),
219
+ solution: extractFirstParagraph(content, '## Proposed Solution'),
220
+ tags: autoGenerateTags(content)
221
+ },
222
+
223
+ successMetrics: extractBulletPoints(content, '## Success Metrics').slice(0, 5),
224
+
225
+ generatedAt: new Date().toISOString(),
226
+ source: proposalPath
227
+ };
228
+ } catch (error) {
229
+ return {
230
+ error: error.message,
231
+ path: proposalPath
232
+ };
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Extract decisions.md metadata (ADRs)
238
+ * @param {string} decisionsPath - Path to decisions.md file
239
+ * @returns {Object} Decisions metadata
240
+ */
241
+ export function extractDecisionsMetadata(decisionsPath) {
242
+ if (!existsSync(decisionsPath)) {
243
+ return {
244
+ error: 'Decisions file not found',
245
+ path: decisionsPath
246
+ };
247
+ }
248
+
249
+ try {
250
+ const content = readFileSync(decisionsPath, 'utf8');
251
+ const decisions = [];
252
+
253
+ // Match ADR pattern: ### ADR-001: Title
254
+ const adrRegex = /### (ADR-\d+): (.+?)\n([\s\S]*?)(?=### ADR-|\n## |$)/g;
255
+ let match;
256
+
257
+ while ((match = adrRegex.exec(content)) !== null) {
258
+ const [, id, title, body] = match;
259
+
260
+ decisions.push({
261
+ id,
262
+ title: title.trim(),
263
+ summary: body.trim().substring(0, 200), // First 200 chars
264
+ fullPath: `${decisionsPath}#${id.toLowerCase()}`
265
+ });
266
+ }
267
+
268
+ return {
269
+ decisions,
270
+ total: decisions.length,
271
+ generatedAt: new Date().toISOString(),
272
+ source: decisionsPath
273
+ };
274
+ } catch (error) {
275
+ return {
276
+ error: error.message,
277
+ path: decisionsPath
278
+ };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Extract tasks.json metadata (summary only)
284
+ * @param {string} tasksPath - Path to tasks.json file
285
+ * @returns {Object} Tasks metadata
286
+ */
287
+ export function extractTasksMetadata(tasksPath) {
288
+ if (!existsSync(tasksPath)) {
289
+ return {
290
+ error: 'Tasks file not found',
291
+ path: tasksPath
292
+ };
293
+ }
294
+
295
+ try {
296
+ const content = readFileSync(tasksPath, 'utf8');
297
+ const tasks = JSON.parse(content);
298
+
299
+ const regularTasks = tasks.tasks?.filter(t => t.id?.startsWith('T')) || [];
300
+ const checkpoints = tasks.tasks?.filter(t => t.id?.startsWith('CHECKPOINT')) || [];
301
+
302
+ return {
303
+ total: tasks.tasks?.length || 0,
304
+ regularTasks: regularTasks.length,
305
+ checkpoints: checkpoints.length,
306
+
307
+ categories: tasks.categories || {},
308
+
309
+ byStatus: {
310
+ pending: regularTasks.filter(t => t.status === 'pending').length,
311
+ inProgress: regularTasks.filter(t => t.status === 'in_progress').length,
312
+ done: regularTasks.filter(t => t.status === 'done').length,
313
+ blocked: regularTasks.filter(t => t.status === 'blocked').length
314
+ },
315
+
316
+ estimatedTime: regularTasks.reduce((sum, t) => sum + (t.estimatedMinutes || 0), 0),
317
+
318
+ nextTask: regularTasks.find(t => t.status === 'pending' &&
319
+ (!t.dependencies || t.dependencies.length === 0)),
320
+
321
+ generatedAt: new Date().toISOString(),
322
+ source: tasksPath
323
+ };
324
+ } catch (error) {
325
+ return {
326
+ error: error.message,
327
+ path: tasksPath
328
+ };
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Extract comprehensive feature metadata
334
+ * @param {Object} featureState - Feature state from state.json
335
+ * @returns {Object} Complete feature metadata
336
+ */
337
+ export function extractFeatureMetadata(featureState) {
338
+ const metadata = {
339
+ version: "1.0.0",
340
+ feature: featureState.name || 'unknown',
341
+ status: featureState.status,
342
+ phase: featureState.phase,
343
+ workflow: featureState.workflow,
344
+
345
+ agents: featureState.activeAgents || [],
346
+ checkpoints: featureState.checkpoints || [],
347
+
348
+ outputs: {},
349
+ quickLinks: {}
350
+ };
351
+
352
+ // Extract metadata from each output
353
+ if (featureState.outputs?.proposal?.created) {
354
+ metadata.outputs.proposal = extractProposalMetadata(featureState.outputs.proposal.path);
355
+ metadata.quickLinks.proposal = featureState.outputs.proposal.path;
356
+ }
357
+
358
+ if (featureState.outputs?.spec?.created) {
359
+ metadata.outputs.spec = extractSpecMetadata(featureState.outputs.spec.path);
360
+ metadata.quickLinks.spec = featureState.outputs.spec.path;
361
+ }
362
+
363
+ if (featureState.outputs?.decisions?.created) {
364
+ metadata.outputs.decisions = extractDecisionsMetadata(featureState.outputs.decisions.path);
365
+ metadata.quickLinks.decisions = featureState.outputs.decisions.path;
366
+ }
367
+
368
+ if (featureState.outputs?.tasks?.created) {
369
+ metadata.outputs.tasks = extractTasksMetadata(featureState.outputs.tasks.path);
370
+ metadata.quickLinks.tasks = featureState.outputs.tasks.path;
371
+ }
372
+
373
+ // Calculate progress
374
+ if (featureState.tasks) {
375
+ metadata.progress = {
376
+ total: featureState.tasks.total || 0,
377
+ completed: featureState.tasks.completed || 0,
378
+ percentage: featureState.tasks.total > 0
379
+ ? Math.round((featureState.tasks.completed / featureState.tasks.total) * 100)
380
+ : 0
381
+ };
382
+ }
383
+
384
+ metadata.generatedAt = new Date().toISOString();
385
+
386
+ return metadata;
387
+ }