@shvmgyl15/tsgraph 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 (187) hide show
  1. package/AGENTS.md +64 -0
  2. package/README.md +128 -0
  3. package/TODOS.md +61 -0
  4. package/dist/analysis/analysis.test.d.ts +2 -0
  5. package/dist/analysis/analysis.test.d.ts.map +1 -0
  6. package/dist/analysis/analysis.test.js +359 -0
  7. package/dist/analysis/analysis.test.js.map +1 -0
  8. package/dist/analysis/complexity.d.ts +8 -0
  9. package/dist/analysis/complexity.d.ts.map +1 -0
  10. package/dist/analysis/complexity.js +88 -0
  11. package/dist/analysis/complexity.js.map +1 -0
  12. package/dist/analysis/coupling.d.ts +17 -0
  13. package/dist/analysis/coupling.d.ts.map +1 -0
  14. package/dist/analysis/coupling.js +71 -0
  15. package/dist/analysis/coupling.js.map +1 -0
  16. package/dist/analysis/hotspot.d.ts +10 -0
  17. package/dist/analysis/hotspot.d.ts.map +1 -0
  18. package/dist/analysis/hotspot.js +33 -0
  19. package/dist/analysis/hotspot.js.map +1 -0
  20. package/dist/analysis/index.d.ts +9 -0
  21. package/dist/analysis/index.d.ts.map +1 -0
  22. package/dist/analysis/index.js +5 -0
  23. package/dist/analysis/index.js.map +1 -0
  24. package/dist/boundaries/index.d.ts +25 -0
  25. package/dist/boundaries/index.d.ts.map +1 -0
  26. package/dist/boundaries/index.js +103 -0
  27. package/dist/boundaries/index.js.map +1 -0
  28. package/dist/boundaries/index.test.d.ts +2 -0
  29. package/dist/boundaries/index.test.d.ts.map +1 -0
  30. package/dist/boundaries/index.test.js +293 -0
  31. package/dist/boundaries/index.test.js.map +1 -0
  32. package/dist/changes/index.d.ts +28 -0
  33. package/dist/changes/index.d.ts.map +1 -0
  34. package/dist/changes/index.js +48 -0
  35. package/dist/changes/index.js.map +1 -0
  36. package/dist/changes/index.test.d.ts +2 -0
  37. package/dist/changes/index.test.d.ts.map +1 -0
  38. package/dist/changes/index.test.js +104 -0
  39. package/dist/changes/index.test.js.map +1 -0
  40. package/dist/cli/index.d.ts +3 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +659 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/git/index.d.ts +16 -0
  45. package/dist/git/index.d.ts.map +1 -0
  46. package/dist/git/index.js +73 -0
  47. package/dist/git/index.js.map +1 -0
  48. package/dist/git/index.test.d.ts +2 -0
  49. package/dist/git/index.test.d.ts.map +1 -0
  50. package/dist/git/index.test.js +78 -0
  51. package/dist/git/index.test.js.map +1 -0
  52. package/dist/graph/types.d.ts +156 -0
  53. package/dist/graph/types.d.ts.map +1 -0
  54. package/dist/graph/types.js +166 -0
  55. package/dist/graph/types.js.map +1 -0
  56. package/dist/graph/types.test.d.ts +2 -0
  57. package/dist/graph/types.test.d.ts.map +1 -0
  58. package/dist/graph/types.test.js +326 -0
  59. package/dist/graph/types.test.js.map +1 -0
  60. package/dist/mcp/mcp.test.d.ts +2 -0
  61. package/dist/mcp/mcp.test.d.ts.map +1 -0
  62. package/dist/mcp/mcp.test.js +151 -0
  63. package/dist/mcp/mcp.test.js.map +1 -0
  64. package/dist/mcp/server.d.ts +2 -0
  65. package/dist/mcp/server.d.ts.map +1 -0
  66. package/dist/mcp/server.js +209 -0
  67. package/dist/mcp/server.js.map +1 -0
  68. package/dist/nextjs/index.d.ts +8 -0
  69. package/dist/nextjs/index.d.ts.map +1 -0
  70. package/dist/nextjs/index.js +16 -0
  71. package/dist/nextjs/index.js.map +1 -0
  72. package/dist/nextjs/nextjs.test.d.ts +2 -0
  73. package/dist/nextjs/nextjs.test.d.ts.map +1 -0
  74. package/dist/nextjs/nextjs.test.js +190 -0
  75. package/dist/nextjs/nextjs.test.js.map +1 -0
  76. package/dist/nextjs/pages.d.ts +4 -0
  77. package/dist/nextjs/pages.d.ts.map +1 -0
  78. package/dist/nextjs/pages.js +36 -0
  79. package/dist/nextjs/pages.js.map +1 -0
  80. package/dist/nextjs/react.d.ts +3 -0
  81. package/dist/nextjs/react.d.ts.map +1 -0
  82. package/dist/nextjs/react.js +86 -0
  83. package/dist/nextjs/react.js.map +1 -0
  84. package/dist/nextjs/router.d.ts +4 -0
  85. package/dist/nextjs/router.d.ts.map +1 -0
  86. package/dist/nextjs/router.js +86 -0
  87. package/dist/nextjs/router.js.map +1 -0
  88. package/dist/nextjs/routes.d.ts +4 -0
  89. package/dist/nextjs/routes.d.ts.map +1 -0
  90. package/dist/nextjs/routes.js +58 -0
  91. package/dist/nextjs/routes.js.map +1 -0
  92. package/dist/opencode/index.d.ts +7 -0
  93. package/dist/opencode/index.d.ts.map +1 -0
  94. package/dist/opencode/index.js +71 -0
  95. package/dist/opencode/index.js.map +1 -0
  96. package/dist/opencode/index.test.d.ts +2 -0
  97. package/dist/opencode/index.test.d.ts.map +1 -0
  98. package/dist/opencode/index.test.js +71 -0
  99. package/dist/opencode/index.test.js.map +1 -0
  100. package/dist/parser/index.d.ts +4 -0
  101. package/dist/parser/index.d.ts.map +1 -0
  102. package/dist/parser/index.js +282 -0
  103. package/dist/parser/index.js.map +1 -0
  104. package/dist/parser/parser.test.d.ts +2 -0
  105. package/dist/parser/parser.test.d.ts.map +1 -0
  106. package/dist/parser/parser.test.js +225 -0
  107. package/dist/parser/parser.test.js.map +1 -0
  108. package/dist/plan/index.d.ts +32 -0
  109. package/dist/plan/index.d.ts.map +1 -0
  110. package/dist/plan/index.js +107 -0
  111. package/dist/plan/index.js.map +1 -0
  112. package/dist/plan/index.test.d.ts +2 -0
  113. package/dist/plan/index.test.d.ts.map +1 -0
  114. package/dist/plan/index.test.js +143 -0
  115. package/dist/plan/index.test.js.map +1 -0
  116. package/dist/report/index.d.ts +9 -0
  117. package/dist/report/index.d.ts.map +1 -0
  118. package/dist/report/index.js +108 -0
  119. package/dist/report/index.js.map +1 -0
  120. package/dist/scanner/index.d.ts +13 -0
  121. package/dist/scanner/index.d.ts.map +1 -0
  122. package/dist/scanner/index.js +78 -0
  123. package/dist/scanner/index.js.map +1 -0
  124. package/dist/scanner/scanner.test.d.ts +2 -0
  125. package/dist/scanner/scanner.test.d.ts.map +1 -0
  126. package/dist/scanner/scanner.test.js +113 -0
  127. package/dist/scanner/scanner.test.js.map +1 -0
  128. package/dist/search/index.d.ts +32 -0
  129. package/dist/search/index.d.ts.map +1 -0
  130. package/dist/search/index.js +97 -0
  131. package/dist/search/index.js.map +1 -0
  132. package/dist/search/search.test.d.ts +2 -0
  133. package/dist/search/search.test.d.ts.map +1 -0
  134. package/dist/search/search.test.js +446 -0
  135. package/dist/search/search.test.js.map +1 -0
  136. package/dist/traversal/index.d.ts +5 -0
  137. package/dist/traversal/index.d.ts.map +1 -0
  138. package/dist/traversal/index.js +3 -0
  139. package/dist/traversal/index.js.map +1 -0
  140. package/dist/traversal/traversal.d.ts +31 -0
  141. package/dist/traversal/traversal.d.ts.map +1 -0
  142. package/dist/traversal/traversal.js +130 -0
  143. package/dist/traversal/traversal.js.map +1 -0
  144. package/dist/traversal/traversal.test.d.ts +2 -0
  145. package/dist/traversal/traversal.test.d.ts.map +1 -0
  146. package/dist/traversal/traversal.test.js +224 -0
  147. package/dist/traversal/traversal.test.js.map +1 -0
  148. package/opencode.json +24 -0
  149. package/package.json +29 -0
  150. package/src/analysis/analysis.test.ts +405 -0
  151. package/src/analysis/complexity.ts +107 -0
  152. package/src/analysis/coupling.ts +106 -0
  153. package/src/analysis/hotspot.ts +52 -0
  154. package/src/analysis/index.ts +17 -0
  155. package/src/boundaries/index.test.ts +335 -0
  156. package/src/boundaries/index.ts +137 -0
  157. package/src/changes/index.test.ts +114 -0
  158. package/src/changes/index.ts +95 -0
  159. package/src/cli/index.ts +736 -0
  160. package/src/git/index.test.ts +92 -0
  161. package/src/git/index.ts +86 -0
  162. package/src/graph/types.test.ts +383 -0
  163. package/src/graph/types.ts +353 -0
  164. package/src/mcp/mcp.test.ts +176 -0
  165. package/src/mcp/server.ts +217 -0
  166. package/src/nextjs/index.ts +23 -0
  167. package/src/nextjs/nextjs.test.ts +233 -0
  168. package/src/nextjs/pages.ts +43 -0
  169. package/src/nextjs/react.ts +100 -0
  170. package/src/nextjs/router.ts +102 -0
  171. package/src/nextjs/routes.ts +69 -0
  172. package/src/opencode/index.test.ts +90 -0
  173. package/src/opencode/index.ts +83 -0
  174. package/src/parser/index.ts +339 -0
  175. package/src/parser/parser.test.ts +282 -0
  176. package/src/plan/index.test.ts +162 -0
  177. package/src/plan/index.ts +161 -0
  178. package/src/report/index.ts +128 -0
  179. package/src/scanner/index.ts +97 -0
  180. package/src/scanner/scanner.test.ts +135 -0
  181. package/src/search/index.ts +163 -0
  182. package/src/search/search.test.ts +512 -0
  183. package/src/traversal/index.ts +5 -0
  184. package/src/traversal/traversal.test.ts +266 -0
  185. package/src/traversal/traversal.ts +185 -0
  186. package/tsconfig.json +20 -0
  187. package/vitest.config.ts +7 -0
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import {
6
+ makeGraph,
7
+ makeSymbolNode,
8
+ makeCallEdge,
9
+ makeTestEdge,
10
+ makeFileNode,
11
+ } from "../graph/types.js";
12
+ import { impact, findPath, findOrphans, trace } from "./traversal.js";
13
+
14
+ function createTempDir(): string {
15
+ return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
16
+ }
17
+
18
+ describe("impact", () => {
19
+ it("finds direct callers", () => {
20
+ const graph = makeGraph({
21
+ symbols: [
22
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
23
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
24
+ makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
25
+ ],
26
+ calls: [
27
+ makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
28
+ makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "a", file: "c.ts", line: 1 }),
29
+ ],
30
+ });
31
+
32
+ const results = impact(graph, "a");
33
+ expect(results).toHaveLength(2);
34
+ const names = results.map((r) => r.symbol.name).sort();
35
+ expect(names).toEqual(["b", "c"]);
36
+ expect(results.every((r) => r.depth === 1)).toBe(true);
37
+ });
38
+
39
+ it("traverses multiple levels", () => {
40
+ const graph = makeGraph({
41
+ symbols: [
42
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
43
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
44
+ makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
45
+ ],
46
+ calls: [
47
+ makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
48
+ makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "b", file: "c.ts", line: 1 }),
49
+ ],
50
+ });
51
+
52
+ const results = impact(graph, "a");
53
+ expect(results).toHaveLength(2);
54
+ const b = results.find((r) => r.symbol.name === "b");
55
+ const c = results.find((r) => r.symbol.name === "c");
56
+ expect(b).toBeTruthy();
57
+ expect(c).toBeTruthy();
58
+ expect(b!.depth).toBe(1);
59
+ expect(c!.depth).toBe(2);
60
+ });
61
+
62
+ it("respects max depth", () => {
63
+ const graph = makeGraph({
64
+ symbols: [
65
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
66
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
67
+ makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
68
+ ],
69
+ calls: [
70
+ makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
71
+ makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "b", file: "c.ts", line: 1 }),
72
+ ],
73
+ });
74
+
75
+ const results = impact(graph, "a", 1);
76
+ expect(results).toHaveLength(1);
77
+ expect(results[0].symbol.name).toBe("b");
78
+ });
79
+
80
+ it("returns empty for symbol with no callers", () => {
81
+ const graph = makeGraph({
82
+ symbols: [makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 })],
83
+ });
84
+ const results = impact(graph, "a");
85
+ expect(results).toHaveLength(0);
86
+ });
87
+
88
+ it("returns empty for unknown symbol", () => {
89
+ const graph = makeGraph();
90
+ const results = impact(graph, "noop");
91
+ expect(results).toHaveLength(0);
92
+ });
93
+ });
94
+
95
+ describe("findPath", () => {
96
+ it("finds a direct path between two symbols", () => {
97
+ const graph = makeGraph({
98
+ symbols: [
99
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
100
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
101
+ ],
102
+ calls: [makeCallEdge({ callerSymbolId: "a", callerName: "a", calleeRaw: "b", file: "a.ts", line: 1 })],
103
+ });
104
+
105
+ const p = findPath(graph, "a", "b");
106
+ expect(p).toBeTruthy();
107
+ expect(p!.map((n) => n.name)).toEqual(["a", "b"]);
108
+ });
109
+
110
+ it("finds a multi-hop path", () => {
111
+ const graph = makeGraph({
112
+ symbols: [
113
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
114
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
115
+ makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
116
+ ],
117
+ calls: [
118
+ makeCallEdge({ callerSymbolId: "a", callerName: "a", calleeRaw: "b", file: "a.ts", line: 1 }),
119
+ makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "c", file: "b.ts", line: 1 }),
120
+ ],
121
+ });
122
+
123
+ const p = findPath(graph, "a", "c");
124
+ expect(p).toBeTruthy();
125
+ expect(p!.map((n) => n.name)).toEqual(["a", "b", "c"]);
126
+ });
127
+
128
+ it("returns self-path when from === to", () => {
129
+ const graph = makeGraph({
130
+ symbols: [makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 })],
131
+ });
132
+ const p = findPath(graph, "a", "a");
133
+ expect(p).toBeTruthy();
134
+ expect(p!.map((n) => n.name)).toEqual(["a"]);
135
+ });
136
+
137
+ it("returns undefined when no path exists", () => {
138
+ const graph = makeGraph({
139
+ symbols: [
140
+ makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
141
+ makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
142
+ ],
143
+ });
144
+ const p = findPath(graph, "a", "b");
145
+ expect(p).toBeUndefined();
146
+ });
147
+
148
+ it("returns undefined for unknown symbols", () => {
149
+ const graph = makeGraph();
150
+ expect(findPath(graph, "a", "b")).toBeUndefined();
151
+ expect(findPath(graph, "a", "a")).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe("findOrphans", () => {
156
+ it("skips unexported symbols that are called", () => {
157
+ const graph = makeGraph({
158
+ symbols: [
159
+ makeSymbolNode({ id: "a", name: "a", kind: "function", isExported: false }),
160
+ makeSymbolNode({ id: "b", name: "b", kind: "function", isExported: true }),
161
+ ],
162
+ calls: [makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 })],
163
+ });
164
+
165
+ const orphans = findOrphans(graph);
166
+ const deadNames = orphans.map((o) => o.symbol.name);
167
+ expect(deadNames).not.toContain("a"); // "a" is called by "b"
168
+ });
169
+
170
+ it("flags unexported symbols with no callers", () => {
171
+ const graph = makeGraph({
172
+ symbols: [
173
+ makeSymbolNode({ id: "dead", name: "dead", kind: "function", isExported: false }),
174
+ makeSymbolNode({ id: "alive", name: "alive", kind: "function", isExported: false }),
175
+ ],
176
+ calls: [makeCallEdge({ callerSymbolId: "alive", callerName: "alive", calleeRaw: "nonexistent", file: "x.ts", line: 1 })],
177
+ });
178
+
179
+ const orphans = findOrphans(graph);
180
+ expect(orphans.some((o) => o.symbol.name === "dead")).toBe(true);
181
+ });
182
+
183
+ it("flags exported symbols with no callers or tests", () => {
184
+ const graph = makeGraph({
185
+ symbols: [makeSymbolNode({ id: "unused", name: "unused", kind: "function", isExported: true })],
186
+ });
187
+
188
+ const orphans = findOrphans(graph);
189
+ expect(orphans).toHaveLength(1);
190
+ expect(orphans[0].symbol.name).toBe("unused");
191
+ });
192
+
193
+ it("excludes symbols referenced in tests", () => {
194
+ const graph = makeGraph({
195
+ symbols: [makeSymbolNode({ id: "tested", name: "tested", kind: "function", isExported: false })],
196
+ testEdges: [makeTestEdge({ testFunc: "test.ts", target: "tested", file: "test.ts", line: 1 })],
197
+ });
198
+
199
+ const orphans = findOrphans(graph);
200
+ expect(orphans).toHaveLength(0);
201
+ });
202
+ });
203
+
204
+ describe("trace", () => {
205
+ it("finds string matches in symbol bodies and their callers", () => {
206
+ const dir = createTempDir();
207
+ const filePath = path.join(dir, "src/lib.ts");
208
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
209
+ fs.writeFileSync(
210
+ filePath,
211
+ [
212
+ 'function doStuff() {',
213
+ ' throw new Error("boom");',
214
+ '}',
215
+ 'function caller() {',
216
+ ' doStuff();',
217
+ '}',
218
+ ].join("\n"),
219
+ "utf-8",
220
+ );
221
+
222
+ const graph = makeGraph({
223
+ root: dir,
224
+ symbols: [
225
+ makeSymbolNode({ id: "doStuff", name: "doStuff", kind: "function", file: "src/lib.ts", line: 1, endLine: 3 }),
226
+ makeSymbolNode({ id: "caller", name: "caller", kind: "function", file: "src/lib.ts", line: 4, endLine: 6 }),
227
+ ],
228
+ calls: [
229
+ makeCallEdge({ callerSymbolId: "caller", callerName: "caller", calleeRaw: "doStuff", file: "src/lib.ts", line: 5 }),
230
+ ],
231
+ });
232
+
233
+ const results = trace(graph, "boom");
234
+ expect(results).toHaveLength(1);
235
+ expect(results[0].match.symbol.name).toBe("doStuff");
236
+ expect(results[0].match.contextLine).toContain("boom");
237
+ expect(results[0].callers).toHaveLength(1);
238
+ expect(results[0].callers[0].symbol.name).toBe("caller");
239
+
240
+ fs.rmSync(dir, { recursive: true });
241
+ });
242
+
243
+ it("returns empty for no match", () => {
244
+ const graph = makeGraph({
245
+ symbols: [makeSymbolNode({ id: "foo", name: "foo", kind: "function", file: "x.ts", line: 1, endLine: 1 })],
246
+ });
247
+ const results = trace(graph, "nonexistent");
248
+ expect(results).toHaveLength(0);
249
+ });
250
+
251
+ it("is case-insensitive", () => {
252
+ const dir = createTempDir();
253
+ const filePath = path.join(dir, "x.ts");
254
+ fs.writeFileSync(filePath, 'function foo() { return "HELLO"; }', "utf-8");
255
+
256
+ const graph = makeGraph({
257
+ root: dir,
258
+ symbols: [makeSymbolNode({ id: "foo", name: "foo", kind: "function", file: "x.ts", line: 1, endLine: 1 })],
259
+ });
260
+
261
+ const results = trace(graph, "hello");
262
+ expect(results).toHaveLength(1);
263
+
264
+ fs.rmSync(dir, { recursive: true });
265
+ });
266
+ });
@@ -0,0 +1,185 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Graph, SymbolNode } from "../graph/types.js";
4
+
5
+ export interface ImpactNode {
6
+ symbol: SymbolNode;
7
+ depth: number;
8
+ callChain: string[];
9
+ }
10
+
11
+ export function impact(
12
+ graph: Graph,
13
+ symbolName: string,
14
+ maxDepth: number = 5,
15
+ ): ImpactNode[] {
16
+ const start = graph.symbols.find((s) => s.name === symbolName);
17
+ if (!start) return [];
18
+
19
+ const results: ImpactNode[] = [];
20
+ const visited = new Set<string>();
21
+ const queue: { sym: SymbolNode; depth: number; chain: string[] }[] = [
22
+ { sym: start, depth: 0, chain: [start.name] },
23
+ ];
24
+ visited.add(start.id);
25
+
26
+ while (queue.length > 0) {
27
+ const { sym, depth, chain } = queue.shift()!;
28
+ if (depth > 0) {
29
+ results.push({ symbol: sym, depth, callChain: chain });
30
+ }
31
+ if (depth >= maxDepth) continue;
32
+
33
+ const callers = graph.calls.filter((c) => c.calleeRaw === sym.name);
34
+ for (const edge of callers) {
35
+ const caller = graph.symbols.find((s) => s.id === edge.callerSymbolId);
36
+ if (!caller || visited.has(caller.id)) continue;
37
+ visited.add(caller.id);
38
+ queue.push({
39
+ sym: caller,
40
+ depth: depth + 1,
41
+ chain: [...chain, caller.name],
42
+ });
43
+ }
44
+ }
45
+
46
+ return results;
47
+ }
48
+
49
+ export interface PathNode {
50
+ name: string;
51
+ kind: string;
52
+ file: string;
53
+ line: number;
54
+ }
55
+
56
+ export function findPath(
57
+ graph: Graph,
58
+ fromName: string,
59
+ toName: string,
60
+ maxDepth: number = 10,
61
+ ): PathNode[] | undefined {
62
+ const from = graph.symbols.find((s) => s.name === fromName);
63
+ const to = graph.symbols.find((s) => s.name === toName);
64
+ if (!from || !to) return undefined;
65
+ if (from.id === to.id) return [{ name: from.name, kind: from.kind, file: from.file, line: from.line }];
66
+
67
+ const visited = new Set<string>();
68
+ const parent = new Map<string, { prev: string; edge: { name: string; kind: string; file: string; line: number } }>();
69
+ const queue: string[] = [from.id];
70
+ visited.add(from.id);
71
+
72
+ while (queue.length > 0) {
73
+ const currentId = queue.shift()!;
74
+ const current = graph.symbols.find((s) => s.id === currentId);
75
+ if (!current) continue;
76
+
77
+ const calleeEdges = graph.calls.filter((c) => c.callerSymbolId === currentId);
78
+ for (const edge of calleeEdges) {
79
+ const callee = graph.symbols.find(
80
+ (s) => s.name === edge.calleeRaw,
81
+ );
82
+ if (!callee || visited.has(callee.id)) continue;
83
+ visited.add(callee.id);
84
+ parent.set(callee.id, {
85
+ prev: currentId,
86
+ edge: { name: callee.name, kind: callee.kind, file: callee.file, line: callee.line },
87
+ });
88
+ if (callee.id === to.id) {
89
+ const pathNodes: PathNode[] = [];
90
+ let step: string | undefined = to.id;
91
+ while (step) {
92
+ const sym = graph.symbols.find((s) => s.id === step)!;
93
+ pathNodes.unshift({ name: sym.name, kind: sym.kind, file: sym.file, line: sym.line });
94
+ const p = parent.get(step);
95
+ step = p?.prev;
96
+ }
97
+ return pathNodes;
98
+ }
99
+ queue.push(callee.id);
100
+ }
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ export interface OrphanResult {
107
+ symbol: SymbolNode;
108
+ reason: string;
109
+ }
110
+
111
+ export function findOrphans(graph: Graph): OrphanResult[] {
112
+ const calledNames = new Set(graph.calls.map((c) => c.calleeRaw));
113
+ const testTargets = new Set(graph.testEdges.map((t) => t.target));
114
+
115
+ const results: OrphanResult[] = [];
116
+
117
+ for (const sym of graph.symbols) {
118
+ if (sym.isExported) {
119
+ const incomingCallers = graph.calls.filter((c) => c.calleeRaw === sym.name);
120
+ if (incomingCallers.length === 0 && !testTargets.has(sym.name)) {
121
+ results.push({ symbol: sym, reason: "exported but no callers or tests" });
122
+ }
123
+ } else {
124
+ if (!calledNames.has(sym.name) && !testTargets.has(sym.name)) {
125
+ results.push({ symbol: sym, reason: "unexported, no callers or tests" });
126
+ }
127
+ }
128
+ }
129
+
130
+ return results;
131
+ }
132
+
133
+ export interface TraceMatch {
134
+ symbol: SymbolNode;
135
+ file: string;
136
+ line: number;
137
+ contextLine: string;
138
+ }
139
+
140
+ export interface TraceResult {
141
+ match: TraceMatch;
142
+ callers: ImpactNode[];
143
+ }
144
+
145
+ export function trace(
146
+ graph: Graph,
147
+ searchString: string,
148
+ maxDepth: number = 5,
149
+ ): TraceResult[] {
150
+ const lowerSearch = searchString.toLowerCase();
151
+ const results: TraceResult[] = [];
152
+
153
+ for (const sym of graph.symbols) {
154
+ if (sym.kind !== "function" && sym.kind !== "method") continue;
155
+
156
+ const filePath = path.join(graph.root, sym.file);
157
+ let content: string;
158
+ try {
159
+ content = fs.readFileSync(filePath, "utf-8");
160
+ } catch {
161
+ continue;
162
+ }
163
+
164
+ const lines = content.split("\n");
165
+ const start = Math.max(0, sym.line - 1);
166
+ const end = Math.min(lines.length, sym.endLine);
167
+
168
+ for (let i = start; i < end; i++) {
169
+ if (lines[i].toLowerCase().includes(lowerSearch)) {
170
+ const impactNodes = impact(graph, sym.name, maxDepth);
171
+ results.push({
172
+ match: {
173
+ symbol: sym,
174
+ file: sym.file,
175
+ line: i + 1,
176
+ contextLine: lines[i].trim(),
177
+ },
178
+ callers: impactNodes,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ return results;
185
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ESNext"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "outDir": "dist",
16
+ "rootDir": "src"
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ },
7
+ });