@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,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,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();
|