@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,515 @@
1
+ /**
2
+ * Adapter Registry Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { AdapterRegistry } from '../registry.js';
7
+ import type {
8
+ FormatAdapter,
9
+ DetectionResult,
10
+ AdapterMetadata,
11
+ PathDetectionHint,
12
+ } from '../types.js';
13
+ import { createDetection } from '../utils.js';
14
+
15
+ // Mock adapter for testing
16
+ class MockAdapter implements FormatAdapter {
17
+ constructor(
18
+ public readonly metadata: AdapterMetadata,
19
+ private detectionResult: DetectionResult = createDetection(true, 100)
20
+ ) {}
21
+
22
+ detect(): DetectionResult {
23
+ return this.detectionResult;
24
+ }
25
+
26
+ async parse() {
27
+ return { success: true };
28
+ }
29
+
30
+ async serialize() {
31
+ return { success: true };
32
+ }
33
+
34
+ async validate() {
35
+ return { valid: true, summary: 'Valid' };
36
+ }
37
+
38
+ canImport(format: string): boolean {
39
+ return this.metadata.formats.includes(format) || this.metadata.extensions.includes(format);
40
+ }
41
+
42
+ canExport(format: string): boolean {
43
+ return this.canImport(format);
44
+ }
45
+ }
46
+
47
+ describe('AdapterRegistry', () => {
48
+ let registry: AdapterRegistry;
49
+
50
+ beforeEach(() => {
51
+ AdapterRegistry.resetInstance();
52
+ registry = AdapterRegistry.getInstance();
53
+ });
54
+
55
+ afterEach(() => {
56
+ registry.clear();
57
+ });
58
+
59
+ describe('Singleton Pattern', () => {
60
+ it('should return same instance', () => {
61
+ const instance1 = AdapterRegistry.getInstance();
62
+ const instance2 = AdapterRegistry.getInstance();
63
+ expect(instance1).toBe(instance2);
64
+ });
65
+
66
+ it('should reset instance', () => {
67
+ const instance1 = AdapterRegistry.getInstance();
68
+ AdapterRegistry.resetInstance();
69
+ const instance2 = AdapterRegistry.getInstance();
70
+ expect(instance1).not.toBe(instance2);
71
+ });
72
+ });
73
+
74
+ describe('Registration', () => {
75
+ it('should register an adapter', () => {
76
+ const adapter = new MockAdapter({
77
+ name: 'test',
78
+ version: '1.0.0',
79
+ displayName: 'Test Adapter',
80
+ description: 'Test adapter',
81
+ extensions: ['.test'],
82
+ formats: ['test'],
83
+ });
84
+
85
+ registry.register(adapter);
86
+ expect(registry.size).toBe(1);
87
+ expect(registry.getAdapter('test')).toBe(adapter);
88
+ });
89
+
90
+ it('should throw error when registering duplicate adapter', () => {
91
+ const adapter1 = new MockAdapter({
92
+ name: 'test',
93
+ version: '1.0.0',
94
+ displayName: 'Test',
95
+ description: 'Test',
96
+ extensions: [],
97
+ formats: [],
98
+ });
99
+
100
+ const adapter2 = new MockAdapter({
101
+ name: 'test',
102
+ version: '2.0.0',
103
+ displayName: 'Test 2',
104
+ description: 'Test 2',
105
+ extensions: [],
106
+ formats: [],
107
+ });
108
+
109
+ registry.register(adapter1);
110
+ expect(() => registry.register(adapter2)).toThrow("Adapter 'test' is already registered");
111
+ });
112
+
113
+ it('should unregister an adapter', () => {
114
+ const adapter = new MockAdapter({
115
+ name: 'test',
116
+ version: '1.0.0',
117
+ displayName: 'Test',
118
+ description: 'Test',
119
+ extensions: [],
120
+ formats: [],
121
+ });
122
+
123
+ registry.register(adapter);
124
+ expect(registry.size).toBe(1);
125
+
126
+ const removed = registry.unregister('test');
127
+ expect(removed).toBe(true);
128
+ expect(registry.size).toBe(0);
129
+ });
130
+
131
+ it('should return false when unregistering non-existent adapter', () => {
132
+ const removed = registry.unregister('nonexistent');
133
+ expect(removed).toBe(false);
134
+ });
135
+ });
136
+
137
+ describe('Lookup', () => {
138
+ beforeEach(() => {
139
+ registry.register(
140
+ new MockAdapter({
141
+ name: 'markdown',
142
+ version: '1.0.0',
143
+ displayName: 'Markdown',
144
+ description: 'Markdown adapter',
145
+ extensions: ['.md', '.markdown'],
146
+ formats: ['markdown', 'md'],
147
+ })
148
+ );
149
+
150
+ registry.register(
151
+ new MockAdapter({
152
+ name: 'json',
153
+ version: '1.0.0',
154
+ displayName: 'JSON',
155
+ description: 'JSON adapter',
156
+ extensions: ['.json'],
157
+ formats: ['json'],
158
+ })
159
+ );
160
+ });
161
+
162
+ it('should get adapter by name', () => {
163
+ const adapter = registry.getAdapter('markdown');
164
+ expect(adapter).toBeDefined();
165
+ expect(adapter?.metadata.name).toBe('markdown');
166
+ });
167
+
168
+ it('should return undefined for unknown adapter', () => {
169
+ const adapter = registry.getAdapter('unknown');
170
+ expect(adapter).toBeUndefined();
171
+ });
172
+
173
+ it('should get adapter by format', () => {
174
+ const adapter = registry.getAdapterForFormat('markdown');
175
+ expect(adapter).toBeDefined();
176
+ expect(adapter?.metadata.name).toBe('markdown');
177
+ });
178
+
179
+ it('should get adapter by extension', () => {
180
+ const adapter = registry.getAdapterForFormat('.md');
181
+ expect(adapter).toBeDefined();
182
+ expect(adapter?.metadata.name).toBe('markdown');
183
+ });
184
+
185
+ it('should return undefined for unsupported format', () => {
186
+ const adapter = registry.getAdapterForFormat('yaml');
187
+ expect(adapter).toBeUndefined();
188
+ });
189
+ });
190
+
191
+ describe('Detection', () => {
192
+ it('should detect adapter from content', () => {
193
+ const highConfAdapter = new MockAdapter(
194
+ {
195
+ name: 'high',
196
+ version: '1.0.0',
197
+ displayName: 'High',
198
+ description: 'High confidence',
199
+ extensions: [],
200
+ formats: ['high'],
201
+ },
202
+ createDetection(true, 90)
203
+ );
204
+
205
+ const lowConfAdapter = new MockAdapter(
206
+ {
207
+ name: 'low',
208
+ version: '1.0.0',
209
+ displayName: 'Low',
210
+ description: 'Low confidence',
211
+ extensions: [],
212
+ formats: ['low'],
213
+ },
214
+ createDetection(true, 50)
215
+ );
216
+
217
+ registry.register(highConfAdapter);
218
+ registry.register(lowConfAdapter);
219
+
220
+ const result = registry.detectAdapter('test content');
221
+ expect(result).toBeDefined();
222
+ expect(result?.adapter.metadata.name).toBe('high');
223
+ expect(result?.detection.confidence).toBe(90);
224
+ });
225
+
226
+ it('should respect minimum confidence threshold', () => {
227
+ const lowConfAdapter = new MockAdapter(
228
+ {
229
+ name: 'low',
230
+ version: '1.0.0',
231
+ displayName: 'Low',
232
+ description: 'Low',
233
+ extensions: [],
234
+ formats: [],
235
+ },
236
+ createDetection(true, 40)
237
+ );
238
+
239
+ registry.register(lowConfAdapter);
240
+
241
+ const result = registry.detectAdapter('test', 50);
242
+ expect(result).toBeUndefined();
243
+ });
244
+
245
+ it('should detect all adapters', () => {
246
+ const adapter1 = new MockAdapter(
247
+ {
248
+ name: 'first',
249
+ version: '1.0.0',
250
+ displayName: 'First',
251
+ description: 'First',
252
+ extensions: [],
253
+ formats: [],
254
+ },
255
+ createDetection(true, 80)
256
+ );
257
+
258
+ const adapter2 = new MockAdapter(
259
+ {
260
+ name: 'second',
261
+ version: '1.0.0',
262
+ displayName: 'Second',
263
+ description: 'Second',
264
+ extensions: [],
265
+ formats: [],
266
+ },
267
+ createDetection(true, 60)
268
+ );
269
+
270
+ registry.register(adapter1);
271
+ registry.register(adapter2);
272
+
273
+ const results = registry.detectAll('test');
274
+ expect(results).toHaveLength(2);
275
+ expect(results[0].adapter.metadata.name).toBe('first');
276
+ expect(results[1].adapter.metadata.name).toBe('second');
277
+ });
278
+ });
279
+
280
+ describe('Listing', () => {
281
+ beforeEach(() => {
282
+ registry.register(
283
+ new MockAdapter({
284
+ name: 'adapter1',
285
+ version: '1.0.0',
286
+ displayName: 'Adapter 1',
287
+ description: 'First',
288
+ extensions: ['.a1', '.a'],
289
+ formats: ['a1', 'adapter1'],
290
+ })
291
+ );
292
+
293
+ registry.register(
294
+ new MockAdapter({
295
+ name: 'adapter2',
296
+ version: '1.0.0',
297
+ displayName: 'Adapter 2',
298
+ description: 'Second',
299
+ extensions: ['.a2'],
300
+ formats: ['a2'],
301
+ })
302
+ );
303
+ });
304
+
305
+ it('should list all adapters', () => {
306
+ const adapters = registry.listAdapters();
307
+ expect(adapters).toHaveLength(2);
308
+ });
309
+
310
+ it('should list adapter names', () => {
311
+ const names = registry.listAdapterNames();
312
+ expect(names).toEqual(['adapter1', 'adapter2']);
313
+ });
314
+
315
+ it('should list supported formats', () => {
316
+ const formats = registry.listSupportedFormats();
317
+ expect(formats).toContain('a1');
318
+ expect(formats).toContain('a2');
319
+ expect(formats).toContain('adapter1');
320
+ });
321
+
322
+ it('should list supported extensions', () => {
323
+ const extensions = registry.listSupportedExtensions();
324
+ expect(extensions).toContain('.a1');
325
+ expect(extensions).toContain('.a2');
326
+ expect(extensions).toContain('.a');
327
+ });
328
+ });
329
+
330
+ describe('Import/Export Adapters', () => {
331
+ beforeEach(() => {
332
+ registry.register(
333
+ new MockAdapter({
334
+ name: 'markdown',
335
+ version: '1.0.0',
336
+ displayName: 'Markdown',
337
+ description: 'Markdown',
338
+ extensions: ['.md'],
339
+ formats: ['markdown'],
340
+ })
341
+ );
342
+
343
+ registry.register(
344
+ new MockAdapter({
345
+ name: 'json',
346
+ version: '1.0.0',
347
+ displayName: 'JSON',
348
+ description: 'JSON',
349
+ extensions: ['.json'],
350
+ formats: ['json'],
351
+ })
352
+ );
353
+ });
354
+
355
+ it('should get import adapters for format', () => {
356
+ const adapters = registry.getImportAdapters('markdown');
357
+ expect(adapters).toHaveLength(1);
358
+ expect(adapters[0].metadata.name).toBe('markdown');
359
+ });
360
+
361
+ it('should get export adapters for format', () => {
362
+ const adapters = registry.getExportAdapters('.json');
363
+ expect(adapters).toHaveLength(1);
364
+ expect(adapters[0].metadata.name).toBe('json');
365
+ });
366
+
367
+ it('should check if format is supported', () => {
368
+ expect(registry.isFormatSupported('markdown')).toBe(true);
369
+ expect(registry.isFormatSupported('yaml')).toBe(false);
370
+ });
371
+ });
372
+
373
+ describe('Clear', () => {
374
+ it('should clear all adapters', () => {
375
+ registry.register(
376
+ new MockAdapter({
377
+ name: 'test',
378
+ version: '1.0.0',
379
+ displayName: 'Test',
380
+ description: 'Test',
381
+ extensions: [],
382
+ formats: [],
383
+ })
384
+ );
385
+
386
+ expect(registry.size).toBe(1);
387
+ registry.clear();
388
+ expect(registry.size).toBe(0);
389
+ });
390
+ });
391
+
392
+ describe('Path-aware Detection (registry)', () => {
393
+ it('should use detectWithPath when available', () => {
394
+ const pathAwareAdapter = new MockAdapter(
395
+ {
396
+ name: 'path-aware',
397
+ version: '1.0.0',
398
+ displayName: 'Path Aware',
399
+ description: 'Has detectWithPath',
400
+ extensions: [],
401
+ formats: [],
402
+ },
403
+ createDetection(true, 60)
404
+ );
405
+
406
+ // Add detectWithPath that returns higher confidence
407
+ (pathAwareAdapter as FormatAdapter).detectWithPath = (
408
+ _content: string,
409
+ _hint: PathDetectionHint
410
+ ) => createDetection(true, 90, 'path-boosted');
411
+
412
+ registry.register(pathAwareAdapter);
413
+
414
+ const hint: PathDetectionHint = {
415
+ filePath: '/project/_bmad/doc.md',
416
+ parentDirs: ['_bmad', 'project'],
417
+ };
418
+
419
+ const result = registry.detectAdapterWithPath('test content', hint);
420
+ expect(result).toBeDefined();
421
+ expect(result?.detection.confidence).toBe(90);
422
+ expect(result?.detection.reason).toBe('path-boosted');
423
+ });
424
+
425
+ it('should fall back to detect when detectWithPath is not available', () => {
426
+ const standardAdapter = new MockAdapter(
427
+ {
428
+ name: 'standard',
429
+ version: '1.0.0',
430
+ displayName: 'Standard',
431
+ description: 'No detectWithPath',
432
+ extensions: [],
433
+ formats: [],
434
+ },
435
+ createDetection(true, 70)
436
+ );
437
+
438
+ registry.register(standardAdapter);
439
+
440
+ const hint: PathDetectionHint = {
441
+ filePath: '/project/doc.md',
442
+ };
443
+
444
+ const result = registry.detectAdapterWithPath('test content', hint);
445
+ expect(result).toBeDefined();
446
+ expect(result?.detection.confidence).toBe(70);
447
+ });
448
+
449
+ it('should select highest confidence from path-aware detection', () => {
450
+ const lowAdapter = new MockAdapter(
451
+ {
452
+ name: 'low-path',
453
+ version: '1.0.0',
454
+ displayName: 'Low',
455
+ description: 'Low confidence',
456
+ extensions: [],
457
+ formats: [],
458
+ },
459
+ createDetection(true, 50)
460
+ );
461
+
462
+ const highAdapter = new MockAdapter(
463
+ {
464
+ name: 'high-path',
465
+ version: '1.0.0',
466
+ displayName: 'High',
467
+ description: 'High confidence',
468
+ extensions: [],
469
+ formats: [],
470
+ },
471
+ createDetection(true, 60)
472
+ );
473
+
474
+ (highAdapter as FormatAdapter).detectWithPath = (
475
+ _content: string,
476
+ _hint: PathDetectionHint
477
+ ) => createDetection(true, 95, 'path-detected');
478
+
479
+ registry.register(lowAdapter);
480
+ registry.register(highAdapter);
481
+
482
+ const hint: PathDetectionHint = {
483
+ filePath: '/project/_bmad/doc.md',
484
+ parentDirs: ['_bmad'],
485
+ };
486
+
487
+ const result = registry.detectAdapterWithPath('test', hint);
488
+ expect(result?.adapter.metadata.name).toBe('high-path');
489
+ expect(result?.detection.confidence).toBe(95);
490
+ });
491
+
492
+ it('should respect minimum confidence threshold for path detection', () => {
493
+ const lowAdapter = new MockAdapter(
494
+ {
495
+ name: 'low-threshold',
496
+ version: '1.0.0',
497
+ displayName: 'Low',
498
+ description: 'Low confidence',
499
+ extensions: [],
500
+ formats: [],
501
+ },
502
+ createDetection(true, 30)
503
+ );
504
+
505
+ registry.register(lowAdapter);
506
+
507
+ const hint: PathDetectionHint = {
508
+ filePath: '/project/doc.md',
509
+ };
510
+
511
+ const result = registry.detectAdapterWithPath('test', hint, 50);
512
+ expect(result).toBeUndefined();
513
+ });
514
+ });
515
+ });