bluera-knowledge 0.10.0 → 0.11.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +98 -2
- package/commands/sync.md +96 -0
- package/dist/{chunk-ITH6FWQY.js → chunk-2WBITQWZ.js} +24 -3
- package/dist/{chunk-ITH6FWQY.js.map → chunk-2WBITQWZ.js.map} +1 -1
- package/dist/{chunk-CUHYSPRV.js → chunk-565OVW3C.js} +999 -2
- package/dist/chunk-565OVW3C.js.map +1 -0
- package/dist/{chunk-DWAIT2OD.js → chunk-TRDMYKGC.js} +190 -5
- package/dist/chunk-TRDMYKGC.js.map +1 -0
- package/dist/index.js +217 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/package.json +1 -1
- package/src/analysis/adapter-registry.test.ts +211 -0
- package/src/analysis/adapter-registry.ts +155 -0
- package/src/analysis/language-adapter.ts +127 -0
- package/src/analysis/parser-factory.test.ts +79 -1
- package/src/analysis/parser-factory.ts +8 -0
- package/src/analysis/zil/index.ts +34 -0
- package/src/analysis/zil/zil-adapter.test.ts +187 -0
- package/src/analysis/zil/zil-adapter.ts +121 -0
- package/src/analysis/zil/zil-lexer.test.ts +222 -0
- package/src/analysis/zil/zil-lexer.ts +239 -0
- package/src/analysis/zil/zil-parser.test.ts +210 -0
- package/src/analysis/zil/zil-parser.ts +360 -0
- package/src/analysis/zil/zil-special-forms.ts +193 -0
- package/src/cli/commands/sync.test.ts +54 -0
- package/src/cli/commands/sync.ts +264 -0
- package/src/cli/index.ts +1 -0
- package/src/crawl/claude-client.test.ts +56 -0
- package/src/crawl/claude-client.ts +27 -1
- package/src/index.ts +8 -0
- package/src/mcp/commands/index.ts +2 -0
- package/src/mcp/commands/sync.commands.test.ts +283 -0
- package/src/mcp/commands/sync.commands.ts +233 -0
- package/src/mcp/server.ts +9 -1
- package/src/services/gitignore.service.test.ts +157 -0
- package/src/services/gitignore.service.ts +132 -0
- package/src/services/store-definition.service.test.ts +440 -0
- package/src/services/store-definition.service.ts +198 -0
- package/src/services/store.service.test.ts +279 -1
- package/src/services/store.service.ts +101 -4
- package/src/types/index.ts +18 -0
- package/src/types/store-definition.test.ts +492 -0
- package/src/types/store-definition.ts +129 -0
- package/dist/chunk-CUHYSPRV.js.map +0 -1
- package/dist/chunk-DWAIT2OD.js.map +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
IntelligentCrawler
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-2WBITQWZ.js";
|
|
5
5
|
import {
|
|
6
6
|
JobService,
|
|
7
7
|
createDocumentId,
|
|
8
8
|
createServices,
|
|
9
9
|
createStoreId
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-TRDMYKGC.js";
|
|
11
11
|
import "../chunk-6FHWC36B.js";
|
|
12
12
|
|
|
13
13
|
// src/workers/background-worker.ts
|
package/package.json
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AdapterRegistry } from './adapter-registry.js';
|
|
3
|
+
import type { LanguageAdapter } from './language-adapter.js';
|
|
4
|
+
import type { CodeNode, ImportInfo } from './ast-parser.js';
|
|
5
|
+
import type { GraphEdge } from './code-graph.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mock adapter for testing
|
|
9
|
+
*/
|
|
10
|
+
function createMockAdapter(overrides: Partial<LanguageAdapter> = {}): LanguageAdapter {
|
|
11
|
+
return {
|
|
12
|
+
languageId: 'test-lang',
|
|
13
|
+
extensions: ['.test'],
|
|
14
|
+
displayName: 'Test Language',
|
|
15
|
+
parse: (): CodeNode[] => [],
|
|
16
|
+
extractImports: (): ImportInfo[] => [],
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('AdapterRegistry', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Reset singleton for each test
|
|
24
|
+
AdapterRegistry.resetInstance();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getInstance', () => {
|
|
28
|
+
it('should return the same instance on multiple calls', () => {
|
|
29
|
+
const instance1 = AdapterRegistry.getInstance();
|
|
30
|
+
const instance2 = AdapterRegistry.getInstance();
|
|
31
|
+
expect(instance1).toBe(instance2);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('register', () => {
|
|
36
|
+
it('should register an adapter', () => {
|
|
37
|
+
const registry = AdapterRegistry.getInstance();
|
|
38
|
+
const adapter = createMockAdapter();
|
|
39
|
+
|
|
40
|
+
registry.register(adapter);
|
|
41
|
+
|
|
42
|
+
expect(registry.getByLanguageId('test-lang')).toBe(adapter);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should be idempotent when registering same languageId', () => {
|
|
46
|
+
const registry = AdapterRegistry.getInstance();
|
|
47
|
+
const adapter1 = createMockAdapter();
|
|
48
|
+
const adapter2 = createMockAdapter();
|
|
49
|
+
|
|
50
|
+
registry.register(adapter1);
|
|
51
|
+
registry.register(adapter2); // Should not throw, just skip
|
|
52
|
+
|
|
53
|
+
// First adapter should still be registered
|
|
54
|
+
expect(registry.getByLanguageId('test-lang')).toBe(adapter1);
|
|
55
|
+
expect(registry.getAllAdapters()).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw if extension is already registered by another adapter', () => {
|
|
59
|
+
const registry = AdapterRegistry.getInstance();
|
|
60
|
+
const adapter1 = createMockAdapter({ languageId: 'lang1', extensions: ['.test'] });
|
|
61
|
+
const adapter2 = createMockAdapter({ languageId: 'lang2', extensions: ['.test'] });
|
|
62
|
+
|
|
63
|
+
registry.register(adapter1);
|
|
64
|
+
|
|
65
|
+
expect(() => registry.register(adapter2)).toThrow(
|
|
66
|
+
'Extension ".test" is already registered by adapter "lang1"'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getByExtension', () => {
|
|
72
|
+
it('should return adapter by extension', () => {
|
|
73
|
+
const registry = AdapterRegistry.getInstance();
|
|
74
|
+
const adapter = createMockAdapter({ extensions: ['.zil', '.mud'] });
|
|
75
|
+
|
|
76
|
+
registry.register(adapter);
|
|
77
|
+
|
|
78
|
+
expect(registry.getByExtension('.zil')).toBe(adapter);
|
|
79
|
+
expect(registry.getByExtension('.mud')).toBe(adapter);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return undefined for unregistered extension', () => {
|
|
83
|
+
const registry = AdapterRegistry.getInstance();
|
|
84
|
+
|
|
85
|
+
expect(registry.getByExtension('.unknown')).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should normalize extension with leading dot', () => {
|
|
89
|
+
const registry = AdapterRegistry.getInstance();
|
|
90
|
+
const adapter = createMockAdapter({ extensions: ['.zil'] });
|
|
91
|
+
|
|
92
|
+
registry.register(adapter);
|
|
93
|
+
|
|
94
|
+
// Both with and without dot should work
|
|
95
|
+
expect(registry.getByExtension('.zil')).toBe(adapter);
|
|
96
|
+
expect(registry.getByExtension('zil')).toBe(adapter);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('getByLanguageId', () => {
|
|
101
|
+
it('should return adapter by language ID', () => {
|
|
102
|
+
const registry = AdapterRegistry.getInstance();
|
|
103
|
+
const adapter = createMockAdapter({ languageId: 'zil' });
|
|
104
|
+
|
|
105
|
+
registry.register(adapter);
|
|
106
|
+
|
|
107
|
+
expect(registry.getByLanguageId('zil')).toBe(adapter);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return undefined for unregistered language ID', () => {
|
|
111
|
+
const registry = AdapterRegistry.getInstance();
|
|
112
|
+
|
|
113
|
+
expect(registry.getByLanguageId('unknown')).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('getAllAdapters', () => {
|
|
118
|
+
it('should return empty array when no adapters registered', () => {
|
|
119
|
+
const registry = AdapterRegistry.getInstance();
|
|
120
|
+
|
|
121
|
+
expect(registry.getAllAdapters()).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return all registered adapters', () => {
|
|
125
|
+
const registry = AdapterRegistry.getInstance();
|
|
126
|
+
const adapter1 = createMockAdapter({ languageId: 'lang1', extensions: ['.l1'] });
|
|
127
|
+
const adapter2 = createMockAdapter({ languageId: 'lang2', extensions: ['.l2'] });
|
|
128
|
+
|
|
129
|
+
registry.register(adapter1);
|
|
130
|
+
registry.register(adapter2);
|
|
131
|
+
|
|
132
|
+
const adapters = registry.getAllAdapters();
|
|
133
|
+
expect(adapters).toHaveLength(2);
|
|
134
|
+
expect(adapters).toContain(adapter1);
|
|
135
|
+
expect(adapters).toContain(adapter2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('unregister', () => {
|
|
140
|
+
it('should unregister an adapter by language ID', () => {
|
|
141
|
+
const registry = AdapterRegistry.getInstance();
|
|
142
|
+
const adapter = createMockAdapter({ languageId: 'zil', extensions: ['.zil'] });
|
|
143
|
+
|
|
144
|
+
registry.register(adapter);
|
|
145
|
+
expect(registry.getByLanguageId('zil')).toBe(adapter);
|
|
146
|
+
|
|
147
|
+
const result = registry.unregister('zil');
|
|
148
|
+
expect(result).toBe(true);
|
|
149
|
+
expect(registry.getByLanguageId('zil')).toBeUndefined();
|
|
150
|
+
expect(registry.getByExtension('.zil')).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return false when unregistering non-existent adapter', () => {
|
|
154
|
+
const registry = AdapterRegistry.getInstance();
|
|
155
|
+
|
|
156
|
+
const result = registry.unregister('nonexistent');
|
|
157
|
+
expect(result).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('hasExtension', () => {
|
|
162
|
+
it('should return true for registered extension', () => {
|
|
163
|
+
const registry = AdapterRegistry.getInstance();
|
|
164
|
+
const adapter = createMockAdapter({ extensions: ['.zil'] });
|
|
165
|
+
|
|
166
|
+
registry.register(adapter);
|
|
167
|
+
|
|
168
|
+
expect(registry.hasExtension('.zil')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return false for unregistered extension', () => {
|
|
172
|
+
const registry = AdapterRegistry.getInstance();
|
|
173
|
+
|
|
174
|
+
expect(registry.hasExtension('.unknown')).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('adapter with optional methods', () => {
|
|
179
|
+
it('should support adapters with chunk method', () => {
|
|
180
|
+
const registry = AdapterRegistry.getInstance();
|
|
181
|
+
const adapter = createMockAdapter({
|
|
182
|
+
chunk: () => [{ content: 'test', startLine: 1, endLine: 10 }],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
registry.register(adapter);
|
|
186
|
+
|
|
187
|
+
const retrieved = registry.getByLanguageId('test-lang');
|
|
188
|
+
expect(retrieved?.chunk).toBeDefined();
|
|
189
|
+
expect(retrieved?.chunk?.('', '')).toEqual([{ content: 'test', startLine: 1, endLine: 10 }]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should support adapters with analyzeCallRelationships method', () => {
|
|
193
|
+
const registry = AdapterRegistry.getInstance();
|
|
194
|
+
const mockEdge: GraphEdge = {
|
|
195
|
+
from: 'a',
|
|
196
|
+
to: 'b',
|
|
197
|
+
type: 'calls',
|
|
198
|
+
confidence: 1.0,
|
|
199
|
+
};
|
|
200
|
+
const adapter = createMockAdapter({
|
|
201
|
+
analyzeCallRelationships: () => [mockEdge],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
registry.register(adapter);
|
|
205
|
+
|
|
206
|
+
const retrieved = registry.getByLanguageId('test-lang');
|
|
207
|
+
expect(retrieved?.analyzeCallRelationships).toBeDefined();
|
|
208
|
+
expect(retrieved?.analyzeCallRelationships?.('', '')).toEqual([mockEdge]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Registry
|
|
3
|
+
*
|
|
4
|
+
* Singleton registry for language adapters. Provides lookup by extension
|
|
5
|
+
* or language ID.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Register an adapter
|
|
10
|
+
* const registry = AdapterRegistry.getInstance();
|
|
11
|
+
* registry.register(zilAdapter);
|
|
12
|
+
*
|
|
13
|
+
* // Look up by extension
|
|
14
|
+
* const adapter = registry.getByExtension('.zil');
|
|
15
|
+
* if (adapter) {
|
|
16
|
+
* const nodes = adapter.parse(content, filePath);
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { LanguageAdapter } from './language-adapter.js';
|
|
22
|
+
|
|
23
|
+
export class AdapterRegistry {
|
|
24
|
+
private static instance: AdapterRegistry | undefined;
|
|
25
|
+
|
|
26
|
+
/** Map from languageId to adapter */
|
|
27
|
+
private readonly adaptersByLanguageId = new Map<string, LanguageAdapter>();
|
|
28
|
+
|
|
29
|
+
/** Map from extension to adapter */
|
|
30
|
+
private readonly adaptersByExtension = new Map<string, LanguageAdapter>();
|
|
31
|
+
|
|
32
|
+
private constructor() {
|
|
33
|
+
// Private constructor for singleton
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the singleton instance of the registry.
|
|
38
|
+
*/
|
|
39
|
+
static getInstance(): AdapterRegistry {
|
|
40
|
+
AdapterRegistry.instance ??= new AdapterRegistry();
|
|
41
|
+
return AdapterRegistry.instance;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reset the singleton instance (for testing).
|
|
46
|
+
*/
|
|
47
|
+
static resetInstance(): void {
|
|
48
|
+
AdapterRegistry.instance = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a language adapter.
|
|
53
|
+
*
|
|
54
|
+
* @param adapter - The adapter to register
|
|
55
|
+
* @throws If a different adapter with the same extension is already registered
|
|
56
|
+
*/
|
|
57
|
+
register(adapter: LanguageAdapter): void {
|
|
58
|
+
// Skip if already registered with same languageId (idempotent)
|
|
59
|
+
if (this.adaptersByLanguageId.has(adapter.languageId)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for extension conflicts with other adapters
|
|
64
|
+
for (const ext of adapter.extensions) {
|
|
65
|
+
const normalizedExt = this.normalizeExtension(ext);
|
|
66
|
+
const existingAdapter = this.adaptersByExtension.get(normalizedExt);
|
|
67
|
+
if (existingAdapter !== undefined) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Extension "${normalizedExt}" is already registered by adapter "${existingAdapter.languageId}"`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Register by languageId
|
|
75
|
+
this.adaptersByLanguageId.set(adapter.languageId, adapter);
|
|
76
|
+
|
|
77
|
+
// Register by each extension
|
|
78
|
+
for (const ext of adapter.extensions) {
|
|
79
|
+
const normalizedExt = this.normalizeExtension(ext);
|
|
80
|
+
this.adaptersByExtension.set(normalizedExt, adapter);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unregister a language adapter by its language ID.
|
|
86
|
+
*
|
|
87
|
+
* @param languageId - The language ID to unregister
|
|
88
|
+
* @returns true if the adapter was found and removed, false otherwise
|
|
89
|
+
*/
|
|
90
|
+
unregister(languageId: string): boolean {
|
|
91
|
+
const adapter = this.adaptersByLanguageId.get(languageId);
|
|
92
|
+
if (adapter === undefined) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove from languageId map
|
|
97
|
+
this.adaptersByLanguageId.delete(languageId);
|
|
98
|
+
|
|
99
|
+
// Remove from extension map
|
|
100
|
+
for (const ext of adapter.extensions) {
|
|
101
|
+
const normalizedExt = this.normalizeExtension(ext);
|
|
102
|
+
this.adaptersByExtension.delete(normalizedExt);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get an adapter by file extension.
|
|
110
|
+
*
|
|
111
|
+
* @param ext - File extension (with or without leading dot)
|
|
112
|
+
* @returns The adapter if found, undefined otherwise
|
|
113
|
+
*/
|
|
114
|
+
getByExtension(ext: string): LanguageAdapter | undefined {
|
|
115
|
+
const normalizedExt = this.normalizeExtension(ext);
|
|
116
|
+
return this.adaptersByExtension.get(normalizedExt);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get an adapter by language ID.
|
|
121
|
+
*
|
|
122
|
+
* @param languageId - The unique language identifier
|
|
123
|
+
* @returns The adapter if found, undefined otherwise
|
|
124
|
+
*/
|
|
125
|
+
getByLanguageId(languageId: string): LanguageAdapter | undefined {
|
|
126
|
+
return this.adaptersByLanguageId.get(languageId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all registered adapters.
|
|
131
|
+
*
|
|
132
|
+
* @returns Array of all registered adapters
|
|
133
|
+
*/
|
|
134
|
+
getAllAdapters(): LanguageAdapter[] {
|
|
135
|
+
return Array.from(this.adaptersByLanguageId.values());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if an extension is registered.
|
|
140
|
+
*
|
|
141
|
+
* @param ext - File extension (with or without leading dot)
|
|
142
|
+
* @returns true if the extension is registered
|
|
143
|
+
*/
|
|
144
|
+
hasExtension(ext: string): boolean {
|
|
145
|
+
const normalizedExt = this.normalizeExtension(ext);
|
|
146
|
+
return this.adaptersByExtension.has(normalizedExt);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalize extension to always have a leading dot.
|
|
151
|
+
*/
|
|
152
|
+
private normalizeExtension(ext: string): string {
|
|
153
|
+
return ext.startsWith('.') ? ext : `.${ext}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for custom language support in bluera-knowledge.
|
|
5
|
+
* Adapters provide language-specific parsing, import extraction, chunking,
|
|
6
|
+
* and call relationship analysis.
|
|
7
|
+
*
|
|
8
|
+
* Built-in adapters: ZIL (Zork Implementation Language)
|
|
9
|
+
* Users can create custom adapters for any language.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CodeNode, ImportInfo } from './ast-parser.js';
|
|
13
|
+
import type { GraphEdge } from './code-graph.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of chunking a file into logical units
|
|
17
|
+
*/
|
|
18
|
+
export interface ChunkResult {
|
|
19
|
+
/** The content of the chunk */
|
|
20
|
+
content: string;
|
|
21
|
+
/** Starting line number (1-based) */
|
|
22
|
+
startLine: number;
|
|
23
|
+
/** Ending line number (1-based) */
|
|
24
|
+
endLine: number;
|
|
25
|
+
/** Optional symbol name this chunk represents */
|
|
26
|
+
symbolName?: string;
|
|
27
|
+
/** Optional symbol kind (routine, object, class, etc.) */
|
|
28
|
+
symbolKind?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Interface for language-specific parsing and analysis.
|
|
33
|
+
*
|
|
34
|
+
* Adapters enable full graph support for custom languages:
|
|
35
|
+
* - Smart chunking by language constructs
|
|
36
|
+
* - Symbol extraction (functions, classes, objects)
|
|
37
|
+
* - Import/include relationship tracking
|
|
38
|
+
* - Call graph analysis
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const zilAdapter: LanguageAdapter = {
|
|
43
|
+
* languageId: 'zil',
|
|
44
|
+
* extensions: ['.zil', '.mud'],
|
|
45
|
+
* displayName: 'ZIL (Zork Implementation Language)',
|
|
46
|
+
*
|
|
47
|
+
* parse(content, filePath) {
|
|
48
|
+
* // Extract routines, objects, rooms, globals
|
|
49
|
+
* return [...];
|
|
50
|
+
* },
|
|
51
|
+
*
|
|
52
|
+
* extractImports(content, filePath) {
|
|
53
|
+
* // Find INSERT-FILE directives
|
|
54
|
+
* return [...];
|
|
55
|
+
* },
|
|
56
|
+
*
|
|
57
|
+
* chunk(content, filePath) {
|
|
58
|
+
* // Split by top-level forms
|
|
59
|
+
* return [...];
|
|
60
|
+
* }
|
|
61
|
+
* };
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export interface LanguageAdapter {
|
|
65
|
+
/**
|
|
66
|
+
* Unique identifier for this language.
|
|
67
|
+
* Used for registry lookup and configuration.
|
|
68
|
+
* @example 'zil', 'cobol', 'fortran'
|
|
69
|
+
*/
|
|
70
|
+
readonly languageId: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* File extensions this adapter handles (with leading dot).
|
|
74
|
+
* @example ['.zil', '.mud'] or ['.cbl', '.cob']
|
|
75
|
+
*/
|
|
76
|
+
readonly extensions: string[];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Human-readable name for the language.
|
|
80
|
+
* @example 'ZIL (Zork Implementation Language)'
|
|
81
|
+
*/
|
|
82
|
+
readonly displayName: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse file content and extract code symbols.
|
|
86
|
+
*
|
|
87
|
+
* @param content - File content as string
|
|
88
|
+
* @param filePath - Path to the file (for error messages)
|
|
89
|
+
* @returns Array of code nodes (functions, classes, etc.)
|
|
90
|
+
*/
|
|
91
|
+
parse(content: string, filePath: string): CodeNode[];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract import/include statements from file content.
|
|
95
|
+
*
|
|
96
|
+
* @param content - File content as string
|
|
97
|
+
* @param filePath - Path to the file (for resolving relative imports)
|
|
98
|
+
* @returns Array of import information
|
|
99
|
+
*/
|
|
100
|
+
extractImports(content: string, filePath: string): ImportInfo[];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional: Split file into logical chunks for indexing.
|
|
104
|
+
*
|
|
105
|
+
* If not provided, the default chunking strategy is used.
|
|
106
|
+
* Custom chunking improves search quality by aligning chunks
|
|
107
|
+
* with language constructs (functions, classes, etc.).
|
|
108
|
+
*
|
|
109
|
+
* @param content - File content as string
|
|
110
|
+
* @param filePath - Path to the file
|
|
111
|
+
* @returns Array of chunk results
|
|
112
|
+
*/
|
|
113
|
+
chunk?(content: string, filePath: string): ChunkResult[];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Optional: Analyze call relationships within a file.
|
|
117
|
+
*
|
|
118
|
+
* If not provided, the default regex-based call detection is used.
|
|
119
|
+
* Custom analysis can filter language-specific special forms
|
|
120
|
+
* and provide higher-confidence edges.
|
|
121
|
+
*
|
|
122
|
+
* @param content - File content as string
|
|
123
|
+
* @param filePath - Path to the file
|
|
124
|
+
* @returns Array of graph edges representing calls
|
|
125
|
+
*/
|
|
126
|
+
analyzeCallRelationships?(content: string, filePath: string): GraphEdge[];
|
|
127
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { ParserFactory } from './parser-factory.js';
|
|
3
3
|
import type { PythonBridge, ParsePythonResult } from '../crawl/bridge.js';
|
|
4
|
+
import { AdapterRegistry } from './adapter-registry.js';
|
|
5
|
+
import type { LanguageAdapter } from './language-adapter.js';
|
|
6
|
+
import type { CodeNode, ImportInfo } from './ast-parser.js';
|
|
4
7
|
|
|
5
8
|
describe('ParserFactory', () => {
|
|
6
9
|
describe('parseFile', () => {
|
|
@@ -129,4 +132,79 @@ describe('ParserFactory', () => {
|
|
|
129
132
|
expect(nodes).toEqual([]);
|
|
130
133
|
});
|
|
131
134
|
});
|
|
135
|
+
|
|
136
|
+
describe('adapter integration', () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
// Reset adapter registry before each test
|
|
139
|
+
AdapterRegistry.resetInstance();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should use registered adapter for unknown extension', async () => {
|
|
143
|
+
const mockNodes: CodeNode[] = [
|
|
144
|
+
{
|
|
145
|
+
type: 'function',
|
|
146
|
+
name: 'V-LOOK',
|
|
147
|
+
exported: true,
|
|
148
|
+
startLine: 1,
|
|
149
|
+
endLine: 5,
|
|
150
|
+
signature: 'ROUTINE V-LOOK ()',
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const mockAdapter: LanguageAdapter = {
|
|
155
|
+
languageId: 'zil',
|
|
156
|
+
extensions: ['.zil'],
|
|
157
|
+
displayName: 'ZIL',
|
|
158
|
+
parse: vi.fn().mockReturnValue(mockNodes),
|
|
159
|
+
extractImports: vi.fn().mockReturnValue([]),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const registry = AdapterRegistry.getInstance();
|
|
163
|
+
registry.register(mockAdapter);
|
|
164
|
+
|
|
165
|
+
const factory = new ParserFactory();
|
|
166
|
+
const code = '<ROUTINE V-LOOK () <TELL "You see nothing special.">>';
|
|
167
|
+
const nodes = await factory.parseFile('actions.zil', code);
|
|
168
|
+
|
|
169
|
+
expect(mockAdapter.parse).toHaveBeenCalledWith(code, 'actions.zil');
|
|
170
|
+
expect(nodes).toEqual(mockNodes);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should prefer built-in parser over adapter for supported extensions', async () => {
|
|
174
|
+
// Register an adapter that claims to handle .ts files
|
|
175
|
+
const mockAdapter: LanguageAdapter = {
|
|
176
|
+
languageId: 'fake-ts',
|
|
177
|
+
extensions: ['.ts'],
|
|
178
|
+
displayName: 'Fake TypeScript',
|
|
179
|
+
parse: vi.fn().mockReturnValue([]),
|
|
180
|
+
extractImports: vi.fn().mockReturnValue([]),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// This should throw because .ts is handled by built-in
|
|
184
|
+
// But we want built-in to take precedence, so adapter shouldn't even be called
|
|
185
|
+
// Actually, registration should work, but built-in should be used first
|
|
186
|
+
|
|
187
|
+
// For now, test that built-in works even if we could register
|
|
188
|
+
const factory = new ParserFactory();
|
|
189
|
+
const code = 'export function hello(): string { return "world"; }';
|
|
190
|
+
const nodes = await factory.parseFile('test.ts', code);
|
|
191
|
+
|
|
192
|
+
// Built-in parser should work
|
|
193
|
+
expect(nodes).toHaveLength(1);
|
|
194
|
+
expect(nodes[0]).toMatchObject({
|
|
195
|
+
type: 'function',
|
|
196
|
+
name: 'hello',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Adapter should not have been called (it wasn't registered in this test)
|
|
200
|
+
expect(mockAdapter.parse).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should return empty array when no adapter and unsupported extension', async () => {
|
|
204
|
+
const factory = new ParserFactory();
|
|
205
|
+
const nodes = await factory.parseFile('unknown.xyz', 'some content');
|
|
206
|
+
|
|
207
|
+
expect(nodes).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
132
210
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { AdapterRegistry } from './adapter-registry.js';
|
|
2
3
|
import { ASTParser, type CodeNode } from './ast-parser.js';
|
|
3
4
|
import { GoASTParser } from './go-ast-parser.js';
|
|
4
5
|
import { PythonASTParser } from './python-ast-parser.js';
|
|
@@ -39,6 +40,13 @@ export class ParserFactory {
|
|
|
39
40
|
return parser.parse(code, filePath);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// Check for registered language adapters
|
|
44
|
+
const registry = AdapterRegistry.getInstance();
|
|
45
|
+
const adapter = registry.getByExtension(ext);
|
|
46
|
+
if (adapter !== undefined) {
|
|
47
|
+
return adapter.parse(code, filePath);
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
return [];
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZIL (Zork Implementation Language) Support
|
|
3
|
+
*
|
|
4
|
+
* Provides full graph support for ZIL source files:
|
|
5
|
+
* - Lexer: Tokenizes ZIL syntax
|
|
6
|
+
* - Parser: Extracts symbols, imports, and calls
|
|
7
|
+
* - Adapter: Implements LanguageAdapter interface
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { ZilAdapter } from './zil/index.js';
|
|
12
|
+
* import { AdapterRegistry } from './adapter-registry.js';
|
|
13
|
+
*
|
|
14
|
+
* // Register ZIL support
|
|
15
|
+
* const registry = AdapterRegistry.getInstance();
|
|
16
|
+
* registry.register(new ZilAdapter());
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { ZilLexer, TokenType, type Token } from './zil-lexer.js';
|
|
21
|
+
export {
|
|
22
|
+
ZilParser,
|
|
23
|
+
type ZilForm,
|
|
24
|
+
type ZilNode,
|
|
25
|
+
type ZilSymbol,
|
|
26
|
+
type ZilCall,
|
|
27
|
+
} from './zil-parser.js';
|
|
28
|
+
export { ZilAdapter } from './zil-adapter.js';
|
|
29
|
+
export {
|
|
30
|
+
ZIL_SPECIAL_FORMS,
|
|
31
|
+
ZIL_DEFINITION_FORMS,
|
|
32
|
+
isSpecialForm,
|
|
33
|
+
isDefinitionForm,
|
|
34
|
+
} from './zil-special-forms.js';
|