brainclaw 1.24.0 → 1.26.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 (46) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli/register-code-map.js +9 -2
  3. package/dist/commands/code-map.js +120 -6
  4. package/dist/commands/mcp-catalog.js +46 -0
  5. package/dist/commands/mcp.js +58 -6
  6. package/dist/commands/session-start.js +84 -13
  7. package/dist/core/bootstrap.js +28 -4
  8. package/dist/core/code-map/aggregate.js +36 -31
  9. package/dist/core/code-map/backend.js +162 -5
  10. package/dist/core/code-map/core.js +1 -0
  11. package/dist/core/code-map/export.js +212 -0
  12. package/dist/core/code-map/finalizer.js +57 -2
  13. package/dist/core/code-map/freshness.js +81 -15
  14. package/dist/core/code-map/impact.js +409 -0
  15. package/dist/core/code-map/indexes.js +64 -3
  16. package/dist/core/code-map/lang/python/index.js +4 -2
  17. package/dist/core/code-map/lang/query-runtime.js +2 -0
  18. package/dist/core/code-map/lang/typescript/config.js +271 -0
  19. package/dist/core/code-map/lang/typescript/index.js +24 -6
  20. package/dist/core/code-map/lang/usages.js +333 -0
  21. package/dist/core/code-map/memory-reader.js +15 -0
  22. package/dist/core/code-map/query.js +285 -71
  23. package/dist/core/code-map/refresh.js +0 -0
  24. package/dist/core/code-map/resolve.js +28 -2
  25. package/dist/core/code-map/store.js +1 -0
  26. package/dist/core/code-map/types.js +70 -9
  27. package/dist/core/code-map/vocabulary.js +6 -0
  28. package/dist/core/code-map/work-section.js +12 -14
  29. package/dist/core/context-diff.js +17 -3
  30. package/dist/core/entity-operations.js +14 -2
  31. package/dist/core/federation-pull.js +151 -3
  32. package/dist/core/federation-push.js +16 -3
  33. package/dist/core/hint-aging.js +4 -1
  34. package/dist/core/identity.js +69 -17
  35. package/dist/core/io.js +27 -0
  36. package/dist/core/project-discovery.js +7 -1
  37. package/dist/core/protocol-tool-policy.js +3 -0
  38. package/dist/core/runtime.js +23 -0
  39. package/dist/core/worktree.js +89 -2
  40. package/dist/facts.js +15 -12
  41. package/dist/facts.json +14 -11
  42. package/docs/cli.md +8 -0
  43. package/docs/code-map.md +60 -28
  44. package/docs/integrations/mcp.md +5 -2
  45. package/docs/mcp-schema-changelog.md +11 -1
  46. package/package.json +1 -1
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Bounded, local TypeScript/JavaScript resolver configuration.
3
+ *
4
+ * Only root tsconfig/jsconfig, local extends, baseUrl and paths are supported.
5
+ * Package extends and node_modules are deliberately never read. Any malformed,
6
+ * escaping, cyclic, or ambiguous configuration is invalid so callers abstain.
7
+ */
8
+ import crypto from 'node:crypto';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ const MAX_EXTENDS_DEPTH = 8;
12
+ const CONFIG_FILENAMES = ['tsconfig.json', 'jsconfig.json'];
13
+ function isObject(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+ function toPosix(value) {
17
+ return value.replace(/\\/g, '/');
18
+ }
19
+ function configFingerprint(parts) {
20
+ return `sha256:${crypto.createHash('sha256').update(parts.join('\n'), 'utf8').digest('hex')}`;
21
+ }
22
+ /** Strip JSONC comments without changing comment-looking text inside strings. */
23
+ function stripJsonComments(source) {
24
+ let out = '';
25
+ let inString = false;
26
+ let escaped = false;
27
+ for (let i = 0; i < source.length; i++) {
28
+ const ch = source[i];
29
+ const next = source[i + 1];
30
+ if (inString) {
31
+ out += ch;
32
+ if (escaped)
33
+ escaped = false;
34
+ else if (ch === '\\')
35
+ escaped = true;
36
+ else if (ch === '"')
37
+ inString = false;
38
+ continue;
39
+ }
40
+ if (ch === '"') {
41
+ inString = true;
42
+ out += ch;
43
+ continue;
44
+ }
45
+ if (ch === '/' && next === '/') {
46
+ i++;
47
+ while (i + 1 < source.length && source[i + 1] !== '\n' && source[i + 1] !== '\r')
48
+ i++;
49
+ continue;
50
+ }
51
+ if (ch === '/' && next === '*') {
52
+ const close = source.indexOf('*/', i + 2);
53
+ if (close < 0)
54
+ return null;
55
+ i = close + 1;
56
+ continue;
57
+ }
58
+ out += ch;
59
+ }
60
+ return inString ? null : out;
61
+ }
62
+ /** JSONC permits trailing commas; remove them only outside quoted strings. */
63
+ function stripTrailingCommas(source) {
64
+ let out = '';
65
+ let inString = false;
66
+ let escaped = false;
67
+ for (let i = 0; i < source.length; i++) {
68
+ const ch = source[i];
69
+ if (inString) {
70
+ out += ch;
71
+ if (escaped)
72
+ escaped = false;
73
+ else if (ch === '\\')
74
+ escaped = true;
75
+ else if (ch === '"')
76
+ inString = false;
77
+ continue;
78
+ }
79
+ if (ch === '"') {
80
+ inString = true;
81
+ out += ch;
82
+ continue;
83
+ }
84
+ if (ch === ',') {
85
+ let next = i + 1;
86
+ while (next < source.length && /\s/.test(source[next]))
87
+ next++;
88
+ if (source[next] === '}' || source[next] === ']')
89
+ continue;
90
+ }
91
+ out += ch;
92
+ }
93
+ return out;
94
+ }
95
+ function parseJsonc(source) {
96
+ const withoutComments = stripJsonComments(source);
97
+ if (withoutComments === null)
98
+ return null;
99
+ try {
100
+ const parsed = JSON.parse(stripTrailingCommas(withoutComments));
101
+ return isObject(parsed) ? parsed : null;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ function isWithin(root, absolute) {
108
+ const rel = path.relative(root, absolute);
109
+ return rel === '' || (!path.isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${path.sep}`));
110
+ }
111
+ function projectRelative(root, absolute) {
112
+ return isWithin(root, absolute) ? toPosix(path.relative(root, absolute)) : null;
113
+ }
114
+ /** Resolve a config-local directory, refusing absolute and project-escaping values. */
115
+ function localDirectory(root, configDir, value) {
116
+ if (path.isAbsolute(value))
117
+ return null;
118
+ return projectRelative(root, path.resolve(configDir, value));
119
+ }
120
+ function starCount(value) {
121
+ return [...value].filter((c) => c === '*').length;
122
+ }
123
+ function parsePaths(raw, root, configDir, baseUrl) {
124
+ if (!isObject(raw))
125
+ return null;
126
+ const targetDir = baseUrl === null ? configDir : path.resolve(root, baseUrl);
127
+ const mappings = [];
128
+ for (const pattern of Object.keys(raw).sort()) {
129
+ const values = raw[pattern];
130
+ const stars = starCount(pattern);
131
+ if (pattern.length === 0 || stars > 1 || !Array.isArray(values) || values.length === 0)
132
+ return null;
133
+ const targets = [];
134
+ for (const value of values) {
135
+ if (typeof value !== 'string' || starCount(value) > 1 || (stars === 0 && starCount(value) !== 0))
136
+ return null;
137
+ const target = localDirectory(root, targetDir, value);
138
+ if (target === null)
139
+ return null;
140
+ targets.push(target);
141
+ }
142
+ mappings.push({ pattern, targets });
143
+ }
144
+ return mappings;
145
+ }
146
+ function localExtendsPath(root, configDir, value) {
147
+ // Bare values name packages; never follow them, even if node_modules is present.
148
+ if (!value.startsWith('./') && !value.startsWith('../'))
149
+ return null;
150
+ const candidate = path.resolve(configDir, value);
151
+ if (!isWithin(root, candidate))
152
+ return null;
153
+ const jsonPath = path.extname(candidate) ? candidate : `${candidate}.json`;
154
+ return isWithin(root, jsonPath) ? jsonPath : null;
155
+ }
156
+ function readConfig(filename, depth, state) {
157
+ const empty = { baseUrl: null, paths: [] };
158
+ if (depth > MAX_EXTENDS_DEPTH || state.seen.has(filename) || !isWithin(state.root, filename)) {
159
+ return { valid: false, options: empty };
160
+ }
161
+ state.seen.add(filename);
162
+ let source;
163
+ try {
164
+ source = fs.readFileSync(filename, 'utf8');
165
+ }
166
+ catch {
167
+ return { valid: false, options: empty };
168
+ }
169
+ state.fingerprintParts.push(`${projectRelative(state.root, filename) ?? filename}\u0000${source}`);
170
+ const json = parseJsonc(source);
171
+ if (!json)
172
+ return { valid: false, options: empty };
173
+ const configDir = path.dirname(filename);
174
+ let inherited = empty;
175
+ if (json.extends !== undefined) {
176
+ if (typeof json.extends !== 'string')
177
+ return { valid: false, options: empty };
178
+ const parent = localExtendsPath(state.root, configDir, json.extends);
179
+ if (!parent)
180
+ return { valid: false, options: empty };
181
+ const parentResult = readConfig(parent, depth + 1, state);
182
+ if (!parentResult.valid)
183
+ return { valid: false, options: empty };
184
+ inherited = parentResult.options;
185
+ }
186
+ if (json.compilerOptions !== undefined && !isObject(json.compilerOptions))
187
+ return { valid: false, options: empty };
188
+ const options = json.compilerOptions ?? {};
189
+ let baseUrl = inherited.baseUrl;
190
+ if (options.baseUrl !== undefined) {
191
+ if (typeof options.baseUrl !== 'string')
192
+ return { valid: false, options: empty };
193
+ baseUrl = localDirectory(state.root, configDir, options.baseUrl);
194
+ if (baseUrl === null)
195
+ return { valid: false, options: empty };
196
+ }
197
+ let paths = inherited.paths;
198
+ if (options.paths !== undefined) {
199
+ const parsedPaths = parsePaths(options.paths, state.root, configDir, baseUrl);
200
+ if (!parsedPaths)
201
+ return { valid: false, options: empty };
202
+ paths = parsedPaths;
203
+ }
204
+ return { valid: true, options: { baseUrl, paths } };
205
+ }
206
+ /**
207
+ * Read exactly one root configuration. Two root configs are intentionally
208
+ * ambiguous: TypeScript tooling can choose based on invocation, Code Map cannot.
209
+ */
210
+ export function loadTypeScriptResolutionConfig(projectRoot) {
211
+ const root = path.resolve(projectRoot);
212
+ const found = CONFIG_FILENAMES.map((name) => path.join(root, name)).filter((filename) => fs.existsSync(filename));
213
+ if (found.length === 0) {
214
+ return { kind: 'typescript-resolution-config', fingerprint: configFingerprint([]), valid: true, baseUrl: null, paths: [] };
215
+ }
216
+ if (found.length > 1) {
217
+ const parts = found.map((filename) => {
218
+ try {
219
+ return `${path.basename(filename)}\u0000${fs.readFileSync(filename, 'utf8')}`;
220
+ }
221
+ catch {
222
+ return `${path.basename(filename)}\u0000<unreadable>`;
223
+ }
224
+ });
225
+ return { kind: 'typescript-resolution-config', fingerprint: configFingerprint(parts), valid: false, baseUrl: null, paths: [] };
226
+ }
227
+ const state = { root, seen: new Set(), fingerprintParts: [] };
228
+ const result = readConfig(found[0], 0, state);
229
+ return {
230
+ kind: 'typescript-resolution-config',
231
+ fingerprint: configFingerprint(state.fingerprintParts),
232
+ valid: result.valid,
233
+ baseUrl: result.valid ? result.options.baseUrl : null,
234
+ paths: result.valid ? result.options.paths : [],
235
+ };
236
+ }
237
+ export function isTypeScriptResolutionConfig(value) {
238
+ return !!value && typeof value === 'object' && value.kind === 'typescript-resolution-config';
239
+ }
240
+ function matchPattern(pattern, specifier) {
241
+ const star = pattern.indexOf('*');
242
+ if (star < 0)
243
+ return pattern === specifier ? '' : null;
244
+ const prefix = pattern.slice(0, star);
245
+ const suffix = pattern.slice(star + 1);
246
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix))
247
+ return null;
248
+ return specifier.slice(prefix.length, specifier.length - suffix.length);
249
+ }
250
+ /** Map a bare specifier to project-relative candidate bases, or no candidates. */
251
+ export function typeScriptSpecifierBases(specifier, config) {
252
+ if (!config?.valid)
253
+ return [];
254
+ const matches = config.paths
255
+ .map((mapping) => ({ mapping, wildcard: matchPattern(mapping.pattern, specifier) }))
256
+ .filter((match) => match.wildcard !== null);
257
+ // Overlapping paths patterns are intentionally not ranked: ambiguity abstains.
258
+ if (matches.length > 1)
259
+ return [];
260
+ if (matches.length === 1) {
261
+ const { mapping, wildcard } = matches[0];
262
+ // `parsePaths` garantit AU PLUS une `*` par cible (starCount > 1 ⇒ config invalide,
263
+ // donc abstention totale) : split/join et replace-première-occurrence sont ici
264
+ // équivalents. split/join est préféré parce qu'il reste correct même si cet
265
+ // invariant bougeait — et il est lisible par l'analyse statique (js/incomplete-
266
+ // sanitization signalait le replace sans pouvoir voir l'invariant amont).
267
+ return mapping.targets.map((target) => target.split('*').join(wildcard));
268
+ }
269
+ return config.baseUrl === null ? [] : [path.posix.join(config.baseUrl, specifier)];
270
+ }
271
+ //# sourceMappingURL=config.js.map
@@ -26,6 +26,8 @@ import path from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { grammarHash, grammarName, loadGrammar } from '../../wasm-loader.js';
28
28
  import { extractWithQueries } from '../query-runtime.js';
29
+ import { extractLexicalUsages } from '../usages.js';
30
+ import { isTypeScriptResolutionConfig, typeScriptSpecifierBases, } from './config.js';
29
31
  const HERE = path.dirname(fileURLToPath(import.meta.url));
30
32
  /** Resolve a vendored `.scm` next to this module (dist) or from the source tree. */
31
33
  function readScm(basename) {
@@ -149,7 +151,7 @@ const queries = {
149
151
  };
150
152
  const vocabulary = {
151
153
  nodeSubtypes: ['function', 'class', 'type', 'interface', 'variable', 'component', 'hook', 'export'],
152
- edgeKinds: ['contains', 'defines', 'imports', 'exports'],
154
+ edgeKinds: ['contains', 'defines', 'imports', 'exports', 'calls', 'references', 'possible_textual_match'],
153
155
  captureMap: queries.captureMap,
154
156
  };
155
157
  const capabilities = {
@@ -173,10 +175,7 @@ const JS_LIKE_EXTS = new Set(['.js', '.jsx', '.mjs', '.cjs']);
173
175
  * extension, then `<candidate>/index.<ext>`. Return the FIRST that is an indexed
174
176
  * file. Bare/external specifiers (`react`, `@scope/x`) → no resolution (no edge).
175
177
  */
176
- function resolveTsImport(spec, fromPath, ctx) {
177
- if (!spec.startsWith('./') && !spec.startsWith('../'))
178
- return null; // external/bare → no edge
179
- const base = path.posix.join(path.posix.dirname(fromPath), spec); // normalized project-relative
178
+ function resolveTsCandidate(base, ctx) {
180
179
  const ext = path.posix.extname(base);
181
180
  const candidates = [];
182
181
  if (ext) {
@@ -198,6 +197,24 @@ function resolveTsImport(spec, fromPath, ctx) {
198
197
  }
199
198
  return null;
200
199
  }
200
+ function resolveTsImport(spec, fromPath, ctx) {
201
+ if (spec.startsWith('./') || spec.startsWith('../')) {
202
+ return resolveTsCandidate(path.posix.join(path.posix.dirname(fromPath), spec), ctx);
203
+ }
204
+ const config = isTypeScriptResolutionConfig(ctx.resolverConfig) ? ctx.resolverConfig : undefined;
205
+ const candidates = typeScriptSpecifierBases(spec, config);
206
+ if (candidates.length === 0)
207
+ return null; // external, invalid, or ambiguous config
208
+ const resolved = new Set();
209
+ for (const candidate of candidates) {
210
+ const target = resolveTsCandidate(candidate, ctx);
211
+ if (target)
212
+ resolved.add(target);
213
+ }
214
+ // Do not adopt TypeScript's fallback preference when config candidates produce
215
+ // different indexed files: Code Map is deliberately soundness-first.
216
+ return resolved.size === 1 ? [...resolved][0] : null;
217
+ }
201
218
  function isDefSourceNode(v) {
202
219
  return (typeof v === 'object' &&
203
220
  v !== null &&
@@ -289,7 +306,8 @@ export class TypeScriptProvider {
289
306
  }
290
307
  return d;
291
308
  });
292
- return { ...draft, definitions };
309
+ const tree = draft.attributes?.__tree;
310
+ return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'js-ts') };
293
311
  }
294
312
  /**
295
313
  * P1c file-level import resolution (intra-project, relative specifiers). Returns
@@ -0,0 +1,333 @@
1
+ const FUNCTION_SUBTYPES = new Set(['function', 'component', 'hook']);
2
+ const LOW_CONFIDENCE_TEXTUAL_MATCH = 0.2;
3
+ function sourceOf(definition) {
4
+ const value = definition.sourceNode;
5
+ if (!value || typeof value !== 'object' || !('node' in value) || !('nameNode' in value))
6
+ return null;
7
+ const candidate = value;
8
+ return candidate.node && candidate.nameNode ? candidate : null;
9
+ }
10
+ function spanOf(node) {
11
+ return {
12
+ start_line: node.startPosition.row + 1,
13
+ start_col: node.startPosition.column + 1,
14
+ end_line: node.endPosition.row + 1,
15
+ end_col: node.endPosition.column + 1,
16
+ };
17
+ }
18
+ function nodeKey(node) {
19
+ return `${node.type}:${node.startIndex}:${node.endIndex}`;
20
+ }
21
+ function sameNode(a, b) {
22
+ return !!a && a.type === b.type && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
23
+ }
24
+ function field(node, name) {
25
+ try {
26
+ return node.childForFieldName(name);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function walk(node, visit) {
33
+ visit(node);
34
+ for (let i = 0; i < node.namedChildCount; i++) {
35
+ const child = node.namedChild(i);
36
+ if (child)
37
+ walk(child, visit);
38
+ }
39
+ }
40
+ function walkScope(node, rootScope, language, visit) {
41
+ visit(node);
42
+ for (let i = 0; i < node.namedChildCount; i++) {
43
+ const child = node.namedChild(i);
44
+ if (!child)
45
+ continue;
46
+ // Bindings in a nested function/class/lambda cannot shadow a use in this
47
+ // scope. Skipping them avoids false abstention between sibling functions.
48
+ if (child !== rootScope && isScope(child, language))
49
+ continue;
50
+ walkScope(child, rootScope, language, visit);
51
+ }
52
+ }
53
+ function hasAncestor(node, types) {
54
+ for (let parent = node.parent; parent; parent = parent.parent) {
55
+ if (types.has(parent.type))
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ function isTopLevelPythonFunction(definition) {
61
+ const source = sourceOf(definition);
62
+ if (!source)
63
+ return false;
64
+ let parent = source.node.parent;
65
+ if (parent?.type === 'decorated_definition')
66
+ parent = parent.parent;
67
+ return parent?.type === 'module';
68
+ }
69
+ function isLocalFunctionTarget(definition, language) {
70
+ if (!FUNCTION_SUBTYPES.has(definition.subtype))
71
+ return false;
72
+ // JS/TS tags are already program-anchored. Python captures nested functions too,
73
+ // so retain only module bindings: nested/method names need a real scope engine.
74
+ return language === 'js-ts' || isTopLevelPythonFunction(definition);
75
+ }
76
+ function isCallerDefinition(definition) {
77
+ return FUNCTION_SUBTYPES.has(definition.subtype);
78
+ }
79
+ function definitionRange(definition) {
80
+ const source = sourceOf(definition);
81
+ if (!source)
82
+ return null;
83
+ // A lexical declaration can carry several declarators. The declarator, not the
84
+ // shared statement span, is the smallest safe caller container for an arrow.
85
+ return source.nameNode.parent ?? source.node;
86
+ }
87
+ function contains(container, node) {
88
+ return container.startIndex <= node.startIndex && container.endIndex >= node.endIndex;
89
+ }
90
+ function callerFor(node, definitions) {
91
+ let winner;
92
+ for (const definition of definitions) {
93
+ if (!isCallerDefinition(definition))
94
+ continue;
95
+ const range = definitionRange(definition);
96
+ if (!range || !contains(range, node))
97
+ continue;
98
+ const width = range.endIndex - range.startIndex;
99
+ if (!winner || width < winner.width || (width === winner.width && definition.ordinal > winner.ordinal)) {
100
+ winner = { ordinal: definition.ordinal, width };
101
+ }
102
+ }
103
+ return winner?.ordinal;
104
+ }
105
+ function isScope(node, language) {
106
+ if (language === 'python')
107
+ return ['module', 'function_definition', 'lambda', 'class_definition'].includes(node.type);
108
+ return ['program', 'function_declaration', 'function_expression', 'arrow_function', 'method_definition'].includes(node.type);
109
+ }
110
+ function isParameterIdentifier(node, language) {
111
+ const parameterTypes = language === 'python'
112
+ ? new Set(['parameters', 'lambda_parameters'])
113
+ : new Set(['formal_parameters', 'required_parameter', 'optional_parameter', 'rest_pattern']);
114
+ return hasAncestor(node, parameterTypes);
115
+ }
116
+ function isBindingIdentifier(node, language) {
117
+ if (node.type !== 'identifier')
118
+ return false;
119
+ const parent = node.parent;
120
+ if (!parent)
121
+ return false;
122
+ if (sameNode(field(parent, 'name'), node) && [
123
+ 'variable_declarator', 'function_declaration', 'generator_function_declaration',
124
+ 'function_definition', 'class_declaration', 'class_definition', 'aliased_import',
125
+ ].includes(parent.type))
126
+ return true;
127
+ if (sameNode(field(parent, 'left'), node) && [
128
+ 'assignment', 'augmented_assignment', 'for_statement', 'with_item',
129
+ ].includes(parent.type))
130
+ return true;
131
+ return isParameterIdentifier(node, language);
132
+ }
133
+ function scopeBinds(scope, name, language, allowed) {
134
+ let found = false;
135
+ walkScope(scope, scope, language, (candidate) => {
136
+ if (found || candidate.text !== name || !isBindingIdentifier(candidate, language))
137
+ return;
138
+ if (!allowed.has(nodeKey(candidate)))
139
+ found = true;
140
+ });
141
+ return found;
142
+ }
143
+ function isShadowed(node, name, language, allowed) {
144
+ for (let parent = node.parent; parent; parent = parent.parent) {
145
+ if (isScope(parent, language) && scopeBinds(parent, name, language, allowed))
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+ function propertyOf(node) {
151
+ return field(node, 'property') ?? field(node, 'attribute');
152
+ }
153
+ function isProperty(node) {
154
+ const parent = node.parent;
155
+ return !!parent && sameNode(propertyOf(parent), node)
156
+ && ['member_expression', 'optional_member_expression', 'attribute'].includes(parent.type);
157
+ }
158
+ function isValueReference(node, language) {
159
+ if (node.type !== 'identifier' || isBindingIdentifier(node, language))
160
+ return false;
161
+ const parent = node.parent;
162
+ if (!parent || isProperty(node))
163
+ return false;
164
+ if (hasAncestor(node, new Set(['import_statement', 'import_from_statement'])))
165
+ return false;
166
+ if (sameNode(field(parent, 'key'), node) || ['export_specifier', 'export_clause'].includes(parent.type))
167
+ return false;
168
+ // Type positions have distinct namespaces in TS and do not prove a value-level use.
169
+ if (hasAncestor(node, new Set(['type_annotation', 'type_alias_declaration', 'interface_declaration', 'type_identifier'])))
170
+ return false;
171
+ return true;
172
+ }
173
+ function addBinding(bindings, localName, binding) {
174
+ const previous = bindings.get(localName);
175
+ bindings.set(localName, previous === undefined ? binding : null);
176
+ }
177
+ function stripQuotes(text) {
178
+ return text.replace(/^['"`]|['"`]$/g, '');
179
+ }
180
+ function jsTsBindings(root) {
181
+ const bindings = new Map();
182
+ walk(root, (statement) => {
183
+ if (statement.type !== 'import_statement')
184
+ return;
185
+ const source = field(statement, 'source');
186
+ if (!source)
187
+ return;
188
+ const module = stripQuotes(source.text);
189
+ let clause = null;
190
+ for (let i = 0; i < statement.namedChildCount; i++) {
191
+ const child = statement.namedChild(i);
192
+ if (child?.type === 'import_clause') {
193
+ clause = child;
194
+ break;
195
+ }
196
+ }
197
+ if (!clause)
198
+ return;
199
+ walk(clause, (candidate) => {
200
+ if (candidate.type !== 'import_specifier')
201
+ return;
202
+ const imported = field(candidate, 'name');
203
+ if (!imported || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported.text))
204
+ return;
205
+ const alias = field(candidate, 'alias');
206
+ addBinding(bindings, (alias ?? imported).text, {
207
+ module,
208
+ importedName: imported.text,
209
+ bindingNode: alias ?? imported,
210
+ });
211
+ });
212
+ });
213
+ return bindings;
214
+ }
215
+ function pythonBindings(root) {
216
+ const bindings = new Map();
217
+ walk(root, (statement) => {
218
+ if (statement.type !== 'import_from_statement')
219
+ return;
220
+ const source = field(statement, 'module_name');
221
+ if (!source)
222
+ return;
223
+ const module = source.text;
224
+ for (let i = 0; i < statement.namedChildCount; i++) {
225
+ const child = statement.namedChild(i);
226
+ if (!child || sameNode(child, source))
227
+ continue;
228
+ if (child.type === 'dotted_name' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(child.text)) {
229
+ addBinding(bindings, child.text, { module, importedName: child.text, bindingNode: child });
230
+ }
231
+ else if (child.type === 'aliased_import') {
232
+ const imported = field(child, 'name');
233
+ const alias = field(child, 'alias');
234
+ if (imported && alias && /^[A-Za-z_][A-Za-z0-9_]*$/.test(imported.text)) {
235
+ addBinding(bindings, alias.text, { module, importedName: imported.text, bindingNode: alias });
236
+ }
237
+ }
238
+ }
239
+ });
240
+ return bindings;
241
+ }
242
+ /** Extract provider-local, soundness-first usage drafts from an already parsed tree. */
243
+ export function extractLexicalUsages(root, definitions, language) {
244
+ if (!root)
245
+ return [];
246
+ const localTargets = new Map();
247
+ const allowedBindings = new Set();
248
+ for (const definition of definitions) {
249
+ if (!isLocalFunctionTarget(definition, language))
250
+ continue;
251
+ const previous = localTargets.get(definition.name);
252
+ localTargets.set(definition.name, previous === undefined ? definition : null);
253
+ const source = sourceOf(definition);
254
+ if (source)
255
+ allowedBindings.add(nodeKey(source.nameNode));
256
+ }
257
+ const bindings = language === 'js-ts' ? jsTsBindings(root) : pythonBindings(root);
258
+ for (const binding of bindings.values())
259
+ if (binding)
260
+ allowedBindings.add(nodeKey(binding.bindingNode));
261
+ const usages = [];
262
+ const seen = new Set();
263
+ const handledCallIdentifiers = new Set();
264
+ const handledProperties = new Set();
265
+ const add = (node, kind, target) => {
266
+ const caller = callerFor(node, definitions);
267
+ const targetKey = target.kind === 'local'
268
+ ? `local:${target.definitionOrdinal}`
269
+ : `import:${target.module}:${target.importedName}`;
270
+ const key = `${nodeKey(node)}:${kind}:${caller ?? 'file'}:${targetKey}`;
271
+ if (seen.has(key))
272
+ return;
273
+ seen.add(key);
274
+ usages.push({
275
+ kind,
276
+ ...(caller === undefined ? {} : { fromDefinitionOrdinal: caller }),
277
+ target,
278
+ span: spanOf(node),
279
+ confidence: kind === 'possible_textual_match' ? LOW_CONFIDENCE_TEXTUAL_MATCH : 1.0,
280
+ });
281
+ };
282
+ const directUse = (node, kind) => {
283
+ const name = node.text;
284
+ const local = localTargets.get(name);
285
+ const imported = bindings.get(name);
286
+ if ((local === undefined || local === null) && (!imported || imported === null))
287
+ return;
288
+ if (isShadowed(node, name, language, allowedBindings))
289
+ return;
290
+ if (local && local !== null) {
291
+ add(node, kind, { kind: 'local', definitionOrdinal: local.ordinal });
292
+ }
293
+ else if (imported) {
294
+ add(node, kind, { kind: 'import', module: imported.module, importedName: imported.importedName });
295
+ }
296
+ };
297
+ walk(root, (node) => {
298
+ if (node.type !== 'call_expression' && node.type !== 'call')
299
+ return;
300
+ const callee = field(node, 'function');
301
+ if (callee?.type === 'identifier') {
302
+ handledCallIdentifiers.add(nodeKey(callee));
303
+ directUse(callee, 'calls');
304
+ return;
305
+ }
306
+ if (!callee)
307
+ return;
308
+ const property = propertyOf(callee);
309
+ const local = property ? localTargets.get(property.text) : undefined;
310
+ if (property && local && local !== null) {
311
+ handledProperties.add(nodeKey(property));
312
+ add(property, 'possible_textual_match', { kind: 'local', definitionOrdinal: local.ordinal });
313
+ }
314
+ });
315
+ walk(root, (node) => {
316
+ if (node.type !== 'identifier')
317
+ return;
318
+ if (handledCallIdentifiers.has(nodeKey(node)) || handledProperties.has(nodeKey(node)))
319
+ return;
320
+ if (isProperty(node)) {
321
+ const local = localTargets.get(node.text);
322
+ if (local && local !== null)
323
+ add(node, 'possible_textual_match', { kind: 'local', definitionOrdinal: local.ordinal });
324
+ return;
325
+ }
326
+ if (isValueReference(node, language))
327
+ directUse(node, 'references');
328
+ });
329
+ return usages.sort((a, b) => a.span.start_line - b.span.start_line
330
+ || a.span.start_col - b.span.start_col
331
+ || a.kind.localeCompare(b.kind));
332
+ }
333
+ //# sourceMappingURL=usages.js.map