@polymorphism-tech/morph-spec 4.10.9 → 4.10.10

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