@mishasinitcyn/betterrank 0.2.9 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mishasinitcyn/betterrank",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/cli.js CHANGED
@@ -51,6 +51,10 @@ function/class signatures with bodies replaced by "... (N lines)".
51
51
  With symbol names (comma-separated): shows the full source of those
52
52
  specific functions/classes with line numbers.
53
53
 
54
+ Also works well for indexed JS/TS object members like router procedures:
55
+ use \`outline file.ts getTeamStatistics\` instead of expanding a whole giant
56
+ \`createTRPCRouter({ ... })\` blob.
57
+
54
58
  Options:
55
59
  --root <path> Resolve file path relative to this directory
56
60
  --annotate Show caller counts next to each function (requires --root)
@@ -67,7 +71,8 @@ Examples:
67
71
  Aider-style repo map: the most structurally important definitions ranked by PageRank.
68
72
 
69
73
  Options:
70
- --focus <files> Comma-separated files to bias ranking toward
74
+ --focus <files> Comma-separated files to strongly bias ranking toward the
75
+ local import neighborhood of those files
71
76
  --count Return total symbol count only
72
77
  --offset N Skip first N symbols
73
78
  --limit N Max symbols to return (default: ${DEFAULT_LIMIT})
@@ -83,7 +88,7 @@ Substring search on symbol names + full signatures (param names, types, defaults
83
88
  Results ranked by PageRank (most structurally important first).
84
89
 
85
90
  Options:
86
- --kind <type> Filter: function, class, type, variable, namespace, import
91
+ --kind <type> Filter: function, class, type, variable, property, namespace, import
87
92
  --count Return match count only
88
93
  --offset N Skip first N results
89
94
  --limit N Max results (default: ${DEFAULT_LIMIT})
@@ -92,6 +97,7 @@ Tips:
92
97
  Use short substrings (3-5 chars) — PageRank ranking handles noise.
93
98
  "imp" finds encrypt_imp_payload, increment_impression, etc.
94
99
  Searches match against both symbol names AND full signatures (param names, types).
100
+ JS/TS router procedures and other indexed object members appear as [property].
95
101
 
96
102
  Examples:
97
103
  betterrank search resolve --root ./backend
@@ -115,19 +121,23 @@ Results ranked by PageRank (most structurally important first).
115
121
 
116
122
  Options:
117
123
  --file <path> Filter to a specific file (relative to --root)
118
- --kind <type> Filter: function, class, type, variable, namespace, import
124
+ --kind <type> Filter: function, class, type, variable, property, namespace, import
119
125
  --count Return count only
120
126
  --offset N Skip first N results
121
127
  --limit N Max results (default: ${DEFAULT_LIMIT})
122
128
 
123
129
  Examples:
124
130
  betterrank symbols --file src/auth/handlers.ts --root ./backend
125
- betterrank symbols --kind class --root . --limit 20`,
131
+ betterrank symbols --kind class --root . --limit 20
132
+ betterrank symbols --file src/server/api/routers/project.ts --kind property --root .`,
126
133
 
127
134
  callers: `betterrank callers <symbol> [--file path] [--context [N]] [--root <path>]
128
135
 
129
136
  Find all files that reference a symbol. Ranked by file-level PageRank.
130
137
 
138
+ For React/TS apps this includes JSX render sites like <Providers /> and
139
+ property access sites like api.project.getTeamStatistics.useQuery().
140
+
131
141
  Options:
132
142
  --file <path> Disambiguate when multiple symbols share a name
133
143
  --context [N] Show N lines of context around each call site (default: 2)
@@ -142,7 +152,7 @@ Examples:
142
152
 
143
153
  context: `betterrank context <symbol> [--file path] [--root <path>]
144
154
 
145
- Everything you need to understand a function in one shot.
155
+ Everything you need to understand a function or indexed property in one shot.
146
156
 
147
157
  Shows: the function's source, signatures of all functions/types it references,
148
158
  expanded type definitions from the signature, and a callers summary.
@@ -155,7 +165,8 @@ Options:
155
165
 
156
166
  Examples:
157
167
  betterrank context calculate_bid --root .
158
- betterrank context Router --file src/llm.py --root .`,
168
+ betterrank context Router --file src/llm.py --root .
169
+ betterrank context getTeamStatistics --file src/server/api/routers/project.ts --root .`,
159
170
 
160
171
  history: `betterrank history <symbol> [--file path] [--patch] [--limit N] [--root <path>]
161
172
 
@@ -212,6 +223,8 @@ Examples:
212
223
 
213
224
  What this file imports / depends on. Ranked by PageRank.
214
225
 
226
+ Resolves explicit import paths including TS/JS aliases and Python relative imports.
227
+
215
228
  Options:
216
229
  --count Return count only
217
230
  --offset N Skip first N results
@@ -224,6 +237,9 @@ Examples:
224
237
 
225
238
  What files import this file. Ranked by PageRank.
226
239
 
240
+ Resolves explicit import paths including side-effect imports, TS/JS aliases,
241
+ and Python relative imports.
242
+
227
243
  Options:
228
244
  --count Return count only
229
245
  --offset N Skip first N results
package/src/graph.js CHANGED
@@ -84,6 +84,37 @@ function buildFileLookup(graph) {
84
84
  return fileLookup;
85
85
  }
86
86
 
87
+ function isPythonFile(filePath) {
88
+ return normalizeFilePath(filePath).endsWith('.py');
89
+ }
90
+
91
+ function resolvePythonImportBase(sourceFile, specifier) {
92
+ if (!isPythonFile(sourceFile)) return null;
93
+ if (!specifier) return null;
94
+
95
+ if (specifier.startsWith('.')) {
96
+ const match = specifier.match(/^(\.+)(.*)$/);
97
+ if (!match) return null;
98
+
99
+ const dots = match[1].length;
100
+ let baseDir = posix.dirname(sourceFile);
101
+ for (let i = 1; i < dots; i++) {
102
+ baseDir = posix.dirname(baseDir);
103
+ }
104
+
105
+ const remainder = match[2].replace(/^\./, '');
106
+ if (!remainder) return baseDir;
107
+
108
+ return posix.normalize(posix.join(baseDir, remainder.replace(/\./g, '/')));
109
+ }
110
+
111
+ if (/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(specifier)) {
112
+ return specifier.replace(/\./g, '/');
113
+ }
114
+
115
+ return null;
116
+ }
117
+
87
118
  function resolveImportBase(sourceFile, specifier) {
88
119
  const normalizedSource = normalizeFilePath(sourceFile);
89
120
  const normalizedSpecifier = normalizeFilePath(specifier);
@@ -105,6 +136,9 @@ function resolveImportBase(sourceFile, specifier) {
105
136
  return posix.normalize(normalizedSpecifier);
106
137
  }
107
138
 
139
+ const pythonBase = resolvePythonImportBase(normalizedSource, normalizedSpecifier);
140
+ if (pythonBase) return pythonBase;
141
+
108
142
  return null;
109
143
  }
110
144
 
@@ -121,8 +155,11 @@ function resolveImportTargetFile(sourceFile, specifier, fileLookup) {
121
155
  for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
122
156
  candidates.push(`${importBase}${ext}`);
123
157
  }
158
+ const packageEntryNames = isPythonFile(sourceFile) ? ['__init__'] : ['index'];
124
159
  for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
125
- candidates.push(posix.join(importBase, `index${ext}`));
160
+ for (const entryName of packageEntryNames) {
161
+ candidates.push(posix.join(importBase, `${entryName}${ext}`));
162
+ }
126
163
  }
127
164
  }
128
165
 
@@ -236,6 +273,10 @@ function updateGraphFiles(graph, removedFiles, newSymbols) {
236
273
  continue;
237
274
  }
238
275
 
276
+ if (!graph.hasNode(filePath)) {
277
+ continue;
278
+ }
279
+
239
280
  const outgoingEdges = [];
240
281
  graph.forEachOutEdge(filePath, edge => {
241
282
  outgoingEdges.push(edge);
package/src/index.js CHANGED
@@ -232,6 +232,26 @@ function paginate(arr, { offset = 0, limit } = {}) {
232
232
  return { items, total };
233
233
  }
234
234
 
235
+ const CONTEXT_NOISE_NAMES = new Set([
236
+ 'get', 'set', 'put', 'post', 'delete', 'head', 'patch',
237
+ 'start', 'stop', 'run', 'main', 'init', 'setup', 'close',
238
+ 'dict', 'list', 'str', 'int', 'bool', 'float', 'type',
239
+ 'key', 'value', 'name', 'data', 'config', 'result', 'error',
240
+ 'test', 'self', 'cls', 'app', 'log', 'logger',
241
+ 'enabled', 'default', 'constructor', 'length', 'size',
242
+ 'fetch', 'send', 'table', 'one', 'append', 'write', 'read',
243
+ 'update', 'create', 'find', 'add', 'remove', 'index', 'map',
244
+ 'filter', 'sort', 'join', 'split', 'trim', 'replace',
245
+ 'push', 'pop', 'shift', 'reduce', 'keys', 'values', 'items',
246
+ 'search', 'match', 'query', 'count', 'call', 'apply', 'bind',
247
+ ]);
248
+
249
+ const CONTEXT_KIND_ORDER = { function: 0, class: 1, type: 2, property: 3, variable: 4 };
250
+
251
+ function escapeRegExp(value) {
252
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
253
+ }
254
+
235
255
  class CodeIndex {
236
256
  constructor(projectRoot, opts = {}) {
237
257
  this.projectRoot = projectRoot;
@@ -1117,55 +1137,60 @@ class CodeIndex {
1117
1137
  const bodyLines = lines.slice(target.lineStart - 1, target.lineEnd);
1118
1138
  const bodyText = bodyLines.join('\n');
1119
1139
 
1120
- // Build a set of all symbol names in the graph (for matching)
1121
- const allSymbols = new Map(); // name -> [{ file, kind, signature, lineStart }]
1140
+ // Build a set of all symbol names in the graph.
1141
+ const allSymbols = new Map(); // name -> [{ file, kind, signature, lineStart, score }]
1122
1142
  graph.forEachNode((node, attrs) => {
1123
1143
  if (attrs.type !== 'symbol') return;
1124
1144
  if (attrs.file === target.file && attrs.name === target.name) return; // skip self
1125
1145
  if (!allSymbols.has(attrs.name)) allSymbols.set(attrs.name, []);
1126
1146
  allSymbols.get(attrs.name).push({
1147
+ key: node,
1127
1148
  file: attrs.file,
1128
1149
  kind: attrs.kind,
1129
1150
  signature: attrs.signature,
1130
1151
  lineStart: attrs.lineStart,
1131
1152
  lineEnd: attrs.lineEnd,
1153
+ score: scoreMap.get(node) || 0,
1132
1154
  });
1133
1155
  });
1134
1156
 
1135
- // Find symbols referenced in the body
1136
- // Use word-boundary matching for each known symbol name
1137
- // Skip very common names that cause false positives
1138
- const NOISE_NAMES = new Set([
1139
- 'get', 'set', 'put', 'post', 'delete', 'head', 'patch',
1140
- 'start', 'stop', 'run', 'main', 'init', 'setup', 'close',
1141
- 'dict', 'list', 'str', 'int', 'bool', 'float', 'type',
1142
- 'key', 'value', 'name', 'data', 'config', 'result', 'error',
1143
- 'test', 'self', 'cls', 'app', 'log', 'logger',
1144
- 'enabled', 'default', 'constructor', 'length', 'size',
1145
- 'fetch', 'send', 'table', 'one', 'append', 'write', 'read',
1146
- 'update', 'create', 'find', 'add', 'remove', 'index', 'map',
1147
- 'filter', 'sort', 'join', 'split', 'trim', 'replace',
1148
- 'push', 'pop', 'shift', 'reduce', 'keys', 'values', 'items',
1149
- 'search', 'match', 'query', 'count', 'call', 'apply', 'bind',
1150
- ]);
1151
- const usedSymbols = [];
1152
- const seen = new Set();
1153
- for (const [name, defs] of allSymbols) {
1154
- if (name.length < 3) continue; // skip very short names
1155
- if (NOISE_NAMES.has(name)) continue;
1156
- if (seen.has(name)) continue;
1157
- const pattern = new RegExp(`(?<![a-zA-Z0-9_])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-zA-Z0-9_])`);
1158
- if (pattern.test(bodyText)) {
1159
- seen.add(name);
1160
- // Pick the best definition (same-file first, then highest PageRank)
1161
- const sameFile = defs.find(d => d.file === target.file);
1162
- const best = sameFile || defs[0];
1163
- usedSymbols.push({ name, ...best });
1157
+ for (const defs of allSymbols.values()) {
1158
+ defs.sort((a, b) =>
1159
+ Number(b.file === target.file) - Number(a.file === target.file)
1160
+ || (b.score || 0) - (a.score || 0)
1161
+ || a.file.localeCompare(b.file)
1162
+ || a.lineStart - b.lineStart,
1163
+ );
1164
+ }
1165
+
1166
+ // Find symbols referenced in the body, preferring parser-scoped local refs.
1167
+ const referenceNames = new Set();
1168
+ for (const name of target.localRefs || []) {
1169
+ if (!name || name.length < 3) continue;
1170
+ if (CONTEXT_NOISE_NAMES.has(name)) continue;
1171
+ if (!allSymbols.has(name)) continue;
1172
+ referenceNames.add(name);
1173
+ }
1174
+
1175
+ // Fallback for older caches or definitions without scoped refs.
1176
+ if (referenceNames.size === 0) {
1177
+ for (const name of allSymbols.keys()) {
1178
+ if (name.length < 3) continue;
1179
+ if (CONTEXT_NOISE_NAMES.has(name)) continue;
1180
+ const pattern = new RegExp(`(?<![a-zA-Z0-9_])${escapeRegExp(name)}(?![a-zA-Z0-9_])`);
1181
+ if (pattern.test(bodyText)) {
1182
+ referenceNames.add(name);
1183
+ }
1164
1184
  }
1165
1185
  }
1166
- // Sort: functions first, then types, then by name
1167
- const kindOrder = { function: 0, class: 1, type: 2, variable: 3 };
1168
- usedSymbols.sort((a, b) => (kindOrder[a.kind] ?? 9) - (kindOrder[b.kind] ?? 9) || a.name.localeCompare(b.name));
1186
+
1187
+ const usedSymbols = [];
1188
+ for (const name of referenceNames) {
1189
+ const defs = allSymbols.get(name);
1190
+ if (!defs || defs.length === 0) continue;
1191
+ usedSymbols.push({ name, ...defs[0] });
1192
+ }
1193
+ usedSymbols.sort((a, b) => (CONTEXT_KIND_ORDER[a.kind] ?? 9) - (CONTEXT_KIND_ORDER[b.kind] ?? 9) || a.name.localeCompare(b.name));
1169
1194
 
1170
1195
  // Resolve type annotations in the signature
1171
1196
  // Extract type-like tokens from the signature (capitalized words, common patterns)
@@ -1181,7 +1206,7 @@ class CodeIndex {
1181
1206
  const typeDefs = allSymbols.get(typeName);
1182
1207
  if (!typeDefs) continue;
1183
1208
  // Find the type definition and get its fields (expand its body)
1184
- const best = typeDefs.find(d => d.file === target.file) || typeDefs[0];
1209
+ const best = typeDefs[0];
1185
1210
  if (best.kind === 'class' || best.kind === 'type') {
1186
1211
  let fields = null;
1187
1212
  try {
package/src/parser.js CHANGED
@@ -693,6 +693,52 @@ function normalizeImportPathCapture(capture) {
693
693
  return text.replace(/^['"`]|['"`]$/g, '');
694
694
  }
695
695
 
696
+ function createImportEntry(path, filePath, node) {
697
+ if (!path || !filePath || !node) return null;
698
+ return {
699
+ path,
700
+ file: filePath,
701
+ line: node.startPosition.row + 1,
702
+ };
703
+ }
704
+
705
+ function collectPythonImports(rootNode, filePath) {
706
+ const imports = [];
707
+
708
+ function push(path, node) {
709
+ const entry = createImportEntry(path, filePath, node);
710
+ if (entry) imports.push(entry);
711
+ }
712
+
713
+ function visit(node) {
714
+ if (node.type === 'import_statement') {
715
+ for (let i = 0; i < node.namedChildCount; i++) {
716
+ const child = node.namedChild(i);
717
+ if (child.type === 'dotted_name') {
718
+ push(child.text, child);
719
+ continue;
720
+ }
721
+ if (child.type === 'aliased_import') {
722
+ const moduleNode = child.namedChild(0);
723
+ if (moduleNode?.type === 'dotted_name') {
724
+ push(moduleNode.text, moduleNode);
725
+ }
726
+ }
727
+ }
728
+ } else if (node.type === 'import_from_statement') {
729
+ const moduleNode = node.childForFieldName('module_name');
730
+ if (moduleNode) push(moduleNode.text, moduleNode);
731
+ }
732
+
733
+ for (let i = 0; i < node.namedChildCount; i++) {
734
+ visit(node.namedChild(i));
735
+ }
736
+ }
737
+
738
+ visit(rootNode);
739
+ return imports;
740
+ }
741
+
696
742
  /**
697
743
  * Parse a single source file and extract definitions, references, and imports.
698
744
  * Returns null if the language is unsupported.
@@ -781,6 +827,10 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
781
827
  }
782
828
  }
783
829
 
830
+ if (langName === 'python') {
831
+ imports.push(...collectPythonImports(tree.rootNode, filePath));
832
+ }
833
+
784
834
  // Associate each reference with its enclosing definition (by line range).
785
835
  // This gives us per-function reference sets for similarity analysis.
786
836
  // Sort definitions by lineStart for binary search.