@polymorphism-tech/morph-spec 4.10.7 → 4.10.9

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 (123) hide show
  1. package/CLAUDE.md +3 -3
  2. package/LICENSE +71 -71
  3. package/README.md +379 -379
  4. package/bin/morph-spec.js +1 -1
  5. package/claude-plugin.json +14 -14
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +206 -206
  8. package/framework/agents/README.md +14 -14
  9. package/framework/agents/architecture/standards-architect.md +159 -159
  10. package/framework/agents/frontend/blazor-builder.md +13 -0
  11. package/framework/agents/frontend/css-specialist.md +87 -0
  12. package/framework/agents/frontend/nextjs-expert.md +13 -0
  13. package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
  14. package/framework/agents/frontend/ui-ux-designer.md +80 -10
  15. package/framework/agents/infrastructure/azure-architect.md +149 -149
  16. package/framework/agents/infrastructure/infra-architect.md +48 -48
  17. package/framework/agents.json +2268 -2204
  18. package/framework/hooks/README.md +204 -204
  19. package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
  20. package/framework/hooks/shared/phase-utils.js +139 -139
  21. package/framework/hooks/shared/worktree-helpers.js +30 -8
  22. package/framework/rules/csharp-standards.md +13 -13
  23. package/framework/rules/frontend-standards.md +13 -13
  24. package/framework/rules/infrastructure-standards.md +16 -16
  25. package/framework/rules/testing-standards.md +14 -14
  26. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
  27. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  28. package/framework/skills/level-0-meta/morph-init/SKILL.md +136 -3
  29. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
  30. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
  31. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
  32. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -500
  33. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
  34. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -262
  35. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
  36. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +457 -323
  37. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  38. package/framework/standards/STANDARDS.json +2040 -2040
  39. package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
  40. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  41. package/framework/templates/README.md +814 -814
  42. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  43. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  44. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  45. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  46. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  47. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  48. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  49. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  50. package/framework/templates/code/dotnet/test.cs +239 -239
  51. package/framework/templates/code/sql/rls-policy.sql +57 -57
  52. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  53. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  54. package/framework/templates/code/typescript/contracts.ts +168 -168
  55. package/framework/templates/context/CONTEXT.md +181 -181
  56. package/framework/templates/context/design-system.md +211 -0
  57. package/framework/templates/docs/proposal.md +182 -182
  58. package/framework/templates/docs/user-stories.md +34 -34
  59. package/framework/templates/examples/design-system-examples.md +357 -357
  60. package/framework/templates/examples/spec-examples.md +90 -90
  61. package/framework/templates/feature/decisions.md +187 -187
  62. package/framework/templates/feature/recap.md +146 -146
  63. package/framework/templates/feature/tasks.md +199 -199
  64. package/framework/templates/infrastructure/azure/README.md +286 -286
  65. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  66. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  67. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  68. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  69. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  70. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  71. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  72. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  73. package/package.json +82 -82
  74. package/src/commands/agents/index.js +13 -13
  75. package/src/commands/project/install-plugin-cmd.js +28 -28
  76. package/src/commands/project/setup-infra-cmd.js +12 -12
  77. package/src/commands/project/worktree.js +58 -2
  78. package/src/commands/tasks/index.js +4 -4
  79. package/src/commands/templates/index.js +5 -5
  80. package/src/commands/utils/index.js +3 -3
  81. package/src/commands/validation/index.js +4 -4
  82. package/src/core/state/phase-state-machine.js +217 -217
  83. package/src/lib/detectors/design-system-detector.js +73 -31
  84. package/src/lib/generators/design-system-generator.js +298 -298
  85. package/src/lib/generators/index.js +10 -10
  86. package/src/lib/generators/metadata-extractor.js +387 -387
  87. package/src/lib/stack-filter.js +65 -65
  88. package/src/lib/standards/index.js +7 -7
  89. package/src/lib/standards/standards-context-injector.js +302 -302
  90. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  91. package/src/lib/validators/architecture/index.js +7 -7
  92. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  93. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  94. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  95. package/src/lib/validators/blazor/index.js +9 -9
  96. package/src/lib/validators/content/content-validator.js +287 -287
  97. package/src/lib/validators/content/index.js +7 -7
  98. package/src/lib/validators/contracts/index.js +7 -7
  99. package/src/lib/validators/css/css-validator.js +352 -352
  100. package/src/lib/validators/css/index.js +7 -7
  101. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  102. package/src/lib/validators/design-system/index.js +7 -7
  103. package/src/lib/validators/nextjs/index.js +6 -6
  104. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  105. package/src/lib/validators/packages/index.js +7 -7
  106. package/src/lib/validators/packages/package-validator.js +360 -360
  107. package/src/lib/validators/shared/index.js +12 -12
  108. package/src/lib/validators/shared/issue-counter.js +18 -18
  109. package/src/lib/validators/shared/result-formatter.js +124 -124
  110. package/src/lib/validators/ui/index.js +7 -7
  111. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  112. package/src/scripts/global-install.js +58 -58
  113. package/src/scripts/install-plugin.js +126 -126
  114. package/src/utils/claude-md-injector.js +90 -90
  115. package/src/utils/file-copier.js +200 -196
  116. package/src/utils/hooks-installer.js +556 -494
  117. package/src/utils/logger.js +32 -32
  118. package/src/utils/skills-installer.js +178 -178
  119. package/src/utils/version-checker.js +175 -175
  120. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  121. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  122. package/framework/templates/.idea/morph-templates.xml +0 -92
  123. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -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
+ }