@mishasinitcyn/betterrank 0.2.1 → 0.2.2
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 +34 -0
- package/package.json +1 -1
- package/src/cli.js +82 -0
- package/src/index.js +161 -0
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ betterrank search auth --root /path/to/project
|
|
|
28
28
|
# Who calls this function? (with call site context)
|
|
29
29
|
betterrank callers authenticateUser --root /path/to/project --context
|
|
30
30
|
|
|
31
|
+
# Everything about a function: source, types, deps, callers
|
|
32
|
+
betterrank context calculate_bid --root /path/to/project
|
|
33
|
+
|
|
31
34
|
# Trace the full call chain from entry point to function
|
|
32
35
|
betterrank trace calculate_bid --root /path/to/project
|
|
33
36
|
|
|
@@ -152,6 +155,37 @@ src/api/serve.py:
|
|
|
152
155
|
145│ bid = await run_auction(searched, publisher=pub_id)
|
|
153
156
|
```
|
|
154
157
|
|
|
158
|
+
### `context` — Full function context in one shot
|
|
159
|
+
|
|
160
|
+
Everything you need to understand a function: its source, the types in its signature (expanded inline), the functions it calls (with signatures), and a callers summary. Eliminates the multi-command chase.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
betterrank context calculate_bid --root /path/to/project
|
|
164
|
+
|
|
165
|
+
# Skip the source, just show deps/types/callers
|
|
166
|
+
betterrank context calculate_bid --root /path/to/project --no-source
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Example output (`--no-source`):**
|
|
170
|
+
```
|
|
171
|
+
── calculate_bid (src/engine/bidding.py:489-718) ──
|
|
172
|
+
|
|
173
|
+
Types:
|
|
174
|
+
AuctionData2 (src/engine/bidding.py:74)
|
|
175
|
+
publisher_id: str
|
|
176
|
+
campaign_metrics: Dict[str, CampaignMetrics]
|
|
177
|
+
time_remaining_pct: float
|
|
178
|
+
|
|
179
|
+
References:
|
|
180
|
+
[function] def from_microdollars(microdollars) -> Decimal (src/core/currency.py:108)
|
|
181
|
+
[function] def get_config() -> ValuePredictorConfig (src/engine/predictor/config.py:316)
|
|
182
|
+
[function] def get_value_predictor() -> ValuePredictor (src/engine/predictor/persistence.py:123)
|
|
183
|
+
[class] class ValuePredictor (src/engine/predictor/predictor.py:43)
|
|
184
|
+
|
|
185
|
+
Callers (1 file):
|
|
186
|
+
src/engine/bidding.py
|
|
187
|
+
```
|
|
188
|
+
|
|
155
189
|
### `trace` — Recursive caller chain
|
|
156
190
|
|
|
157
191
|
Walk UP the call graph from a symbol to see the full path from entry points to your function. At each hop, resolves which function in the caller file contains the call site.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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
|
@@ -18,6 +18,7 @@ Commands:
|
|
|
18
18
|
structure [--depth N] File tree with symbol counts (default depth: ${DEFAULT_DEPTH})
|
|
19
19
|
symbols [--file path] [--kind type] List definitions (ranked by PageRank)
|
|
20
20
|
callers <symbol> [--file path] [--context] All call sites (ranked, with context lines)
|
|
21
|
+
context <symbol> [--file path] Full context: source, deps, types, callers
|
|
21
22
|
trace <symbol> [--depth N] Recursive caller chain (call tree)
|
|
22
23
|
diff [--ref <commit>] Git-aware blast radius (changed symbols + callers)
|
|
23
24
|
deps <file> What this file imports (ranked)
|
|
@@ -136,6 +137,23 @@ Examples:
|
|
|
136
137
|
betterrank callers authenticateUser --root ./backend --context
|
|
137
138
|
betterrank callers resolve --file src/utils.ts --root . --context 3`,
|
|
138
139
|
|
|
140
|
+
context: `betterrank context <symbol> [--file path] [--root <path>]
|
|
141
|
+
|
|
142
|
+
Everything you need to understand a function in one shot.
|
|
143
|
+
|
|
144
|
+
Shows: the function's source, signatures of all functions/types it references,
|
|
145
|
+
expanded type definitions from the signature, and a callers summary.
|
|
146
|
+
|
|
147
|
+
Eliminates the multi-command chase of: outline → expand → search types → callers.
|
|
148
|
+
|
|
149
|
+
Options:
|
|
150
|
+
--file <path> Disambiguate when multiple symbols share a name
|
|
151
|
+
--no-source Skip the function source (show only deps/types/callers)
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
betterrank context calculate_bid --root .
|
|
155
|
+
betterrank context Router --file src/llm.py --root .`,
|
|
156
|
+
|
|
139
157
|
trace: `betterrank trace <symbol> [--depth N] [--file path] [--root <path>]
|
|
140
158
|
|
|
141
159
|
Recursive caller chain — walk UP the call graph from a symbol to see
|
|
@@ -509,6 +527,70 @@ async function main() {
|
|
|
509
527
|
break;
|
|
510
528
|
}
|
|
511
529
|
|
|
530
|
+
case 'context': {
|
|
531
|
+
const symbol = flags._positional[0];
|
|
532
|
+
if (!symbol) { console.error('Usage: betterrank context <symbol> [--file path]'); process.exit(1); }
|
|
533
|
+
const noSource = flags['no-source'] === true;
|
|
534
|
+
const result = await idx.context({ symbol, file: normalizeFilePath(flags.file) });
|
|
535
|
+
if (!result) {
|
|
536
|
+
console.log(`(symbol "${symbol}" not found)`);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const def = result.definition;
|
|
541
|
+
const pad = Math.max(String(def.lineEnd).length, 4);
|
|
542
|
+
|
|
543
|
+
// Header
|
|
544
|
+
console.log(`── ${def.name} (${def.file}:${def.lineStart}-${def.lineEnd}) ──`);
|
|
545
|
+
console.log('');
|
|
546
|
+
|
|
547
|
+
// Source (or just the signature in --no-source mode)
|
|
548
|
+
if (!noSource) {
|
|
549
|
+
for (let i = 0; i < def.source.length; i++) {
|
|
550
|
+
console.log(` ${String(def.lineStart + i).padStart(pad)}│ ${def.source[i]}`);
|
|
551
|
+
}
|
|
552
|
+
console.log('');
|
|
553
|
+
} else if (def.signature) {
|
|
554
|
+
console.log(` ${def.signature}`);
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Type references from signature
|
|
559
|
+
if (result.typeRefs.length > 0) {
|
|
560
|
+
console.log('Types:');
|
|
561
|
+
for (const t of result.typeRefs) {
|
|
562
|
+
console.log(` ${t.name} (${t.file}:${t.lineStart})`);
|
|
563
|
+
if (t.fields) {
|
|
564
|
+
for (const line of t.fields) {
|
|
565
|
+
console.log(` ${line}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
console.log('');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Used symbols (functions/types referenced in the body)
|
|
573
|
+
if (result.usedSymbols.length > 0) {
|
|
574
|
+
console.log('References:');
|
|
575
|
+
for (const s of result.usedSymbols) {
|
|
576
|
+
const loc = s.file === def.file ? `line ${s.lineStart}` : `${s.file}:${s.lineStart}`;
|
|
577
|
+
console.log(` [${s.kind}] ${s.signature || s.name} (${loc})`);
|
|
578
|
+
}
|
|
579
|
+
console.log('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Callers
|
|
583
|
+
if (result.callers.length > 0) {
|
|
584
|
+
console.log(`Callers (${result.callers.length} file${result.callers.length === 1 ? '' : 's'}):`);
|
|
585
|
+
for (const f of result.callers) {
|
|
586
|
+
console.log(` ${f}`);
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
console.log('Callers: (none detected)');
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
512
594
|
case 'trace': {
|
|
513
595
|
const symbol = flags._positional[0];
|
|
514
596
|
if (!symbol) { console.error('Usage: betterrank trace <symbol> [--depth N]'); process.exit(1); }
|
package/src/index.js
CHANGED
|
@@ -996,6 +996,167 @@ class CodeIndex {
|
|
|
996
996
|
return counts;
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* One-shot context: everything needed to understand a function.
|
|
1001
|
+
*
|
|
1002
|
+
* Returns the function's source, the signatures of functions/types it
|
|
1003
|
+
* references, and a callers summary — all from a single command.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {object} opts
|
|
1006
|
+
* @param {string} opts.symbol - Symbol name
|
|
1007
|
+
* @param {string} [opts.file] - Disambiguate by file
|
|
1008
|
+
* @returns {object|null} { definition, usedSymbols, typeRefs, callers }
|
|
1009
|
+
*/
|
|
1010
|
+
async context({ symbol, file }) {
|
|
1011
|
+
await this._ensureReady();
|
|
1012
|
+
const graph = this.cache.getGraph();
|
|
1013
|
+
if (!graph) return null;
|
|
1014
|
+
|
|
1015
|
+
// Find the target symbol (highest PageRank if multiple)
|
|
1016
|
+
const candidates = [];
|
|
1017
|
+
graph.forEachNode((node, attrs) => {
|
|
1018
|
+
if (attrs.type !== 'symbol') return;
|
|
1019
|
+
if (attrs.name !== symbol) return;
|
|
1020
|
+
if (file && attrs.file !== file) return;
|
|
1021
|
+
candidates.push({ key: node, ...attrs });
|
|
1022
|
+
});
|
|
1023
|
+
if (candidates.length === 0) return null;
|
|
1024
|
+
|
|
1025
|
+
const ranked = this._getRanked();
|
|
1026
|
+
const scoreMap = new Map(ranked);
|
|
1027
|
+
candidates.sort((a, b) => (scoreMap.get(b.key) || 0) - (scoreMap.get(a.key) || 0));
|
|
1028
|
+
const target = candidates[0];
|
|
1029
|
+
|
|
1030
|
+
// Read the source file
|
|
1031
|
+
let source, lines;
|
|
1032
|
+
try {
|
|
1033
|
+
const absPath = join(this.projectRoot, target.file);
|
|
1034
|
+
source = await readFile(absPath, 'utf-8');
|
|
1035
|
+
lines = source.split('\n');
|
|
1036
|
+
} catch {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Extract the function body text
|
|
1041
|
+
const bodyLines = lines.slice(target.lineStart - 1, target.lineEnd);
|
|
1042
|
+
const bodyText = bodyLines.join('\n');
|
|
1043
|
+
|
|
1044
|
+
// Build a set of all symbol names in the graph (for matching)
|
|
1045
|
+
const allSymbols = new Map(); // name -> [{ file, kind, signature, lineStart }]
|
|
1046
|
+
graph.forEachNode((node, attrs) => {
|
|
1047
|
+
if (attrs.type !== 'symbol') return;
|
|
1048
|
+
if (attrs.file === target.file && attrs.name === target.name) return; // skip self
|
|
1049
|
+
if (!allSymbols.has(attrs.name)) allSymbols.set(attrs.name, []);
|
|
1050
|
+
allSymbols.get(attrs.name).push({
|
|
1051
|
+
file: attrs.file,
|
|
1052
|
+
kind: attrs.kind,
|
|
1053
|
+
signature: attrs.signature,
|
|
1054
|
+
lineStart: attrs.lineStart,
|
|
1055
|
+
lineEnd: attrs.lineEnd,
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Find symbols referenced in the body
|
|
1060
|
+
// Use word-boundary matching for each known symbol name
|
|
1061
|
+
// Skip very common names that cause false positives
|
|
1062
|
+
const NOISE_NAMES = new Set([
|
|
1063
|
+
'get', 'set', 'put', 'post', 'delete', 'head', 'patch',
|
|
1064
|
+
'start', 'stop', 'run', 'main', 'init', 'setup', 'close',
|
|
1065
|
+
'dict', 'list', 'str', 'int', 'bool', 'float', 'type',
|
|
1066
|
+
'key', 'value', 'name', 'data', 'config', 'result', 'error',
|
|
1067
|
+
'test', 'self', 'cls', 'app', 'log', 'logger',
|
|
1068
|
+
'enabled', 'default', 'constructor', 'length', 'size',
|
|
1069
|
+
'fetch', 'send', 'table', 'one', 'append', 'write', 'read',
|
|
1070
|
+
'update', 'create', 'find', 'add', 'remove', 'index', 'map',
|
|
1071
|
+
'filter', 'sort', 'join', 'split', 'trim', 'replace',
|
|
1072
|
+
'push', 'pop', 'shift', 'reduce', 'keys', 'values', 'items',
|
|
1073
|
+
'search', 'match', 'query', 'count', 'call', 'apply', 'bind',
|
|
1074
|
+
]);
|
|
1075
|
+
const usedSymbols = [];
|
|
1076
|
+
const seen = new Set();
|
|
1077
|
+
for (const [name, defs] of allSymbols) {
|
|
1078
|
+
if (name.length < 3) continue; // skip very short names
|
|
1079
|
+
if (NOISE_NAMES.has(name)) continue;
|
|
1080
|
+
if (seen.has(name)) continue;
|
|
1081
|
+
const pattern = new RegExp(`(?<![a-zA-Z0-9_])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-zA-Z0-9_])`);
|
|
1082
|
+
if (pattern.test(bodyText)) {
|
|
1083
|
+
seen.add(name);
|
|
1084
|
+
// Pick the best definition (same-file first, then highest PageRank)
|
|
1085
|
+
const sameFile = defs.find(d => d.file === target.file);
|
|
1086
|
+
const best = sameFile || defs[0];
|
|
1087
|
+
usedSymbols.push({ name, ...best });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Sort: functions first, then types, then by name
|
|
1091
|
+
const kindOrder = { function: 0, class: 1, type: 2, variable: 3 };
|
|
1092
|
+
usedSymbols.sort((a, b) => (kindOrder[a.kind] ?? 9) - (kindOrder[b.kind] ?? 9) || a.name.localeCompare(b.name));
|
|
1093
|
+
|
|
1094
|
+
// Resolve type annotations in the signature
|
|
1095
|
+
// Extract type-like tokens from the signature (capitalized words, common patterns)
|
|
1096
|
+
const typeRefs = [];
|
|
1097
|
+
const sigText = target.signature || '';
|
|
1098
|
+
const typePattern = /(?<![a-zA-Z0-9_])([A-Z][a-zA-Z0-9_]+)(?![a-zA-Z0-9_])/g;
|
|
1099
|
+
const seenTypes = new Set();
|
|
1100
|
+
let match;
|
|
1101
|
+
while ((match = typePattern.exec(sigText)) !== null) {
|
|
1102
|
+
const typeName = match[1];
|
|
1103
|
+
if (seenTypes.has(typeName)) continue;
|
|
1104
|
+
seenTypes.add(typeName);
|
|
1105
|
+
const typeDefs = allSymbols.get(typeName);
|
|
1106
|
+
if (!typeDefs) continue;
|
|
1107
|
+
// Find the type definition and get its fields (expand its body)
|
|
1108
|
+
const best = typeDefs.find(d => d.file === target.file) || typeDefs[0];
|
|
1109
|
+
if (best.kind === 'class' || best.kind === 'type') {
|
|
1110
|
+
let fields = null;
|
|
1111
|
+
try {
|
|
1112
|
+
const typeAbsPath = join(this.projectRoot, best.file);
|
|
1113
|
+
const typeSource = await readFile(typeAbsPath, 'utf-8');
|
|
1114
|
+
const typeLines = typeSource.split('\n');
|
|
1115
|
+
// Extract the body lines (up to 15 lines to keep it compact)
|
|
1116
|
+
const maxPreview = 15;
|
|
1117
|
+
const bodyStart = best.lineStart; // 1-indexed
|
|
1118
|
+
const bodyEnd = Math.min(best.lineEnd, best.lineStart + maxPreview - 1);
|
|
1119
|
+
fields = typeLines.slice(bodyStart - 1, bodyEnd).map(l => l);
|
|
1120
|
+
if (best.lineEnd > bodyEnd) {
|
|
1121
|
+
fields.push(` ... (${best.lineEnd - bodyEnd} more lines)`);
|
|
1122
|
+
}
|
|
1123
|
+
} catch { /* skip */ }
|
|
1124
|
+
typeRefs.push({ name: typeName, file: best.file, lineStart: best.lineStart, kind: best.kind, fields });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Get callers (external files only) — union across ALL same-name nodes
|
|
1129
|
+
// to handle ambiguous graph resolution (same name in different files)
|
|
1130
|
+
const callerFiles = new Set();
|
|
1131
|
+
for (const c of candidates) {
|
|
1132
|
+
graph.forEachInEdge(c.key, (_edge, attrs, src) => {
|
|
1133
|
+
if (attrs.type !== 'REFERENCES') return;
|
|
1134
|
+
const srcAttrs = graph.getNodeAttributes(src);
|
|
1135
|
+
const sf = srcAttrs.file || src;
|
|
1136
|
+
if (sf !== target.file) callerFiles.add(sf);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Remove symbols already shown in typeRefs to avoid redundancy
|
|
1141
|
+
const typeRefNames = new Set(typeRefs.map(t => t.name));
|
|
1142
|
+
const filteredSymbols = usedSymbols.filter(s => !typeRefNames.has(s.name));
|
|
1143
|
+
|
|
1144
|
+
return {
|
|
1145
|
+
definition: {
|
|
1146
|
+
name: target.name,
|
|
1147
|
+
kind: target.kind,
|
|
1148
|
+
file: target.file,
|
|
1149
|
+
lineStart: target.lineStart,
|
|
1150
|
+
lineEnd: target.lineEnd,
|
|
1151
|
+
signature: target.signature,
|
|
1152
|
+
source: bodyLines,
|
|
1153
|
+
},
|
|
1154
|
+
usedSymbols: filteredSymbols,
|
|
1155
|
+
typeRefs,
|
|
1156
|
+
callers: [...callerFiles].sort(),
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
999
1160
|
/**
|
|
1000
1161
|
* Recursive caller chain — walk UP the call graph from a symbol.
|
|
1001
1162
|
* At each hop, resolves which function in the caller file contains
|