@wrongstack/tools 0.298.2 → 0.299.0

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.
@@ -5,6 +5,7 @@
5
5
  * Errors cross the boundary as strings and are re-wrapped by the host.
6
6
  */
7
7
  import type { CodeMapGraph, IndexResult, IndexStats, SearchResult } from './schema.js';
8
+ import type { IncomingCallsResult, OutgoingCallsResult } from './index-service.js';
8
9
  export interface IndexOpArgs {
9
10
  projectRoot: string;
10
11
  indexDir?: string | undefined;
@@ -33,6 +34,11 @@ export interface FileGraphOpArgs extends StatsOpArgs {
33
34
  export interface SymbolGraphOpArgs extends StatsOpArgs {
34
35
  fileFilter: string;
35
36
  }
37
+ export interface CallRefsOpArgs extends StatsOpArgs {
38
+ symbol: string;
39
+ file?: string | undefined;
40
+ limit?: number | undefined;
41
+ }
36
42
  export interface SearchOpResult {
37
43
  results: SearchResult[];
38
44
  total: number;
@@ -63,6 +69,14 @@ export interface OpShapes {
63
69
  args: SymbolGraphOpArgs;
64
70
  result: CodeMapGraph;
65
71
  };
72
+ incomingCalls: {
73
+ args: CallRefsOpArgs;
74
+ result: IncomingCallsResult;
75
+ };
76
+ outgoingCalls: {
77
+ args: CallRefsOpArgs;
78
+ result: OutgoingCallsResult;
79
+ };
66
80
  }
67
81
  export type OpName = keyof OpShapes;
68
82
  export type HostToWorker = {
@@ -2658,6 +2658,136 @@ function mapWriterRefRow(row) {
2658
2658
  }
2659
2659
 
2660
2660
  // src/codebase-index/writer-graph-reader.ts
2661
+ var MAX_SQL_VARS = 900;
2662
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
2663
+ const results = [];
2664
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2665
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2666
+ const placeholders = chunk.map(() => "?").join(",");
2667
+ const sql = buildSql(placeholders);
2668
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
2669
+ }
2670
+ return results;
2671
+ }
2672
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
2673
+ let total = 0;
2674
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2675
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2676
+ const placeholders = chunk.map(() => "?").join(",");
2677
+ const sql = buildSql(placeholders);
2678
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
2679
+ total += rows[0]?.n ?? 0;
2680
+ }
2681
+ return total;
2682
+ }
2683
+ function mapCallSiteRow(row) {
2684
+ return {
2685
+ symbol: {
2686
+ id: row.sym_id,
2687
+ name: row.sym_name,
2688
+ kind: row.sym_kind,
2689
+ lang: row.sym_lang,
2690
+ file: row.sym_file,
2691
+ line: row.sym_line,
2692
+ signature: row.sym_signature
2693
+ },
2694
+ callType: row.call_type,
2695
+ line: row.ref_line
2696
+ };
2697
+ }
2698
+ function resolveSymbolIds(stmt, symbolName, file) {
2699
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
2700
+ const args = file ? [symbolName, file] : [symbolName];
2701
+ const rows = stmt(baseSql).all(...args);
2702
+ return rows.map((r) => r.id);
2703
+ }
2704
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
2705
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
2706
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
2707
+ let matchIds = targetIds;
2708
+ let ambiguous = false;
2709
+ if (file !== void 0) {
2710
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
2711
+ if (allNamedIds.length > targetIds.length) {
2712
+ matchIds = allNamedIds;
2713
+ ambiguous = true;
2714
+ }
2715
+ }
2716
+ const useFallback = !file;
2717
+ const rows = chunkedIdQuery(
2718
+ stmt,
2719
+ matchIds,
2720
+ (ph) => `SELECT
2721
+ s.id AS sym_id,
2722
+ s.name AS sym_name,
2723
+ s.kind AS sym_kind,
2724
+ s.lang AS sym_lang,
2725
+ s.file AS sym_file,
2726
+ s.line AS sym_line,
2727
+ s.signature AS sym_signature,
2728
+ r.call_type,
2729
+ r.line AS ref_line
2730
+ FROM refs r
2731
+ JOIN symbols s ON s.id = r.from_id
2732
+ WHERE r.to_id IN (${ph})
2733
+ ORDER BY r.line, r.id`,
2734
+ []
2735
+ );
2736
+ if (useFallback) {
2737
+ const fallbackRows = stmt(
2738
+ `SELECT
2739
+ s.id AS sym_id,
2740
+ s.name AS sym_name,
2741
+ s.kind AS sym_kind,
2742
+ s.lang AS sym_lang,
2743
+ s.file AS sym_file,
2744
+ s.line AS sym_line,
2745
+ s.signature AS sym_signature,
2746
+ r.call_type,
2747
+ r.line AS ref_line
2748
+ FROM refs r
2749
+ JOIN symbols s ON s.id = r.from_id
2750
+ WHERE r.to_id IS NULL AND r.to_name = ?
2751
+ ORDER BY r.line, r.id`
2752
+ ).all(symbolName);
2753
+ rows.push(...fallbackRows);
2754
+ }
2755
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2756
+ const allCalls = rows.map(mapCallSiteRow);
2757
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
2758
+ }
2759
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
2760
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
2761
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
2762
+ const unresolvedCount = chunkedIdScalar(
2763
+ stmt,
2764
+ sourceIds,
2765
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
2766
+ );
2767
+ const rows = chunkedIdQuery(
2768
+ stmt,
2769
+ sourceIds,
2770
+ (ph) => `SELECT
2771
+ s.id AS sym_id,
2772
+ s.name AS sym_name,
2773
+ s.kind AS sym_kind,
2774
+ s.lang AS sym_lang,
2775
+ s.file AS sym_file,
2776
+ s.line AS sym_line,
2777
+ s.signature AS sym_signature,
2778
+ r.call_type,
2779
+ r.line AS ref_line
2780
+ FROM refs r
2781
+ JOIN symbols s ON s.id = r.to_id
2782
+ WHERE r.from_id IN (${ph})
2783
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
2784
+ ORDER BY r.line, r.id`,
2785
+ []
2786
+ );
2787
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2788
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
2789
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
2790
+ }
2661
2791
  function findRefsToWithStatement(stmt, symbolId) {
2662
2792
  return stmt(
2663
2793
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
@@ -3889,6 +4019,20 @@ var IndexStore = class _IndexStore {
3889
4019
  return false;
3890
4020
  }
3891
4021
  }
4022
+ /**
4023
+ * Find all symbols that reference the named target symbol (incoming callers).
4024
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4025
+ */
4026
+ findIncomingCallsByName(symbolName, file, limit = 100) {
4027
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4028
+ }
4029
+ /**
4030
+ * Find all symbols that the named source symbol references (outgoing callees).
4031
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4032
+ */
4033
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
4034
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4035
+ }
3892
4036
  /**
3893
4037
  * Find all references TO a given symbol (who calls / uses this symbol?).
3894
4038
  */
@@ -4473,6 +4617,22 @@ function symbolGraphService(args) {
4473
4617
  indexStorePool.release(store);
4474
4618
  }
4475
4619
  }
4620
+ function incomingCallsService(args) {
4621
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4622
+ try {
4623
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
4624
+ } finally {
4625
+ indexStorePool.release(store);
4626
+ }
4627
+ }
4628
+ function outgoingCallsService(args) {
4629
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4630
+ try {
4631
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
4632
+ } finally {
4633
+ indexStorePool.release(store);
4634
+ }
4635
+ }
4476
4636
 
4477
4637
  // src/codebase-index/worker.ts
4478
4638
  if (!parentPort) throw new Error("codebase-index worker must be started as a worker thread");
@@ -4505,6 +4665,10 @@ async function dispatch(msg) {
4505
4665
  return fileGraphService(msg.args);
4506
4666
  case "symbolGraph":
4507
4667
  return symbolGraphService(msg.args);
4668
+ case "incomingCalls":
4669
+ return incomingCallsService(msg.args);
4670
+ case "outgoingCalls":
4671
+ return outgoingCallsService(msg.args);
4508
4672
  default:
4509
4673
  throw new Error(`unknown index op: ${msg.op}`);
4510
4674
  }
@@ -1,7 +1,30 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
- import type { CodeMapGraph, Ref } from './schema.js';
2
+ import type { CallSite, CodeMapGraph, Ref } from './schema.js';
3
3
  type Statement = ReturnType<DatabaseSync['prepare']>;
4
4
  type PrepareStatement = (sql: string) => Statement;
5
+ /**
6
+ * Find all symbols that CALL/USE the named target symbol (incoming callers).
7
+ *
8
+ * Returns one `CallSite` per ref edge, with the caller's full metadata so the
9
+ * agent sees file, line, kind, and signature without a second lookup.
10
+ */
11
+ export declare function findIncomingCallsByName(stmt: PrepareStatement, symbolName: string, file: string | undefined, limit: number): {
12
+ calls: CallSite[];
13
+ symbolFound: boolean;
14
+ ambiguous: boolean;
15
+ totalMatches: number;
16
+ };
17
+ /**
18
+ * Find all symbols that the named source symbol CALLS/USES (outgoing callees).
19
+ *
20
+ * Returns one `CallSite` per ref edge, with the callee's full metadata.
21
+ */
22
+ export declare function findOutgoingCallsByName(stmt: PrepareStatement, symbolName: string, file: string | undefined, limit: number): {
23
+ calls: CallSite[];
24
+ symbolFound: boolean;
25
+ unresolvedCount: number;
26
+ totalMatches: number;
27
+ };
5
28
  export declare function findRefsToWithStatement(stmt: PrepareStatement, symbolId: number): Ref[];
6
29
  export declare function findRefsFromWithStatement(stmt: PrepareStatement, symbolId: number): Ref[];
7
30
  export declare function getPackageGraphWithStatement(stmt: PrepareStatement): CodeMapGraph;
@@ -1,4 +1,4 @@
1
- import type { CodeMapGraph, FileMeta, IndexStats, Symbol as IndexSymbol, Ref, SearchResult, SymbolKind, SymbolLang } from './schema.js';
1
+ import type { CallSite, CodeMapGraph, FileMeta, IndexStats, Symbol as IndexSymbol, Ref, SearchResult, SymbolKind, SymbolLang } from './schema.js';
2
2
  import { type WriterSearchFilter } from './writer-search-helpers.js';
3
3
  import { StorePool } from './writer-store-pool.js';
4
4
  export { codebaseIndexDirOverride, resolveIndexDir } from './writer-helpers.js';
@@ -252,6 +252,26 @@ export declare class IndexStore {
252
252
  minBytes?: number;
253
253
  minFreeRatio?: number;
254
254
  }): boolean;
255
+ /**
256
+ * Find all symbols that reference the named target symbol (incoming callers).
257
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
258
+ */
259
+ findIncomingCallsByName(symbolName: string, file?: string, limit?: number): {
260
+ calls: CallSite[];
261
+ symbolFound: boolean;
262
+ ambiguous: boolean;
263
+ totalMatches: number;
264
+ };
265
+ /**
266
+ * Find all symbols that the named source symbol references (outgoing callees).
267
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
268
+ */
269
+ findOutgoingCallsByName(symbolName: string, file?: string, limit?: number): {
270
+ calls: CallSite[];
271
+ symbolFound: boolean;
272
+ unresolvedCount: number;
273
+ totalMatches: number;
274
+ };
255
275
  /**
256
276
  * Find all references TO a given symbol (who calls / uses this symbol?).
257
277
  */
package/dist/exec.js CHANGED
@@ -1284,6 +1284,10 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1284
1284
  const start = Date.now();
1285
1285
  const pidStr = String(process.pid);
1286
1286
  const hostStr = os2.hostname();
1287
+ try {
1288
+ await fs2.mkdir(path4.dirname(lockfilePath), { recursive: true });
1289
+ } catch {
1290
+ }
1287
1291
  while (Date.now() - start < timeoutMs) {
1288
1292
  try {
1289
1293
  await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });