brainclaw 1.9.1 → 1.10.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/README.md +47 -1
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/mcp.js +121 -0
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/switch.js +100 -89
- package/dist/core/agent-files.js +12 -0
- package/dist/core/code-map/backend.js +123 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +108 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +568 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +288 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/store-resolution.js +41 -4
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/cli.md +46 -8
- package/docs/code-map.md +198 -0
- package/docs/integrations/mcp.md +13 -6
- package/docs/mcp-schema-changelog.md +7 -3
- package/docs/quickstart.md +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { getParser, getQueryClass } from '../wasm-loader.js';
|
|
2
|
+
/** Process-wide compiled-query cache, keyed `${providerId}|${lang}|${queryHash}`. */
|
|
3
|
+
const queryCache = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Compile a query asset ONCE per `(providerId, lang, queryHash)` and cache it.
|
|
6
|
+
* Compile failure is fatal + loud (spec §7 — a broken bundled asset fails here,
|
|
7
|
+
* never a silent per-file skip).
|
|
8
|
+
*/
|
|
9
|
+
async function compileCached(providerId, lang, queryHash, grammar, source) {
|
|
10
|
+
const key = `${providerId}|${lang}|${queryHash}`;
|
|
11
|
+
const hit = queryCache.get(key);
|
|
12
|
+
if (hit)
|
|
13
|
+
return hit;
|
|
14
|
+
const Query = await getQueryClass();
|
|
15
|
+
// The loader's `Query` comes from the SAME engine-glue instance the grammar was
|
|
16
|
+
// loaded against (and after initEngine), so the Emscripten Module is live here.
|
|
17
|
+
// Cast to the structural shape we consume.
|
|
18
|
+
const compiled = new Query(grammar, source); // throws loudly on a broken asset
|
|
19
|
+
queryCache.set(key, compiled);
|
|
20
|
+
return compiled;
|
|
21
|
+
}
|
|
22
|
+
/** Test/maintenance seam: drop the compiled-query cache. */
|
|
23
|
+
export function __resetQueryCache() {
|
|
24
|
+
queryCache.clear();
|
|
25
|
+
}
|
|
26
|
+
/** Test seam: number of distinct compiled queries currently cached. */
|
|
27
|
+
export function __queryCacheSize() {
|
|
28
|
+
return queryCache.size;
|
|
29
|
+
}
|
|
30
|
+
function spanOf(node) {
|
|
31
|
+
return {
|
|
32
|
+
start_line: node.startPosition.row + 1,
|
|
33
|
+
start_col: node.startPosition.column + 1,
|
|
34
|
+
end_line: node.endPosition.row + 1,
|
|
35
|
+
end_col: node.endPosition.column + 1,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Strip surrounding quotes from a module specifier text. */
|
|
39
|
+
function stripQuotes(text) {
|
|
40
|
+
return text.replace(/^['"`]|['"`]$/g, '');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Walk up from a capture node to the enclosing import/export STATEMENT for
|
|
44
|
+
* span/ordinal anchoring, using the PROVIDER-LOCAL set of grammar node types that
|
|
45
|
+
* count as an import/export statement (cadrage §3 / Codex R1 langs#3-4).
|
|
46
|
+
*
|
|
47
|
+
* Each provider declares its OWN statement node types via
|
|
48
|
+
* {@link QueryRuntimeInput.enclosingStatementNodeTypes} (JS/TS
|
|
49
|
+
* `import_statement`/`export_statement`; Python `import_statement`/
|
|
50
|
+
* `import_from_statement`; PHP `namespace_use_declaration`; Java
|
|
51
|
+
* `import_declaration`). The runtime uses ONLY the set passed for the CURRENT file
|
|
52
|
+
* and NEVER imports/consults a central registry — adding a language is a provider
|
|
53
|
+
* declaration, not an edit here. This is the rule-of-N generalization the P1b
|
|
54
|
+
* review predicted: the runtime previously hard-coded the JS/Python statement
|
|
55
|
+
* names, which would have silently mis-spanned PHP/Java import statements.
|
|
56
|
+
*/
|
|
57
|
+
function enclosingStatement(node, statementTypes) {
|
|
58
|
+
let n = node;
|
|
59
|
+
while (n) {
|
|
60
|
+
if (statementTypes.has(n.type))
|
|
61
|
+
return n;
|
|
62
|
+
n = n.parent;
|
|
63
|
+
}
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
/** Parse a `@definition.<subtype>.<role>` capture name into its parts. */
|
|
67
|
+
function parseDefinitionCapture(name) {
|
|
68
|
+
const m = /^definition\.(.+)\.(node|name|exported)$/.exec(name);
|
|
69
|
+
if (!m)
|
|
70
|
+
return null;
|
|
71
|
+
return { subtype: m[1], role: m[2] };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The fixed import/export capture roles the runtime `switch` honors. Together with
|
|
75
|
+
* the `definition.<subtype>.(node|name|exported)` pattern, this is the COMPLETE
|
|
76
|
+
* hard-coded capture convention — THE contract (P1b §3.4). A provider's captureMap
|
|
77
|
+
* may only use these capture names.
|
|
78
|
+
*/
|
|
79
|
+
const FIXED_IMPORT_EXPORT_CAPTURES = new Set([
|
|
80
|
+
'import.source',
|
|
81
|
+
'import.default.name',
|
|
82
|
+
'import.namespace.name',
|
|
83
|
+
'import.named.name',
|
|
84
|
+
'export.name',
|
|
85
|
+
]);
|
|
86
|
+
/** True iff `name` is a capture role the hard-coded runtime convention recognizes. */
|
|
87
|
+
export function isConventionCapture(name) {
|
|
88
|
+
return parseDefinitionCapture(name) !== null || FIXED_IMPORT_EXPORT_CAPTURES.has(name);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validate that a provider's declared `captureMap` mirrors the hard-coded capture
|
|
92
|
+
* convention the runtime actually drives off (P1b §3.4). Throws (loud — a provider
|
|
93
|
+
* authoring bug, not a per-file error) listing any capture the runtime would
|
|
94
|
+
* silently ignore. The runtime does NOT call this on the hot path; providers/tests
|
|
95
|
+
* call it once to assert their captureMap is honest. Returns the offending captures
|
|
96
|
+
* for assertion ergonomics.
|
|
97
|
+
*/
|
|
98
|
+
export function assertCaptureMapConforms(captureMap) {
|
|
99
|
+
const unknown = captureMap.map((m) => m.capture).filter((c) => !isConventionCapture(c));
|
|
100
|
+
if (unknown.length > 0) {
|
|
101
|
+
throw new Error(`captureMap declares captures the runtime convention does not recognize: ${unknown.join(', ')}. ` +
|
|
102
|
+
`The hard-coded convention is THE contract (definition.<subtype>.(node|name|exported) + ` +
|
|
103
|
+
`import.source/import.default.name/import.namespace.name/import.named.name/export.name).`);
|
|
104
|
+
}
|
|
105
|
+
return unknown;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Run the provider's tags + imports queries over one file and produce a draft.
|
|
109
|
+
* Never throws: parse/timeout failures set `attributes.parseStatus='parse_error'`
|
|
110
|
+
* and return a file-only draft; query exceptions return the partial draft + an
|
|
111
|
+
* `extraction_error` fact.
|
|
112
|
+
*/
|
|
113
|
+
export async function extractWithQueries(input) {
|
|
114
|
+
const facts = [];
|
|
115
|
+
// PROVIDER-LOCAL enclosing-statement node types — the only source of truth for
|
|
116
|
+
// import/export span anchoring (cadrage §3 / Codex R1). No registry lookup.
|
|
117
|
+
const statementTypes = new Set(input.enclosingStatementNodeTypes);
|
|
118
|
+
const emptyDraft = (parseStatus, tree) => ({
|
|
119
|
+
file: { path: input.path },
|
|
120
|
+
definitions: [],
|
|
121
|
+
imports: [],
|
|
122
|
+
exports: [],
|
|
123
|
+
tests: [],
|
|
124
|
+
facts,
|
|
125
|
+
attributes: { parseStatus, __tree: tree },
|
|
126
|
+
});
|
|
127
|
+
// Oversized — never parse (mirrors legacy skipped_too_large; core emits file node).
|
|
128
|
+
if (input.sizeBytes > input.maxParseFileBytes) {
|
|
129
|
+
facts.push({
|
|
130
|
+
code: 'skipped_too_large',
|
|
131
|
+
message: `file ${input.sizeBytes} bytes exceeds max_parse_file_bytes ${input.maxParseFileBytes}`,
|
|
132
|
+
});
|
|
133
|
+
return emptyDraft('skipped_too_large', null);
|
|
134
|
+
}
|
|
135
|
+
// --- Parse (bounded by max_query_wait_ms) ---
|
|
136
|
+
let tree;
|
|
137
|
+
let parser = null;
|
|
138
|
+
try {
|
|
139
|
+
const grammar = await input.grammarForLang(input.lang);
|
|
140
|
+
const Parser = await getParser();
|
|
141
|
+
parser = new Parser();
|
|
142
|
+
// `grammarForLang` returns the engine-opaque grammar handle (typed `unknown` so
|
|
143
|
+
// the runtime stays provider-agnostic); it IS a web-tree-sitter Language here.
|
|
144
|
+
parser.setLanguage(grammar);
|
|
145
|
+
const deadline = input.maxQueryWaitMs;
|
|
146
|
+
if (typeof deadline === 'number' && deadline > 0) {
|
|
147
|
+
// web-tree-sitter parse is synchronous; the bound is advisory. We honor it by
|
|
148
|
+
// measuring wall-clock and flagging overruns (the legacy path had no timeout,
|
|
149
|
+
// so overrun → parse_error keeps us strictly no-worse than legacy).
|
|
150
|
+
const t0 = Date.now();
|
|
151
|
+
const parsed = parser.parse(input.source);
|
|
152
|
+
if (Date.now() - t0 > deadline) {
|
|
153
|
+
try {
|
|
154
|
+
parsed?.delete();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* best effort */
|
|
158
|
+
}
|
|
159
|
+
facts.push({ code: 'parse_error', message: `parse exceeded max_query_wait_ms ${deadline}` });
|
|
160
|
+
return emptyDraft('parse_error', null);
|
|
161
|
+
}
|
|
162
|
+
if (!parsed) {
|
|
163
|
+
facts.push({ code: 'parse_error', message: 'parser returned null' });
|
|
164
|
+
return emptyDraft('parse_error', null);
|
|
165
|
+
}
|
|
166
|
+
tree = parsed;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const parsed = parser.parse(input.source);
|
|
170
|
+
if (!parsed) {
|
|
171
|
+
facts.push({ code: 'parse_error', message: 'parser returned null' });
|
|
172
|
+
return emptyDraft('parse_error', null);
|
|
173
|
+
}
|
|
174
|
+
tree = parsed;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
facts.push({ code: 'parse_error', message: err instanceof Error ? err.message : String(err) });
|
|
179
|
+
return emptyDraft('parse_error', null);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
// The per-file parser is no longer needed once parse() returns; the TREE stays
|
|
183
|
+
// alive for refine/finalize. Delete the parser (best effort) so each file does
|
|
184
|
+
// not leak a web-tree-sitter Parser instance.
|
|
185
|
+
if (parser) {
|
|
186
|
+
try {
|
|
187
|
+
parser.delete();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* best effort */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const definitions = [];
|
|
195
|
+
const imports = [];
|
|
196
|
+
const exports = [];
|
|
197
|
+
try {
|
|
198
|
+
const grammar = await input.grammarForLang(input.lang);
|
|
199
|
+
const tagsQuery = await compileCached(input.providerId, input.lang, input.tagsHash, grammar, input.tagsSource);
|
|
200
|
+
const importsQuery = await compileCached(input.providerId, input.lang, input.importsHash, grammar, input.importsSource);
|
|
201
|
+
// --- DEFINITIONS: one DefScratch per match (each match has a .node + .name). ---
|
|
202
|
+
const defScratch = [];
|
|
203
|
+
for (const match of tagsQuery.matches(tree.rootNode)) {
|
|
204
|
+
let subtype = '';
|
|
205
|
+
let nodeNode = null;
|
|
206
|
+
let nameNode = null;
|
|
207
|
+
let exported = false;
|
|
208
|
+
for (const cap of match.captures) {
|
|
209
|
+
const parsed = parseDefinitionCapture(cap.name);
|
|
210
|
+
if (!parsed)
|
|
211
|
+
continue;
|
|
212
|
+
if (parsed.role === 'node') {
|
|
213
|
+
subtype = parsed.subtype;
|
|
214
|
+
nodeNode = cap.node;
|
|
215
|
+
}
|
|
216
|
+
else if (parsed.role === 'name') {
|
|
217
|
+
nameNode = cap.node;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
exported = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!nodeNode || !nameNode || !subtype)
|
|
224
|
+
continue;
|
|
225
|
+
defScratch.push({
|
|
226
|
+
subtype,
|
|
227
|
+
name: nameNode.text,
|
|
228
|
+
node: nodeNode,
|
|
229
|
+
nameNode,
|
|
230
|
+
exported,
|
|
231
|
+
ordinalIndex: nameNode.startIndex,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// --- IMPORTS / RE-EXPORTS / LOCAL EXPORTS ---
|
|
235
|
+
// Keyed by the captured @import.source node id so one statement with N
|
|
236
|
+
// sources yields N module nodes (multi-source aware). JS/TS = one source per
|
|
237
|
+
// statement → identical to the legacy per-statement grouping.
|
|
238
|
+
const importBySource = new Map();
|
|
239
|
+
const exportScratch = [];
|
|
240
|
+
for (const match of importsQuery.matches(tree.rootNode)) {
|
|
241
|
+
let sourceNode = null;
|
|
242
|
+
let defaultName = null;
|
|
243
|
+
let namespaceName = null;
|
|
244
|
+
let namedName = null;
|
|
245
|
+
let exportName = null;
|
|
246
|
+
for (const cap of match.captures) {
|
|
247
|
+
switch (cap.name) {
|
|
248
|
+
case 'import.source':
|
|
249
|
+
sourceNode = cap.node;
|
|
250
|
+
break;
|
|
251
|
+
case 'import.default.name':
|
|
252
|
+
defaultName = cap.node;
|
|
253
|
+
break;
|
|
254
|
+
case 'import.namespace.name':
|
|
255
|
+
namespaceName = cap.node;
|
|
256
|
+
break;
|
|
257
|
+
case 'import.named.name':
|
|
258
|
+
namedName = cap.node;
|
|
259
|
+
break;
|
|
260
|
+
case 'export.name':
|
|
261
|
+
exportName = cap.node;
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (sourceNode) {
|
|
268
|
+
// import statement OR re-export (export … from). Group PER captured source
|
|
269
|
+
// node so a statement with N sources emits N module nodes (Python
|
|
270
|
+
// `import a, b`). span/ordinal stay anchored on the enclosing statement,
|
|
271
|
+
// which for single-source JS/TS is byte-identical to per-statement grouping.
|
|
272
|
+
const stmt = enclosingStatement(sourceNode, statementTypes);
|
|
273
|
+
const isReExport = stmt.type === 'export_statement';
|
|
274
|
+
let scratch = importBySource.get(sourceNode.id);
|
|
275
|
+
if (!scratch) {
|
|
276
|
+
scratch = {
|
|
277
|
+
source: stripQuotes(sourceNode.text),
|
|
278
|
+
statement: stmt,
|
|
279
|
+
span: spanOf(stmt),
|
|
280
|
+
names: [],
|
|
281
|
+
ordinalIndex: stmt.startIndex,
|
|
282
|
+
isReExport,
|
|
283
|
+
};
|
|
284
|
+
importBySource.set(sourceNode.id, scratch);
|
|
285
|
+
}
|
|
286
|
+
if (defaultName)
|
|
287
|
+
scratch.names.push('default');
|
|
288
|
+
if (namespaceName)
|
|
289
|
+
scratch.names.push('*');
|
|
290
|
+
if (namedName)
|
|
291
|
+
scratch.names.push(namedName.text);
|
|
292
|
+
}
|
|
293
|
+
else if (exportName) {
|
|
294
|
+
// Local export clause / default-identifier — mark-or-add at finalize.
|
|
295
|
+
exportScratch.push({
|
|
296
|
+
name: exportName.text,
|
|
297
|
+
node: exportName,
|
|
298
|
+
ordinalIndex: exportName.startIndex,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// `export * from 'm'` matches a pattern with no name capture → names empty.
|
|
303
|
+
// Legacy supplies "*" for a sourced re-export that carried no specifier.
|
|
304
|
+
for (const scratch of importBySource.values()) {
|
|
305
|
+
if (scratch.isReExport && scratch.names.length === 0)
|
|
306
|
+
scratch.names.push('*');
|
|
307
|
+
}
|
|
308
|
+
const anchored = [];
|
|
309
|
+
for (const d of defScratch) {
|
|
310
|
+
anchored.push({
|
|
311
|
+
ordinalIndex: d.ordinalIndex,
|
|
312
|
+
emit: (ordinal) => {
|
|
313
|
+
definitions.push({
|
|
314
|
+
ordinal,
|
|
315
|
+
captureName: `definition.${d.subtype}.node`,
|
|
316
|
+
name: d.name,
|
|
317
|
+
subtype: d.subtype,
|
|
318
|
+
span: spanOf(d.node),
|
|
319
|
+
nameSpan: spanOf(d.nameNode),
|
|
320
|
+
exported: d.exported || undefined,
|
|
321
|
+
sourceNode: { node: d.node, nameNode: d.nameNode },
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
for (const im of importBySource.values()) {
|
|
327
|
+
anchored.push({
|
|
328
|
+
ordinalIndex: im.ordinalIndex,
|
|
329
|
+
emit: (ordinal) => {
|
|
330
|
+
imports.push({
|
|
331
|
+
ordinal,
|
|
332
|
+
source: im.source,
|
|
333
|
+
span: im.span,
|
|
334
|
+
importedNames: im.names,
|
|
335
|
+
isReExport: im.isReExport || undefined,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
for (const ex of exportScratch) {
|
|
341
|
+
anchored.push({
|
|
342
|
+
ordinalIndex: ex.ordinalIndex,
|
|
343
|
+
emit: (ordinal) => {
|
|
344
|
+
exports.push({ ordinal, name: ex.name, span: spanOf(enclosingStatement(ex.node, statementTypes)) });
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Stable sort by source byte offset; ties keep insertion (def→import→export) order.
|
|
349
|
+
anchored
|
|
350
|
+
.map((a, i) => ({ a, i }))
|
|
351
|
+
.sort((x, y) => (x.a.ordinalIndex - y.a.ordinalIndex) || (x.i - y.i))
|
|
352
|
+
.forEach((wrapped, ordinal) => wrapped.a.emit(ordinal));
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
facts.push({
|
|
356
|
+
code: 'extraction_error',
|
|
357
|
+
message: err instanceof Error ? err.message : String(err),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const parseStatus = tree.rootNode.hasError ? 'parse_error' : 'parsed';
|
|
361
|
+
if (parseStatus === 'parse_error') {
|
|
362
|
+
facts.push({ code: 'parse_error', message: 'tree contains syntax errors' });
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
file: { path: input.path },
|
|
366
|
+
definitions,
|
|
367
|
+
imports,
|
|
368
|
+
exports,
|
|
369
|
+
tests: [],
|
|
370
|
+
facts,
|
|
371
|
+
attributes: { parseStatus, __tree: tree },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
//# sourceMappingURL=query-runtime.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map P1a — CodeLanguageRegistry implementation (spec §4).
|
|
3
|
+
*
|
|
4
|
+
* Maps file extensions → providers (deterministic on collision: higher `priority`
|
|
5
|
+
* wins, then earlier registration), and runtime langs → providers. Surfaces the
|
|
6
|
+
* freshness inputs (provider versions + every query-asset hash per lang) and the
|
|
7
|
+
* `manifest.languages` entries keyed by runtime CodeLang.
|
|
8
|
+
*
|
|
9
|
+
* No behavior change in P1a — refresh.ts is NOT yet routed through this (Sprint 4
|
|
10
|
+
* cutover). The registry exists so the new core path can resolve a provider for a
|
|
11
|
+
* path and so freshness can fold in query-asset hashes.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
export class DefaultCodeLanguageRegistry {
|
|
15
|
+
registrations = [];
|
|
16
|
+
nextOrder = 0;
|
|
17
|
+
register(p) {
|
|
18
|
+
this.registrations.push({ provider: p, order: this.nextOrder++ });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the provider owning a path's extension. On collision: highest
|
|
22
|
+
* `priority` (default 0) wins; ties broken by EARLIEST registration order.
|
|
23
|
+
*/
|
|
24
|
+
providerForPath(path_) {
|
|
25
|
+
const ext = path.extname(path_).toLowerCase();
|
|
26
|
+
if (!ext)
|
|
27
|
+
return null;
|
|
28
|
+
const candidates = this.registrations.filter((r) => r.provider.extensions.some((e) => e.toLowerCase() === ext));
|
|
29
|
+
if (candidates.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
candidates.sort((a, b) => {
|
|
32
|
+
const pa = a.provider.priority ?? 0;
|
|
33
|
+
const pb = b.provider.priority ?? 0;
|
|
34
|
+
if (pa !== pb)
|
|
35
|
+
return pb - pa; // higher priority first
|
|
36
|
+
return a.order - b.order; // then earliest registration
|
|
37
|
+
});
|
|
38
|
+
const provider = candidates[0].provider;
|
|
39
|
+
return { provider, lang: provider.langForPath(path_) };
|
|
40
|
+
}
|
|
41
|
+
providerForLang(lang) {
|
|
42
|
+
// Same collision rule as providerForPath, applied over `languages`.
|
|
43
|
+
const candidates = this.registrations.filter((r) => r.provider.languages.includes(lang));
|
|
44
|
+
if (candidates.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
candidates.sort((a, b) => {
|
|
47
|
+
const pa = a.provider.priority ?? 0;
|
|
48
|
+
const pb = b.provider.priority ?? 0;
|
|
49
|
+
if (pa !== pb)
|
|
50
|
+
return pb - pa;
|
|
51
|
+
return a.order - b.order;
|
|
52
|
+
});
|
|
53
|
+
return candidates[0].provider;
|
|
54
|
+
}
|
|
55
|
+
activeLanguages() {
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const { provider } of this.registrations) {
|
|
59
|
+
for (const lang of provider.languages) {
|
|
60
|
+
if (!seen.has(lang)) {
|
|
61
|
+
seen.add(lang);
|
|
62
|
+
out.push(lang);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
includedExtensions() {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const { provider } of this.registrations) {
|
|
72
|
+
for (const ext of provider.extensions) {
|
|
73
|
+
const e = ext.toLowerCase();
|
|
74
|
+
if (!seen.has(e)) {
|
|
75
|
+
seen.add(e);
|
|
76
|
+
out.push(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `manifest.languages` entries keyed by runtime CodeLang. The winning provider
|
|
84
|
+
* per lang (collision rule) supplies the grammar name/version/hash.
|
|
85
|
+
*/
|
|
86
|
+
languageEntries() {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const lang of this.activeLanguages()) {
|
|
89
|
+
const provider = this.providerForLang(lang);
|
|
90
|
+
if (!provider)
|
|
91
|
+
continue;
|
|
92
|
+
out[lang] = {
|
|
93
|
+
enabled: true,
|
|
94
|
+
grammar_name: provider.parser.grammarNameForLang(lang),
|
|
95
|
+
grammar_version: provider.version,
|
|
96
|
+
tree_sitter_grammar_hash: provider.parser.grammarHashForLang(lang),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Freshness inputs: per provider, its `version` + every query-asset hash for
|
|
103
|
+
* every lang it owns. Editing a `.scm` flips a hash here → `stale_extractor`.
|
|
104
|
+
* Deterministically ordered (registration order, then lang order).
|
|
105
|
+
*/
|
|
106
|
+
configHashInputs() {
|
|
107
|
+
return this.registrations.map(({ provider }) => ({
|
|
108
|
+
id: provider.id,
|
|
109
|
+
version: provider.version,
|
|
110
|
+
queries: provider.languages.map((lang) => ({
|
|
111
|
+
lang,
|
|
112
|
+
tags: provider.queries.tags.hashForLang(lang),
|
|
113
|
+
imports: provider.queries.imports.hashForLang(lang),
|
|
114
|
+
})),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Convenience: build a registry pre-loaded with the given providers. */
|
|
119
|
+
export function createRegistry(...providers) {
|
|
120
|
+
const reg = new DefaultCodeLanguageRegistry();
|
|
121
|
+
for (const p of providers)
|
|
122
|
+
reg.register(p);
|
|
123
|
+
return reg;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
; Brainclaw Code Map — TypeScript/TSX/JavaScript IMPORTS + EXPORTS query (curated, vendored).
|
|
2
|
+
; See ./README.md for the capture-name convention and provenance.
|
|
3
|
+
;
|
|
4
|
+
; Capture -> draft mapping performed by the query-runtime:
|
|
5
|
+
; @import.source -> ImportDraft.source (string literal text; quotes stripped
|
|
6
|
+
; by the runtime). The @import.source node ALSO anchors the
|
|
7
|
+
; ImportDraft.span = enclosing import/export statement span.
|
|
8
|
+
; @import.default.name -> contributes source-side imported name "default"
|
|
9
|
+
; @import.namespace.name -> contributes source-side imported name "*"
|
|
10
|
+
; @import.named.name -> contributes source-side imported name = specifier `name`
|
|
11
|
+
; (NOT the local alias)
|
|
12
|
+
; @export.name -> ExportDraft.name (a re-export / mark-or-add export target)
|
|
13
|
+
;
|
|
14
|
+
; This asset covers: `import` statements, `export { ... }` clauses (no source),
|
|
15
|
+
; `export ... from '...'` / `export * from '...'` re-exports (WITH source), and
|
|
16
|
+
; `export default <identifier>`. Declaration exports (`export function/class/...`) are
|
|
17
|
+
; NOT handled here — they are definitions and carry their `exported` flag from tags.scm.
|
|
18
|
+
;
|
|
19
|
+
; Re-export source detection is the discriminator the provider's refine() uses to build
|
|
20
|
+
; a module node (no phantom symbol) vs. a local `export {a}` mark-or-add. The runtime
|
|
21
|
+
; surfaces both @import.source and @export.name on a re-export match; refine() routes
|
|
22
|
+
; on source presence.
|
|
23
|
+
|
|
24
|
+
; ===========================================================================
|
|
25
|
+
; IMPORT STATEMENTS
|
|
26
|
+
; ===========================================================================
|
|
27
|
+
|
|
28
|
+
; bare side-effect import: import 'm';
|
|
29
|
+
(import_statement
|
|
30
|
+
source: (string (string_fragment) @import.source))
|
|
31
|
+
|
|
32
|
+
; default import: import def from 'm';
|
|
33
|
+
(import_statement
|
|
34
|
+
(import_clause (identifier) @import.default.name)
|
|
35
|
+
source: (string (string_fragment) @import.source))
|
|
36
|
+
|
|
37
|
+
; namespace import: import * as ns from 'm';
|
|
38
|
+
(import_statement
|
|
39
|
+
(import_clause (namespace_import (identifier) @import.namespace.name))
|
|
40
|
+
source: (string (string_fragment) @import.source))
|
|
41
|
+
|
|
42
|
+
; named imports: import { a, b as c } from 'm';
|
|
43
|
+
; `name:` is the source-side specifier name (`a`, `b`) — the alias (`c`) is the
|
|
44
|
+
; `alias:` field and is deliberately NOT captured (importedNames are source-side).
|
|
45
|
+
(import_statement
|
|
46
|
+
(import_clause
|
|
47
|
+
(named_imports
|
|
48
|
+
(import_specifier
|
|
49
|
+
name: (identifier) @import.named.name)))
|
|
50
|
+
source: (string (string_fragment) @import.source))
|
|
51
|
+
|
|
52
|
+
; ===========================================================================
|
|
53
|
+
; RE-EXPORTS WITH A SOURCE MODULE (export { a } from 'm'; export * from 'm';)
|
|
54
|
+
; These carry @import.source so refine() builds a module node + imports edge.
|
|
55
|
+
; ===========================================================================
|
|
56
|
+
|
|
57
|
+
; export { a, b as c } from 'm'; -> source-side names a, b
|
|
58
|
+
(export_statement
|
|
59
|
+
(export_clause
|
|
60
|
+
(export_specifier
|
|
61
|
+
name: (identifier) @import.named.name))
|
|
62
|
+
source: (string (string_fragment) @import.source))
|
|
63
|
+
|
|
64
|
+
; export * from 'm'; (no export_clause) -> the runtime supplies "*" for the names
|
|
65
|
+
(export_statement
|
|
66
|
+
"*"
|
|
67
|
+
source: (string (string_fragment) @import.source))
|
|
68
|
+
|
|
69
|
+
; export * as ns from 'm'; -> namespace re-export
|
|
70
|
+
(export_statement
|
|
71
|
+
(namespace_export (identifier) @import.namespace.name)
|
|
72
|
+
source: (string (string_fragment) @import.source))
|
|
73
|
+
|
|
74
|
+
; ===========================================================================
|
|
75
|
+
; LOCAL EXPORT CLAUSES (export { a, b as c }; no source module)
|
|
76
|
+
; -> @export.name per source-side specifier name; refine() does mark-or-add-export.
|
|
77
|
+
; ===========================================================================
|
|
78
|
+
(export_statement
|
|
79
|
+
(export_clause
|
|
80
|
+
(export_specifier
|
|
81
|
+
name: (identifier) @export.name))
|
|
82
|
+
!source)
|
|
83
|
+
|
|
84
|
+
; ===========================================================================
|
|
85
|
+
; DEFAULT EXPORT OF AN IDENTIFIER (export default foo;)
|
|
86
|
+
; -> @export.name = the referenced identifier; refine() links it if it names a symbol.
|
|
87
|
+
; ===========================================================================
|
|
88
|
+
(export_statement
|
|
89
|
+
"default"
|
|
90
|
+
(identifier) @export.name)
|