@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,334 @@
1
+ /**
2
+ * Adapter Testing Utilities
3
+ *
4
+ * Helpers for testing format adapters with common patterns and assertions.
5
+ */
6
+
7
+ import type {
8
+ FormatAdapter,
9
+ ParseResult,
10
+ SerializeResult,
11
+ DetectionResult,
12
+ ParseContext,
13
+ AdapterOptions,
14
+ } from './types.js';
15
+ import type { APSPlan } from '@eddacraft/anvil-core';
16
+
17
+ /**
18
+ * Test fixture for adapter testing
19
+ */
20
+ export interface AdapterTestFixture {
21
+ /** Name of the test case */
22
+ name: string;
23
+ /** Raw content in external format */
24
+ content: string;
25
+ /** Expected detection result (optional) */
26
+ expectedDetection?: Partial<DetectionResult>;
27
+ /** Expected parse success (optional) */
28
+ expectParseSuccess?: boolean;
29
+ /** Context for parsing */
30
+ parseContext?: ParseContext;
31
+ /** Options for operations */
32
+ options?: AdapterOptions;
33
+ /** Expected values in parsed plan (optional) */
34
+ expectedPlan?: Partial<APSPlan>;
35
+ /** Expected error codes (if parse should fail) */
36
+ expectedErrors?: string[];
37
+ /** Expected warning codes */
38
+ expectedWarnings?: string[];
39
+ }
40
+
41
+ /**
42
+ * Test helper for adapter detection
43
+ */
44
+ export class AdapterTester {
45
+ constructor(private adapter: FormatAdapter) {}
46
+
47
+ /**
48
+ * Test detection with a fixture
49
+ */
50
+ testDetection(fixture: AdapterTestFixture): DetectionResult {
51
+ const result = this.adapter.detect(fixture.content);
52
+
53
+ if (fixture.expectedDetection) {
54
+ if (fixture.expectedDetection.detected !== undefined) {
55
+ if (result.detected !== fixture.expectedDetection.detected) {
56
+ throw new Error(
57
+ `Detection mismatch for "${fixture.name}": expected ${fixture.expectedDetection.detected}, got ${result.detected}`
58
+ );
59
+ }
60
+ }
61
+
62
+ if (fixture.expectedDetection.confidence !== undefined) {
63
+ if (result.confidence < fixture.expectedDetection.confidence) {
64
+ throw new Error(
65
+ `Confidence too low for "${fixture.name}": expected >=${fixture.expectedDetection.confidence}, got ${result.confidence}`
66
+ );
67
+ }
68
+ }
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Test parsing with a fixture
76
+ */
77
+ async testParse(fixture: AdapterTestFixture): Promise<ParseResult> {
78
+ const result = await this.adapter.parse(fixture.content, fixture.parseContext, fixture.options);
79
+
80
+ if (fixture.expectParseSuccess !== undefined) {
81
+ if (result.success !== fixture.expectParseSuccess) {
82
+ throw new Error(
83
+ `Parse success mismatch for "${fixture.name}": expected ${fixture.expectParseSuccess}, got ${result.success}`
84
+ );
85
+ }
86
+ }
87
+
88
+ if (fixture.expectedErrors && fixture.expectedErrors.length > 0) {
89
+ if (!result.errors || result.errors.length === 0) {
90
+ throw new Error(`Expected errors for "${fixture.name}" but got none`);
91
+ }
92
+
93
+ const errorCodes = result.errors.map((e) => e.code);
94
+ for (const expectedCode of fixture.expectedErrors) {
95
+ if (!errorCodes.includes(expectedCode)) {
96
+ throw new Error(
97
+ `Expected error code "${expectedCode}" for "${fixture.name}" but found: ${errorCodes.join(', ')}`
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ if (fixture.expectedWarnings && fixture.expectedWarnings.length > 0) {
104
+ if (!result.warnings || result.warnings.length === 0) {
105
+ throw new Error(`Expected warnings for "${fixture.name}" but got none`);
106
+ }
107
+
108
+ const warningCodes = result.warnings.map((w) => w.code);
109
+ for (const expectedCode of fixture.expectedWarnings) {
110
+ if (!warningCodes.includes(expectedCode)) {
111
+ throw new Error(
112
+ `Expected warning code "${expectedCode}" for "${fixture.name}" but found: ${warningCodes.join(', ')}`
113
+ );
114
+ }
115
+ }
116
+ }
117
+
118
+ if (fixture.expectedPlan && result.data) {
119
+ this.assertPlanMatches(result.data, fixture.expectedPlan, fixture.name);
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Test serialization
127
+ */
128
+ async testSerialize(plan: APSPlan, options?: AdapterOptions): Promise<SerializeResult> {
129
+ return this.adapter.serialize(plan, options);
130
+ }
131
+
132
+ /**
133
+ * Test round-trip: parse then serialize then parse again
134
+ */
135
+ async testRoundTrip(fixture: AdapterTestFixture): Promise<{
136
+ firstParse: ParseResult;
137
+ serialized: SerializeResult;
138
+ secondParse: ParseResult;
139
+ planMatches: boolean;
140
+ }> {
141
+ // First parse
142
+ const firstParse = await this.adapter.parse(
143
+ fixture.content,
144
+ fixture.parseContext,
145
+ fixture.options
146
+ );
147
+
148
+ if (!firstParse.success || !firstParse.data) {
149
+ throw new Error(`First parse failed for "${fixture.name}"`);
150
+ }
151
+
152
+ // Serialize
153
+ const serialized = await this.adapter.serialize(firstParse.data, fixture.options);
154
+
155
+ if (!serialized.success || !serialized.content) {
156
+ throw new Error(`Serialization failed for "${fixture.name}"`);
157
+ }
158
+
159
+ // Second parse
160
+ const secondParse = await this.adapter.parse(
161
+ serialized.content,
162
+ fixture.parseContext,
163
+ fixture.options
164
+ );
165
+
166
+ if (!secondParse.success || !secondParse.data) {
167
+ throw new Error(`Second parse failed for "${fixture.name}"`);
168
+ }
169
+
170
+ // Compare plans
171
+ const planMatches = this.plansEqual(firstParse.data, secondParse.data);
172
+
173
+ return {
174
+ firstParse,
175
+ serialized,
176
+ secondParse,
177
+ planMatches,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Assert that parsed plan matches expected values
183
+ */
184
+ private assertPlanMatches(actual: APSPlan, expected: Partial<APSPlan>, testName: string): void {
185
+ if (expected.intent !== undefined && actual.intent !== expected.intent) {
186
+ throw new Error(
187
+ `Intent mismatch for "${testName}": expected "${expected.intent}", got "${actual.intent}"`
188
+ );
189
+ }
190
+
191
+ if (expected.id !== undefined && actual.id !== expected.id) {
192
+ throw new Error(
193
+ `ID mismatch for "${testName}": expected "${expected.id}", got "${actual.id}"`
194
+ );
195
+ }
196
+
197
+ if (
198
+ expected.proposed_changes !== undefined &&
199
+ actual.proposed_changes.length !== expected.proposed_changes.length
200
+ ) {
201
+ throw new Error(
202
+ `Changes count mismatch for "${testName}": expected ${expected.proposed_changes.length}, got ${actual.proposed_changes.length}`
203
+ );
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Compare two plans for equality (ignoring hash and timestamps)
209
+ */
210
+ private plansEqual(plan1: APSPlan, plan2: APSPlan): boolean {
211
+ // Compare intents
212
+ if (plan1.intent !== plan2.intent) return false;
213
+
214
+ // Compare changes count
215
+ if (plan1.proposed_changes.length !== plan2.proposed_changes.length) return false;
216
+
217
+ // Compare each change
218
+ for (let i = 0; i < plan1.proposed_changes.length; i++) {
219
+ const change1 = plan1.proposed_changes[i];
220
+ const change2 = plan2.proposed_changes[i];
221
+
222
+ if (change1.type !== change2.type) return false;
223
+ if (change1.path !== change2.path) return false;
224
+ if (change1.description !== change2.description) return false;
225
+ }
226
+
227
+ return true;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Create a test adapter tester
233
+ */
234
+ export function createTester(adapter: FormatAdapter): AdapterTester {
235
+ return new AdapterTester(adapter);
236
+ }
237
+
238
+ /**
239
+ * Create a basic parse context for testing
240
+ */
241
+ export function createTestContext(overrides?: Partial<ParseContext>): ParseContext {
242
+ return {
243
+ author: 'test-user',
244
+ repositoryPath: '/test/repo',
245
+ branch: 'main',
246
+ commit: 'abc123',
247
+ timestamp: '2024-01-01T00:00:00.000Z',
248
+ ...overrides,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Create basic adapter options for testing
254
+ */
255
+ export function createTestOptions(overrides?: Partial<AdapterOptions>): AdapterOptions {
256
+ return {
257
+ preserveComments: true,
258
+ preserveMetadata: true,
259
+ strict: false,
260
+ ...overrides,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Assert detection result
266
+ */
267
+ export function assertDetection(
268
+ result: DetectionResult,
269
+ expected: { detected: boolean; minConfidence?: number }
270
+ ): void {
271
+ if (result.detected !== expected.detected) {
272
+ throw new Error(
273
+ `Detection mismatch: expected ${expected.detected}, got ${result.detected}. Reason: ${result.reason}`
274
+ );
275
+ }
276
+
277
+ if (expected.minConfidence !== undefined && result.confidence < expected.minConfidence) {
278
+ throw new Error(
279
+ `Confidence too low: expected >=${expected.minConfidence}, got ${result.confidence}`
280
+ );
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Assert parse success
286
+ */
287
+ export function assertParseSuccess(
288
+ result: ParseResult
289
+ ): asserts result is ParseResult & { data: APSPlan } {
290
+ if (!result.success) {
291
+ const errorMsg =
292
+ result.errors?.map((e) => `${e.code}: ${e.message}`).join(', ') || 'Unknown error';
293
+ throw new Error(`Parse failed: ${errorMsg}`);
294
+ }
295
+
296
+ if (!result.data) {
297
+ throw new Error('Parse succeeded but no data returned');
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Assert parse failure
303
+ */
304
+ export function assertParseFailure(result: ParseResult, expectedErrorCode?: string): void {
305
+ if (result.success) {
306
+ throw new Error('Expected parse to fail but it succeeded');
307
+ }
308
+
309
+ if (expectedErrorCode && result.errors) {
310
+ const errorCodes = result.errors.map((e) => e.code);
311
+ if (!errorCodes.includes(expectedErrorCode)) {
312
+ throw new Error(
313
+ `Expected error code "${expectedErrorCode}" but found: ${errorCodes.join(', ')}`
314
+ );
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Assert serialize success
321
+ */
322
+ export function assertSerializeSuccess(
323
+ result: SerializeResult
324
+ ): asserts result is SerializeResult & { content: string } {
325
+ if (!result.success) {
326
+ const errorMsg =
327
+ result.errors?.map((e) => `${e.code}: ${e.message}`).join(', ') || 'Unknown error';
328
+ throw new Error(`Serialize failed: ${errorMsg}`);
329
+ }
330
+
331
+ if (!result.content) {
332
+ throw new Error('Serialize succeeded but no content returned');
333
+ }
334
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Base Adapter Framework Types
3
+ *
4
+ * Defines the core interfaces for format adapters that convert between
5
+ * external planning formats (SpecKit, BMAD, etc.) and APS.
6
+ */
7
+
8
+ import type { APSPlan, ValidationResult } from '@eddacraft/anvil-core';
9
+
10
+ /**
11
+ * Result of format detection
12
+ */
13
+ export interface DetectionResult {
14
+ /** Whether this adapter can handle the content */
15
+ detected: boolean;
16
+ /** Confidence score (0-100) */
17
+ confidence: number;
18
+ /** Reason for detection result */
19
+ reason?: string;
20
+ }
21
+
22
+ /**
23
+ * Result of parsing external format to APS
24
+ */
25
+ export interface ParseResult {
26
+ /** Whether parsing succeeded */
27
+ success: boolean;
28
+ /** Parsed APS plan (if successful) */
29
+ data?: APSPlan;
30
+ /** Errors encountered during parsing */
31
+ errors?: AdapterError[];
32
+ /** Non-fatal warnings */
33
+ warnings?: AdapterWarning[];
34
+ }
35
+
36
+ /**
37
+ * Result of serializing APS to external format
38
+ */
39
+ export interface SerializeResult {
40
+ /** Whether serialization succeeded */
41
+ success: boolean;
42
+ /** Serialized content (if successful) */
43
+ content?: string;
44
+ /** Errors encountered during serialization */
45
+ errors?: AdapterError[];
46
+ /** Non-fatal warnings */
47
+ warnings?: AdapterWarning[];
48
+ /** Additional metadata about serialization */
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+
52
+ /**
53
+ * Error encountered during adapter operation
54
+ */
55
+ export interface AdapterError {
56
+ /** Error code for programmatic handling */
57
+ code: string;
58
+ /** Human-readable error message */
59
+ message: string;
60
+ /** Path to the problematic element (e.g., "changes[0].path") */
61
+ path?: string;
62
+ /** Line number in source content (if applicable) */
63
+ line?: number;
64
+ /** Column number in source content (if applicable) */
65
+ column?: number;
66
+ /** Additional error details */
67
+ details?: unknown;
68
+ }
69
+
70
+ /**
71
+ * Warning encountered during adapter operation
72
+ */
73
+ export interface AdapterWarning {
74
+ /** Warning code for programmatic handling */
75
+ code: string;
76
+ /** Human-readable warning message */
77
+ message: string;
78
+ /** Path to the element (e.g., "changes[0].description") */
79
+ path?: string;
80
+ /** Line number in source content (if applicable) */
81
+ line?: number;
82
+ /** Column number in source content (if applicable) */
83
+ column?: number;
84
+ /** Additional warning details */
85
+ details?: unknown;
86
+ }
87
+
88
+ /**
89
+ * Context provided when parsing external formats
90
+ */
91
+ export interface ParseContext {
92
+ /** Repository path or URL */
93
+ repositoryPath?: string;
94
+ /** Git branch */
95
+ branch?: string;
96
+ /** Git commit hash */
97
+ commit?: string;
98
+ /** Author information */
99
+ author?: string;
100
+ /** Timestamp for provenance */
101
+ timestamp?: string;
102
+ /** Additional context metadata */
103
+ metadata?: Record<string, unknown>;
104
+ /** Pre-generated plan ID (for deterministic parsing) */
105
+ planId?: string;
106
+ }
107
+
108
+ /**
109
+ * Hint for path-based format detection
110
+ *
111
+ * Provides file path and sibling file information to improve
112
+ * detection accuracy for formats that use folder conventions.
113
+ */
114
+ export interface PathDetectionHint {
115
+ /** File path being analyzed */
116
+ filePath: string;
117
+ /** Sibling file names in the same directory */
118
+ siblingFiles?: string[];
119
+ /** Parent directory names (innermost first) */
120
+ parentDirs?: string[];
121
+ }
122
+
123
+ /**
124
+ * Options for adapter operations
125
+ */
126
+ export interface AdapterOptions {
127
+ /** Preserve comments from source */
128
+ preserveComments?: boolean;
129
+ /** Preserve metadata from source */
130
+ preserveMetadata?: boolean;
131
+ /** Strict validation mode */
132
+ strict?: boolean;
133
+ /** Format-specific options */
134
+ formatOptions?: Record<string, unknown>;
135
+ }
136
+
137
+ /**
138
+ * Metadata about an adapter
139
+ */
140
+ export interface AdapterMetadata {
141
+ /** Adapter name (e.g., "speckit", "bmad") */
142
+ name: string;
143
+ /** Adapter version */
144
+ version: string;
145
+ /** Human-readable display name */
146
+ displayName: string;
147
+ /** Short description of what this adapter handles */
148
+ description: string;
149
+ /** File extensions supported (e.g., [".md", ".spec.md"]) */
150
+ extensions: readonly string[];
151
+ /** Format identifiers (e.g., ["speckit", "spec"]) */
152
+ formats: readonly string[];
153
+ }
154
+
155
+ /**
156
+ * Core interface for format adapters
157
+ *
158
+ * Adapters convert between external planning formats and APS.
159
+ * Each adapter should handle one external format (e.g., SpecKit, BMAD).
160
+ */
161
+ export interface FormatAdapter {
162
+ /** Adapter metadata */
163
+ readonly metadata: AdapterMetadata;
164
+
165
+ /**
166
+ * Detect if this adapter can handle the given content
167
+ *
168
+ * @param content - Raw content to analyze
169
+ * @returns Detection result with confidence score
170
+ */
171
+ detect(content: string): DetectionResult;
172
+
173
+ /**
174
+ * Parse external format content into APS
175
+ *
176
+ * @param content - Raw content in external format
177
+ * @param context - Context for provenance tracking
178
+ * @param options - Parsing options
179
+ * @returns Parse result with APS plan or errors
180
+ */
181
+ parse(content: string, context?: ParseContext, options?: AdapterOptions): Promise<ParseResult>;
182
+
183
+ /**
184
+ * Serialize APS plan to external format
185
+ *
186
+ * @param plan - APS plan to serialize
187
+ * @param options - Serialization options
188
+ * @returns Serialize result with content or errors
189
+ */
190
+ serialize(plan: APSPlan, options?: AdapterOptions): Promise<SerializeResult>;
191
+
192
+ /**
193
+ * Validate external format content
194
+ *
195
+ * Validates the content without full conversion to APS.
196
+ * Useful for fast validation feedback.
197
+ *
198
+ * @param content - Raw content to validate
199
+ * @param options - Validation options
200
+ * @returns Validation result
201
+ */
202
+ validate(content: string, options?: AdapterOptions): Promise<ValidationResult>;
203
+
204
+ /**
205
+ * Detect if this adapter can handle content using file path hints
206
+ *
207
+ * Optional method that uses folder structure and sibling files
208
+ * to improve detection accuracy. Falls back to content-only
209
+ * detection if not implemented.
210
+ *
211
+ * @param content - Raw content to analyze
212
+ * @param hint - Path and directory information
213
+ * @returns Detection result with confidence score
214
+ */
215
+ detectWithPath?(content: string, hint: PathDetectionHint): DetectionResult;
216
+
217
+ /**
218
+ * Check if this adapter can import from a given format
219
+ *
220
+ * @param format - Format identifier or file extension
221
+ * @returns True if adapter supports importing from this format
222
+ */
223
+ canImport(format: string): boolean;
224
+
225
+ /**
226
+ * Check if this adapter can export to a given format
227
+ *
228
+ * @param format - Format identifier or file extension
229
+ * @returns True if adapter supports exporting to this format
230
+ */
231
+ canExport(format: string): boolean;
232
+ }
233
+
234
+ /**
235
+ * Abstract base class for format adapters
236
+ *
237
+ * Provides common functionality and structure for concrete adapters.
238
+ */
239
+ export abstract class BaseFormatAdapter implements FormatAdapter {
240
+ abstract readonly metadata: AdapterMetadata;
241
+
242
+ constructor(protected options: AdapterOptions = {}) {}
243
+
244
+ abstract detect(content: string): DetectionResult;
245
+ abstract parse(
246
+ content: string,
247
+ context?: ParseContext,
248
+ options?: AdapterOptions
249
+ ): Promise<ParseResult>;
250
+ abstract serialize(plan: APSPlan, options?: AdapterOptions): Promise<SerializeResult>;
251
+ abstract validate(content: string, options?: AdapterOptions): Promise<ValidationResult>;
252
+
253
+ canImport(format: string): boolean {
254
+ const normalized = format.toLowerCase().replace(/^\./, '');
255
+ return (
256
+ this.metadata.formats.includes(normalized) ||
257
+ this.metadata.extensions.some((ext) => ext.replace(/^\./, '') === normalized)
258
+ );
259
+ }
260
+
261
+ canExport(format: string): boolean {
262
+ return this.canImport(format);
263
+ }
264
+
265
+ /**
266
+ * Helper to create a successful parse result
267
+ */
268
+ protected createParseSuccess(data: APSPlan, warnings?: AdapterWarning[]): ParseResult {
269
+ return {
270
+ success: true,
271
+ data,
272
+ warnings: warnings && warnings.length > 0 ? warnings : undefined,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Helper to create a failed parse result
278
+ */
279
+ protected createParseError(errors: AdapterError[], warnings?: AdapterWarning[]): ParseResult {
280
+ return {
281
+ success: false,
282
+ errors,
283
+ warnings: warnings && warnings.length > 0 ? warnings : undefined,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Helper to create a successful serialize result
289
+ */
290
+ protected createSerializeSuccess(
291
+ content: string,
292
+ warnings?: AdapterWarning[],
293
+ metadata?: Record<string, unknown>
294
+ ): SerializeResult {
295
+ return {
296
+ success: true,
297
+ content,
298
+ warnings: warnings && warnings.length > 0 ? warnings : undefined,
299
+ metadata,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Helper to create a failed serialize result
305
+ */
306
+ protected createSerializeError(
307
+ errors: AdapterError[],
308
+ warnings?: AdapterWarning[]
309
+ ): SerializeResult {
310
+ return {
311
+ success: false,
312
+ errors,
313
+ warnings: warnings && warnings.length > 0 ? warnings : undefined,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Helper to add an error
319
+ */
320
+ protected addError(
321
+ errors: AdapterError[],
322
+ code: string,
323
+ message: string,
324
+ path?: string,
325
+ details?: unknown
326
+ ): void {
327
+ errors.push({ code, message, path, details });
328
+ }
329
+
330
+ /**
331
+ * Helper to add a warning
332
+ */
333
+ protected addWarning(
334
+ warnings: AdapterWarning[],
335
+ code: string,
336
+ message: string,
337
+ path?: string,
338
+ details?: unknown
339
+ ): void {
340
+ warnings.push({ code, message, path, details });
341
+ }
342
+ }