@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,92 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import { execSync } from "node:child_process";
6
+ import {
7
+ isGitRepo,
8
+ getCurrentBranch,
9
+ getDiffFiles,
10
+ getStaleFiles,
11
+ getCommitHistory,
12
+ } from "./index.js";
13
+
14
+ let tmpDir: string;
15
+
16
+ function run(cmd: string) {
17
+ execSync(cmd, { cwd: tmpDir, stdio: "pipe" });
18
+ }
19
+
20
+ function writeFile(relativePath: string, content: string) {
21
+ const fullPath = path.join(tmpDir, relativePath);
22
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
23
+ fs.writeFileSync(fullPath, content, "utf-8");
24
+ }
25
+
26
+ beforeAll(() => {
27
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-git-test-"));
28
+ run("git init");
29
+ run('git config user.email "test@test.com"');
30
+ run('git config user.name "Test"');
31
+ writeFile(".gitkeep", "");
32
+ run("git add . && git commit -m 'initial'");
33
+ });
34
+
35
+ afterAll(() => {
36
+ fs.rmSync(tmpDir, { recursive: true });
37
+ });
38
+
39
+ describe("isGitRepo", () => {
40
+ it("returns true inside a git repo", () => {
41
+ expect(isGitRepo(tmpDir)).toBe(true);
42
+ });
43
+
44
+ it("returns false outside a git repo", () => {
45
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-non-git-"));
46
+ expect(isGitRepo(nonRepo)).toBe(false);
47
+ fs.rmSync(nonRepo, { recursive: true });
48
+ });
49
+ });
50
+
51
+ describe("getCurrentBranch", () => {
52
+ it("returns current branch name", () => {
53
+ const branch = getCurrentBranch(tmpDir);
54
+ expect(branch).toBeTruthy();
55
+ });
56
+ });
57
+
58
+ describe("getDiffFiles", () => {
59
+ it("returns changed files vs base branch", () => {
60
+ writeFile("file1.ts", "content");
61
+ run("git add . && git commit -m 'initial'");
62
+ run("git checkout -b feature");
63
+ writeFile("file2.ts", "content");
64
+ run("git add . && git commit -m 'add file2'");
65
+ writeFile("file3.ts", "content");
66
+ run("git add . && git commit -m 'add file3'");
67
+ const results = getDiffFiles(tmpDir, "main");
68
+ expect(results.length).toBeGreaterThanOrEqual(2);
69
+ const paths = results.map((r) => r.path);
70
+ expect(paths).toContain("file2.ts");
71
+ expect(paths).toContain("file3.ts");
72
+ run("git checkout main");
73
+ });
74
+ });
75
+
76
+ describe("getCommitHistory", () => {
77
+ it("returns recent commit history", () => {
78
+ const history = getCommitHistory(tmpDir, 5);
79
+ expect(history.length).toBeGreaterThan(0);
80
+ expect(history[0]).toHaveProperty("hash");
81
+ expect(history[0]).toHaveProperty("message");
82
+ });
83
+ });
84
+
85
+ describe("getStaleFiles", () => {
86
+ it("does not return recently committed files", () => {
87
+ writeFile("fresh.ts", "fresh content");
88
+ run("git add . && git commit -m 'add fresh'");
89
+ const stale = getStaleFiles(tmpDir, 0);
90
+ expect(stale).not.toContain("fresh.ts");
91
+ });
92
+ });
@@ -0,0 +1,86 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export interface ChangedFile {
4
+ path: string;
5
+ status: "added" | "modified" | "deleted" | "renamed";
6
+ }
7
+
8
+ export interface CommitInfo {
9
+ hash: string;
10
+ date: string;
11
+ message: string;
12
+ author: string;
13
+ }
14
+
15
+ export function isGitRepo(root: string): boolean {
16
+ try {
17
+ execSync("git rev-parse --git-dir", { cwd: root, stdio: "ignore" });
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ export function getCurrentBranch(root: string): string {
25
+ return execSync("git rev-parse --abbrev-ref HEAD", {
26
+ cwd: root,
27
+ encoding: "utf-8",
28
+ }).trim();
29
+ }
30
+
31
+ export function getDiffFiles(root: string, base: string = "main"): ChangedFile[] {
32
+ const output = execSync(`git diff --name-status ${base}...HEAD`, {
33
+ cwd: root,
34
+ encoding: "utf-8",
35
+ }).trim();
36
+ if (!output) return [];
37
+ return output.split("\n").map((line) => {
38
+ const parts = line.split(/\s+/);
39
+ const status = parts[0];
40
+ let fileStatus: ChangedFile["status"];
41
+ let filePath = parts[1] ?? "";
42
+ if (status === "A") fileStatus = "added";
43
+ else if (status === "D") fileStatus = "deleted";
44
+ else if (status === "R" || status.startsWith("R")) fileStatus = "renamed";
45
+ else fileStatus = "modified";
46
+ if (fileStatus === "renamed" && parts[2]) filePath = parts[2];
47
+ return { path: filePath, status: fileStatus };
48
+ });
49
+ }
50
+
51
+ export function getStaleFiles(root: string, thresholdDays: number = 90): string[] {
52
+ const cutoff = new Date();
53
+ cutoff.setDate(cutoff.getDate() - thresholdDays);
54
+ const cutoffStr = cutoff.toISOString().split("T")[0];
55
+ try {
56
+ const output = execSync(
57
+ `git log --pretty=format: --name-only --diff-filter=AM --since=${cutoffStr}`,
58
+ { cwd: root, encoding: "utf-8" },
59
+ ).trim();
60
+ const recentFiles = new Set(output.split("\n").filter(Boolean));
61
+ const allFiles = execSync("git ls-files", {
62
+ cwd: root,
63
+ encoding: "utf-8",
64
+ })
65
+ .trim()
66
+ .split("\n")
67
+ .filter(Boolean);
68
+ return allFiles.filter((f) => !recentFiles.has(f));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ export function getCommitHistory(root: string, count: number = 10): CommitInfo[] {
75
+ const output = execSync(
76
+ `git log --max-count=${count} --format="%H|%ad|%s|%an" --date=short`,
77
+ { cwd: root, encoding: "utf-8" },
78
+ ).trim();
79
+ if (!output) return [];
80
+ return output.split("\n").map((line) => {
81
+ const [hash, date, ...msgParts] = line.split("|");
82
+ const message = msgParts.slice(0, -1).join("|");
83
+ const author = msgParts[msgParts.length - 1] ?? "";
84
+ return { hash, date, message, author };
85
+ });
86
+ }
@@ -0,0 +1,383 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ GRAPH_VERSION,
4
+ makeGraph,
5
+ makePackageNode,
6
+ makeFileNode,
7
+ makeSymbolNode,
8
+ makeCallEdge,
9
+ makeImportEdge,
10
+ makeDependency,
11
+ makeHTTPRoute,
12
+ makeEnvRead,
13
+ makeConcurrencyNode,
14
+ makeTestEdge,
15
+ makeImplementsEdge,
16
+ makeMutationEdge,
17
+ makeErrorEdge,
18
+ makeAppRouterNode,
19
+ makeStructField,
20
+ serialize,
21
+ deserialize,
22
+ } from "./types.js";
23
+
24
+ describe("GRAPH_VERSION", () => {
25
+ it("is 1", () => {
26
+ expect(GRAPH_VERSION).toBe("1");
27
+ });
28
+ });
29
+
30
+ describe("factory functions", () => {
31
+ describe("makeGraph", () => {
32
+ it("returns a graph with defaults", () => {
33
+ const g = makeGraph();
34
+ expect(g.version).toBe(GRAPH_VERSION);
35
+ expect(g.generatedAt).toBeTruthy();
36
+ expect(g.root).toBe("");
37
+ expect(g.packages).toEqual([]);
38
+ expect(g.files).toEqual([]);
39
+ expect(g.symbols).toEqual([]);
40
+ expect(g.imports).toEqual([]);
41
+ expect(g.calls).toEqual([]);
42
+ expect(g.envReads).toEqual([]);
43
+ expect(g.dependencies).toEqual([]);
44
+ expect(g.routes).toEqual([]);
45
+ expect(g.concurrency).toEqual([]);
46
+ expect(g.testEdges).toEqual([]);
47
+ expect(g.implements).toEqual([]);
48
+ expect(g.mutations).toEqual([]);
49
+ expect(g.errors).toEqual([]);
50
+ expect(g.appRouter).toBeUndefined();
51
+ });
52
+
53
+ it("merges overrides", () => {
54
+ const g = makeGraph({ root: "/project", version: "2" });
55
+ expect(g.root).toBe("/project");
56
+ expect(g.version).toBe("2");
57
+ });
58
+
59
+ it("generates an ISO timestamp", () => {
60
+ const g = makeGraph();
61
+ expect(() => new Date(g.generatedAt)).not.toThrow();
62
+ expect(new Date(g.generatedAt).toISOString()).toBe(g.generatedAt);
63
+ });
64
+ });
65
+
66
+ describe("makePackageNode", () => {
67
+ it("returns a package node with defaults", () => {
68
+ const p = makePackageNode();
69
+ expect(p.id).toBeTruthy();
70
+ expect(p.name).toBe("");
71
+ expect(p.importPathBestEffort).toBe("");
72
+ expect(p.dir).toBe("");
73
+ expect(p.files).toEqual([]);
74
+ });
75
+
76
+ it("merges overrides", () => {
77
+ const p = makePackageNode({ name: "utils", dir: "./utils" });
78
+ expect(p.name).toBe("utils");
79
+ expect(p.dir).toBe("./utils");
80
+ expect(p.files).toEqual([]);
81
+ });
82
+ });
83
+
84
+ describe("makeFileNode", () => {
85
+ it("returns a file node with defaults", () => {
86
+ const f = makeFileNode();
87
+ expect(f.id).toBeTruthy();
88
+ expect(f.path).toBe("");
89
+ expect(f.packageName).toBe("");
90
+ expect(f.lines).toBe(0);
91
+ expect(f.generated).toBe(false);
92
+ });
93
+
94
+ it("merges overrides", () => {
95
+ const f = makeFileNode({ path: "src/index.ts", generated: true, lines: 42 });
96
+ expect(f.path).toBe("src/index.ts");
97
+ expect(f.generated).toBe(true);
98
+ expect(f.lines).toBe(42);
99
+ });
100
+ });
101
+
102
+ describe("makeSymbolNode", () => {
103
+ it("returns a symbol node with defaults", () => {
104
+ const s = makeSymbolNode();
105
+ expect(s.id).toBeTruthy();
106
+ expect(s.kind).toBe("function");
107
+ expect(s.name).toBe("");
108
+ expect(s.packageName).toBe("");
109
+ expect(s.file).toBe("");
110
+ expect(s.line).toBe(0);
111
+ expect(s.endLine).toBe(0);
112
+ expect(s.isExported).toBe(false);
113
+ });
114
+
115
+ it("merges overrides", () => {
116
+ const s = makeSymbolNode({ name: "Foo", kind: "class", isExported: true, line: 10, endLine: 30 });
117
+ expect(s.name).toBe("Foo");
118
+ expect(s.kind).toBe("class");
119
+ expect(s.isExported).toBe(true);
120
+ expect(s.line).toBe(10);
121
+ expect(s.endLine).toBe(30);
122
+ });
123
+
124
+ it("preserves optional fields", () => {
125
+ const s = makeSymbolNode({
126
+ doc: "does foo",
127
+ signature: "Foo() => void",
128
+ structFields: [{ name: "bar", type: "string" }],
129
+ embeddedTypes: ["Mixin"],
130
+ arity: 2,
131
+ isClientComponent: true,
132
+ isServerComponent: false,
133
+ });
134
+ expect(s.doc).toBe("does foo");
135
+ expect(s.signature).toBe("Foo() => void");
136
+ expect(s.structFields).toHaveLength(1);
137
+ expect(s.structFields![0].name).toBe("bar");
138
+ expect(s.embeddedTypes).toEqual(["Mixin"]);
139
+ expect(s.arity).toBe(2);
140
+ expect(s.isClientComponent).toBe(true);
141
+ expect(s.isServerComponent).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe("makeCallEdge", () => {
146
+ it("returns defaults", () => {
147
+ const e = makeCallEdge();
148
+ expect(e.callerSymbolId).toBe("");
149
+ expect(e.callerName).toBe("");
150
+ expect(e.calleeRaw).toBe("");
151
+ expect(e.file).toBe("");
152
+ expect(e.line).toBe(0);
153
+ });
154
+ });
155
+
156
+ describe("makeImportEdge", () => {
157
+ it("returns defaults", () => {
158
+ const e = makeImportEdge();
159
+ expect(e.fromFile).toBe("");
160
+ expect(e.fromPackage).toBe("");
161
+ expect(e.importPath).toBe("");
162
+ expect(e.isDefault).toBe(false);
163
+ });
164
+
165
+ it("sets isDefault", () => {
166
+ const e = makeImportEdge({ isDefault: true });
167
+ expect(e.isDefault).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe("makeDependency", () => {
172
+ it("returns defaults", () => {
173
+ const d = makeDependency();
174
+ expect(d.module).toBe("");
175
+ expect(d.version).toBe("");
176
+ });
177
+ });
178
+
179
+ describe("makeHTTPRoute", () => {
180
+ it("returns defaults", () => {
181
+ const r = makeHTTPRoute();
182
+ expect(r.method).toBe("");
183
+ expect(r.path).toBe("");
184
+ expect(r.handler).toBe("");
185
+ expect(r.file).toBe("");
186
+ expect(r.line).toBe(0);
187
+ });
188
+ });
189
+
190
+ describe("makeEnvRead", () => {
191
+ it("returns defaults", () => {
192
+ const e = makeEnvRead();
193
+ expect(e.key).toBe("");
194
+ expect(e.accessor).toBe("");
195
+ expect(e.file).toBe("");
196
+ expect(e.line).toBe(0);
197
+ });
198
+ });
199
+
200
+ describe("makeConcurrencyNode", () => {
201
+ it("returns defaults", () => {
202
+ const c = makeConcurrencyNode();
203
+ expect(c.kind).toBe("async_function");
204
+ expect(c.functionName).toBe("");
205
+ expect(c.file).toBe("");
206
+ expect(c.line).toBe(0);
207
+ });
208
+ });
209
+
210
+ describe("makeTestEdge", () => {
211
+ it("returns defaults", () => {
212
+ const t = makeTestEdge();
213
+ expect(t.testFunc).toBe("");
214
+ expect(t.target).toBe("");
215
+ expect(t.file).toBe("");
216
+ expect(t.line).toBe(0);
217
+ });
218
+ });
219
+
220
+ describe("makeImplementsEdge", () => {
221
+ it("returns defaults", () => {
222
+ const e = makeImplementsEdge();
223
+ expect(e.interface).toBe("");
224
+ expect(e.concrete).toBe("");
225
+ });
226
+ });
227
+
228
+ describe("makeMutationEdge", () => {
229
+ it("returns defaults", () => {
230
+ const m = makeMutationEdge();
231
+ expect(m.field).toBe("");
232
+ expect(m.functionName).toBe("");
233
+ expect(m.file).toBe("");
234
+ expect(m.line).toBe(0);
235
+ });
236
+ });
237
+
238
+ describe("makeErrorEdge", () => {
239
+ it("returns defaults", () => {
240
+ const e = makeErrorEdge();
241
+ expect(e.message).toBe("");
242
+ expect(e.functionName).toBe("");
243
+ expect(e.file).toBe("");
244
+ expect(e.line).toBe(0);
245
+ });
246
+ });
247
+
248
+ describe("makeAppRouterNode", () => {
249
+ it("returns defaults", () => {
250
+ const n = makeAppRouterNode();
251
+ expect(n.path).toBe("");
252
+ expect(n.dir).toBe("");
253
+ expect(n.files).toEqual({});
254
+ expect(n.children).toEqual([]);
255
+ });
256
+
257
+ it("supports nested children", () => {
258
+ const child = makeAppRouterNode({ path: "/child" });
259
+ const parent = makeAppRouterNode({ path: "/", children: [child] });
260
+ expect(parent.children).toHaveLength(1);
261
+ expect(parent.children[0].path).toBe("/child");
262
+ });
263
+ });
264
+
265
+ describe("makeStructField", () => {
266
+ it("returns defaults", () => {
267
+ const f = makeStructField();
268
+ expect(f.name).toBe("");
269
+ expect(f.type).toBe("");
270
+ });
271
+
272
+ it("merges overrides", () => {
273
+ const f = makeStructField({ name: "id", type: "string", tag: "json:\"id\"" });
274
+ expect(f.name).toBe("id");
275
+ expect(f.type).toBe("string");
276
+ expect(f.tag).toBe("json:\"id\"");
277
+ });
278
+ });
279
+ });
280
+
281
+ describe("serialize / deserialize", () => {
282
+ describe("serialize", () => {
283
+ it("produces valid JSON with 2-space indentation", () => {
284
+ const g = makeGraph();
285
+ const json = serialize(g);
286
+ expect(json).toBeTruthy();
287
+ expect(json.startsWith("{")).toBe(true);
288
+ const parsed = JSON.parse(json);
289
+ expect(parsed.version).toBe(GRAPH_VERSION);
290
+ });
291
+
292
+ it("throws on version mismatch", () => {
293
+ const g = makeGraph({ version: "999" });
294
+ expect(() => serialize(g)).toThrow("version mismatch");
295
+ });
296
+ });
297
+
298
+ describe("deserialize", () => {
299
+ it("round-trips a graph with all fields", () => {
300
+ const original = makeGraph({
301
+ root: "/test",
302
+ packages: [makePackageNode({ name: "main", dir: "." })],
303
+ files: [makeFileNode({ path: "index.ts", packageName: "main" })],
304
+ symbols: [makeSymbolNode({ name: "run", kind: "function", file: "index.ts", line: 1, endLine: 5 })],
305
+ calls: [makeCallEdge({ callerSymbolId: "s1", callerName: "run", calleeRaw: "log", file: "index.ts", line: 2 })],
306
+ imports: [makeImportEdge({ fromFile: "index.ts", importPath: "fs" })],
307
+ dependencies: [makeDependency({ module: "react", version: "^18" })],
308
+ routes: [makeHTTPRoute({ method: "GET", path: "/api", handler: "getHandler", file: "route.ts", line: 3 })],
309
+ envReads: [makeEnvRead({ key: "PORT", accessor: "process.env.PORT", file: "config.ts", line: 5 })],
310
+ concurrency: [makeConcurrencyNode({ kind: "promise_all", functionName: "fetchAll", file: "fetch.ts", line: 10 })],
311
+ testEdges: [makeTestEdge({ testFunc: "TestRun", target: "run", file: "index_test.ts", line: 1 })],
312
+ implements: [makeImplementsEdge({ interface: "Runner", concrete: "FastRunner" })],
313
+ mutations: [makeMutationEdge({ field: "User.name", functionName: "rename", file: "user.ts", line: 8 })],
314
+ errors: [makeErrorEdge({ message: "not found", functionName: "findUser", file: "user.ts", line: 12 })],
315
+ appRouter: makeAppRouterNode({
316
+ path: "/",
317
+ dir: "app",
318
+ files: { page: "page.tsx", layout: "layout.tsx" },
319
+ children: [makeAppRouterNode({ path: "/about", dir: "about", files: { page: "page.tsx" } })],
320
+ }),
321
+ });
322
+
323
+ const json = serialize(original);
324
+ const restored = deserialize(json);
325
+
326
+ expect(restored.version).toBe(original.version);
327
+ expect(restored.root).toBe(original.root);
328
+ expect(restored.packages).toHaveLength(1);
329
+ expect(restored.files).toHaveLength(1);
330
+ expect(restored.symbols).toHaveLength(1);
331
+ expect(restored.calls).toHaveLength(1);
332
+ expect(restored.imports).toHaveLength(1);
333
+ expect(restored.dependencies).toHaveLength(1);
334
+ expect(restored.routes).toHaveLength(1);
335
+ expect(restored.envReads).toHaveLength(1);
336
+ expect(restored.concurrency).toHaveLength(1);
337
+ expect(restored.testEdges).toHaveLength(1);
338
+ expect(restored.implements).toHaveLength(1);
339
+ expect(restored.mutations).toHaveLength(1);
340
+ expect(restored.errors).toHaveLength(1);
341
+ expect(restored.appRouter).toBeTruthy();
342
+ expect(restored.appRouter!.children).toHaveLength(1);
343
+ });
344
+
345
+ it("round-trips an empty graph", () => {
346
+ const original = makeGraph({ root: "/empty" });
347
+ const json = serialize(original);
348
+ const restored = deserialize(json);
349
+ expect(restored.root).toBe("/empty");
350
+ expect(restored.packages).toEqual([]);
351
+ });
352
+
353
+ it("rejects malformed JSON", () => {
354
+ expect(() => deserialize("not json")).toThrow("Invalid JSON");
355
+ });
356
+
357
+ it("rejects null", () => {
358
+ expect(() => deserialize("null")).toThrow("Invalid graph structure");
359
+ });
360
+
361
+ it("rejects missing version", () => {
362
+ const json = JSON.stringify({ root: "/x", generatedAt: "", packages: [], files: [], symbols: [], imports: [], calls: [], envReads: [], dependencies: [], routes: [], concurrency: [], testEdges: [], implements: [], mutations: [], errors: [] });
363
+ expect(() => deserialize(json)).toThrow("Invalid graph structure");
364
+ });
365
+
366
+ it("rejects version mismatch", () => {
367
+ const g = makeGraph({ version: "2" });
368
+ expect(() => deserialize(serialize(g))).toThrow("version mismatch");
369
+ });
370
+
371
+ it("rejects missing array fields", () => {
372
+ const json = JSON.stringify({ version: GRAPH_VERSION, generatedAt: "", root: "" });
373
+ expect(() => deserialize(json)).toThrow("Invalid graph structure");
374
+ });
375
+
376
+ it("accepts extra unknown fields (forward compat)", () => {
377
+ const g = makeGraph({ root: "/future" });
378
+ const json = JSON.stringify({ ...JSON.parse(serialize(g)), futureField: "hello" });
379
+ const restored = deserialize(json);
380
+ expect(restored.root).toBe("/future");
381
+ });
382
+ });
383
+ });