@nithin554/code-crawler 0.1.55 → 0.1.56

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
@@ -107,7 +107,7 @@ Requires Node.js ≥ 20.
107
107
  ```bash
108
108
  npm install
109
109
  npm run build # tsc → dist/
110
- npm test # vitest (174 tests; run with CODECRAWLER_BENCH=1 for the scale test)
110
+ npm test # vitest (215 tests; run with CODECRAWLER_BENCH=1 for the scale test)
111
111
  npm run bench # crawl/query benchmark on a generated ~137k-line project
112
112
  ```
113
113
 
@@ -321,6 +321,11 @@ See [`USAGE.md`](USAGE.md) for the full task-oriented walkthrough.
321
321
  | `find_injections` | DI-supplied dependencies (`@Autowired`, NestJS, FastAPI `Depends`) |
322
322
  | `find_reflections` | Run-time lookups: `Class.forName`, `getattr`, `importlib`, dynamic `import()` |
323
323
  | `find_external_dependencies` | Third-party packages with reference and file counts |
324
+ | `find_unresolved` | Call sites the resolver could not bind, grouped by failure reason |
325
+ | `impact_analysis` | Callers + entry points + routes + overrides + risk band in one call |
326
+ | `explain_symbol` | One-shot digest: members, decorators, callers, callees, wiring |
327
+ | `find_cycles` | Call cycles (SCCs) with a sample closed path, plus direct recursion |
328
+ | `export_graph` | Neighborhood rendered as Mermaid or Graphviz DOT |
324
329
  | `get_subgraph` | Neighborhood extraction for visualization |
325
330
  | `get_schema` | Describe the graph model |
326
331
  | `clear_index` | Reset the database |
@@ -328,6 +333,20 @@ See [`USAGE.md`](USAGE.md) for the full task-oriented walkthrough.
328
333
  **Resources**: `codecrawler://schema`, `codecrawler://status`
329
334
  **Prompt**: `trace-call-path(from, to)`
330
335
 
336
+ **Token budgeting.** The 18 list-returning query tools — every tool whose payload is a
337
+ single result list — accept `max_tokens` (100–200000) and `cursor`. Passing either adds a
338
+ `page` block with `totalCount`, `returnedCount`, `truncated`, `estimatedTokens`, and
339
+ `nextCursor`; omit both and the response shape is unchanged, so existing clients keep
340
+ working. Paging never returns an empty page while items remain, so a continuation loop
341
+ always terminates. Token counts are estimates from JSON length (~4 characters per token) —
342
+ no tokenizer dependency, deterministic across models.
343
+
344
+ `get_subgraph` is the one exception: its `nodes` and `edges` are two views of the same slice,
345
+ so cutting one list would leave edges pointing at omitted nodes. It is bounded by `radius` and
346
+ `limit` instead. Aggregate tools (`impact_analysis`, `explain_symbol`, `export_graph`) and
347
+ single-object tools (`get_node`, `index_status`, `get_schema`) return one object and take no
348
+ budget.
349
+
331
350
  ## Graph model
332
351
 
333
352
  **Nodes** — `kind` is one of `file | package | module | class | interface | enum | method |
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Token budgeting and cursor pagination for tool responses.
3
+ *
4
+ * Token counts are *estimates*: JSON payloads average roughly four characters
5
+ * per token for source-code-adjacent text. Estimating avoids a tokenizer
6
+ * dependency and keeps responses deterministic across models. The budget only
7
+ * decides where to cut a list; it is never a hard failure condition.
8
+ */
9
+ export interface BudgetOptions {
10
+ /** Soft cap on the estimated tokens the returned items may cost. */
11
+ maxTokens?: number;
12
+ /** Zero-based offset into the item list. */
13
+ cursor?: number;
14
+ }
15
+ export interface BudgetPage<T> {
16
+ items: T[];
17
+ /** Number of items available in the window handed to applyBudget. */
18
+ totalCount: number;
19
+ returnedCount: number;
20
+ cursor: number;
21
+ /** Offset to pass as `cursor` to fetch the next page. */
22
+ nextCursor?: number;
23
+ truncated: boolean;
24
+ estimatedTokens: number;
25
+ }
26
+ /** Metadata describing a page, without the items themselves. */
27
+ export interface PageMeta {
28
+ totalCount: number;
29
+ returnedCount: number;
30
+ cursor: number;
31
+ nextCursor?: number;
32
+ truncated: boolean;
33
+ estimatedTokens: number;
34
+ }
35
+ export declare function estimateTokens(value: unknown): number;
36
+ /**
37
+ * Slice a list to a token budget, starting at `cursor`.
38
+ *
39
+ * A single item is always returned even when it exceeds the budget, so a page
40
+ * can never come back empty while items remain.
41
+ */
42
+ export declare function applyBudget<T>(items: readonly T[], opts?: BudgetOptions): BudgetPage<T>;
43
+ /** Strip the items from a page so it can be attached as response metadata. */
44
+ export declare function pageMeta<T>(page: BudgetPage<T>): PageMeta;
45
+ /** True when the caller opted into budgeted output in any way. */
46
+ export declare function budgetRequested(opts: BudgetOptions): boolean;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Token budgeting and cursor pagination for tool responses.
3
+ *
4
+ * Token counts are *estimates*: JSON payloads average roughly four characters
5
+ * per token for source-code-adjacent text. Estimating avoids a tokenizer
6
+ * dependency and keeps responses deterministic across models. The budget only
7
+ * decides where to cut a list; it is never a hard failure condition.
8
+ */
9
+ export function estimateTokens(value) {
10
+ if (value === undefined)
11
+ return 0;
12
+ const text = JSON.stringify(value) ?? String(value);
13
+ return Math.ceil(text.length / 4);
14
+ }
15
+ /**
16
+ * Slice a list to a token budget, starting at `cursor`.
17
+ *
18
+ * A single item is always returned even when it exceeds the budget, so a page
19
+ * can never come back empty while items remain.
20
+ */
21
+ export function applyBudget(items, opts = {}) {
22
+ const totalCount = items.length;
23
+ const cursor = Math.max(0, Math.floor(opts.cursor ?? 0));
24
+ const available = cursor >= totalCount ? [] : items.slice(cursor);
25
+ const maxTokens = opts.maxTokens;
26
+ if (maxTokens === undefined || maxTokens <= 0) {
27
+ return {
28
+ items: available,
29
+ totalCount,
30
+ returnedCount: available.length,
31
+ cursor,
32
+ truncated: false,
33
+ estimatedTokens: estimateTokens(available),
34
+ };
35
+ }
36
+ const page = [];
37
+ let used = 0;
38
+ for (const item of available) {
39
+ const cost = estimateTokens(item);
40
+ if (page.length > 0 && used + cost > maxTokens)
41
+ break;
42
+ page.push(item);
43
+ used += cost;
44
+ }
45
+ const nextCursor = cursor + page.length;
46
+ const truncated = nextCursor < totalCount;
47
+ return {
48
+ items: page,
49
+ totalCount,
50
+ returnedCount: page.length,
51
+ cursor,
52
+ ...(truncated ? { nextCursor } : {}),
53
+ truncated,
54
+ estimatedTokens: used,
55
+ };
56
+ }
57
+ /** Strip the items from a page so it can be attached as response metadata. */
58
+ export function pageMeta(page) {
59
+ return {
60
+ totalCount: page.totalCount,
61
+ returnedCount: page.returnedCount,
62
+ cursor: page.cursor,
63
+ ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),
64
+ truncated: page.truncated,
65
+ estimatedTokens: page.estimatedTokens,
66
+ };
67
+ }
68
+ /** True when the caller opted into budgeted output in any way. */
69
+ export function budgetRequested(opts) {
70
+ return opts.maxTokens !== undefined || opts.cursor !== undefined;
71
+ }
72
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/graph/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA+BH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAI,KAAmB,EAAE,IAAI,GAAkB,EAAE;IAC1E,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAEjC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAC9C,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,UAAU;YACV,aAAa,EAAE,SAAS,CAAC,MAAM;YAC/B,MAAM;YACN,SAAS,EAAE,KAAK;YAChB,eAAe,EAAE,cAAc,CAAC,SAAS,CAAC;SAC3C,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,GAAG,IAAI,GAAG,SAAS;YAAE,MAAM;QACtD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,IAAI,IAAI,CAAC;IACf,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IACxC,MAAM,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC1C,OAAO;QACL,KAAK,EAAE,IAAI;QACX,UAAU;QACV,aAAa,EAAE,IAAI,CAAC,MAAM;QAC1B,MAAM;QACN,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,SAAS;QACT,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,QAAQ,CAAI,IAAmB;IAC7C,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,eAAe,EAAE,IAAI,CAAC,eAAe;KACtC,CAAC;AACJ,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,OAAO,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC;AACnE,CAAC"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Higher-level analyses built on top of the primitive queries.
3
+ *
4
+ * These power the "agent-facing" tools: what the index could not resolve, what
5
+ * breaks if a symbol changes, a one-shot explanation of a symbol, call cycles,
6
+ * and graph export in diagram formats.
7
+ */
8
+ import type { GraphStore } from "./store.js";
9
+ import type { EdgeKind, NodeKind } from "./schema.js";
10
+ import { type DecoratorInfo, type SymbolRef, type WiringEdge } from "./queries.js";
11
+ export interface UnresolvedCall {
12
+ caller: SymbolRef;
13
+ /** Placeholder node id the call edge points at (`__unknown__:<label>`). */
14
+ targetId: string;
15
+ /** Callee as written in source, e.g. `System.out.println`. */
16
+ targetLabel: string;
17
+ /** Why the resolver gave up. */
18
+ reason: string;
19
+ line?: number;
20
+ edgeKind: EdgeKind;
21
+ }
22
+ export interface ReasonCount {
23
+ reason: string;
24
+ count: number;
25
+ }
26
+ export interface UnresolvedReport {
27
+ /** Total unresolved call edges matching the filters, ignoring `limit`. */
28
+ total: number;
29
+ /** Unresolved edges grouped by resolver reason, most frequent first. */
30
+ byReason: ReasonCount[];
31
+ calls: UnresolvedCall[];
32
+ /** True when `calls` hit the scan limit before `total` did. */
33
+ scanTruncated: boolean;
34
+ /** `kind` filter that was applied to the caller node kind. */
35
+ callerKind?: NodeKind;
36
+ /** `reason` filter that was applied (substring, case-insensitive). */
37
+ reasonFilter?: string;
38
+ }
39
+ export interface UnresolvedQueryOptions {
40
+ /** Substring match against the resolver reason, case-insensitive. */
41
+ reason?: string;
42
+ /** Restrict to unresolved calls made by callers of this node kind. */
43
+ kind?: NodeKind;
44
+ /** Cap on rows scanned (default 2000). */
45
+ limit?: number;
46
+ }
47
+ /**
48
+ * Every call the resolver could not bind to a node, with the reason it failed.
49
+ *
50
+ * Unresolved calls are the honest boundary of a static index: they show where
51
+ * analysis stops (dynamic dispatch, untyped receivers, missing dependencies)
52
+ * and they are the highest-signal input for improving the parsers.
53
+ */
54
+ export declare function findUnresolved(store: GraphStore, opts?: UnresolvedQueryOptions): UnresolvedReport;
55
+ export interface NodeUnresolvedInfo {
56
+ /** Calls this node makes that could not be resolved. */
57
+ outgoing: UnresolvedCall[];
58
+ /** Unresolved calls elsewhere in the graph that point at this node. */
59
+ incoming: {
60
+ caller: SymbolRef;
61
+ line?: number;
62
+ }[];
63
+ }
64
+ /**
65
+ * Unresolved call detail for a single node, used to annotate `get_node`.
66
+ * Returns `undefined` when the node has no unresolved edges in either
67
+ * direction so callers can omit the block entirely.
68
+ */
69
+ export declare function unresolvedForNode(store: GraphStore, nodeId: string): NodeUnresolvedInfo | undefined;
70
+ export interface ImpactCaller {
71
+ caller: SymbolRef;
72
+ depth: number;
73
+ callSiteLine?: number;
74
+ edgeKind: EdgeKind;
75
+ }
76
+ export interface ImpactImplementation {
77
+ type: SymbolRef;
78
+ /** Override of the analyzed method, when the symbol is a member. */
79
+ method?: SymbolRef;
80
+ depth: number;
81
+ via: "EXTENDS" | "IMPLEMENTS";
82
+ }
83
+ export interface ImpactSummary {
84
+ totalCallers: number;
85
+ directCallers: number;
86
+ entryPoints: number;
87
+ routes: number;
88
+ implementations: number;
89
+ affectedFiles: number;
90
+ /**
91
+ * Heuristic blast radius: `high` when a framework entry point reaches the
92
+ * symbol, `medium` when at least one caller exists, else `low`.
93
+ */
94
+ risk: "low" | "medium" | "high";
95
+ }
96
+ export interface ImpactAnalysis {
97
+ symbol: SymbolRef;
98
+ summary: ImpactSummary;
99
+ callers: ImpactCaller[];
100
+ entryPoints: {
101
+ symbol: SymbolRef;
102
+ entryPointKind: string;
103
+ detail?: string;
104
+ viaDepth?: number;
105
+ }[];
106
+ routes: WiringEdge[];
107
+ implementations: ImpactImplementation[];
108
+ affectedFiles: string[];
109
+ }
110
+ export interface ImpactOptions {
111
+ maxDepth?: number;
112
+ includeDynamic?: boolean;
113
+ limit?: number;
114
+ }
115
+ /**
116
+ * "What breaks if I change this?" — reverse callers, framework entry points,
117
+ * routes and implementations in a single response.
118
+ *
119
+ * A type aggregates the callers of its contained methods, since call edges bind
120
+ * to methods and a class is usually changed through its members.
121
+ */
122
+ export declare function impactAnalysis(store: GraphStore, symbol: string, opts?: ImpactOptions): ImpactAnalysis | null;
123
+ export interface ExplainedMember {
124
+ symbol: SymbolRef;
125
+ line?: number;
126
+ decorators: DecoratorInfo[];
127
+ }
128
+ export interface SymbolExplanation {
129
+ symbol: SymbolRef;
130
+ line?: number;
131
+ isEntryPoint?: boolean;
132
+ decorators: DecoratorInfo[];
133
+ members: ExplainedMember[];
134
+ callers: ImpactCaller[];
135
+ callees: ImpactCaller[];
136
+ implementations: ImpactImplementation[];
137
+ routes: WiringEdge[];
138
+ injections: WiringEdge[];
139
+ reflections: WiringEdge[];
140
+ summary: {
141
+ members: number;
142
+ callers: number;
143
+ callees: number;
144
+ implementations: number;
145
+ wiringEdges: number;
146
+ };
147
+ }
148
+ export interface ExplainOptions {
149
+ maxDepth?: number;
150
+ includeDynamic?: boolean;
151
+ limit?: number;
152
+ }
153
+ /**
154
+ * One-shot digest of a symbol: what it is, what it is annotated with, what it
155
+ * contains, who calls it, what it calls, and how frameworks reach it.
156
+ *
157
+ * Intended to replace a chain of five tool calls with one round trip.
158
+ */
159
+ export declare function explainSymbol(store: GraphStore, symbol: string, opts?: ExplainOptions): SymbolExplanation | null;
160
+ export interface CallCycle {
161
+ size: number;
162
+ members: SymbolRef[];
163
+ /** Shortest closed walk through the cycle, starting at the first member. */
164
+ sample: string[];
165
+ }
166
+ export interface CycleReport {
167
+ /** Number of multi-node cycles reported (after `minSize`). */
168
+ cycleCount: number;
169
+ cycles: CallCycle[];
170
+ /** Symbols that call themselves directly (size-1 cycles). */
171
+ selfRecursive: SymbolRef[];
172
+ edgeKinds: EdgeKind[];
173
+ scannedNodes: number;
174
+ scannedEdges: number;
175
+ /** True when the edge scan hit `maxNodes` and results may be incomplete. */
176
+ scanTruncated: boolean;
177
+ }
178
+ export interface CycleOptions {
179
+ edgeKinds?: EdgeKind[];
180
+ includeDynamic?: boolean;
181
+ /** Minimum cycle size to report. Default 2 (direct recursion is separate). */
182
+ minSize?: number;
183
+ /** Cap on edges scanned, guarding against very large graphs. */
184
+ maxEdges?: number;
185
+ limit?: number;
186
+ }
187
+ /**
188
+ * Find strongly connected components in the call graph with an iterative
189
+ * Tarjan. A component larger than one node means the members are mutually
190
+ * reachable, i.e. a call cycle; a self-edge is direct recursion.
191
+ *
192
+ * Iterative rather than recursive so deep call chains cannot blow the stack.
193
+ */
194
+ export declare function findCycles(store: GraphStore, opts?: CycleOptions): CycleReport;
195
+ export type ExportFormat = "mermaid" | "dot";
196
+ export interface GraphExport {
197
+ format: ExportFormat;
198
+ diagram: string;
199
+ nodeCount: number;
200
+ edgeCount: number;
201
+ truncated: boolean;
202
+ /** Symbol the export was centered on, when one was given. */
203
+ center?: SymbolRef;
204
+ }
205
+ export interface ExportOptions {
206
+ symbol?: string;
207
+ radius?: number;
208
+ edgeKinds?: EdgeKind[];
209
+ format?: ExportFormat;
210
+ direction?: "LR" | "TB" | "RL" | "BT";
211
+ limit?: number;
212
+ }
213
+ /**
214
+ * Render a neighborhood (or the whole graph) as Mermaid or Graphviz DOT so an
215
+ * agent can show the user a diagram instead of a list of edges.
216
+ */
217
+ export declare function exportGraph(store: GraphStore, opts?: ExportOptions): GraphExport | null;