@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,305 @@
1
+ /**
2
+ * File Discovery Utility
3
+ *
4
+ * Discovers planning documents in repositories by searching for common patterns.
5
+ */
6
+
7
+ import { readdir, stat } from 'node:fs/promises';
8
+ import { join, basename } from 'node:path';
9
+
10
+ /**
11
+ * Discovered planning file
12
+ */
13
+ export interface DiscoveredFile {
14
+ /** Full path to file */
15
+ path: string;
16
+ /** File name */
17
+ name: string;
18
+ /** File size in bytes */
19
+ size: number;
20
+ /** Last modified timestamp */
21
+ modified: Date;
22
+ /** Confidence score that this is a planning document (0-100) */
23
+ confidence: number;
24
+ /** Reason for detection */
25
+ reason: string;
26
+ }
27
+
28
+ /**
29
+ * Search options
30
+ */
31
+ export interface SearchOptions {
32
+ /** Root directory to search from */
33
+ rootPath: string;
34
+ /** Maximum depth to search */
35
+ maxDepth?: number;
36
+ /** Directories to exclude */
37
+ excludeDirs?: string[];
38
+ /** File patterns to search for */
39
+ patterns?: string[];
40
+ }
41
+
42
+ /**
43
+ * Maximum file size to consider (2MB)
44
+ */
45
+ const MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
46
+
47
+ /**
48
+ * Maximum directory depth for recursive search
49
+ */
50
+ const MAX_DEPTH = 20;
51
+
52
+ /**
53
+ * Default directories to exclude
54
+ */
55
+ const DEFAULT_EXCLUDE_DIRS = [
56
+ 'node_modules',
57
+ '.git',
58
+ 'dist',
59
+ 'build',
60
+ 'coverage',
61
+ '.next',
62
+ '.nuxt',
63
+ 'out',
64
+ 'target',
65
+ 'vendor',
66
+ ];
67
+
68
+ /**
69
+ * Default file patterns for planning documents
70
+ */
71
+ const DEFAULT_PATTERNS = [
72
+ 'prd',
73
+ 'plan',
74
+ 'todo',
75
+ 'tasks',
76
+ 'spec',
77
+ 'requirements',
78
+ 'rfc',
79
+ 'adr',
80
+ 'design',
81
+ 'proposal',
82
+ 'roadmap',
83
+ ];
84
+
85
+ /**
86
+ * Calculate confidence score for a file name
87
+ */
88
+ function calculateFileConfidence(filename: string): { confidence: number; reason: string } {
89
+ const lower = filename.toLowerCase();
90
+ let confidence = 0;
91
+ const reasons: string[] = [];
92
+
93
+ // Exact matches (high confidence)
94
+ const exactMatches = ['prd.md', 'plan.md', 'todo.md', 'tasks.md', 'spec.md', 'requirements.md'];
95
+ if (exactMatches.some((pattern) => lower === pattern)) {
96
+ confidence += 80;
97
+ reasons.push('exact-match');
98
+ }
99
+
100
+ // Pattern matches (medium-high confidence)
101
+ if (lower.includes('prd')) {
102
+ confidence += 60;
103
+ reasons.push('prd-pattern');
104
+ }
105
+ if (lower.includes('plan')) {
106
+ confidence += 55;
107
+ reasons.push('plan-pattern');
108
+ }
109
+ if (lower.includes('todo')) {
110
+ confidence += 50;
111
+ reasons.push('todo-pattern');
112
+ }
113
+ if (lower.includes('spec')) {
114
+ confidence += 50;
115
+ reasons.push('spec-pattern');
116
+ }
117
+ if (lower.includes('requirements')) {
118
+ confidence += 55;
119
+ reasons.push('requirements-pattern');
120
+ }
121
+ if (lower.includes('task')) {
122
+ confidence += 45;
123
+ reasons.push('task-pattern');
124
+ }
125
+ if (lower.includes('rfc')) {
126
+ confidence += 50;
127
+ reasons.push('rfc-pattern');
128
+ }
129
+ if (lower.includes('adr')) {
130
+ confidence += 50;
131
+ reasons.push('adr-pattern');
132
+ }
133
+ if (lower.includes('design')) {
134
+ confidence += 40;
135
+ reasons.push('design-pattern');
136
+ }
137
+ if (lower.includes('proposal')) {
138
+ confidence += 45;
139
+ reasons.push('proposal-pattern');
140
+ }
141
+
142
+ // Check for common planning directories
143
+ if (lower.includes('docs/') || lower.includes('/docs/')) {
144
+ confidence += 10;
145
+ reasons.push('in-docs-dir');
146
+ }
147
+ if (lower.includes('.anvil/') || lower.includes('/.anvil/')) {
148
+ confidence += 15;
149
+ reasons.push('in-anvil-dir');
150
+ }
151
+
152
+ // Markdown extension
153
+ if (lower.endsWith('.md') || lower.endsWith('.markdown')) {
154
+ confidence += 5;
155
+ reasons.push('markdown');
156
+ }
157
+
158
+ return {
159
+ confidence: Math.min(100, confidence),
160
+ reason: reasons.join(', ') || 'no-match',
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Search directory recursively for planning files
166
+ */
167
+ async function searchDirectory(
168
+ dirPath: string,
169
+ options: Required<SearchOptions>,
170
+ currentDepth: number = 0,
171
+ results: DiscoveredFile[] = []
172
+ ): Promise<DiscoveredFile[]> {
173
+ // Stop if max depth reached (clamped to safety limit)
174
+ const effectiveMaxDepth = Math.min(options.maxDepth, MAX_DEPTH);
175
+ if (currentDepth > effectiveMaxDepth) {
176
+ return results;
177
+ }
178
+
179
+ try {
180
+ const entries = await readdir(dirPath, { withFileTypes: true });
181
+
182
+ for (const entry of entries) {
183
+ const fullPath = join(dirPath, entry.name);
184
+
185
+ if (entry.isDirectory()) {
186
+ // Skip excluded directories
187
+ if (options.excludeDirs.includes(entry.name)) {
188
+ continue;
189
+ }
190
+
191
+ // Recurse into directory
192
+ await searchDirectory(fullPath, options, currentDepth + 1, results);
193
+ } else if (entry.isFile()) {
194
+ // Check if file matches patterns
195
+ const lower = entry.name.toLowerCase();
196
+ const matchesPattern = options.patterns.some((pattern) => lower.includes(pattern));
197
+
198
+ if (matchesPattern && (lower.endsWith('.md') || lower.endsWith('.markdown'))) {
199
+ try {
200
+ const stats = await stat(fullPath);
201
+
202
+ // Skip files exceeding size limit
203
+ if (stats.size > MAX_FILE_SIZE_BYTES) {
204
+ continue;
205
+ }
206
+
207
+ const { confidence, reason } = calculateFileConfidence(fullPath);
208
+
209
+ if (confidence >= 40) {
210
+ // Threshold for inclusion
211
+ results.push({
212
+ path: fullPath,
213
+ name: entry.name,
214
+ size: stats.size,
215
+ modified: stats.mtime,
216
+ confidence,
217
+ reason,
218
+ });
219
+ }
220
+ } catch (statError) {
221
+ console.error(`[FileDiscovery] Failed to stat file ${fullPath}:`, statError);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ } catch (readError) {
227
+ console.error(`[FileDiscovery] Failed to read directory ${dirPath}:`, readError);
228
+ }
229
+
230
+ return results;
231
+ }
232
+
233
+ /**
234
+ * Discover planning documents in a directory
235
+ *
236
+ * Searches for common planning document patterns like prd.md, plan.md, todo.md, etc.
237
+ *
238
+ * @param options - Search options
239
+ * @returns Array of discovered files, sorted by confidence
240
+ */
241
+ export async function discoverPlanningFiles(options: SearchOptions): Promise<DiscoveredFile[]> {
242
+ const searchOptions: Required<SearchOptions> = {
243
+ rootPath: options.rootPath,
244
+ maxDepth: options.maxDepth ?? 5,
245
+ excludeDirs: options.excludeDirs ?? DEFAULT_EXCLUDE_DIRS,
246
+ patterns: options.patterns ?? DEFAULT_PATTERNS,
247
+ };
248
+
249
+ const results = await searchDirectory(searchOptions.rootPath, searchOptions);
250
+
251
+ // Sort by confidence (descending), then by modified date (newest first)
252
+ return results.sort((a, b) => {
253
+ if (a.confidence !== b.confidence) {
254
+ return b.confidence - a.confidence;
255
+ }
256
+ return b.modified.getTime() - a.modified.getTime();
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Find the most likely planning document
262
+ *
263
+ * Returns the single most likely planning document based on confidence and recency.
264
+ *
265
+ * @param options - Search options
266
+ * @returns The most likely planning document, or undefined if none found
267
+ */
268
+ export async function findBestPlanningFile(
269
+ options: SearchOptions
270
+ ): Promise<DiscoveredFile | undefined> {
271
+ const files = await discoverPlanningFiles(options);
272
+ return files[0];
273
+ }
274
+
275
+ /**
276
+ * Group discovered files by name pattern
277
+ *
278
+ * Groups files like "prd.md", "plan.md" etc. for easier selection.
279
+ *
280
+ * @param files - Discovered files
281
+ * @returns Map of pattern to files
282
+ */
283
+ export function groupFilesByPattern(files: DiscoveredFile[]): Map<string, DiscoveredFile[]> {
284
+ const groups = new Map<string, DiscoveredFile[]>();
285
+
286
+ for (const file of files) {
287
+ const lower = basename(file.name).toLowerCase();
288
+
289
+ let pattern = 'other';
290
+ if (lower.includes('prd')) pattern = 'prd';
291
+ else if (lower.includes('plan')) pattern = 'plan';
292
+ else if (lower.includes('todo')) pattern = 'todo';
293
+ else if (lower.includes('spec')) pattern = 'spec';
294
+ else if (lower.includes('requirements')) pattern = 'requirements';
295
+ else if (lower.includes('rfc')) pattern = 'rfc';
296
+ else if (lower.includes('adr')) pattern = 'adr';
297
+
298
+ if (!groups.has(pattern)) {
299
+ groups.set(pattern, []);
300
+ }
301
+ groups.get(pattern)!.push(file);
302
+ }
303
+
304
+ return groups;
305
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Base Adapter Framework
3
+ *
4
+ * Core types, interfaces, and registry for format adapters.
5
+ */
6
+
7
+ export * from './types.js';
8
+ export * from './registry.js';
9
+ export * from './utils.js';
10
+ export * from './file-discovery.js';
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Adapter Registry
3
+ *
4
+ * Central registry for format adapters with auto-detection and lookup capabilities.
5
+ */
6
+
7
+ import type { FormatAdapter, DetectionResult, PathDetectionHint } from './types.js';
8
+
9
+ /**
10
+ * Registry for managing format adapters
11
+ *
12
+ * Provides:
13
+ * - Adapter registration and lookup
14
+ * - Auto-detection of formats from content
15
+ * - Extension-based adapter selection
16
+ */
17
+ export class AdapterRegistry {
18
+ private static instance: AdapterRegistry | null = null;
19
+ private adapters: Map<string, FormatAdapter> = new Map();
20
+
21
+ private constructor() {}
22
+
23
+ /**
24
+ * Get singleton instance
25
+ */
26
+ static getInstance(): AdapterRegistry {
27
+ if (!AdapterRegistry.instance) {
28
+ AdapterRegistry.instance = new AdapterRegistry();
29
+ }
30
+ return AdapterRegistry.instance;
31
+ }
32
+
33
+ /**
34
+ * Reset singleton instance (useful for testing)
35
+ */
36
+ static resetInstance(): void {
37
+ AdapterRegistry.instance = null;
38
+ }
39
+
40
+ /**
41
+ * Register an adapter
42
+ *
43
+ * @param adapter - Adapter to register
44
+ * @throws Error if adapter with same name already registered
45
+ */
46
+ register(adapter: FormatAdapter): void {
47
+ if (this.adapters.has(adapter.metadata.name)) {
48
+ throw new Error(`Adapter '${adapter.metadata.name}' is already registered`);
49
+ }
50
+ this.adapters.set(adapter.metadata.name, adapter);
51
+ }
52
+
53
+ /**
54
+ * Unregister an adapter by name
55
+ *
56
+ * @param name - Name of adapter to unregister
57
+ * @returns True if adapter was removed, false if not found
58
+ */
59
+ unregister(name: string): boolean {
60
+ return this.adapters.delete(name);
61
+ }
62
+
63
+ /**
64
+ * Get adapter by name
65
+ *
66
+ * @param name - Adapter name
67
+ * @returns Adapter instance or undefined
68
+ */
69
+ getAdapter(name: string): FormatAdapter | undefined {
70
+ return this.adapters.get(name);
71
+ }
72
+
73
+ /**
74
+ * Get adapter for a specific format identifier or extension
75
+ *
76
+ * @param format - Format identifier (e.g., "speckit") or extension (e.g., ".md")
77
+ * @returns First matching adapter or undefined
78
+ */
79
+ getAdapterForFormat(format: string): FormatAdapter | undefined {
80
+ for (const adapter of this.adapters.values()) {
81
+ if (adapter.canImport(format)) {
82
+ return adapter;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ /**
89
+ * Detect adapter from content
90
+ *
91
+ * Runs detection on all registered adapters and returns the best match.
92
+ *
93
+ * @param content - Content to analyze
94
+ * @param minConfidence - Minimum confidence score (0-100) to accept
95
+ * @returns Best matching adapter and detection result, or undefined
96
+ */
97
+ detectAdapter(
98
+ content: string,
99
+ minConfidence: number = 50
100
+ ): { adapter: FormatAdapter; detection: DetectionResult } | undefined {
101
+ let bestMatch: { adapter: FormatAdapter; detection: DetectionResult } | undefined;
102
+ let bestConfidence = minConfidence - 1;
103
+
104
+ for (const adapter of this.adapters.values()) {
105
+ const detection = adapter.detect(content);
106
+ if (detection.detected && detection.confidence > bestConfidence) {
107
+ bestConfidence = detection.confidence;
108
+ bestMatch = { adapter, detection };
109
+ }
110
+ }
111
+
112
+ return bestMatch;
113
+ }
114
+
115
+ /**
116
+ * Detect adapter from content with path hints
117
+ *
118
+ * Like detectAdapter but passes file path information to adapters
119
+ * that implement detectWithPath for improved accuracy.
120
+ *
121
+ * @param content - Content to analyze
122
+ * @param hint - File path and directory information
123
+ * @param minConfidence - Minimum confidence score (0-100) to accept
124
+ * @returns Best matching adapter and detection result, or undefined
125
+ */
126
+ detectAdapterWithPath(
127
+ content: string,
128
+ hint: PathDetectionHint,
129
+ minConfidence: number = 50
130
+ ): { adapter: FormatAdapter; detection: DetectionResult } | undefined {
131
+ let bestMatch: { adapter: FormatAdapter; detection: DetectionResult } | undefined;
132
+ let bestConfidence = minConfidence - 1;
133
+
134
+ for (const adapter of this.adapters.values()) {
135
+ const detection = adapter.detectWithPath
136
+ ? adapter.detectWithPath(content, hint)
137
+ : adapter.detect(content);
138
+
139
+ if (detection.detected && detection.confidence > bestConfidence) {
140
+ bestConfidence = detection.confidence;
141
+ bestMatch = { adapter, detection };
142
+ }
143
+ }
144
+
145
+ return bestMatch;
146
+ }
147
+
148
+ /**
149
+ * Get all detection results for content
150
+ *
151
+ * Useful for debugging or showing user multiple format possibilities.
152
+ *
153
+ * @param content - Content to analyze
154
+ * @returns Array of detection results for all adapters, sorted by confidence
155
+ */
156
+ detectAll(content: string): Array<{ adapter: FormatAdapter; detection: DetectionResult }> {
157
+ const results = Array.from(this.adapters.values()).map((adapter) => ({
158
+ adapter,
159
+ detection: adapter.detect(content),
160
+ }));
161
+
162
+ return results.sort((a, b) => b.detection.confidence - a.detection.confidence);
163
+ }
164
+
165
+ /**
166
+ * List all registered adapters
167
+ *
168
+ * @returns Array of all adapters
169
+ */
170
+ listAdapters(): ReadonlyArray<FormatAdapter> {
171
+ return Array.from(this.adapters.values());
172
+ }
173
+
174
+ /**
175
+ * List all adapter names
176
+ *
177
+ * @returns Array of adapter names
178
+ */
179
+ listAdapterNames(): ReadonlyArray<string> {
180
+ return Array.from(this.adapters.keys());
181
+ }
182
+
183
+ /**
184
+ * List all supported formats across all adapters
185
+ *
186
+ * @returns Array of unique format identifiers
187
+ */
188
+ listSupportedFormats(): ReadonlyArray<string> {
189
+ const formats = new Set<string>();
190
+ for (const adapter of this.adapters.values()) {
191
+ for (const format of adapter.metadata.formats) {
192
+ formats.add(format);
193
+ }
194
+ }
195
+ return Array.from(formats).sort();
196
+ }
197
+
198
+ /**
199
+ * List all supported extensions across all adapters
200
+ *
201
+ * @returns Array of unique file extensions
202
+ */
203
+ listSupportedExtensions(): ReadonlyArray<string> {
204
+ const extensions = new Set<string>();
205
+ for (const adapter of this.adapters.values()) {
206
+ for (const ext of adapter.metadata.extensions) {
207
+ extensions.add(ext);
208
+ }
209
+ }
210
+ return Array.from(extensions).sort();
211
+ }
212
+
213
+ /**
214
+ * Get adapters that can import a specific format
215
+ *
216
+ * @param format - Format identifier or extension
217
+ * @returns Array of adapters that support this format for import
218
+ */
219
+ getImportAdapters(format: string): ReadonlyArray<FormatAdapter> {
220
+ return Array.from(this.adapters.values()).filter((adapter) => adapter.canImport(format));
221
+ }
222
+
223
+ /**
224
+ * Get adapters that can export to a specific format
225
+ *
226
+ * @param format - Format identifier or extension
227
+ * @returns Array of adapters that support this format for export
228
+ */
229
+ getExportAdapters(format: string): ReadonlyArray<FormatAdapter> {
230
+ return Array.from(this.adapters.values()).filter((adapter) => adapter.canExport(format));
231
+ }
232
+
233
+ /**
234
+ * Check if any adapter supports a format
235
+ *
236
+ * @param format - Format identifier or extension
237
+ * @returns True if at least one adapter supports this format
238
+ */
239
+ isFormatSupported(format: string): boolean {
240
+ return this.getAdapterForFormat(format) !== undefined;
241
+ }
242
+
243
+ /**
244
+ * Clear all registered adapters
245
+ *
246
+ * Mainly useful for testing.
247
+ */
248
+ clear(): void {
249
+ this.adapters.clear();
250
+ }
251
+
252
+ /**
253
+ * Get number of registered adapters
254
+ */
255
+ get size(): number {
256
+ return this.adapters.size;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Default singleton instance
262
+ */
263
+ export const registry = AdapterRegistry.getInstance();