@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 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.1",
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