@eddacraft/anvil-adapters 0.1.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 (183) hide show
  1. package/AGENTS.md +180 -0
  2. package/BMAD_ADAPTER_SPEC.md +489 -0
  3. package/LICENSE +14 -0
  4. package/README.md +500 -0
  5. package/dist/aps-markdown/adapter.d.ts +102 -0
  6. package/dist/aps-markdown/adapter.d.ts.map +1 -0
  7. package/dist/aps-markdown/adapter.js +351 -0
  8. package/dist/aps-markdown/index.d.ts +8 -0
  9. package/dist/aps-markdown/index.d.ts.map +1 -0
  10. package/dist/aps-markdown/index.js +7 -0
  11. package/dist/base/file-discovery.d.ts +63 -0
  12. package/dist/base/file-discovery.d.ts.map +1 -0
  13. package/dist/base/file-discovery.js +246 -0
  14. package/dist/base/index.d.ts +10 -0
  15. package/dist/base/index.d.ts.map +1 -0
  16. package/dist/base/index.js +9 -0
  17. package/dist/base/registry.d.ts +155 -0
  18. package/dist/base/registry.d.ts.map +1 -0
  19. package/dist/base/registry.js +227 -0
  20. package/dist/base/testing.d.ts +102 -0
  21. package/dist/base/testing.d.ts.map +1 -0
  22. package/dist/base/testing.js +221 -0
  23. package/dist/base/types.d.ts +255 -0
  24. package/dist/base/types.d.ts.map +1 -0
  25. package/dist/base/types.js +78 -0
  26. package/dist/base/utils.d.ts +127 -0
  27. package/dist/base/utils.d.ts.map +1 -0
  28. package/dist/base/utils.js +254 -0
  29. package/dist/bmad/format-adapter.d.ts +76 -0
  30. package/dist/bmad/format-adapter.d.ts.map +1 -0
  31. package/dist/bmad/format-adapter.js +186 -0
  32. package/dist/bmad/index.d.ts +12 -0
  33. package/dist/bmad/index.d.ts.map +1 -0
  34. package/dist/bmad/index.js +10 -0
  35. package/dist/bmad/parser.d.ts +12 -0
  36. package/dist/bmad/parser.d.ts.map +1 -0
  37. package/dist/bmad/parser.js +181 -0
  38. package/dist/bmad/serializer.d.ts +16 -0
  39. package/dist/bmad/serializer.d.ts.map +1 -0
  40. package/dist/bmad/serializer.js +170 -0
  41. package/dist/bmad/types.d.ts +127 -0
  42. package/dist/bmad/types.d.ts.map +1 -0
  43. package/dist/bmad/types.js +47 -0
  44. package/dist/bmad/utils.d.ts +120 -0
  45. package/dist/bmad/utils.d.ts.map +1 -0
  46. package/dist/bmad/utils.js +480 -0
  47. package/dist/common/index.d.ts +3 -0
  48. package/dist/common/index.d.ts.map +1 -0
  49. package/dist/common/index.js +2 -0
  50. package/dist/common/registry.d.ts +18 -0
  51. package/dist/common/registry.d.ts.map +1 -0
  52. package/dist/common/registry.js +58 -0
  53. package/dist/common/types.d.ts +68 -0
  54. package/dist/common/types.d.ts.map +1 -0
  55. package/dist/common/types.js +12 -0
  56. package/dist/generic/format-adapter.d.ts +64 -0
  57. package/dist/generic/format-adapter.d.ts.map +1 -0
  58. package/dist/generic/format-adapter.js +159 -0
  59. package/dist/generic/index.d.ts +10 -0
  60. package/dist/generic/index.d.ts.map +1 -0
  61. package/dist/generic/index.js +9 -0
  62. package/dist/generic/parser.d.ts +11 -0
  63. package/dist/generic/parser.d.ts.map +1 -0
  64. package/dist/generic/parser.js +106 -0
  65. package/dist/generic/serializer.d.ts +11 -0
  66. package/dist/generic/serializer.d.ts.map +1 -0
  67. package/dist/generic/serializer.js +118 -0
  68. package/dist/generic/types.d.ts +52 -0
  69. package/dist/generic/types.d.ts.map +1 -0
  70. package/dist/generic/types.js +6 -0
  71. package/dist/generic/utils.d.ts +51 -0
  72. package/dist/generic/utils.d.ts.map +1 -0
  73. package/dist/generic/utils.js +232 -0
  74. package/dist/index.d.ts +15 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +31 -0
  77. package/dist/speckit/export.d.ts +22 -0
  78. package/dist/speckit/export.d.ts.map +1 -0
  79. package/dist/speckit/export.js +384 -0
  80. package/dist/speckit/format-adapter.d.ts +104 -0
  81. package/dist/speckit/format-adapter.d.ts.map +1 -0
  82. package/dist/speckit/format-adapter.js +488 -0
  83. package/dist/speckit/import-v2.d.ts +33 -0
  84. package/dist/speckit/import-v2.d.ts.map +1 -0
  85. package/dist/speckit/import-v2.js +361 -0
  86. package/dist/speckit/import.d.ts +16 -0
  87. package/dist/speckit/import.d.ts.map +1 -0
  88. package/dist/speckit/import.js +247 -0
  89. package/dist/speckit/index.d.ts +5 -0
  90. package/dist/speckit/index.d.ts.map +1 -0
  91. package/dist/speckit/index.js +4 -0
  92. package/dist/speckit/parser.d.ts +28 -0
  93. package/dist/speckit/parser.d.ts.map +1 -0
  94. package/dist/speckit/parser.js +283 -0
  95. package/dist/speckit/parsers/plan-parser.d.ts +71 -0
  96. package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
  97. package/dist/speckit/parsers/plan-parser.js +216 -0
  98. package/dist/speckit/parsers/spec-parser.d.ts +67 -0
  99. package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
  100. package/dist/speckit/parsers/spec-parser.js +255 -0
  101. package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
  102. package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
  103. package/dist/speckit/parsers/tasks-parser.js +157 -0
  104. package/package.json +23 -0
  105. package/project.json +29 -0
  106. package/src/__tests__/adapter-edge-cases.test.ts +937 -0
  107. package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
  108. package/src/__tests__/fixtures/aps/expected-output.json +83 -0
  109. package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
  110. package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
  111. package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
  112. package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
  113. package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
  114. package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
  115. package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
  116. package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
  117. package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
  118. package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
  119. package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
  120. package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
  121. package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
  122. package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
  123. package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
  124. package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
  125. package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
  126. package/src/__tests__/fixtures/generic/todo-list.md +23 -0
  127. package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
  128. package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
  129. package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
  130. package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
  131. package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
  132. package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
  133. package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
  134. package/src/__tests__/generic-format-adapter.test.ts +398 -0
  135. package/src/__tests__/speckit-export.test.ts +233 -0
  136. package/src/__tests__/speckit-format-adapter.test.ts +832 -0
  137. package/src/__tests__/speckit-import-v2.test.ts +253 -0
  138. package/src/__tests__/speckit-import.test.ts +209 -0
  139. package/src/__tests__/speckit-parser.test.ts +219 -0
  140. package/src/__tests__/speckit-spec-parser.test.ts +120 -0
  141. package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
  142. package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
  143. package/src/aps-markdown/adapter.ts +455 -0
  144. package/src/aps-markdown/index.ts +8 -0
  145. package/src/base/__tests__/registry.test.ts +515 -0
  146. package/src/base/file-discovery.ts +305 -0
  147. package/src/base/index.ts +10 -0
  148. package/src/base/registry.ts +263 -0
  149. package/src/base/testing.ts +334 -0
  150. package/src/base/types.ts +342 -0
  151. package/src/base/utils.ts +306 -0
  152. package/src/bmad/format-adapter.ts +227 -0
  153. package/src/bmad/index.ts +21 -0
  154. package/src/bmad/parser.ts +224 -0
  155. package/src/bmad/serializer.ts +206 -0
  156. package/src/bmad/types.ts +135 -0
  157. package/src/bmad/utils.ts +575 -0
  158. package/src/common/index.ts +2 -0
  159. package/src/common/registry.ts +72 -0
  160. package/src/common/types.ts +84 -0
  161. package/src/generic/__tests__/serializer.test.ts +167 -0
  162. package/src/generic/format-adapter.ts +200 -0
  163. package/src/generic/index.ts +11 -0
  164. package/src/generic/parser.ts +129 -0
  165. package/src/generic/serializer.ts +134 -0
  166. package/src/generic/types.ts +53 -0
  167. package/src/generic/utils.ts +270 -0
  168. package/src/index.ts +48 -0
  169. package/src/speckit/export.ts +489 -0
  170. package/src/speckit/format-adapter.ts +595 -0
  171. package/src/speckit/import-v2.ts +445 -0
  172. package/src/speckit/import.ts +305 -0
  173. package/src/speckit/index.ts +4 -0
  174. package/src/speckit/parser.ts +351 -0
  175. package/src/speckit/parsers/plan-parser.ts +342 -0
  176. package/src/speckit/parsers/spec-parser.ts +379 -0
  177. package/src/speckit/parsers/tasks-parser.ts +246 -0
  178. package/tsconfig.json +26 -0
  179. package/tsconfig.lib.json +21 -0
  180. package/tsconfig.lib.tsbuildinfo +1 -0
  181. package/tsconfig.spec.json +9 -0
  182. package/tsconfig.tsbuildinfo +1 -0
  183. package/vitest.config.ts +14 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Parser for official GitHub Spec-Kit plan.md format
3
+ *
4
+ * Plan.md focuses on HOW (technical implementation):
5
+ * - Summary
6
+ * - Technical Context (stack, dependencies, constraints)
7
+ * - Constitution Check (compliance with project principles)
8
+ * - Project Structure (directories, files)
9
+ * - Implementation Details (API endpoints, database schema, etc.)
10
+ * - Complexity Tracking (design decisions)
11
+ */
12
+
13
+ interface PlanMetadata {
14
+ feature?: string;
15
+ branch?: string;
16
+ date?: string;
17
+ spec?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ interface TechnicalContext {
22
+ language?: string;
23
+ dependencies?: string[];
24
+ storage?: string;
25
+ testing?: string;
26
+ target?: string;
27
+ type?: string;
28
+ performanceGoals?: string[];
29
+ constraints?: string[];
30
+ scale?: string;
31
+ }
32
+
33
+ interface ConstitutionCheck {
34
+ modularity?: boolean | string;
35
+ testability?: boolean | string;
36
+ security?: boolean | string;
37
+ performance?: boolean | string;
38
+ maintainability?: boolean | string;
39
+ documentation?: boolean | string;
40
+ [key: string]: boolean | string | undefined;
41
+ }
42
+
43
+ interface ProjectStructure {
44
+ documentation?: string;
45
+ sourceCode?: string;
46
+ selectedOption?: string;
47
+ }
48
+
49
+ interface ComplexityDecision {
50
+ title: string;
51
+ problem: string;
52
+ solution: string;
53
+ justification: string;
54
+ }
55
+
56
+ export interface ParsedPlan {
57
+ metadata: PlanMetadata;
58
+ summary: string;
59
+ technicalContext: TechnicalContext;
60
+ constitutionCheck: ConstitutionCheck;
61
+ projectStructure: ProjectStructure;
62
+ implementationDetails: Map<string, string>; // Section title -> content
63
+ complexityDecisions: ComplexityDecision[];
64
+ }
65
+
66
+ export class PlanParser {
67
+ parsePlan(content: string): ParsedPlan {
68
+ const result: ParsedPlan = {
69
+ metadata: {},
70
+ summary: '',
71
+ technicalContext: {},
72
+ constitutionCheck: {},
73
+ projectStructure: {},
74
+ implementationDetails: new Map(),
75
+ complexityDecisions: [],
76
+ };
77
+
78
+ // Extract metadata from first section
79
+ result.metadata = this.extractMetadata(content);
80
+
81
+ // Parse summary section
82
+ result.summary = this.extractSummary(content);
83
+
84
+ // Parse technical context
85
+ result.technicalContext = this.parseTechnicalContext(content);
86
+
87
+ // Parse constitution check
88
+ result.constitutionCheck = this.parseConstitutionCheck(content);
89
+
90
+ // Parse project structure
91
+ result.projectStructure = this.parseProjectStructure(content);
92
+
93
+ // Parse implementation details (flexible sections)
94
+ result.implementationDetails = this.parseImplementationDetails(content);
95
+
96
+ // Parse complexity tracking
97
+ result.complexityDecisions = this.parseComplexityDecisions(content);
98
+
99
+ return result;
100
+ }
101
+
102
+ private extractMetadata(content: string): PlanMetadata {
103
+ const metadata: PlanMetadata = {};
104
+
105
+ // Extract title (# Implementation Plan: ...)
106
+ const titleMatch = content.match(/^#\s+Implementation Plan:\s+(.+)$/m);
107
+ if (titleMatch) {
108
+ metadata.feature = titleMatch[1].trim();
109
+ }
110
+
111
+ // Extract bold key-value pairs
112
+ const metadataRegex = /\*\*([^*]+)\*\*:\s*(?:`([^`\n]+)`|\[([^\]]+)\]\(([^)]+)\)|([^\n]+))/g;
113
+ let match;
114
+ while ((match = metadataRegex.exec(content)) !== null) {
115
+ const key = match[1].trim().toLowerCase().replace(/\s+/g, '_');
116
+ const value = match[2] || match[3] || match[5] || '';
117
+ metadata[key] = value.trim();
118
+ }
119
+
120
+ return metadata;
121
+ }
122
+
123
+ private extractSummary(content: string): string {
124
+ const summaryMatch = content.match(/##\s+Summary\s+([\s\S]*?)(?=\n##\s|$)/i);
125
+ return summaryMatch?.[1]?.trim() || '';
126
+ }
127
+
128
+ private parseTechnicalContext(content: string): TechnicalContext {
129
+ const context: TechnicalContext = {};
130
+
131
+ const contextMatch = content.match(/##\s+Technical Context([\s\S]*?)(?=\n##\s|$)/i);
132
+
133
+ if (!contextMatch) {
134
+ return context;
135
+ }
136
+
137
+ const contextSection = contextMatch[1];
138
+
139
+ // Parse bullet points with key-value pairs
140
+ const languageMatch = contextSection.match(/[-*]\s+\*\*Language\/Version\*\*:\s+(.+)/i);
141
+ if (languageMatch) {
142
+ context.language = languageMatch[1].trim();
143
+ }
144
+
145
+ const storageMatch = contextSection.match(/[-*]\s+\*\*Storage\*\*:\s+(.+)/i);
146
+ if (storageMatch) {
147
+ context.storage = storageMatch[1].trim();
148
+ }
149
+
150
+ const testingMatch = contextSection.match(/[-*]\s+\*\*Testing\*\*:\s+(.+)/i);
151
+ if (testingMatch) {
152
+ context.testing = testingMatch[1].trim();
153
+ }
154
+
155
+ const targetMatch = contextSection.match(/[-*]\s+\*\*Target\*\*:\s+(.+)/i);
156
+ if (targetMatch) {
157
+ context.target = targetMatch[1].trim();
158
+ }
159
+
160
+ const typeMatch = contextSection.match(/[-*]\s+\*\*Type\*\*:\s+(.+)/i);
161
+ if (typeMatch) {
162
+ context.type = typeMatch[1].trim();
163
+ }
164
+
165
+ const scaleMatch = contextSection.match(/[-*]\s+\*\*Scale\*\*:\s+(.+)/i);
166
+ if (scaleMatch) {
167
+ context.scale = scaleMatch[1].trim();
168
+ }
169
+
170
+ // Parse dependencies (multi-line list)
171
+ const depsMatch = contextSection.match(
172
+ /[-*]\s+\*\*Dependencies\*\*:\s+([\s\S]*?)(?=\n[-*]\s+\*\*|$)/i
173
+ );
174
+ if (depsMatch) {
175
+ const deps = depsMatch[1]
176
+ .split(/\n\s*[-*]\s+/)
177
+ .map((d) => d.trim())
178
+ .filter((d) => d.length > 0);
179
+ context.dependencies = deps;
180
+ }
181
+
182
+ // Parse performance goals
183
+ const perfMatch = contextSection.match(
184
+ /[-*]\s+\*\*Performance Goals\*\*:\s+([\s\S]*?)(?=\n[-*]\s+\*\*|$)/i
185
+ );
186
+ if (perfMatch) {
187
+ const goals = perfMatch[1]
188
+ .split(/\n\s*[-*]\s+/)
189
+ .map((g) => g.trim())
190
+ .filter((g) => g.length > 0);
191
+ context.performanceGoals = goals;
192
+ }
193
+
194
+ // Parse constraints
195
+ const constraintsMatch = contextSection.match(
196
+ /[-*]\s+\*\*Constraints\*\*:\s+([\s\S]*?)(?=\n[-*]\s+\*\*|$)/i
197
+ );
198
+ if (constraintsMatch) {
199
+ const constraints = constraintsMatch[1]
200
+ .split(/\n\s*[-*]\s+/)
201
+ .map((c) => c.trim())
202
+ .filter((c) => c.length > 0);
203
+ context.constraints = constraints;
204
+ }
205
+
206
+ return context;
207
+ }
208
+
209
+ private parseConstitutionCheck(content: string): ConstitutionCheck {
210
+ const check: ConstitutionCheck = {};
211
+
212
+ const checkMatch = content.match(/##\s+Constitution Check([\s\S]*?)(?=\n##\s|$)/i);
213
+
214
+ if (!checkMatch) {
215
+ return check;
216
+ }
217
+
218
+ const checkSection = checkMatch[1];
219
+
220
+ // Parse ✅ or ❌ followed by **Key**: Description
221
+ const checkRegex = /([✅❌✓✗×])\s+\*\*([^*]+)\*\*:\s+(.+)/g;
222
+ let match;
223
+
224
+ while ((match = checkRegex.exec(checkSection)) !== null) {
225
+ const status = match[1];
226
+ const key = match[2].trim().toLowerCase().replace(/\s+/g, '_');
227
+ const description = match[3].trim();
228
+
229
+ // Check if it passes or fails
230
+ const passes = status === '✅' || status === '✓';
231
+ check[key] = passes ? description : `FAIL: ${description}`;
232
+ }
233
+
234
+ return check;
235
+ }
236
+
237
+ private parseProjectStructure(content: string): ProjectStructure {
238
+ const structure: ProjectStructure = {};
239
+
240
+ const structureMatch = content.match(/##\s+Project Structure([\s\S]*?)(?=\n##\s|$)/i);
241
+
242
+ if (!structureMatch) {
243
+ return structure;
244
+ }
245
+
246
+ const structureSection = structureMatch[1];
247
+
248
+ // Extract documentation structure (code block)
249
+ const docsMatch = structureSection.match(
250
+ /###\s+Documentation[\s\S]*?```[\s\S]*?\n([\s\S]*?)```/i
251
+ );
252
+ if (docsMatch) {
253
+ structure.documentation = docsMatch[1].trim();
254
+ }
255
+
256
+ // Extract source code structure
257
+ const sourceMatch = structureSection.match(
258
+ /###\s+Source Code Structure[\s\S]*?```[\s\S]*?\n([\s\S]*?)```/i
259
+ );
260
+ if (sourceMatch) {
261
+ structure.sourceCode = sourceMatch[1].trim();
262
+ }
263
+
264
+ // Check for "Selected" or "(Selected)" marker
265
+ const selectedMatch = structureSection.match(/####\s+Option \d+:([^(]+)\(Selected\)/i);
266
+ if (selectedMatch) {
267
+ structure.selectedOption = selectedMatch[1].trim();
268
+ }
269
+
270
+ return structure;
271
+ }
272
+
273
+ private parseImplementationDetails(content: string): Map<string, string> {
274
+ const details = new Map<string, string>();
275
+
276
+ // Find "Implementation Details" section
277
+ const detailsMatch = content.match(
278
+ /##\s+Implementation Details([\s\S]*?)(?=\n##\s+Complexity|$)/i
279
+ );
280
+
281
+ if (!detailsMatch) {
282
+ return details;
283
+ }
284
+
285
+ const detailsSection = detailsMatch[1];
286
+
287
+ // Split by ### headers
288
+ const subsectionRegex = /###\s+([^\n]+)\n([\s\S]*?)(?=\n###|$)/g;
289
+ let match;
290
+
291
+ while ((match = subsectionRegex.exec(detailsSection)) !== null) {
292
+ const title = match[1].trim();
293
+ const content = match[2].trim();
294
+ details.set(title, content);
295
+ }
296
+
297
+ return details;
298
+ }
299
+
300
+ private parseComplexityDecisions(content: string): ComplexityDecision[] {
301
+ const decisions: ComplexityDecision[] = [];
302
+
303
+ // Find "Complexity Tracking" section
304
+ const complexityMatch = content.match(/##\s+Complexity Tracking([\s\S]*?)$/i);
305
+
306
+ if (!complexityMatch) {
307
+ return decisions;
308
+ }
309
+
310
+ const complexitySection = complexityMatch[1];
311
+
312
+ // Split by ### headers (each decision)
313
+ const decisionBlocks = complexitySection.split(/###\s+/).slice(1);
314
+
315
+ for (const block of decisionBlocks) {
316
+ const decision = this.parseComplexityDecision(block);
317
+ if (decision) {
318
+ decisions.push(decision);
319
+ }
320
+ }
321
+
322
+ return decisions;
323
+ }
324
+
325
+ private parseComplexityDecision(block: string): ComplexityDecision | null {
326
+ const titleMatch = block.match(/^([^\n]+)/);
327
+ const problemMatch = block.match(/\*\*Problem\*\*:\s+(.+?)(?=\n\*\*|$)/s);
328
+ const solutionMatch = block.match(/\*\*Solution\*\*:\s+([\s\S]+?)(?=\n\*\*|$)/);
329
+ const justificationMatch = block.match(/\*\*Justification\*\*:\s+([\s\S]+?)(?=\n###|$)/);
330
+
331
+ if (!titleMatch) {
332
+ return null;
333
+ }
334
+
335
+ return {
336
+ title: titleMatch[1].trim(),
337
+ problem: problemMatch?.[1]?.trim() || '',
338
+ solution: solutionMatch?.[1]?.trim() || '',
339
+ justification: justificationMatch?.[1]?.trim() || '',
340
+ };
341
+ }
342
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Parser for official GitHub Spec-Kit spec.md format
3
+ *
4
+ * Spec.md focuses on WHAT and WHY (not HOW):
5
+ * - Feature metadata
6
+ * - User Scenarios & Testing (prioritized user stories)
7
+ * - Requirements (functional requirements, key entities)
8
+ * - Success Criteria (measurable outcomes)
9
+ */
10
+
11
+ interface SpecMetadata {
12
+ feature?: string;
13
+ branch?: string;
14
+ date?: string;
15
+ status?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ interface UserScenario {
20
+ priority: 'P1' | 'P2' | 'P3' | string;
21
+ title: string;
22
+ asA: string;
23
+ iWantTo: string;
24
+ soThat: string;
25
+ acceptanceScenarios: string[];
26
+ edgeCases: string[];
27
+ }
28
+
29
+ interface FunctionalRequirement {
30
+ code: string;
31
+ description: string;
32
+ needsClarification: boolean;
33
+ clarificationQuestion?: string;
34
+ }
35
+
36
+ interface EntityDefinition {
37
+ name: string;
38
+ represents: string;
39
+ keyAttributes: string[];
40
+ relationships: string[];
41
+ }
42
+
43
+ interface SuccessCriteria {
44
+ quantitative: string[];
45
+ qualitative: string[];
46
+ security?: string[];
47
+ performance?: string[];
48
+ }
49
+
50
+ export interface ParsedSpec {
51
+ metadata: SpecMetadata;
52
+ userScenarios: UserScenario[];
53
+ requirements: {
54
+ functional: FunctionalRequirement[];
55
+ entities: EntityDefinition[];
56
+ };
57
+ successCriteria: SuccessCriteria;
58
+ clarifications: string[]; // All [NEEDS CLARIFICATION] markers
59
+ }
60
+
61
+ export class SpecParser {
62
+ parseSpec(content: string): ParsedSpec {
63
+ const result: ParsedSpec = {
64
+ metadata: {},
65
+ userScenarios: [],
66
+ requirements: {
67
+ functional: [],
68
+ entities: [],
69
+ },
70
+ successCriteria: {
71
+ quantitative: [],
72
+ qualitative: [],
73
+ },
74
+ clarifications: [],
75
+ };
76
+
77
+ // Extract metadata from first section (before ## headers)
78
+ result.metadata = this.extractMetadata(content);
79
+
80
+ // Parse user scenarios section
81
+ result.userScenarios = this.parseUserScenarios(content);
82
+
83
+ // Parse requirements section
84
+ result.requirements = this.parseRequirements(content);
85
+
86
+ // Parse success criteria
87
+ result.successCriteria = this.parseSuccessCriteria(content);
88
+
89
+ // Extract all clarifications
90
+ result.clarifications = this.extractClarifications(content);
91
+
92
+ return result;
93
+ }
94
+
95
+ private extractMetadata(content: string): SpecMetadata {
96
+ const metadata: SpecMetadata = {};
97
+
98
+ // Extract title (# Feature: ...)
99
+ const titleMatch = content.match(/^#\s+Feature:\s+(.+)$/m);
100
+ if (titleMatch) {
101
+ metadata.feature = titleMatch[1].trim();
102
+ }
103
+
104
+ // Extract bold key-value pairs (** Key **: value)
105
+ // Stop at next ** or newline to handle multiple metadata on same line
106
+ const metadataRegex = /\*\*([^*]+)\*\*:\s*`?([^`*\n]+?)`?\s*(?=\*\*|\n|$)/g;
107
+ let match;
108
+ while ((match = metadataRegex.exec(content)) !== null) {
109
+ const key = match[1].trim().toLowerCase().replace(/\s+/g, '_');
110
+ const value = match[2].trim();
111
+ metadata[key] = value;
112
+ }
113
+
114
+ return metadata;
115
+ }
116
+
117
+ private parseUserScenarios(content: string): UserScenario[] {
118
+ const scenarios: UserScenario[] = [];
119
+
120
+ // Find "User Scenarios & Testing" section
121
+ const scenarioSectionMatch = content.match(
122
+ /##\s+User Scenarios?\s*(?:&|and)?\s*Testing([\s\S]*?)(?=\n##\s|\n#\s|$)/i
123
+ );
124
+
125
+ if (!scenarioSectionMatch) {
126
+ return scenarios;
127
+ }
128
+
129
+ const scenarioSection = scenarioSectionMatch[1];
130
+
131
+ // Split by ### headers (each scenario)
132
+ const scenarioBlocks = scenarioSection.split(/###\s+/).slice(1);
133
+
134
+ for (const block of scenarioBlocks) {
135
+ const scenario = this.parseUserScenario(block);
136
+ if (scenario) {
137
+ scenarios.push(scenario);
138
+ }
139
+ }
140
+
141
+ return scenarios;
142
+ }
143
+
144
+ private parseUserScenario(block: string): UserScenario | null {
145
+ // Extract priority and title from first line (P1: Title)
146
+ const titleMatch = block.match(/^(P\d+):\s+(.+)$/m);
147
+ if (!titleMatch) {
148
+ return null;
149
+ }
150
+
151
+ const priority = titleMatch[1];
152
+ const title = titleMatch[2].trim();
153
+
154
+ // Extract user story components (handle multiline with line breaks)
155
+ const asAMatch = block.match(/\*\*As a\*\*\s+(.+?)(?=\s*\*\*|\n\n|$)/is);
156
+ const iWantToMatch = block.match(/\*\*I want(?:\s+to)?\*\*\s+(.+?)(?=\s*\*\*|\n\n|$)/is);
157
+ const soThatMatch = block.match(/\*\*So(?:\s+|\n)that\*\*\s+(.+?)(?=\s*\n\n|$)/is);
158
+
159
+ // Extract acceptance scenarios
160
+ const acceptanceScenarios: string[] = [];
161
+ const acceptanceMatch = block.match(
162
+ /\*\*Acceptance Scenarios:\*\*([\s\S]*?)(?=\*\*Edge Cases:|\*\*\[NEEDS|###|$)/i
163
+ );
164
+ if (acceptanceMatch) {
165
+ const scenarios = acceptanceMatch[1]
166
+ .split(/\n[-*]\s+/)
167
+ .map((s) => s.trim())
168
+ .filter((s) => s.length > 0);
169
+ acceptanceScenarios.push(...scenarios);
170
+ }
171
+
172
+ // Extract edge cases
173
+ const edgeCases: string[] = [];
174
+ const edgeCaseMatch = block.match(/\*\*Edge Cases:\*\*([\s\S]*?)(?=###|$)/i);
175
+ if (edgeCaseMatch) {
176
+ const cases = edgeCaseMatch[1]
177
+ .split(/\n[-*]\s+/)
178
+ .map((s) => s.trim())
179
+ .filter((s) => s.length > 0);
180
+ edgeCases.push(...cases);
181
+ }
182
+
183
+ return {
184
+ priority,
185
+ title,
186
+ asA: asAMatch?.[1]?.trim() || '',
187
+ iWantTo: iWantToMatch?.[1]?.trim() || '',
188
+ soThat: soThatMatch?.[1]?.trim() || '',
189
+ acceptanceScenarios,
190
+ edgeCases,
191
+ };
192
+ }
193
+
194
+ private parseRequirements(content: string): {
195
+ functional: FunctionalRequirement[];
196
+ entities: EntityDefinition[];
197
+ } {
198
+ const requirements = {
199
+ functional: [] as FunctionalRequirement[],
200
+ entities: [] as EntityDefinition[],
201
+ };
202
+
203
+ // Find "Requirements" section
204
+ const reqSectionMatch = content.match(/##\s+Requirements([\s\S]*?)(?=\n##\s|\n#\s|$)/i);
205
+
206
+ if (!reqSectionMatch) {
207
+ return requirements;
208
+ }
209
+
210
+ const reqSection = reqSectionMatch[1];
211
+
212
+ // Parse functional requirements
213
+ requirements.functional = this.parseFunctionalRequirements(reqSection);
214
+
215
+ // Parse entity definitions
216
+ requirements.entities = this.parseEntities(reqSection);
217
+
218
+ return requirements;
219
+ }
220
+
221
+ private parseFunctionalRequirements(section: string): FunctionalRequirement[] {
222
+ const requirements: FunctionalRequirement[] = [];
223
+
224
+ // Find "Functional Requirements" subsection
225
+ const functionalMatch = section.match(/###\s+Functional Requirements([\s\S]*?)(?=\n###|$)/i);
226
+
227
+ if (!functionalMatch) {
228
+ return requirements;
229
+ }
230
+
231
+ const functionalSection = functionalMatch[1];
232
+
233
+ // Match FR-XXX: Description or [NEEDS CLARIFICATION: question]
234
+ const reqRegex = /\*\*(FR-\d+)\*\*:\s+(.+?)(?=\n\*\*FR-|\n###|$)/gs;
235
+ let match;
236
+
237
+ while ((match = reqRegex.exec(functionalSection)) !== null) {
238
+ const code = match[1];
239
+ const description = match[2].trim();
240
+
241
+ // Check if this is a clarification request
242
+ const clarificationMatch = description.match(/\[NEEDS CLARIFICATION:\s*(.+?)\]/);
243
+
244
+ requirements.push({
245
+ code,
246
+ description: clarificationMatch ? description : description,
247
+ needsClarification: !!clarificationMatch,
248
+ clarificationQuestion: clarificationMatch?.[1]?.trim(),
249
+ });
250
+ }
251
+
252
+ return requirements;
253
+ }
254
+
255
+ private parseEntities(section: string): EntityDefinition[] {
256
+ const entities: EntityDefinition[] = [];
257
+
258
+ // Find "Key Entities" subsection
259
+ const entitiesMatch = section.match(/###\s+Key Entities([\s\S]*?)(?=\n##|$)/i);
260
+
261
+ if (!entitiesMatch) {
262
+ return entities;
263
+ }
264
+
265
+ const entitiesSection = entitiesMatch[1];
266
+
267
+ // Split by ** EntityName **
268
+ const entityBlocks = entitiesSection.split(/\*\*([^*]+)\*\*/g).slice(1);
269
+
270
+ for (let i = 0; i < entityBlocks.length; i += 2) {
271
+ const entityName = entityBlocks[i].trim();
272
+ const entityContent = entityBlocks[i + 1] || '';
273
+
274
+ if (!entityName || !entityContent.trim()) {
275
+ continue;
276
+ }
277
+
278
+ const entity = this.parseEntity(entityName, entityContent);
279
+ if (entity) {
280
+ entities.push(entity);
281
+ }
282
+ }
283
+
284
+ return entities;
285
+ }
286
+
287
+ private parseEntity(name: string, content: string): EntityDefinition | null {
288
+ const representsMatch = content.match(/[-*]\s+Represents:\s+(.+)/i);
289
+ const attributesMatch = content.match(/[-*]\s+Key Attributes:\s+(.+)/i);
290
+ const relationshipsMatch = content.match(/[-*]\s+Relationships:\s+(.+)/i);
291
+
292
+ return {
293
+ name,
294
+ represents: representsMatch?.[1]?.trim() || '',
295
+ keyAttributes:
296
+ attributesMatch?.[1]
297
+ ?.split(',')
298
+ .map((a) => a.trim())
299
+ .filter((a) => a.length > 0) || [],
300
+ relationships:
301
+ relationshipsMatch?.[1]
302
+ ?.split(',')
303
+ .map((r) => r.trim())
304
+ .filter((r) => r.length > 0) || [],
305
+ };
306
+ }
307
+
308
+ private parseSuccessCriteria(content: string): SuccessCriteria {
309
+ const criteria: SuccessCriteria = {
310
+ quantitative: [],
311
+ qualitative: [],
312
+ };
313
+
314
+ // Find "Success Criteria" section
315
+ const criteriaSectionMatch = content.match(
316
+ /##\s+Success Criteria([\s\S]*?)(?=\n##\s|\n#\s|$)/i
317
+ );
318
+
319
+ if (!criteriaSectionMatch) {
320
+ return criteria;
321
+ }
322
+
323
+ const criteriaSection = criteriaSectionMatch[1];
324
+
325
+ // Parse quantitative metrics
326
+ const quantMatch = criteriaSection.match(/###\s+Quantitative Metrics([\s\S]*?)(?=\n###|$)/i);
327
+ if (quantMatch) {
328
+ const metrics = quantMatch[1]
329
+ .split(/\n[-*]\s+/)
330
+ .map((m) => m.trim())
331
+ .filter((m) => m.length > 0);
332
+ criteria.quantitative = metrics;
333
+ }
334
+
335
+ // Parse qualitative metrics
336
+ const qualMatch = criteriaSection.match(/###\s+Qualitative Metrics([\s\S]*?)(?=\n###|$)/i);
337
+ if (qualMatch) {
338
+ const metrics = qualMatch[1]
339
+ .split(/\n[-*]\s+/)
340
+ .map((m) => m.trim())
341
+ .filter((m) => m.length > 0);
342
+ criteria.qualitative = metrics;
343
+ }
344
+
345
+ // Parse security metrics (if present)
346
+ const securityMatch = criteriaSection.match(/###\s+Security Metrics([\s\S]*?)(?=\n###|$)/i);
347
+ if (securityMatch) {
348
+ const metrics = securityMatch[1]
349
+ .split(/\n[-*]\s+/)
350
+ .map((m) => m.trim())
351
+ .filter((m) => m.length > 0);
352
+ criteria.security = metrics;
353
+ }
354
+
355
+ // Parse performance metrics (if present)
356
+ const perfMatch = criteriaSection.match(/###\s+Performance Metrics([\s\S]*?)(?=\n###|$)/i);
357
+ if (perfMatch) {
358
+ const metrics = perfMatch[1]
359
+ .split(/\n[-*]\s+/)
360
+ .map((m) => m.trim())
361
+ .filter((m) => m.length > 0);
362
+ criteria.performance = metrics;
363
+ }
364
+
365
+ return criteria;
366
+ }
367
+
368
+ private extractClarifications(content: string): string[] {
369
+ const clarifications: string[] = [];
370
+ const clarificationRegex = /\[NEEDS CLARIFICATION:\s*([^\]]+)\]/g;
371
+ let match;
372
+
373
+ while ((match = clarificationRegex.exec(content)) !== null) {
374
+ clarifications.push(match[1].trim());
375
+ }
376
+
377
+ return clarifications;
378
+ }
379
+ }