brainclaw 1.25.0 → 1.26.1

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 (37) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/commands/code-map.js +1 -4
  3. package/dist/commands/mcp.js +7 -7
  4. package/dist/commands/session-start.js +137 -15
  5. package/dist/core/bootstrap.js +28 -4
  6. package/dist/core/code-map/aggregate.js +36 -31
  7. package/dist/core/code-map/backend.js +4 -4
  8. package/dist/core/code-map/core.js +1 -0
  9. package/dist/core/code-map/export.js +4 -4
  10. package/dist/core/code-map/finalizer.js +57 -2
  11. package/dist/core/code-map/freshness.js +78 -13
  12. package/dist/core/code-map/impact.js +36 -4
  13. package/dist/core/code-map/indexes.js +37 -0
  14. package/dist/core/code-map/lang/python/index.js +4 -2
  15. package/dist/core/code-map/lang/query-runtime.js +2 -0
  16. package/dist/core/code-map/lang/typescript/index.js +4 -2
  17. package/dist/core/code-map/lang/usages.js +333 -0
  18. package/dist/core/code-map/memory-reader.js +15 -0
  19. package/dist/core/code-map/query.js +209 -58
  20. package/dist/core/code-map/refresh.js +0 -0
  21. package/dist/core/code-map/resolve.js +27 -2
  22. package/dist/core/code-map/store.js +1 -0
  23. package/dist/core/code-map/types.js +55 -9
  24. package/dist/core/code-map/vocabulary.js +6 -0
  25. package/dist/core/code-map/work-section.js +12 -14
  26. package/dist/core/context-diff.js +17 -3
  27. package/dist/core/entity-operations.js +14 -2
  28. package/dist/core/hint-aging.js +4 -1
  29. package/dist/core/identity.js +284 -91
  30. package/dist/core/io.js +192 -0
  31. package/dist/core/project-discovery.js +7 -1
  32. package/dist/core/runtime.js +99 -11
  33. package/dist/core/store-resolution.js +5 -21
  34. package/dist/facts.js +12 -12
  35. package/dist/facts.json +11 -11
  36. package/docs/code-map.md +36 -27
  37. package/package.json +1 -1
@@ -45,9 +45,72 @@ export function coarseFreshness(status) {
45
45
  }
46
46
  }
47
47
  }
48
- /** pln#601 — stamp/refresh a badge's `coarse` rollup from its (possibly just-adjusted) status. */
49
- export function withCoarse(b) {
50
- return { ...b, coarse: coarseFreshness(b.status) };
48
+ /**
49
+ * Build the canonical, surface-uniform badge. `freshness` is derived solely from
50
+ * the index state supplied as `status`; a query's bounded spot-check is diagnostic
51
+ * evidence under `details.spot_check`, never a competing top-level badge.
52
+ */
53
+ export function makeFreshnessBadge(status, options = {}) {
54
+ const spot = options.spotCheck ?? {};
55
+ return {
56
+ freshness: coarseFreshness(status),
57
+ status,
58
+ details: {
59
+ ...(options.extra ?? {}),
60
+ index: {
61
+ status,
62
+ stale_file_count: options.staleFileCount ?? 0,
63
+ partial_reason: options.partialReason ?? null,
64
+ git_head_changed: options.gitHeadChanged ?? null,
65
+ },
66
+ spot_check: {
67
+ status: spot.status ?? 'not_run',
68
+ checked_files: spot.checked_files ?? 0,
69
+ stale_changed_files: spot.stale_changed_files ?? [],
70
+ deleted_files: spot.deleted_files ?? [],
71
+ unchecked_files: spot.unchecked_files ?? [],
72
+ budget_exhausted: spot.budget_exhausted ?? false,
73
+ partial_reason: spot.partial_reason ?? null,
74
+ },
75
+ },
76
+ };
77
+ }
78
+ /**
79
+ * Compatibility normalizer for internal callers that previously constructed a
80
+ * `{ status, details }` badge. It preserves non-freshness metadata while always
81
+ * adding the two canonical detail sections.
82
+ */
83
+ export function withFreshness(b) {
84
+ const raw = b.details ?? {};
85
+ const index = raw.index;
86
+ const spot = raw.spot_check;
87
+ const known = new Set([
88
+ 'index', 'spot_check', 'stale_file_count', 'partial_reason', 'git_head_changed',
89
+ 'stale_changed_files', 'deleted_files', 'unchecked_files', 'budget',
90
+ ]);
91
+ const extra = Object.fromEntries(Object.entries(raw).filter(([key]) => !known.has(key)));
92
+ const stringArray = (value) => Array.isArray(value) ? value.map(String).sort() : [];
93
+ const numberValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
94
+ const nullableString = (value) => typeof value === 'string' ? value : value === null ? null : undefined;
95
+ const git = (index?.git_head_changed ?? raw.git_head_changed);
96
+ const gitHeadChanged = git && typeof git.index_head === 'string' && typeof git.current_head === 'string'
97
+ ? { index_head: git.index_head, current_head: git.current_head }
98
+ : null;
99
+ return makeFreshnessBadge(b.status, {
100
+ staleFileCount: numberValue(index?.stale_file_count ?? raw.stale_file_count),
101
+ partialReason: nullableString(index?.partial_reason ?? raw.partial_reason),
102
+ gitHeadChanged,
103
+ spotCheck: {
104
+ status: spot?.status,
105
+ checked_files: numberValue(spot?.checked_files),
106
+ stale_changed_files: stringArray(spot?.stale_changed_files ?? raw.stale_changed_files),
107
+ deleted_files: stringArray(spot?.deleted_files ?? raw.deleted_files),
108
+ unchecked_files: stringArray(spot?.unchecked_files ?? raw.unchecked_files),
109
+ budget_exhausted: spot?.budget_exhausted === true,
110
+ partial_reason: nullableString(spot?.partial_reason),
111
+ },
112
+ extra,
113
+ });
51
114
  }
52
115
  /** Stable serialization: sort object keys recursively so hashing is order-independent. */
53
116
  function stableStringify(value) {
@@ -165,16 +228,18 @@ export function summarizeFreshness(shards) {
165
228
  * actionable status; only the cause detail is added.
166
229
  */
167
230
  export function applyGitHeadDrift(badge, indexHead, currentHead) {
231
+ const normalized = withFreshness(badge);
232
+ const currentIndex = normalized.details.index;
168
233
  if (!indexHead || !currentHead || indexHead === currentHead)
169
- return withCoarse(badge);
170
- const status = badge.status === 'fresh' ? 'stale_git_head' : badge.status;
171
- return {
172
- status,
173
- coarse: coarseFreshness(status),
174
- details: {
175
- ...badge.details,
176
- git_head_changed: { index_head: indexHead, current_head: currentHead },
177
- },
178
- };
234
+ return normalized;
235
+ const status = normalized.status === 'fresh' ? 'stale_git_head' : normalized.status;
236
+ const extra = Object.fromEntries(Object.entries(normalized.details).filter(([key]) => key !== 'index' && key !== 'spot_check'));
237
+ return makeFreshnessBadge(status, {
238
+ staleFileCount: currentIndex.stale_file_count,
239
+ partialReason: currentIndex.partial_reason,
240
+ gitHeadChanged: { index_head: indexHead, current_head: currentHead },
241
+ spotCheck: normalized.details.spot_check,
242
+ extra,
243
+ });
179
244
  }
180
245
  //# sourceMappingURL=freshness.js.map
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import path from 'node:path';
10
10
  import { fileId } from './ids.js';
11
+ import { makeFreshnessBadge } from './freshness.js';
11
12
  import { readManifest, readResolutionIndex, readShard, readSymbolsIndex, } from './store.js';
12
13
  import { deriveBadge, isTestPath, makeLazyChecker, newAccumulator, validateStoreEntry, } from './query.js';
13
14
  /** Direct results and each optional transitive layer are independently bounded. */
@@ -100,12 +101,14 @@ function causeKey(cause) {
100
101
  cause.target.kind,
101
102
  cause.target.path,
102
103
  cause.target.node_id ?? '',
104
+ cause.caller?.node_id ?? '',
103
105
  ].join('\u0001');
104
106
  }
105
107
  function compareCause(a, b) {
106
108
  return a.kind.localeCompare(b.kind)
107
109
  || a.target.path.localeCompare(b.target.path)
108
110
  || (a.target.node_id ?? '').localeCompare(b.target.node_id ?? '')
111
+ || (a.caller?.node_id ?? '').localeCompare(b.caller?.node_id ?? '')
109
112
  || (a.module ?? '').localeCompare(b.module ?? '')
110
113
  || (a.source_line ?? -1) - (b.source_line ?? -1)
111
114
  || a.imported.join('\u0000').localeCompare(b.imported.join('\u0000'))
@@ -138,6 +141,33 @@ function addRelation(rows, entry, depth, target, kind) {
138
141
  current.causes.sort(compareCause);
139
142
  rows.set(entry.path, current);
140
143
  }
144
+ function addUsageRelation(rows, entry, depth, target) {
145
+ const current = rows.get(entry.path) ?? {
146
+ path: entry.path,
147
+ file_id: entry.file_id,
148
+ depth,
149
+ causes: [],
150
+ causeKeys: new Set(),
151
+ };
152
+ current.depth = Math.min(current.depth, depth);
153
+ for (const reason of entry.reasons) {
154
+ const cause = {
155
+ kind: reason.kind,
156
+ imported: [],
157
+ confidence: reason.confidence,
158
+ ...(reason.source_line !== undefined ? { source_line: reason.source_line } : {}),
159
+ caller: { node_id: reason.caller_node_id },
160
+ target,
161
+ };
162
+ const key = causeKey(cause);
163
+ if (!current.causeKeys.has(key)) {
164
+ current.causeKeys.add(key);
165
+ current.causes.push(cause);
166
+ }
167
+ }
168
+ current.causes.sort(compareCause);
169
+ rows.set(entry.path, current);
170
+ }
141
171
  function publicRelation(row) {
142
172
  return {
143
173
  path: row.path,
@@ -205,7 +235,7 @@ export function impact(target, options, ctx) {
205
235
  freshness_badge: freshness,
206
236
  });
207
237
  if (!symbolsIndex || !manifest) {
208
- return empty({ status: 'missing_index', coarse: 'missing', details: { hint: 'run refresh' } });
238
+ return empty(makeFreshnessBadge('missing_index', { extra: { hint: 'run refresh' } }));
209
239
  }
210
240
  let matchKind = 'none';
211
241
  let rawDefinitions = [];
@@ -257,10 +287,12 @@ export function impact(target, options, ctx) {
257
287
  const directRows = new Map();
258
288
  if (resolution) {
259
289
  for (const definition of definitionByNodeId.values()) {
290
+ const target = { kind: 'symbol', path: definition.path, node_id: definition.node_id, name: definition.name };
260
291
  for (const dependent of resolution.dependents_by_symbol[definition.node_id] ?? []) {
261
- addRelation(directRows, dependent, 1, {
262
- kind: 'symbol', path: definition.path, node_id: definition.node_id, name: definition.name,
263
- }, 'imports_symbol');
292
+ addRelation(directRows, dependent, 1, target, 'imports_symbol');
293
+ }
294
+ for (const dependent of resolution.usages_by_symbol[definition.node_id] ?? []) {
295
+ addUsageRelation(directRows, dependent, 1, target);
264
296
  }
265
297
  }
266
298
  for (const definitionPath of definitionPaths) {
@@ -130,6 +130,29 @@ export function buildResolutionIndex(projectId, shards) {
130
130
  // target key -> (importer path -> merged entry)
131
131
  const byFile = new Map();
132
132
  const bySymbol = new Map();
133
+ const byUsageSymbol = new Map();
134
+ const addUsage = (targetSymbolId, importerPath, importerFileId, edge) => {
135
+ const perImporter = byUsageSymbol.get(targetSymbolId) ?? new Map();
136
+ const entry = perImporter.get(importerPath) ?? { path: importerPath, file_id: importerFileId, reasons: [] };
137
+ const reason = {
138
+ kind: edge.kind,
139
+ caller_node_id: edge.from,
140
+ confidence: edge.confidence,
141
+ ...(edge.source?.line !== undefined ? { source_line: edge.source.line } : {}),
142
+ };
143
+ if (!entry.reasons.some((existing) => existing.kind === reason.kind
144
+ && existing.caller_node_id === reason.caller_node_id
145
+ && existing.confidence === reason.confidence
146
+ && existing.source_line === reason.source_line)) {
147
+ entry.reasons.push(reason);
148
+ entry.reasons.sort((a, b) => a.kind.localeCompare(b.kind)
149
+ || a.caller_node_id.localeCompare(b.caller_node_id)
150
+ || (a.source_line ?? -1) - (b.source_line ?? -1)
151
+ || a.confidence - b.confidence);
152
+ }
153
+ perImporter.set(importerPath, entry);
154
+ byUsageSymbol.set(targetSymbolId, perImporter);
155
+ };
133
156
  const addDependent = (bucket, targetKey, importerPath, importerFileId, module, imported, confidence, kind, sourceLine) => {
134
157
  const perImporter = bucket.get(targetKey) ?? new Map();
135
158
  const prev = perImporter.get(importerPath);
@@ -189,6 +212,12 @@ export function buildResolutionIndex(projectId, shards) {
189
212
  moduleById.set(n.id, { name: n.name, imported: n.imported_names ?? [] });
190
213
  }
191
214
  for (const e of shard.edges) {
215
+ if (e.kind === 'calls' || e.kind === 'references') {
216
+ // `possible_textual_match` remains deliberately absent: it is a hint on
217
+ // the shard, never an impact dependency.
218
+ addUsage(e.to, shard.path, shard.file_id, { ...e, kind: e.kind });
219
+ continue;
220
+ }
192
221
  if (e.kind !== 'resolves_to' && e.kind !== 'imports_symbol')
193
222
  continue;
194
223
  const mod = moduleById.get(e.from);
@@ -210,12 +239,20 @@ export function buildResolutionIndex(projectId, shards) {
210
239
  }
211
240
  return out;
212
241
  };
242
+ const finalizeUsages = (bucket) => {
243
+ const out = Object.create(null);
244
+ for (const key of [...bucket.keys()].sort()) {
245
+ out[key] = [...bucket.get(key).values()].sort((a, b) => a.path.localeCompare(b.path));
246
+ }
247
+ return out;
248
+ };
213
249
  return {
214
250
  schema_version: CODE_MAP_SCHEMA_VERSION,
215
251
  project_id: projectId,
216
252
  updated_at: new Date().toISOString(),
217
253
  dependents_by_file: finalize(byFile),
218
254
  dependents_by_symbol: finalize(bySymbol),
255
+ usages_by_symbol: finalizeUsages(byUsageSymbol),
219
256
  };
220
257
  }
221
258
  //# sourceMappingURL=indexes.js.map
@@ -30,6 +30,7 @@ import path from 'node:path';
30
30
  import { fileURLToPath } from 'node:url';
31
31
  import { loadGrammarWasm, grammarHashForWasm } from '../../wasm-loader.js';
32
32
  import { extractWithQueries } from '../query-runtime.js';
33
+ import { extractLexicalUsages } from '../usages.js';
33
34
  const HERE = path.dirname(fileURLToPath(import.meta.url));
34
35
  /** The python grammar .wasm: dist basename + node_modules devDep fallback spec. */
35
36
  const PY_WASM_BASENAME = 'tree-sitter-python.wasm';
@@ -106,7 +107,7 @@ const queries = {
106
107
  };
107
108
  const vocabulary = {
108
109
  nodeSubtypes: ['function', 'method', 'class', 'variable', 'constant', 'property'],
109
- edgeKinds: ['contains', 'defines', 'imports'],
110
+ edgeKinds: ['contains', 'defines', 'imports', 'calls', 'references', 'possible_textual_match'],
110
111
  captureMap: queries.captureMap,
111
112
  };
112
113
  const capabilities = {
@@ -284,7 +285,8 @@ export class PythonProvider {
284
285
  }
285
286
  return d;
286
287
  });
287
- return { ...draft, definitions };
288
+ const tree = draft.attributes?.__tree;
289
+ return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'python') };
288
290
  }
289
291
  /**
290
292
  * P1c file-level import resolution (T3). Returns at most one resolution per import:
@@ -121,6 +121,7 @@ export async function extractWithQueries(input) {
121
121
  imports: [],
122
122
  exports: [],
123
123
  tests: [],
124
+ usages: [],
124
125
  facts,
125
126
  attributes: { parseStatus, __tree: tree },
126
127
  });
@@ -367,6 +368,7 @@ export async function extractWithQueries(input) {
367
368
  imports,
368
369
  exports,
369
370
  tests: [],
371
+ usages: [],
370
372
  facts,
371
373
  attributes: { parseStatus, __tree: tree },
372
374
  };
@@ -26,6 +26,7 @@ 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';
29
30
  import { isTypeScriptResolutionConfig, typeScriptSpecifierBases, } from './config.js';
30
31
  const HERE = path.dirname(fileURLToPath(import.meta.url));
31
32
  /** Resolve a vendored `.scm` next to this module (dist) or from the source tree. */
@@ -150,7 +151,7 @@ const queries = {
150
151
  };
151
152
  const vocabulary = {
152
153
  nodeSubtypes: ['function', 'class', 'type', 'interface', 'variable', 'component', 'hook', 'export'],
153
- edgeKinds: ['contains', 'defines', 'imports', 'exports'],
154
+ edgeKinds: ['contains', 'defines', 'imports', 'exports', 'calls', 'references', 'possible_textual_match'],
154
155
  captureMap: queries.captureMap,
155
156
  };
156
157
  const capabilities = {
@@ -305,7 +306,8 @@ export class TypeScriptProvider {
305
306
  }
306
307
  return d;
307
308
  });
308
- return { ...draft, definitions };
309
+ const tree = draft.attributes?.__tree;
310
+ return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'js-ts') };
309
311
  }
310
312
  /**
311
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