diffninja 0.1.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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/calltree.d.ts +47 -0
  4. package/dist/calltree.js +296 -0
  5. package/dist/cli.d.ts +57 -0
  6. package/dist/cli.js +340 -0
  7. package/dist/diff.d.ts +7 -0
  8. package/dist/diff.js +114 -0
  9. package/dist/extract.d.ts +26 -0
  10. package/dist/extract.js +152 -0
  11. package/dist/git.d.ts +40 -0
  12. package/dist/git.js +288 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +8 -0
  15. package/dist/infer.d.ts +21 -0
  16. package/dist/infer.js +189 -0
  17. package/dist/languages/bash.d.ts +2 -0
  18. package/dist/languages/bash.js +208 -0
  19. package/dist/languages/c.d.ts +2 -0
  20. package/dist/languages/c.js +218 -0
  21. package/dist/languages/call-syntax.d.ts +125 -0
  22. package/dist/languages/call-syntax.js +997 -0
  23. package/dist/languages/cpp.d.ts +2 -0
  24. package/dist/languages/cpp.js +321 -0
  25. package/dist/languages/csharp.d.ts +2 -0
  26. package/dist/languages/csharp.js +324 -0
  27. package/dist/languages/elixir.d.ts +2 -0
  28. package/dist/languages/elixir.js +331 -0
  29. package/dist/languages/go.d.ts +2 -0
  30. package/dist/languages/go.js +299 -0
  31. package/dist/languages/grammars.d.ts +50 -0
  32. package/dist/languages/grammars.js +351 -0
  33. package/dist/languages/haskell.d.ts +2 -0
  34. package/dist/languages/haskell.js +250 -0
  35. package/dist/languages/java.d.ts +2 -0
  36. package/dist/languages/java.js +351 -0
  37. package/dist/languages/javascript.d.ts +4 -0
  38. package/dist/languages/javascript.js +648 -0
  39. package/dist/languages/kotlin.d.ts +2 -0
  40. package/dist/languages/kotlin.js +368 -0
  41. package/dist/languages/lua.d.ts +2 -0
  42. package/dist/languages/lua.js +212 -0
  43. package/dist/languages/ocaml.d.ts +2 -0
  44. package/dist/languages/ocaml.js +291 -0
  45. package/dist/languages/perl.d.ts +2 -0
  46. package/dist/languages/perl.js +418 -0
  47. package/dist/languages/php.d.ts +2 -0
  48. package/dist/languages/php.js +397 -0
  49. package/dist/languages/python.d.ts +2 -0
  50. package/dist/languages/python.js +376 -0
  51. package/dist/languages/registry.d.ts +7 -0
  52. package/dist/languages/registry.js +69 -0
  53. package/dist/languages/ruby.d.ts +2 -0
  54. package/dist/languages/ruby.js +391 -0
  55. package/dist/languages/rust.d.ts +2 -0
  56. package/dist/languages/rust.js +261 -0
  57. package/dist/languages/scala.d.ts +2 -0
  58. package/dist/languages/scala.js +307 -0
  59. package/dist/languages/solidity.d.ts +2 -0
  60. package/dist/languages/solidity.js +240 -0
  61. package/dist/languages/swift.d.ts +2 -0
  62. package/dist/languages/swift.js +268 -0
  63. package/dist/languages/types.d.ts +36 -0
  64. package/dist/languages/types.js +74 -0
  65. package/dist/languages/typescript-contracts.d.ts +57 -0
  66. package/dist/languages/typescript-contracts.js +528 -0
  67. package/dist/languages/typescript-dispatch.d.ts +68 -0
  68. package/dist/languages/typescript-dispatch.js +710 -0
  69. package/dist/languages/typescript.d.ts +4 -0
  70. package/dist/languages/typescript.js +722 -0
  71. package/dist/languages/zig.d.ts +2 -0
  72. package/dist/languages/zig.js +243 -0
  73. package/dist/loc.d.ts +17 -0
  74. package/dist/loc.js +34 -0
  75. package/dist/reach.d.ts +17 -0
  76. package/dist/reach.js +65 -0
  77. package/dist/render.d.ts +18 -0
  78. package/dist/render.js +83 -0
  79. package/dist/review/brand.d.ts +8 -0
  80. package/dist/review/brand.js +25 -0
  81. package/dist/review/call-context.d.ts +27 -0
  82. package/dist/review/call-context.js +446 -0
  83. package/dist/review/call-flow-html.d.ts +32 -0
  84. package/dist/review/call-flow-html.js +1870 -0
  85. package/dist/review/call-flow-nav.d.ts +151 -0
  86. package/dist/review/call-flow-nav.js +317 -0
  87. package/dist/review/call-flow.d.ts +47 -0
  88. package/dist/review/call-flow.js +229 -0
  89. package/dist/review/change-facts.d.ts +69 -0
  90. package/dist/review/change-facts.js +729 -0
  91. package/dist/review/cli.d.ts +2 -0
  92. package/dist/review/cli.js +50 -0
  93. package/dist/review/connected-analysis.d.ts +100 -0
  94. package/dist/review/connected-analysis.js +163 -0
  95. package/dist/review/connected-html.d.ts +17 -0
  96. package/dist/review/connected-html.js +2853 -0
  97. package/dist/review/connected.d.ts +23 -0
  98. package/dist/review/connected.js +141 -0
  99. package/dist/review/escape-html.d.ts +2 -0
  100. package/dist/review/escape-html.js +9 -0
  101. package/dist/review/evidence-html.d.ts +21 -0
  102. package/dist/review/evidence-html.js +521 -0
  103. package/dist/review/evidence-syntax.d.ts +132 -0
  104. package/dist/review/evidence-syntax.js +478 -0
  105. package/dist/review/evidence-types.d.ts +62 -0
  106. package/dist/review/evidence-types.js +1 -0
  107. package/dist/review/evidence.d.ts +31 -0
  108. package/dist/review/evidence.js +1603 -0
  109. package/dist/review/file-role.d.ts +9 -0
  110. package/dist/review/file-role.js +29 -0
  111. package/dist/review/github.d.ts +204 -0
  112. package/dist/review/github.js +1245 -0
  113. package/dist/review/history.d.ts +101 -0
  114. package/dist/review/history.js +412 -0
  115. package/dist/review/html.d.ts +34 -0
  116. package/dist/review/html.js +1104 -0
  117. package/dist/review/input.d.ts +10 -0
  118. package/dist/review/input.js +113 -0
  119. package/dist/review/intent.d.ts +4 -0
  120. package/dist/review/intent.js +75 -0
  121. package/dist/review/mcp-cli.d.ts +2 -0
  122. package/dist/review/mcp-cli.js +25 -0
  123. package/dist/review/mcp.d.ts +12 -0
  124. package/dist/review/mcp.js +414 -0
  125. package/dist/review/module-resolution.d.ts +2 -0
  126. package/dist/review/module-resolution.js +86 -0
  127. package/dist/review/palette.d.ts +7 -0
  128. package/dist/review/palette.js +104 -0
  129. package/dist/review/pipeline.d.ts +77 -0
  130. package/dist/review/pipeline.js +227 -0
  131. package/dist/review/pr-input.d.ts +19 -0
  132. package/dist/review/pr-input.js +130 -0
  133. package/dist/review/questions.d.ts +201 -0
  134. package/dist/review/questions.js +174 -0
  135. package/dist/review/reference-check.d.ts +7 -0
  136. package/dist/review/reference-check.js +733 -0
  137. package/dist/review/report-pages.d.ts +109 -0
  138. package/dist/review/report-pages.js +328 -0
  139. package/dist/review/service.d.ts +23 -0
  140. package/dist/review/service.js +198 -0
  141. package/dist/review/setup.d.ts +112 -0
  142. package/dist/review/setup.js +549 -0
  143. package/dist/review/source.d.ts +26 -0
  144. package/dist/review/source.js +276 -0
  145. package/dist/review/toml.d.ts +38 -0
  146. package/dist/review/toml.js +565 -0
  147. package/dist/review/types.d.ts +179 -0
  148. package/dist/review/types.js +1 -0
  149. package/dist/run.d.ts +49 -0
  150. package/dist/run.js +311 -0
  151. package/dist/types.d.ts +366 -0
  152. package/dist/types.js +83 -0
  153. package/package.json +88 -0
  154. package/scripts/ensure-native-grammar.mjs +188 -0
@@ -0,0 +1,179 @@
1
+ import type { PullRequestIntent, ReviewEvidence } from "./evidence-types.js";
2
+ import type { ChangeFacts } from "./change-facts.js";
3
+ import type { ReviewQuestion } from "./questions.js";
4
+ import type { HunkHistory, ProjectContext } from "./history.js";
5
+ export type ReviewStatus = "attention" | "uncertain" | "low" | "passed";
6
+ /**
7
+ * Structural status for a call-flow node. The engine only knows `same`,
8
+ * `added`, and `removed`; the report derives `changed` for a `same` node whose
9
+ * descendants changed, which is what makes a same-signature caller visible.
10
+ */
11
+ export type CallFlowStatus = "same" | "added" | "removed" | "changed";
12
+ /**
13
+ * Actual resolved definition of a node, read from the snapshot it lived in.
14
+ * `text` is the definition's own source, never the call site's.
15
+ */
16
+ export interface CallFlowSource {
17
+ file: string;
18
+ line: number;
19
+ endLine: number;
20
+ /**
21
+ * Opaque, printable provenance for the snapshot the text came from: the short
22
+ * commit of the `to` side, or of the `from` side for a removed node.
23
+ */
24
+ ref: string;
25
+ /**
26
+ * Definition source lines `line`..`endLine`, verbatim and complete. Nothing
27
+ * is trimmed: the report has to be readable without the repository, so a
28
+ * function is carried whole or not at all.
29
+ */
30
+ text: string;
31
+ }
32
+ /** One call in a report call-flow tree: root = definition, children = call sites. */
33
+ export interface CallFlowNode {
34
+ key: string;
35
+ label: string;
36
+ status: CallFlowStatus;
37
+ /** File containing the call site (the root's own definition file at depth 0). */
38
+ file?: string;
39
+ line?: number;
40
+ /** End line of a call site spanning several lines. */
41
+ endLine?: number;
42
+ /**
43
+ * Verbatim first prose line of the definition's attached leading comment, or
44
+ * of its Python docstring. Absent when the definition has neither, or when its
45
+ * source could not be read. Never generated.
46
+ */
47
+ description?: string;
48
+ /** Absent when the call has no indexed definition, or its source is unreadable. */
49
+ source?: CallFlowSource;
50
+ children: CallFlowNode[];
51
+ }
52
+ /** Bounded call-flow trees that touch one changed file. */
53
+ export interface CallFlowFile {
54
+ file: string;
55
+ /** Root definitions whose call paths reach this file, in engine order. */
56
+ trees: CallFlowNode[];
57
+ /** True when serialization dropped roots, children, depth, or nodes to fit the bounds. */
58
+ truncated: boolean;
59
+ }
60
+ /**
61
+ * Why structured call flows are present or absent. `available` means at least
62
+ * one changed file has trees; `no-changes` means there was nothing structural to
63
+ * attach (no changed file with text hunks, or no tree reaching one) and is not a
64
+ * safety claim; `needs-git-range` means the input was a patch; `failed` means
65
+ * the analysis threw.
66
+ */
67
+ export type CallFlowAvailability = "available" | "needs-git-range" | "no-changes" | "failed";
68
+ /**
69
+ * One keyed piece of structured review context for a hunk: a changed, caller, or
70
+ * callee definition with its snapshot-bound source and the call/binding evidence
71
+ * extracted for it. Nodes are selected deterministically and feed the evidence
72
+ * agenda and the reviewer's context.
73
+ */
74
+ export interface ReviewContextNode {
75
+ /** Addressable identity inside one hunk, unique across snapshots, e.g. `after:checkout`. */
76
+ key: string;
77
+ /** Extractor label of the definition, e.g. `checkout(order, user)`. */
78
+ label: string;
79
+ file: string;
80
+ /** 1-based definition line, the location half of the node's identity. */
81
+ line: number;
82
+ /**
83
+ * Whole node text: the definition source as the snapshot has it, plus the
84
+ * selected call sites and bindings already extracted for it. Never a preview
85
+ * and never truncated.
86
+ */
87
+ detail: string;
88
+ }
89
+ /** One parsed piece of the input: a text hunk, or a file's metadata-only change. */
90
+ export interface ReviewUnit {
91
+ id: string;
92
+ file: string;
93
+ header: string;
94
+ diff: string;
95
+ added: number;
96
+ removed: number;
97
+ oldStart: number;
98
+ newStart: number;
99
+ /**
100
+ * Why this unit cannot be judged from the diff alone (binary, rename, mode,
101
+ * symbolic link, submodule). A unit that carries one goes to manual review.
102
+ */
103
+ special?: string;
104
+ /**
105
+ * Report blocks for this hunk, in the order the report shows them: both
106
+ * snapshots, the call on a changed line first. They stay the readable report
107
+ * text; {@link ReviewUnit.contextNodes} carries the structured form.
108
+ */
109
+ callFlow?: string[];
110
+ /**
111
+ * Structured context for this hunk, highest retention priority first.
112
+ * An empty array is never set in place of "no context".
113
+ */
114
+ contextNodes?: ReviewContextNode[];
115
+ /** Commits that last changed the lines this hunk removes; git-range reviews only. */
116
+ history?: HunkHistory;
117
+ }
118
+ export interface ReviewItem extends ReviewUnit {
119
+ status: ReviewStatus;
120
+ priority: number;
121
+ /** Fixed templates over the local facts; a cited line is quoted from the diff itself. */
122
+ reasons: string[];
123
+ /** Local change facts; absent for manual units and fact-free passes. */
124
+ facts?: ChangeFacts;
125
+ }
126
+ export interface AgentOrder {
127
+ /** Every item id of the report exactly once, most important first; the report's items follow it. */
128
+ itemIds: string[];
129
+ orderedBy: string;
130
+ orderedAt: string;
131
+ /** diffninja's own order of the same items, kept so the page can still offer it. */
132
+ diffninjaIds: string[];
133
+ }
134
+ /** One line comment the reviewing agent suggests; the human adds it to their review or not. */
135
+ export interface SuggestedComment {
136
+ path: string;
137
+ line: number;
138
+ side: "LEFT" | "RIGHT";
139
+ body: string;
140
+ }
141
+ export interface AgentComments {
142
+ comments: SuggestedComment[];
143
+ suggestedBy: string;
144
+ suggestedAt: string;
145
+ }
146
+ export interface ReviewReport {
147
+ title: string;
148
+ source: string;
149
+ createdAt: string;
150
+ pr?: PullRequestIntent;
151
+ evidence?: ReviewEvidence;
152
+ items: ReviewItem[];
153
+ callFlow: string[];
154
+ /**
155
+ * One entry per changed file with actual text hunks that a call-flow tree
156
+ * reaches, in report rank order. Metadata-only files and files no tree
157
+ * reaches are omitted rather than reported as empty trees.
158
+ */
159
+ callFlows: CallFlowFile[];
160
+ callFlowAvailability: CallFlowAvailability;
161
+ warnings: string[];
162
+ /** Questions for the reviewing agent's model, bound to hunks; answers never reorder the report. */
163
+ questions: ReviewQuestion[];
164
+ /** The reading order the reviewing agent recorded; once present, `items` follow it. */
165
+ agentOrder?: AgentOrder;
166
+ /** Line comments the reviewing agent suggested; nothing is posted until the human submits them. */
167
+ agentComments?: AgentComments;
168
+ /**
169
+ * Local repository context for a git-range review: prior reverts, contributor
170
+ * guidelines, and sibling-file conventions. Absent for a patch.
171
+ */
172
+ project?: ProjectContext;
173
+ }
174
+ export interface ReviewOptions {
175
+ /** Exact PR metadata, treated as untrusted evidence rather than instructions. */
176
+ pr?: PullRequestIntent;
177
+ /** Opt in to the locally installed project TypeScript checker; never a PR script. */
178
+ referenceProject?: string;
179
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/run.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { FunctionIndex } from "./extract.js";
2
+ import type { DiffResult, ReachResult, TreeResult } from "./types.js";
3
+ export type DiffRunOptions = {
4
+ from?: string;
5
+ to?: string;
6
+ /** Symbol entrypoints (`-e`). */
7
+ entries?: string[];
8
+ /** File entrypoints (`--file`): expand to that file's exports. */
9
+ files?: string[];
10
+ paths?: string[];
11
+ cwd?: string;
12
+ maxDepth?: number;
13
+ /** When false, skip ANSI colors in ascii output. Default: true */
14
+ color?: boolean;
15
+ /** When true, append file:line suffixes in ascii. Default: false */
16
+ locs?: boolean;
17
+ /** Reuse parsed snapshots for contextual analysis, even without structural changes. */
18
+ onIndexes?: (before: FunctionIndex, after: FunctionIndex) => void;
19
+ };
20
+ export type TreeRunOptions = {
21
+ ref?: string;
22
+ entries?: string[];
23
+ files?: string[];
24
+ paths?: string[];
25
+ cwd?: string;
26
+ maxDepth?: number;
27
+ color?: boolean;
28
+ locs?: boolean;
29
+ };
30
+ export type ReachRunOptions = {
31
+ ref?: string;
32
+ /** Start symbol(s). */
33
+ entries?: string[];
34
+ /** Start file(s): every export in the file. */
35
+ files?: string[];
36
+ /** Target symbol to reach. */
37
+ to: string;
38
+ paths?: string[];
39
+ cwd?: string;
40
+ maxDepth?: number;
41
+ color?: boolean;
42
+ locs?: boolean;
43
+ };
44
+ /** Diff call stacks between two snapshots. Returns structured data + ASCII. */
45
+ export declare function runDiff(options?: DiffRunOptions): DiffResult;
46
+ /** View call tree(s) for entrypoint(s) from a single snapshot. */
47
+ export declare function runTree(options: TreeRunOptions): TreeResult;
48
+ /** Find all call paths from entrypoint(s) to a target symbol. */
49
+ export declare function runReach(options: ReachRunOptions): ReachResult;
package/dist/run.js ADDED
@@ -0,0 +1,311 @@
1
+ import { buildCallTreeFromInfo, exportsInFile, resolveEntry, resolveEntrypointFile, indexedFiles, } from "./calltree.js";
2
+ import { buildIndex, extractCached } from "./extract.js";
3
+ import { assertGitRepo, describeSnapshot, listSnapshotFiles, resolveDiffSnapshotsAndPaths, resolveSnapshotAndPaths, verifyCommit, visitCommitBlobs, visitWorktreeFiles, } from "./git.js";
4
+ import { diffEntry, diffPinnedEntry, inferEntries, resolveExplicitDiffEntries, } from "./infer.js";
5
+ import { collectPathsTo, findReachPaths } from "./reach.js";
6
+ import { renderDiff, renderTree } from "./render.js";
7
+ import { assignOptionalTreeFields } from "./types.js";
8
+ function loadIndex(cwd, snapshot, pathFilters, cache = new Map()) {
9
+ const files = listSnapshotFiles(cwd, snapshot, pathFilters);
10
+ const extracted = new Map();
11
+ const extract = (file, source) => {
12
+ try {
13
+ extracted.set(file.path, extractCached(file.path, source, cache));
14
+ }
15
+ catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ console.error(`warn: failed to parse ${file.path} @ ${snapshot.ref}: ${message}`);
18
+ }
19
+ };
20
+ if (snapshot.kind === "worktree") {
21
+ visitWorktreeFiles(cwd, files, extract);
22
+ }
23
+ else {
24
+ const filesByOid = new Map();
25
+ for (const file of files) {
26
+ if (!file.oid)
27
+ continue;
28
+ const matches = filesByOid.get(file.oid) ?? [];
29
+ matches.push(file);
30
+ filesByOid.set(file.oid, matches);
31
+ }
32
+ visitCommitBlobs(cwd, files, (oid, source) => {
33
+ for (const file of filesByOid.get(oid) ?? [])
34
+ extract(file, source);
35
+ });
36
+ }
37
+ const functions = [];
38
+ for (const file of files) {
39
+ functions.push(...(extracted.get(file.path) ?? []));
40
+ }
41
+ return buildIndex(functions);
42
+ }
43
+ /** Resolve `-e` symbols to concrete definitions. */
44
+ function resolveSymbolInfos(index, symbols) {
45
+ const resolved = [];
46
+ const seen = new Set();
47
+ for (const entry of symbols) {
48
+ const key = resolveEntry(entry, index);
49
+ if (!key) {
50
+ throw new Error(`Entrypoint not found: ${entry}`);
51
+ }
52
+ const info = index.get(key);
53
+ if (!info) {
54
+ throw new Error(`Entrypoint not found: ${entry}`);
55
+ }
56
+ const id = `${info.file}\0${info.key}\0${info.line ?? ""}`;
57
+ if (seen.has(id))
58
+ continue;
59
+ seen.add(id);
60
+ resolved.push(info);
61
+ }
62
+ return resolved;
63
+ }
64
+ /** Resolve `--file` paths to exported definitions (file-pinned). */
65
+ function resolveFileInfos(index, files) {
66
+ const resolved = [];
67
+ const seen = new Set();
68
+ for (const entry of files) {
69
+ const file = resolveEntrypointFile(entry, indexedFiles(index));
70
+ const infos = exportsInFile(file, index);
71
+ if (infos.length === 0) {
72
+ throw new Error(`No exported entrypoints in ${file}`);
73
+ }
74
+ for (const info of infos) {
75
+ const id = `${info.file}\0${info.key}\0${info.line ?? ""}`;
76
+ if (seen.has(id))
77
+ continue;
78
+ seen.add(id);
79
+ resolved.push(info);
80
+ }
81
+ }
82
+ return resolved;
83
+ }
84
+ function resolveEntrypointInfos(index, symbols, files) {
85
+ return [
86
+ ...resolveSymbolInfos(index, symbols),
87
+ ...resolveFileInfos(index, files),
88
+ ];
89
+ }
90
+ function serializeCallNode(node) {
91
+ const serialized = {
92
+ key: node.key,
93
+ label: node.label,
94
+ children: node.children.map(serializeCallNode),
95
+ };
96
+ return assignOptionalTreeFields(serialized, node);
97
+ }
98
+ function serializeDiffNode(node) {
99
+ const serialized = {
100
+ key: node.key,
101
+ label: node.label,
102
+ status: node.status,
103
+ children: node.children.map(serializeDiffNode),
104
+ };
105
+ return assignOptionalTreeFields(serialized, node);
106
+ }
107
+ /** Diff call stacks between two snapshots. Returns structured data + ASCII. */
108
+ export function runDiff(options = {}) {
109
+ const cwd = options.cwd ?? process.cwd();
110
+ const maxDepth = options.maxDepth ?? 12;
111
+ const entriesOpt = options.entries ?? [];
112
+ const filesOpt = options.files ?? [];
113
+ const color = options.color !== false;
114
+ const locs = options.locs === true;
115
+ const hasExplicit = entriesOpt.length > 0 || filesOpt.length > 0;
116
+ assertGitRepo(cwd);
117
+ const { from, to, paths: resolvedPaths, } = resolveDiffSnapshotsAndPaths(cwd, options.from, options.to, options.paths ?? []);
118
+ if (from.kind === "commit")
119
+ verifyCommit(cwd, from.ref);
120
+ if (to.kind === "commit")
121
+ verifyCommit(cwd, to.ref);
122
+ const extractionCache = new Map();
123
+ const before = loadIndex(cwd, from, resolvedPaths, extractionCache);
124
+ const after = loadIndex(cwd, to, resolvedPaths, extractionCache);
125
+ extractionCache.clear();
126
+ options.onIndexes?.(before, after);
127
+ const fromLabel = describeSnapshot(from);
128
+ const toLabel = describeSnapshot(to);
129
+ const trees = [];
130
+ const asciiParts = [
131
+ `calldiff diff ${fromLabel} → ${toLabel}`,
132
+ "",
133
+ ];
134
+ const pushDiff = (entry, diff) => {
135
+ const ascii = renderDiff(diff, { color, locs });
136
+ trees.push({
137
+ entry,
138
+ ascii: renderDiff(diff, { color: false, locs }),
139
+ tree: serializeDiffNode(diff),
140
+ });
141
+ if (asciiParts.length > 2)
142
+ asciiParts.push("");
143
+ asciiParts.push(ascii);
144
+ };
145
+ if (hasExplicit) {
146
+ const explicit = resolveExplicitDiffEntries(before, after, entriesOpt, filesOpt);
147
+ for (const item of explicit) {
148
+ const diff = item.file
149
+ ? diffPinnedEntry(item.key, item.beforeInfo, item.afterInfo, before, after, maxDepth)
150
+ : diffEntry(item.key, before, after, maxDepth);
151
+ if (!diff)
152
+ continue;
153
+ pushDiff(item.key, diff);
154
+ }
155
+ }
156
+ else {
157
+ const entries = inferEntries(before, after, [], maxDepth);
158
+ if (entries.length === 0) {
159
+ const message = `No callstack changes between ${fromLabel} and ${toLabel}.`;
160
+ return {
161
+ mode: "diff",
162
+ from: fromLabel,
163
+ to: toLabel,
164
+ message,
165
+ trees: [],
166
+ ascii: message,
167
+ };
168
+ }
169
+ for (const entry of entries) {
170
+ const diff = diffEntry(entry, before, after, maxDepth);
171
+ if (!diff)
172
+ continue;
173
+ pushDiff(entry, diff);
174
+ }
175
+ }
176
+ if (trees.length === 0) {
177
+ const message = hasExplicit
178
+ ? "No callstack changes for requested entrypoints."
179
+ : "No callstack changes for inferred entrypoints.";
180
+ return {
181
+ mode: "diff",
182
+ from: fromLabel,
183
+ to: toLabel,
184
+ message,
185
+ trees: [],
186
+ ascii: `${asciiParts[0]}\n\n${message}`,
187
+ };
188
+ }
189
+ return {
190
+ mode: "diff",
191
+ from: fromLabel,
192
+ to: toLabel,
193
+ trees,
194
+ ascii: asciiParts.join("\n"),
195
+ };
196
+ }
197
+ /** View call tree(s) for entrypoint(s) from a single snapshot. */
198
+ export function runTree(options) {
199
+ const cwd = options.cwd ?? process.cwd();
200
+ const maxDepth = options.maxDepth ?? 12;
201
+ const color = options.color !== false;
202
+ const locs = options.locs === true;
203
+ const symbols = options.entries ?? [];
204
+ const files = options.files ?? [];
205
+ assertGitRepo(cwd);
206
+ const { snapshot, paths } = resolveSnapshotAndPaths(cwd, options.ref, options.paths ?? []);
207
+ if (snapshot.kind === "commit")
208
+ verifyCommit(cwd, snapshot.ref);
209
+ const index = loadIndex(cwd, snapshot, paths);
210
+ const infos = resolveEntrypointInfos(index, symbols, files);
211
+ const refLabel = describeSnapshot(snapshot);
212
+ const trees = [];
213
+ const asciiParts = [`calldiff tree ${refLabel}`, ""];
214
+ for (const info of infos) {
215
+ const tree = buildCallTreeFromInfo(info, index, maxDepth);
216
+ const ascii = renderTree(tree, { color, locs });
217
+ trees.push({
218
+ entry: info.key,
219
+ ascii: renderTree(tree, { color: false, locs }),
220
+ tree: serializeCallNode(tree),
221
+ });
222
+ if (asciiParts.length > 2)
223
+ asciiParts.push("");
224
+ asciiParts.push(ascii);
225
+ }
226
+ return {
227
+ mode: "tree",
228
+ ref: refLabel,
229
+ trees,
230
+ ascii: asciiParts.join("\n"),
231
+ };
232
+ }
233
+ /** Find all call paths from entrypoint(s) to a target symbol. */
234
+ export function runReach(options) {
235
+ const cwd = options.cwd ?? process.cwd();
236
+ const maxDepth = options.maxDepth ?? 12;
237
+ const color = options.color !== false;
238
+ const locs = options.locs === true;
239
+ const symbols = options.entries ?? [];
240
+ const files = options.files ?? [];
241
+ assertGitRepo(cwd);
242
+ const { snapshot, paths } = resolveSnapshotAndPaths(cwd, options.ref, options.paths ?? []);
243
+ if (snapshot.kind === "commit")
244
+ verifyCommit(cwd, snapshot.ref);
245
+ const index = loadIndex(cwd, snapshot, paths);
246
+ const targetKey = resolveEntry(options.to, index);
247
+ if (!targetKey) {
248
+ throw new Error(`Target not found: ${options.to}`);
249
+ }
250
+ const refLabel = describeSnapshot(snapshot);
251
+ const pathResults = [];
252
+ const entryKeys = [];
253
+ for (const entry of symbols) {
254
+ const key = resolveEntry(entry, index);
255
+ if (!key) {
256
+ throw new Error(`Entrypoint not found: ${entry}`);
257
+ }
258
+ if (!entryKeys.includes(key))
259
+ entryKeys.push(key);
260
+ const found = findReachPaths(entry, targetKey, index, maxDepth);
261
+ for (const path of found) {
262
+ pathResults.push({
263
+ ascii: renderTree(path, { color: false, locs }),
264
+ tree: serializeCallNode(path),
265
+ });
266
+ }
267
+ }
268
+ for (const info of resolveFileInfos(index, files)) {
269
+ if (!entryKeys.includes(info.key))
270
+ entryKeys.push(info.key);
271
+ const tree = buildCallTreeFromInfo(info, index, maxDepth);
272
+ for (const path of collectPathsTo(tree, targetKey, options.to)) {
273
+ pathResults.push({
274
+ ascii: renderTree(path, { color: false, locs }),
275
+ tree: serializeCallNode(path),
276
+ });
277
+ }
278
+ }
279
+ const fromLabel = entryKeys.length === 1 ? entryKeys[0] : entryKeys.join(", ");
280
+ const header = `calldiff reach ${refLabel}: ${fromLabel} → ${targetKey}`;
281
+ if (pathResults.length === 0) {
282
+ const message = `No paths from ${fromLabel} to ${targetKey}.`;
283
+ return {
284
+ mode: "reach",
285
+ ref: refLabel,
286
+ from: fromLabel,
287
+ to: targetKey,
288
+ message,
289
+ paths: [],
290
+ ascii: `${header}\n\n${message}`,
291
+ };
292
+ }
293
+ const asciiParts = [header, ""];
294
+ for (let i = 0; i < pathResults.length; i++) {
295
+ const path = pathResults[i];
296
+ if (i > 0)
297
+ asciiParts.push("");
298
+ if (pathResults.length > 1) {
299
+ asciiParts.push(`# path ${i + 1}`);
300
+ }
301
+ asciiParts.push(renderTree(path.tree, { color, locs }));
302
+ }
303
+ return {
304
+ mode: "reach",
305
+ ref: refLabel,
306
+ from: fromLabel,
307
+ to: targetKey,
308
+ paths: pathResults,
309
+ ascii: asciiParts.join("\n"),
310
+ };
311
+ }