@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.
- package/AGENTS.md +180 -0
- package/BMAD_ADAPTER_SPEC.md +489 -0
- package/LICENSE +14 -0
- package/README.md +500 -0
- package/dist/aps-markdown/adapter.d.ts +102 -0
- package/dist/aps-markdown/adapter.d.ts.map +1 -0
- package/dist/aps-markdown/adapter.js +351 -0
- package/dist/aps-markdown/index.d.ts +8 -0
- package/dist/aps-markdown/index.d.ts.map +1 -0
- package/dist/aps-markdown/index.js +7 -0
- package/dist/base/file-discovery.d.ts +63 -0
- package/dist/base/file-discovery.d.ts.map +1 -0
- package/dist/base/file-discovery.js +246 -0
- package/dist/base/index.d.ts +10 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +9 -0
- package/dist/base/registry.d.ts +155 -0
- package/dist/base/registry.d.ts.map +1 -0
- package/dist/base/registry.js +227 -0
- package/dist/base/testing.d.ts +102 -0
- package/dist/base/testing.d.ts.map +1 -0
- package/dist/base/testing.js +221 -0
- package/dist/base/types.d.ts +255 -0
- package/dist/base/types.d.ts.map +1 -0
- package/dist/base/types.js +78 -0
- package/dist/base/utils.d.ts +127 -0
- package/dist/base/utils.d.ts.map +1 -0
- package/dist/base/utils.js +254 -0
- package/dist/bmad/format-adapter.d.ts +76 -0
- package/dist/bmad/format-adapter.d.ts.map +1 -0
- package/dist/bmad/format-adapter.js +186 -0
- package/dist/bmad/index.d.ts +12 -0
- package/dist/bmad/index.d.ts.map +1 -0
- package/dist/bmad/index.js +10 -0
- package/dist/bmad/parser.d.ts +12 -0
- package/dist/bmad/parser.d.ts.map +1 -0
- package/dist/bmad/parser.js +181 -0
- package/dist/bmad/serializer.d.ts +16 -0
- package/dist/bmad/serializer.d.ts.map +1 -0
- package/dist/bmad/serializer.js +170 -0
- package/dist/bmad/types.d.ts +127 -0
- package/dist/bmad/types.d.ts.map +1 -0
- package/dist/bmad/types.js +47 -0
- package/dist/bmad/utils.d.ts +120 -0
- package/dist/bmad/utils.d.ts.map +1 -0
- package/dist/bmad/utils.js +480 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +2 -0
- package/dist/common/registry.d.ts +18 -0
- package/dist/common/registry.d.ts.map +1 -0
- package/dist/common/registry.js +58 -0
- package/dist/common/types.d.ts +68 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +12 -0
- package/dist/generic/format-adapter.d.ts +64 -0
- package/dist/generic/format-adapter.d.ts.map +1 -0
- package/dist/generic/format-adapter.js +159 -0
- package/dist/generic/index.d.ts +10 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +9 -0
- package/dist/generic/parser.d.ts +11 -0
- package/dist/generic/parser.d.ts.map +1 -0
- package/dist/generic/parser.js +106 -0
- package/dist/generic/serializer.d.ts +11 -0
- package/dist/generic/serializer.d.ts.map +1 -0
- package/dist/generic/serializer.js +118 -0
- package/dist/generic/types.d.ts +52 -0
- package/dist/generic/types.d.ts.map +1 -0
- package/dist/generic/types.js +6 -0
- package/dist/generic/utils.d.ts +51 -0
- package/dist/generic/utils.d.ts.map +1 -0
- package/dist/generic/utils.js +232 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/speckit/export.d.ts +22 -0
- package/dist/speckit/export.d.ts.map +1 -0
- package/dist/speckit/export.js +384 -0
- package/dist/speckit/format-adapter.d.ts +104 -0
- package/dist/speckit/format-adapter.d.ts.map +1 -0
- package/dist/speckit/format-adapter.js +488 -0
- package/dist/speckit/import-v2.d.ts +33 -0
- package/dist/speckit/import-v2.d.ts.map +1 -0
- package/dist/speckit/import-v2.js +361 -0
- package/dist/speckit/import.d.ts +16 -0
- package/dist/speckit/import.d.ts.map +1 -0
- package/dist/speckit/import.js +247 -0
- package/dist/speckit/index.d.ts +5 -0
- package/dist/speckit/index.d.ts.map +1 -0
- package/dist/speckit/index.js +4 -0
- package/dist/speckit/parser.d.ts +28 -0
- package/dist/speckit/parser.d.ts.map +1 -0
- package/dist/speckit/parser.js +283 -0
- package/dist/speckit/parsers/plan-parser.d.ts +71 -0
- package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/plan-parser.js +216 -0
- package/dist/speckit/parsers/spec-parser.d.ts +67 -0
- package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/spec-parser.js +255 -0
- package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
- package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/tasks-parser.js +157 -0
- package/package.json +23 -0
- package/project.json +29 -0
- package/src/__tests__/adapter-edge-cases.test.ts +937 -0
- package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
- package/src/__tests__/fixtures/aps/expected-output.json +83 -0
- package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
- package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
- package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
- package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
- package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
- package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
- package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
- package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
- package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
- package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
- package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
- package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
- package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
- package/src/__tests__/fixtures/generic/todo-list.md +23 -0
- package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
- package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
- package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
- package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
- package/src/__tests__/generic-format-adapter.test.ts +398 -0
- package/src/__tests__/speckit-export.test.ts +233 -0
- package/src/__tests__/speckit-format-adapter.test.ts +832 -0
- package/src/__tests__/speckit-import-v2.test.ts +253 -0
- package/src/__tests__/speckit-import.test.ts +209 -0
- package/src/__tests__/speckit-parser.test.ts +219 -0
- package/src/__tests__/speckit-spec-parser.test.ts +120 -0
- package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
- package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
- package/src/aps-markdown/adapter.ts +455 -0
- package/src/aps-markdown/index.ts +8 -0
- package/src/base/__tests__/registry.test.ts +515 -0
- package/src/base/file-discovery.ts +305 -0
- package/src/base/index.ts +10 -0
- package/src/base/registry.ts +263 -0
- package/src/base/testing.ts +334 -0
- package/src/base/types.ts +342 -0
- package/src/base/utils.ts +306 -0
- package/src/bmad/format-adapter.ts +227 -0
- package/src/bmad/index.ts +21 -0
- package/src/bmad/parser.ts +224 -0
- package/src/bmad/serializer.ts +206 -0
- package/src/bmad/types.ts +135 -0
- package/src/bmad/utils.ts +575 -0
- package/src/common/index.ts +2 -0
- package/src/common/registry.ts +72 -0
- package/src/common/types.ts +84 -0
- package/src/generic/__tests__/serializer.test.ts +167 -0
- package/src/generic/format-adapter.ts +200 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/parser.ts +129 -0
- package/src/generic/serializer.ts +134 -0
- package/src/generic/types.ts +53 -0
- package/src/generic/utils.ts +270 -0
- package/src/index.ts +48 -0
- package/src/speckit/export.ts +489 -0
- package/src/speckit/format-adapter.ts +595 -0
- package/src/speckit/import-v2.ts +445 -0
- package/src/speckit/import.ts +305 -0
- package/src/speckit/index.ts +4 -0
- package/src/speckit/parser.ts +351 -0
- package/src/speckit/parsers/plan-parser.ts +342 -0
- package/src/speckit/parsers/spec-parser.ts +379 -0
- package/src/speckit/parsers/tasks-parser.ts +246 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +21 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
}
|