@snevins/repo-mapper 1.5.2 → 1.5.3

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/dist/graph.d.ts CHANGED
@@ -9,6 +9,12 @@ export declare function computeFileDegrees(graph: FileGraph): Map<string, FileDe
9
9
  /**
10
10
  * Build file reference graph from parsed tags.
11
11
  * Nodes are files, edges are symbol references from one file to another.
12
+ *
13
+ * Scoped definitions: When a definition has a parent scope (e.g., a function
14
+ * inside a contract/class), edges are only created if the referencing file
15
+ * also references the parent scope. This prevents spurious edges from name
16
+ * collisions (e.g., multiple contracts defining `bid()` won't all get edges
17
+ * from every file that references any `bid()`).
12
18
  */
13
19
  export declare function buildFileGraph(tags: readonly Tag[], options?: GraphBuildOptions): FileGraph;
14
20
  /**
@@ -18,5 +24,8 @@ export declare function buildFileGraph(tags: readonly Tag[], options?: GraphBuil
18
24
  * Unlike buildFileGraph which uses ref counts, this assigns weight 1
19
25
  * to each unique file-to-file import relationship regardless of how
20
26
  * many times symbols are referenced.
27
+ *
28
+ * Uses same scoped definition logic as buildFileGraph to prevent spurious
29
+ * edges from name collisions.
21
30
  */
22
31
  export declare function buildImportGraph(tags: readonly Tag[], options?: GraphBuildOptions): FileGraph;
package/dist/graph.js CHANGED
@@ -47,12 +47,20 @@ export function computeFileDegrees(graph) {
47
47
  /**
48
48
  * Build file reference graph from parsed tags.
49
49
  * Nodes are files, edges are symbol references from one file to another.
50
+ *
51
+ * Scoped definitions: When a definition has a parent scope (e.g., a function
52
+ * inside a contract/class), edges are only created if the referencing file
53
+ * also references the parent scope. This prevents spurious edges from name
54
+ * collisions (e.g., multiple contracts defining `bid()` won't all get edges
55
+ * from every file that references any `bid()`).
50
56
  */
51
57
  export function buildFileGraph(tags, options) {
52
58
  const getMultiplier = options?.edgeWeightMultiplier ?? (() => 1.0);
53
- // First pass: collect nodes and index defs
59
+ // First pass: collect nodes, index defs, and track refs per file
54
60
  const nodeSet = new Set();
55
61
  const defsByName = new Map();
62
+ // Track all symbol names referenced by each file (for parent scope checking)
63
+ const refsByFile = new Map();
56
64
  for (const tag of tags) {
57
65
  nodeSet.add(tag.relPath);
58
66
  if (tag.kind === "def") {
@@ -64,6 +72,15 @@ export function buildFileGraph(tags, options) {
64
72
  defsByName.set(tag.name, [tag]);
65
73
  }
66
74
  }
75
+ else {
76
+ // kind === "ref": Track all refs per file for parent scope checking
77
+ let fileRefs = refsByFile.get(tag.relPath);
78
+ if (!fileRefs) {
79
+ fileRefs = new Set();
80
+ refsByFile.set(tag.relPath, fileRefs);
81
+ }
82
+ fileRefs.add(tag.name);
83
+ }
67
84
  }
68
85
  // Sort nodes for determinism
69
86
  const nodes = [...nodeSet].sort();
@@ -79,10 +96,19 @@ export function buildFileGraph(tags, options) {
79
96
  const defs = defsByName.get(tag.name);
80
97
  if (!defs)
81
98
  continue;
99
+ // Get refs for this file (for parent scope checking)
100
+ const fileRefs = refsByFile.get(tag.relPath);
82
101
  for (const def of defs) {
83
102
  // Skip self-edges
84
103
  if (def.relPath === tag.relPath)
85
104
  continue;
105
+ // Scoped definition check: if the definition has a parent scope,
106
+ // only create edge if the referencing file also references the parent.
107
+ // This prevents spurious edges from name collisions across unrelated
108
+ // contracts/classes (e.g., multiple contracts with `bid()` function).
109
+ if (def.parent && (!fileRefs || !fileRefs.has(def.parent))) {
110
+ continue;
111
+ }
86
112
  const from = tag.relPath;
87
113
  const to = def.relPath;
88
114
  const symbol = tag.name;
@@ -167,12 +193,17 @@ export function buildFileGraph(tags, options) {
167
193
  * Unlike buildFileGraph which uses ref counts, this assigns weight 1
168
194
  * to each unique file-to-file import relationship regardless of how
169
195
  * many times symbols are referenced.
196
+ *
197
+ * Uses same scoped definition logic as buildFileGraph to prevent spurious
198
+ * edges from name collisions.
170
199
  */
171
200
  export function buildImportGraph(tags, options) {
172
201
  const getMultiplier = options?.edgeWeightMultiplier ?? (() => 1.0);
173
- // First pass: collect nodes and index defs
202
+ // First pass: collect nodes, index defs, and track refs per file
174
203
  const nodeSet = new Set();
175
204
  const defsByName = new Map();
205
+ // Track all symbol names referenced by each file (for parent scope checking)
206
+ const refsByFile = new Map();
176
207
  for (const tag of tags) {
177
208
  nodeSet.add(tag.relPath);
178
209
  if (tag.kind === "def") {
@@ -184,6 +215,15 @@ export function buildImportGraph(tags, options) {
184
215
  defsByName.set(tag.name, [tag]);
185
216
  }
186
217
  }
218
+ else {
219
+ // kind === "ref": Track all refs per file for parent scope checking
220
+ let fileRefs = refsByFile.get(tag.relPath);
221
+ if (!fileRefs) {
222
+ fileRefs = new Set();
223
+ refsByFile.set(tag.relPath, fileRefs);
224
+ }
225
+ fileRefs.add(tag.name);
226
+ }
187
227
  }
188
228
  const nodes = [...nodeSet].sort();
189
229
  // Second pass: build binary edges (1 per unique file→file connection)
@@ -197,9 +237,16 @@ export function buildImportGraph(tags, options) {
197
237
  const defs = defsByName.get(tag.name);
198
238
  if (!defs)
199
239
  continue;
240
+ // Get refs for this file (for parent scope checking)
241
+ const fileRefs = refsByFile.get(tag.relPath);
200
242
  for (const def of defs) {
201
243
  if (def.relPath === tag.relPath)
202
244
  continue;
245
+ // Scoped definition check: if the definition has a parent scope,
246
+ // only create edge if the referencing file also references the parent.
247
+ if (def.parent && (!fileRefs || !fileRefs.has(def.parent))) {
248
+ continue;
249
+ }
203
250
  const from = tag.relPath;
204
251
  const to = def.relPath;
205
252
  // Check if we already have this edge (binary: only count once)
package/dist/output.js CHANGED
@@ -83,22 +83,6 @@ function formatRankDisplay(rank, sortedRanks) {
83
83
  const percentile = computePercentile(rank, sortedRanks);
84
84
  return (percentile / 100).toFixed(1);
85
85
  }
86
- /**
87
- * Get all definition tags for a specific file from the graph.
88
- * Used to show all definitions for files that are included in output,
89
- * not just the ones that were cross-file referenced.
90
- */
91
- function getAllDefsForFile(file, graph) {
92
- const result = [];
93
- for (const tags of graph.defsByName.values()) {
94
- for (const tag of tags) {
95
- if (tag.relPath === file) {
96
- result.push(tag);
97
- }
98
- }
99
- }
100
- return result;
101
- }
102
86
  /**
103
87
  * Resolve RankedDefinitions to their Tags by looking up in graph.defsByName.
104
88
  * Returns only definitions that can be found.
@@ -246,18 +230,17 @@ export function formatOutput(defs, graph, fileRanks, focusFiles, maxFilesPerModu
246
230
  lines.push(`## ${mod}${suffix} (${String(modFiles.length)} file${modFiles.length > 1 ? "s" : ""})`);
247
231
  // Files in this module (already sorted by rank)
248
232
  for (const file of modFiles) {
249
- // Get ALL definitions for the file, not just cross-file referenced ones
250
- const allFileTags = getAllDefsForFile(file, graph);
251
- if (allFileTags.length === 0)
233
+ const fileDefs = fileGroups.get(file);
234
+ if (!fileDefs || fileDefs.length === 0)
252
235
  continue;
253
236
  const fileRank = fileRanks.get(file) ?? 0;
254
237
  const deps = getFileDependents(file, graph);
255
238
  const depsStr = deps.length > 0 ? ` (used-by: ${deps.slice(0, 10).join(", ")}${deps.length > 10 ? ", ..." : ""})` : "";
256
239
  lines.push(`${file}: ${formatRankDisplay(fileRank, allRanks)}${depsStr}`);
257
240
  // Sort definitions by line ascending
258
- const sorted = [...allFileTags].sort((a, b) => a.line - b.line);
259
- for (const tag of sorted) {
260
- lines.push(` ${tag.signature ?? tag.name}`);
241
+ const sorted = [...fileDefs].sort((a, b) => a.tag.line - b.tag.line);
242
+ for (const def of sorted) {
243
+ lines.push(` ${def.tag.signature ?? def.tag.name}`);
261
244
  }
262
245
  }
263
246
  // Omission annotation if files were limited
package/dist/parser.js CHANGED
@@ -75,6 +75,36 @@ const EXPORT_BLOCKERS = new Set([
75
75
  "class",
76
76
  "class_expression",
77
77
  ]);
78
+ /**
79
+ * Node types that establish a parent scope for member definitions.
80
+ * Used to track which contract/class/interface a function/struct belongs to.
81
+ * This prevents spurious edges from name collisions across unrelated scopes.
82
+ */
83
+ const PARENT_SCOPE_TYPES = {
84
+ solidity: new Set(["contract_declaration", "interface_declaration", "library_declaration"]),
85
+ typescript: new Set(["class_declaration", "abstract_class_declaration"]),
86
+ tsx: new Set(["class_declaration", "abstract_class_declaration"]),
87
+ javascript: new Set(["class_declaration"]),
88
+ jsx: new Set(["class_declaration"]),
89
+ python: new Set(["class_definition"]),
90
+ rust: new Set(["impl_item", "trait_item"]),
91
+ go: new Set(), // Go uses package-level scope, no class-like containers
92
+ };
93
+ /**
94
+ * Get the name of a parent scope node (contract/class/interface name).
95
+ */
96
+ function getParentScopeName(node) {
97
+ const nameNode = node.childForFieldName("name");
98
+ if (nameNode)
99
+ return nameNode.text;
100
+ // For Rust impl blocks: impl Foo { ... } or impl Trait for Foo { ... }
101
+ if (node.type === "impl_item") {
102
+ const typeNode = node.childForFieldName("type");
103
+ if (typeNode?.type === "type_identifier")
104
+ return typeNode.text;
105
+ }
106
+ return undefined;
107
+ }
78
108
  /**
79
109
  * Check if a variable_declarator is directly exported (not inside a function/class).
80
110
  * Stops at function/class boundaries to prevent local variables from being captured.
@@ -266,13 +296,18 @@ export async function parseFile(absPath, relPath) {
266
296
  // Walk tree to extract definitions and references
267
297
  const cursor = tree.walk();
268
298
  const visitedNodes = new Set();
269
- function walk() {
299
+ // Get parent scope types for this language
300
+ const parentScopeTypes = PARENT_SCOPE_TYPES[langConfig.grammarName] ?? new Set();
301
+ function walk(currentParent) {
270
302
  const node = cursor.currentNode;
271
303
  const nodeId = node.id;
272
304
  // Avoid visiting same node twice
273
305
  if (visitedNodes.has(nodeId))
274
306
  return;
275
307
  visitedNodes.add(nodeId);
308
+ // Check if this node establishes a new parent scope
309
+ const isParentScope = parentScopeTypes.has(node.type);
310
+ const newParent = isParentScope ? getParentScopeName(node) : currentParent;
276
311
  // Check for definition nodes using config's definition types
277
312
  if (langConfig.definitionTypes.has(node.type)) {
278
313
  if (shouldIncludeDefinition(node, langConfig)) {
@@ -294,6 +329,9 @@ export async function parseFile(absPath, relPath) {
294
329
  // For Python/Go/Rust/Solidity: all captured defs are "exported" (public API)
295
330
  nodeIsExported = true;
296
331
  }
332
+ // For parent scope definitions (contract/class), don't set parent
333
+ // (they ARE the parent, not children of another scope)
334
+ const defParent = isParentScope ? undefined : currentParent;
297
335
  tags.push({
298
336
  relPath,
299
337
  absPath,
@@ -302,6 +340,7 @@ export async function parseFile(absPath, relPath) {
302
340
  kind: "def",
303
341
  signature: getSignature(node, sourceLines),
304
342
  isExported: nodeIsExported,
343
+ parent: defParent,
305
344
  });
306
345
  }
307
346
  }
@@ -318,6 +357,7 @@ export async function parseFile(absPath, relPath) {
318
357
  kind: "def",
319
358
  signature: getSignature(node, sourceLines),
320
359
  isExported: true, // isDirectlyExportedVariable already checks this
360
+ parent: currentParent,
321
361
  });
322
362
  }
323
363
  }
@@ -334,10 +374,10 @@ export async function parseFile(absPath, relPath) {
334
374
  });
335
375
  }
336
376
  }
337
- // Recurse into children
377
+ // Recurse into children with updated parent scope
338
378
  if (cursor.gotoFirstChild()) {
339
379
  do {
340
- walk();
380
+ walk(newParent);
341
381
  } while (cursor.gotoNextSibling());
342
382
  cursor.gotoParent();
343
383
  }
package/dist/types.d.ts CHANGED
@@ -10,6 +10,11 @@ export interface Tag {
10
10
  readonly signature?: string;
11
11
  /** Whether this definition is exported (public API). Only set for "def" kind. */
12
12
  readonly isExported?: boolean;
13
+ /**
14
+ * Parent scope for member definitions (e.g., "ReserveAuction" for a function inside that contract).
15
+ * Used to prevent spurious edges from name collisions across unrelated contracts/classes.
16
+ */
17
+ readonly parent?: string;
13
18
  }
14
19
  /**
15
20
  * CLI options parsed from command line arguments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snevins/repo-mapper",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Generate token-budgeted repo maps for LLM context using tree-sitter and PageRank",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",