@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,306 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdapterError,
|
|
5
|
+
AdapterWarning,
|
|
6
|
+
ParseResult,
|
|
7
|
+
SerializeResult,
|
|
8
|
+
DetectionResult,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
export function generateDeterministicPlanId(content: string): string {
|
|
12
|
+
const hash = createHash('sha256').update(content, 'utf8').digest('hex');
|
|
13
|
+
return `aps-${hash.substring(0, 8)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a detection result
|
|
18
|
+
*
|
|
19
|
+
* @param detected - Whether format was detected
|
|
20
|
+
* @param confidence - Confidence score (0-100)
|
|
21
|
+
* @param reason - Optional reason
|
|
22
|
+
* @returns Detection result
|
|
23
|
+
*/
|
|
24
|
+
export function createDetection(
|
|
25
|
+
detected: boolean,
|
|
26
|
+
confidence: number,
|
|
27
|
+
reason?: string
|
|
28
|
+
): DetectionResult {
|
|
29
|
+
return {
|
|
30
|
+
detected,
|
|
31
|
+
confidence: Math.max(0, Math.min(100, confidence)),
|
|
32
|
+
reason,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create an adapter error
|
|
38
|
+
*
|
|
39
|
+
* @param code - Error code
|
|
40
|
+
* @param message - Error message
|
|
41
|
+
* @param options - Additional options
|
|
42
|
+
* @returns Adapter error
|
|
43
|
+
*/
|
|
44
|
+
export function createError(
|
|
45
|
+
code: string,
|
|
46
|
+
message: string,
|
|
47
|
+
options?: {
|
|
48
|
+
path?: string;
|
|
49
|
+
line?: number;
|
|
50
|
+
column?: number;
|
|
51
|
+
details?: unknown;
|
|
52
|
+
}
|
|
53
|
+
): AdapterError {
|
|
54
|
+
return {
|
|
55
|
+
code,
|
|
56
|
+
message,
|
|
57
|
+
...options,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an adapter warning
|
|
63
|
+
*
|
|
64
|
+
* @param code - Warning code
|
|
65
|
+
* @param message - Warning message
|
|
66
|
+
* @param options - Additional options
|
|
67
|
+
* @returns Adapter warning
|
|
68
|
+
*/
|
|
69
|
+
export function createWarning(
|
|
70
|
+
code: string,
|
|
71
|
+
message: string,
|
|
72
|
+
options?: {
|
|
73
|
+
path?: string;
|
|
74
|
+
line?: number;
|
|
75
|
+
column?: number;
|
|
76
|
+
details?: unknown;
|
|
77
|
+
}
|
|
78
|
+
): AdapterWarning {
|
|
79
|
+
return {
|
|
80
|
+
code,
|
|
81
|
+
message,
|
|
82
|
+
...options,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if content matches a pattern
|
|
88
|
+
*
|
|
89
|
+
* Useful for format detection.
|
|
90
|
+
*
|
|
91
|
+
* @param content - Content to check
|
|
92
|
+
* @param patterns - Array of regex patterns or strings
|
|
93
|
+
* @returns Number of patterns matched (0 to patterns.length)
|
|
94
|
+
*/
|
|
95
|
+
export function matchesPatterns(content: string, patterns: Array<string | RegExp>): number {
|
|
96
|
+
let matches = 0;
|
|
97
|
+
for (const pattern of patterns) {
|
|
98
|
+
if (typeof pattern === 'string') {
|
|
99
|
+
if (content.includes(pattern)) {
|
|
100
|
+
matches++;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
if (pattern.test(content)) {
|
|
104
|
+
matches++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return matches;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate confidence score based on pattern matches
|
|
113
|
+
*
|
|
114
|
+
* @param matchCount - Number of patterns matched
|
|
115
|
+
* @param totalPatterns - Total number of patterns checked
|
|
116
|
+
* @param requiredMatches - Minimum matches for 100% confidence
|
|
117
|
+
* @returns Confidence score (0-100)
|
|
118
|
+
*/
|
|
119
|
+
export function calculateConfidence(
|
|
120
|
+
matchCount: number,
|
|
121
|
+
totalPatterns: number,
|
|
122
|
+
requiredMatches: number = totalPatterns
|
|
123
|
+
): number {
|
|
124
|
+
if (matchCount === 0) return 0;
|
|
125
|
+
if (matchCount >= requiredMatches) return 100;
|
|
126
|
+
|
|
127
|
+
// Linear interpolation
|
|
128
|
+
return Math.round((matchCount / requiredMatches) * 100);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract file extension from filename or path
|
|
133
|
+
*
|
|
134
|
+
* @param filename - Filename or path
|
|
135
|
+
* @returns Extension (including dot) or empty string
|
|
136
|
+
*/
|
|
137
|
+
export function getExtension(filename: string): string {
|
|
138
|
+
const match = filename.match(/\.[^.]+$/);
|
|
139
|
+
return match ? match[0] : '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Normalize format identifier
|
|
144
|
+
*
|
|
145
|
+
* Removes leading dot and converts to lowercase.
|
|
146
|
+
*
|
|
147
|
+
* @param format - Format identifier or extension
|
|
148
|
+
* @returns Normalized format
|
|
149
|
+
*/
|
|
150
|
+
export function normalizeFormat(format: string): string {
|
|
151
|
+
return format.toLowerCase().replace(/^\./, '');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format errors for display
|
|
156
|
+
*
|
|
157
|
+
* @param errors - Array of errors
|
|
158
|
+
* @returns Formatted error string
|
|
159
|
+
*/
|
|
160
|
+
export function formatErrors(errors: AdapterError[]): string {
|
|
161
|
+
return errors
|
|
162
|
+
.map((error) => {
|
|
163
|
+
let msg = `[${error.code}] ${error.message}`;
|
|
164
|
+
if (error.path) {
|
|
165
|
+
msg += ` (at ${error.path}`;
|
|
166
|
+
if (error.line !== undefined) {
|
|
167
|
+
msg += `:${error.line}`;
|
|
168
|
+
if (error.column !== undefined) {
|
|
169
|
+
msg += `:${error.column}`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
msg += ')';
|
|
173
|
+
}
|
|
174
|
+
return msg;
|
|
175
|
+
})
|
|
176
|
+
.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Format warnings for display
|
|
181
|
+
*
|
|
182
|
+
* @param warnings - Array of warnings
|
|
183
|
+
* @returns Formatted warning string
|
|
184
|
+
*/
|
|
185
|
+
export function formatWarnings(warnings: AdapterWarning[]): string {
|
|
186
|
+
return warnings
|
|
187
|
+
.map((warning) => {
|
|
188
|
+
let msg = `[${warning.code}] ${warning.message}`;
|
|
189
|
+
if (warning.path) {
|
|
190
|
+
msg += ` (at ${warning.path}`;
|
|
191
|
+
if (warning.line !== undefined) {
|
|
192
|
+
msg += `:${warning.line}`;
|
|
193
|
+
if (warning.column !== undefined) {
|
|
194
|
+
msg += `:${warning.column}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
msg += ')';
|
|
198
|
+
}
|
|
199
|
+
return msg;
|
|
200
|
+
})
|
|
201
|
+
.join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Merge parse results
|
|
206
|
+
*
|
|
207
|
+
* Useful when parsing composite formats.
|
|
208
|
+
*
|
|
209
|
+
* @param results - Array of parse results
|
|
210
|
+
* @returns Merged result
|
|
211
|
+
*/
|
|
212
|
+
export function mergeParseResults(results: ParseResult[]): ParseResult {
|
|
213
|
+
if (results.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
errors: [createError('NO_RESULTS', 'No parse results to merge')],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (results.length === 1) {
|
|
221
|
+
return results[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const allErrors: AdapterError[] = [];
|
|
225
|
+
const allWarnings: AdapterWarning[] = [];
|
|
226
|
+
let finalData = results[0].data;
|
|
227
|
+
|
|
228
|
+
for (const result of results) {
|
|
229
|
+
if (!result.success) {
|
|
230
|
+
if (result.errors) {
|
|
231
|
+
allErrors.push(...result.errors);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (result.warnings) {
|
|
235
|
+
allWarnings.push(...result.warnings);
|
|
236
|
+
}
|
|
237
|
+
if (result.data) {
|
|
238
|
+
finalData = result.data;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (allErrors.length > 0) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
errors: allErrors,
|
|
246
|
+
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
data: finalData,
|
|
253
|
+
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if result has errors
|
|
259
|
+
*
|
|
260
|
+
* @param result - Parse or serialize result
|
|
261
|
+
* @returns True if result has errors
|
|
262
|
+
*/
|
|
263
|
+
export function hasErrors(result: ParseResult | SerializeResult): boolean {
|
|
264
|
+
return !result.success || (result.errors !== undefined && result.errors.length > 0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if result has warnings
|
|
269
|
+
*
|
|
270
|
+
* @param result - Parse or serialize result
|
|
271
|
+
* @returns True if result has warnings
|
|
272
|
+
*/
|
|
273
|
+
export function hasWarnings(result: ParseResult | SerializeResult): boolean {
|
|
274
|
+
return result.warnings !== undefined && result.warnings.length > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extract first line from content (useful for format detection)
|
|
279
|
+
*
|
|
280
|
+
* @param content - Content string
|
|
281
|
+
* @returns First non-empty line or empty string
|
|
282
|
+
*/
|
|
283
|
+
export function getFirstLine(content: string): string {
|
|
284
|
+
const lines = content.split('\n');
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
const trimmed = line.trim();
|
|
287
|
+
if (trimmed) {
|
|
288
|
+
return trimmed;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Count occurrences of pattern in content
|
|
296
|
+
*
|
|
297
|
+
* @param content - Content to search
|
|
298
|
+
* @param pattern - Pattern to count
|
|
299
|
+
* @returns Number of occurrences
|
|
300
|
+
*/
|
|
301
|
+
export function countOccurrences(content: string, pattern: string | RegExp): number {
|
|
302
|
+
if (typeof pattern === 'string') {
|
|
303
|
+
return (content.match(new RegExp(pattern, 'g')) || []).length;
|
|
304
|
+
}
|
|
305
|
+
return (content.match(new RegExp(pattern.source, 'g')) || []).length;
|
|
306
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Format Adapter
|
|
3
|
+
*
|
|
4
|
+
* FormatAdapter implementation for BMAD (Breakthrough Method for Agile AI-Driven Development) format.
|
|
5
|
+
* Handles PRD, Architecture, Epic, Story, and Agent documents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateHash, type APSPlan, type ValidationResult } from '@eddacraft/anvil-core';
|
|
9
|
+
import {
|
|
10
|
+
BaseFormatAdapter,
|
|
11
|
+
type AdapterMetadata,
|
|
12
|
+
type DetectionResult,
|
|
13
|
+
type ParseResult,
|
|
14
|
+
type SerializeResult,
|
|
15
|
+
type ParseContext,
|
|
16
|
+
type AdapterOptions,
|
|
17
|
+
type PathDetectionHint,
|
|
18
|
+
} from '../base/types.js';
|
|
19
|
+
import { createDetection } from '../base/utils.js';
|
|
20
|
+
import {
|
|
21
|
+
analyzeContent,
|
|
22
|
+
calculateConfidenceScore,
|
|
23
|
+
buildDetectionReason,
|
|
24
|
+
extractFrontMatter,
|
|
25
|
+
identifyDocumentType,
|
|
26
|
+
} from './utils.js';
|
|
27
|
+
import { BMADDocumentType } from './types.js';
|
|
28
|
+
import { parseBMAD } from './parser.js';
|
|
29
|
+
import { serializeToBMAD } from './serializer.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* BMAD FormatAdapter implementation
|
|
33
|
+
*
|
|
34
|
+
* Converts between BMAD format documents and APS plans.
|
|
35
|
+
*/
|
|
36
|
+
export class BMADFormatAdapter extends BaseFormatAdapter {
|
|
37
|
+
readonly metadata: AdapterMetadata = {
|
|
38
|
+
name: 'bmad',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
displayName: 'BMAD (Breakthrough Method for Agile AI-Driven Development)',
|
|
41
|
+
description: 'BMAD PRD and architecture document adapter',
|
|
42
|
+
formats: ['bmad', 'prd', 'architecture'],
|
|
43
|
+
extensions: ['.md'],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect if content is BMAD format
|
|
48
|
+
*
|
|
49
|
+
* Uses confidence scoring based on multiple indicators:
|
|
50
|
+
* - YAML front-matter (30 points)
|
|
51
|
+
* - Requirement identifiers FR/NFR/US (25 points)
|
|
52
|
+
* - User story format (20 points)
|
|
53
|
+
* - Change log table (15 points)
|
|
54
|
+
* - Document title (10 points)
|
|
55
|
+
*
|
|
56
|
+
* @param content - Document content to analyze
|
|
57
|
+
* @returns Detection result with confidence score
|
|
58
|
+
*/
|
|
59
|
+
detect(content: string): DetectionResult {
|
|
60
|
+
const indicators = analyzeContent(content);
|
|
61
|
+
const confidence = calculateConfidenceScore(indicators);
|
|
62
|
+
const reason = buildDetectionReason(indicators);
|
|
63
|
+
|
|
64
|
+
// Detection threshold: 50% confidence
|
|
65
|
+
return createDetection(confidence >= 50, confidence, reason);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Detect with file path hints for improved accuracy
|
|
70
|
+
*
|
|
71
|
+
* Uses folder structure (e.g., `_bmad/`, `.bmad/`, `_config/`)
|
|
72
|
+
* to boost detection confidence.
|
|
73
|
+
*
|
|
74
|
+
* @param content - Document content to analyze
|
|
75
|
+
* @param hint - Path and directory information
|
|
76
|
+
* @returns Detection result with confidence score
|
|
77
|
+
*/
|
|
78
|
+
detectWithPath(content: string, hint: PathDetectionHint): DetectionResult {
|
|
79
|
+
const indicators = analyzeContent(content, hint);
|
|
80
|
+
const confidence = calculateConfidenceScore(indicators);
|
|
81
|
+
const reason = buildDetectionReason(indicators);
|
|
82
|
+
|
|
83
|
+
return createDetection(confidence >= 50, confidence, reason);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse BMAD content to APS plan
|
|
88
|
+
*
|
|
89
|
+
* @param content - BMAD markdown content
|
|
90
|
+
* @param context - Parse context for provenance
|
|
91
|
+
* @param options - Adapter options
|
|
92
|
+
* @returns Parse result with APS plan
|
|
93
|
+
*/
|
|
94
|
+
async parse(
|
|
95
|
+
content: string,
|
|
96
|
+
context?: ParseContext,
|
|
97
|
+
_options?: AdapterOptions
|
|
98
|
+
): Promise<ParseResult> {
|
|
99
|
+
try {
|
|
100
|
+
// Parse content to APS plan
|
|
101
|
+
const plan = parseBMAD(content, context);
|
|
102
|
+
|
|
103
|
+
// Generate hash for the plan
|
|
104
|
+
const planWithHash = {
|
|
105
|
+
...plan,
|
|
106
|
+
hash: generateHash(plan),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return this.createParseSuccess(planWithHash);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return this.createParseError([
|
|
112
|
+
{
|
|
113
|
+
code: 'PARSE_ERROR',
|
|
114
|
+
message: error instanceof Error ? error.message : 'Failed to parse BMAD content',
|
|
115
|
+
details: error,
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Serialize APS plan to BMAD format
|
|
123
|
+
*
|
|
124
|
+
* @param plan - APS plan to serialize
|
|
125
|
+
* @param options - Adapter options
|
|
126
|
+
* @returns Serialize result with BMAD markdown
|
|
127
|
+
*/
|
|
128
|
+
async serialize(plan: APSPlan, _options?: AdapterOptions): Promise<SerializeResult> {
|
|
129
|
+
try {
|
|
130
|
+
const content = serializeToBMAD(plan);
|
|
131
|
+
return this.createSerializeSuccess(content);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return this.createSerializeError([
|
|
134
|
+
{
|
|
135
|
+
code: 'SERIALIZE_ERROR',
|
|
136
|
+
message: error instanceof Error ? error.message : 'Failed to serialize to BMAD format',
|
|
137
|
+
details: error,
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate BMAD content
|
|
145
|
+
*
|
|
146
|
+
* Checks for required BMAD elements without full conversion.
|
|
147
|
+
*
|
|
148
|
+
* @param content - BMAD content to validate
|
|
149
|
+
* @param options - Validation options
|
|
150
|
+
* @returns Validation result
|
|
151
|
+
*/
|
|
152
|
+
async validate(content: string, _options?: AdapterOptions): Promise<ValidationResult> {
|
|
153
|
+
const issues: Array<{
|
|
154
|
+
path: string;
|
|
155
|
+
message: string;
|
|
156
|
+
code: string;
|
|
157
|
+
severity: 'error' | 'warning';
|
|
158
|
+
}> = [];
|
|
159
|
+
|
|
160
|
+
// Check for minimum content length
|
|
161
|
+
if (content.trim().length < 100) {
|
|
162
|
+
issues.push({
|
|
163
|
+
code: 'CONTENT_TOO_SHORT',
|
|
164
|
+
path: 'content',
|
|
165
|
+
message: 'Content is too short to be a valid BMAD document',
|
|
166
|
+
severity: 'error',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Analyze content for BMAD indicators
|
|
171
|
+
const indicators = analyzeContent(content);
|
|
172
|
+
const confidence = calculateConfidenceScore(indicators);
|
|
173
|
+
|
|
174
|
+
// Low confidence suggests invalid BMAD format
|
|
175
|
+
if (confidence < 50) {
|
|
176
|
+
issues.push({
|
|
177
|
+
code: 'LOW_CONFIDENCE',
|
|
178
|
+
path: 'content',
|
|
179
|
+
message: `Content does not appear to be a valid BMAD document (confidence: ${confidence}%)`,
|
|
180
|
+
severity: 'error',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check for at least some requirements or stories
|
|
185
|
+
if (indicators.requirementCount === 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
code: 'NO_REQUIREMENTS',
|
|
188
|
+
path: 'content',
|
|
189
|
+
message: 'No requirements (FR/NFR/US) found in document',
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// v6: Warn on agent docs missing hasSidecar
|
|
195
|
+
const frontMatter = extractFrontMatter(content);
|
|
196
|
+
const docType = identifyDocumentType(content, frontMatter);
|
|
197
|
+
if (docType === BMADDocumentType.AGENT && frontMatter?.hasSidecar === undefined) {
|
|
198
|
+
issues.push({
|
|
199
|
+
code: 'MISSING_HAS_SIDECAR',
|
|
200
|
+
path: 'frontMatter.hasSidecar',
|
|
201
|
+
message: 'Agent document is missing the hasSidecar field (expected for BMAD v6)',
|
|
202
|
+
severity: 'warning',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
valid: issues.filter((i) => i.severity === 'error').length === 0,
|
|
208
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
209
|
+
summary:
|
|
210
|
+
issues.filter((i) => i.severity === 'error').length === 0 && issues.length === 0
|
|
211
|
+
? 'BMAD document is valid'
|
|
212
|
+
: issues.filter((i) => i.severity === 'error').length === 0
|
|
213
|
+
? `Valid with ${issues.length} warning${issues.length > 1 ? 's' : ''}`
|
|
214
|
+
: `Found ${issues.filter((i) => i.severity === 'error').length} error${issues.filter((i) => i.severity === 'error').length > 1 ? 's' : ''}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a new BMAD format adapter instance
|
|
221
|
+
*
|
|
222
|
+
* @param options - Adapter options
|
|
223
|
+
* @returns BMAD adapter instance
|
|
224
|
+
*/
|
|
225
|
+
export function createBMADAdapter(options?: AdapterOptions): BMADFormatAdapter {
|
|
226
|
+
return new BMADFormatAdapter(options);
|
|
227
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Adapter Module
|
|
3
|
+
*
|
|
4
|
+
* Export all BMAD adapter functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { BMADFormatAdapter, createBMADAdapter } from './format-adapter.js';
|
|
8
|
+
export { BMAD_FOLDERS } from './types.js';
|
|
9
|
+
export type {
|
|
10
|
+
BMADDocument,
|
|
11
|
+
BMADDocumentType,
|
|
12
|
+
BMADRequirement,
|
|
13
|
+
BMADUserStory,
|
|
14
|
+
BMADFrontMatter,
|
|
15
|
+
BMADChangeLogEntry,
|
|
16
|
+
RequirementType,
|
|
17
|
+
DetectionIndicators,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
export { parseBMAD, parseBMADDocument, bmadToAPS } from './parser.js';
|
|
20
|
+
export { serializeToBMAD } from './serializer.js';
|
|
21
|
+
export * from './utils.js';
|