@mishasinitcyn/betterrank 0.2.2 → 0.2.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/README.md CHANGED
@@ -186,6 +186,31 @@ Callers (1 file):
186
186
  src/engine/bidding.py
187
187
  ```
188
188
 
189
+ ### `history` — Git history of a specific function
190
+
191
+ Shows only commits that touched a function's lines. Uses tree-sitter line ranges for accuracy (better than git's heuristic `:funcname:` detection). Add `--patch` to include function-scoped diffs.
192
+
193
+ ```bash
194
+ # Commit list (compact)
195
+ betterrank history calculate_bid --root /path/to/project
196
+
197
+ # With function-scoped diffs
198
+ betterrank history calculate_bid --root /path/to/project --patch --limit 3
199
+
200
+ # Paginate through older commits
201
+ betterrank history calculate_bid --root /path/to/project --offset 5 --limit 5
202
+ ```
203
+
204
+ **Example output:**
205
+ ```
206
+ calculate_bid (src/engine/bidding.py:489-718)
207
+
208
+ 082b9d5 2026-02-24 fix: restore GSP auction pricing
209
+ c75f5ff 2026-02-14 fix: resolve lint errors from main merge
210
+ 623429c 2026-02-13 hot fix
211
+ 5d236d3 2026-02-06 feat: wire ad_position to ValuePredictor
212
+ ```
213
+
189
214
  ### `trace` — Recursive caller chain
190
215
 
191
216
  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.2",
3
+ "version": "0.2.3",
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
@@ -19,6 +19,7 @@ Commands:
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
21
  context <symbol> [--file path] Full context: source, deps, types, callers
22
+ history <symbol> [--file path] Git history of a specific function
22
23
  trace <symbol> [--depth N] Recursive caller chain (call tree)
23
24
  diff [--ref <commit>] Git-aware blast radius (changed symbols + callers)
24
25
  deps <file> What this file imports (ranked)
@@ -154,6 +155,24 @@ Examples:
154
155
  betterrank context calculate_bid --root .
155
156
  betterrank context Router --file src/llm.py --root .`,
156
157
 
158
+ history: `betterrank history <symbol> [--file path] [--patch] [--limit N] [--root <path>]
159
+
160
+ Git history of a specific function. Uses the tree-sitter line range to
161
+ show only commits that touched that function's lines.
162
+
163
+ More accurate than git log -L :funcname: because betterrank knows the
164
+ exact line range from tree-sitter, not git's heuristic function detection.
165
+
166
+ Options:
167
+ --file <path> Disambiguate when multiple symbols share a name
168
+ --patch, -p Include function-scoped diffs (not just commit list)
169
+ --limit N Max commits to show (default: 20)
170
+
171
+ Examples:
172
+ betterrank history calculate_bid --root .
173
+ betterrank history calculate_bid --root . --patch --limit 3
174
+ betterrank history Router --file src/llm.py --root .`,
175
+
157
176
  trace: `betterrank trace <symbol> [--depth N] [--file path] [--root <path>]
158
177
 
159
178
  Recursive caller chain — walk UP the call graph from a symbol to see
@@ -591,6 +610,36 @@ async function main() {
591
610
  break;
592
611
  }
593
612
 
613
+ case 'history': {
614
+ const symbol = flags._positional[0];
615
+ if (!symbol) { console.error('Usage: betterrank history <symbol> [--file path] [--patch]'); process.exit(1); }
616
+ const histLimit = flags.limit ? parseInt(flags.limit, 10) : 20;
617
+ const histOffset = flags.offset ? parseInt(flags.offset, 10) : 0;
618
+ const showPatch = flags.patch === true || flags.p === true;
619
+ const result = await idx.history({ symbol, file: normalizeFilePath(flags.file), offset: histOffset, limit: histLimit, patch: showPatch });
620
+ if (!result) {
621
+ console.log(`(symbol "${symbol}" not found)`);
622
+ } else if (result.error) {
623
+ console.error(result.error);
624
+ } else if (result.raw) {
625
+ // --patch mode: print git's full output
626
+ const def = result.definition;
627
+ console.log(`${def.name} (${def.file}:${def.lineStart}-${def.lineEnd})\n`);
628
+ console.log(result.raw);
629
+ } else {
630
+ const def = result.definition;
631
+ console.log(`${def.name} (${def.file}:${def.lineStart}-${def.lineEnd})\n`);
632
+ if (result.commits.length === 0) {
633
+ console.log('(no commits found)');
634
+ } else {
635
+ for (const line of result.commits) {
636
+ console.log(` ${line}`);
637
+ }
638
+ }
639
+ }
640
+ break;
641
+ }
642
+
594
643
  case 'trace': {
595
644
  const symbol = flags._positional[0];
596
645
  if (!symbol) { console.error('Usage: betterrank trace <symbol> [--depth N]'); process.exit(1); }
package/src/index.js CHANGED
@@ -1157,6 +1157,62 @@ class CodeIndex {
1157
1157
  };
1158
1158
  }
1159
1159
 
1160
+ /**
1161
+ * Git history of a specific function, using its tree-sitter line range.
1162
+ *
1163
+ * @param {object} opts
1164
+ * @param {string} opts.symbol - Symbol name
1165
+ * @param {string} [opts.file] - Disambiguate by file
1166
+ * @param {number} [opts.offset=0] - Skip first N commits
1167
+ * @param {number} [opts.limit=20] - Max commits to show
1168
+ * @param {boolean} [opts.patch=false] - Include function-scoped diffs
1169
+ * @returns {object|null} { definition, commits, raw? }
1170
+ */
1171
+ async history({ symbol, file, offset = 0, limit = 20, patch = false }) {
1172
+ await this._ensureReady();
1173
+ const graph = this.cache.getGraph();
1174
+ if (!graph) return null;
1175
+
1176
+ const candidates = [];
1177
+ graph.forEachNode((node, attrs) => {
1178
+ if (attrs.type !== 'symbol') return;
1179
+ if (attrs.name !== symbol) return;
1180
+ if (file && attrs.file !== file) return;
1181
+ candidates.push(attrs);
1182
+ });
1183
+ if (candidates.length === 0) return null;
1184
+
1185
+ const ranked = this._getRanked();
1186
+ const scoreMap = new Map(ranked);
1187
+ candidates.sort((a, b) => {
1188
+ const aKey = `${a.file}::${a.name}`;
1189
+ const bKey = `${b.file}::${b.name}`;
1190
+ return (scoreMap.get(bKey) || 0) - (scoreMap.get(aKey) || 0);
1191
+ });
1192
+ const target = candidates[0];
1193
+
1194
+ const { execSync } = await import('child_process');
1195
+ try {
1196
+ if (patch) {
1197
+ // Full output with diffs — return raw text
1198
+ const raw = execSync(
1199
+ `git log -L ${target.lineStart},${target.lineEnd}:${target.file} --skip=${offset} -n ${limit}`,
1200
+ { cwd: this.projectRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000 }
1201
+ ).trim();
1202
+ return { definition: target, commits: [], raw };
1203
+ }
1204
+ // Summary only
1205
+ const output = execSync(
1206
+ `git log -L ${target.lineStart},${target.lineEnd}:${target.file} --no-patch --format="%h %ad %s" --date=short --skip=${offset} -n ${limit}`,
1207
+ { cwd: this.projectRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000 }
1208
+ ).trim();
1209
+ const commits = output ? output.split('\n').filter(Boolean) : [];
1210
+ return { definition: target, commits };
1211
+ } catch {
1212
+ return { definition: target, commits: [], error: 'git log failed — is this a git repo?' };
1213
+ }
1214
+ }
1215
+
1160
1216
  /**
1161
1217
  * Recursive caller chain — walk UP the call graph from a symbol.
1162
1218
  * At each hop, resolves which function in the caller file contains