devlensio 0.2.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 (136) hide show
  1. package/LICENSE +674 -0
  2. package/dist/clustering/index.d.ts +27 -0
  3. package/dist/clustering/index.js +149 -0
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +78 -0
  6. package/dist/config/providers/file.d.ts +19 -0
  7. package/dist/config/providers/file.js +215 -0
  8. package/dist/config/providers/request.d.ts +2 -0
  9. package/dist/config/providers/request.js +72 -0
  10. package/dist/config/types.d.ts +46 -0
  11. package/dist/config/types.js +81 -0
  12. package/dist/config/writer.d.ts +29 -0
  13. package/dist/config/writer.js +103 -0
  14. package/dist/filesystem/appRouter.d.ts +2 -0
  15. package/dist/filesystem/appRouter.js +126 -0
  16. package/dist/filesystem/backendRoutes.d.ts +2 -0
  17. package/dist/filesystem/backendRoutes.js +161 -0
  18. package/dist/filesystem/index.d.ts +2 -0
  19. package/dist/filesystem/index.js +28 -0
  20. package/dist/filesystem/index.test.d.ts +1 -0
  21. package/dist/filesystem/index.test.js +178 -0
  22. package/dist/filesystem/pagesRouter.d.ts +2 -0
  23. package/dist/filesystem/pagesRouter.js +109 -0
  24. package/dist/fingerprint/detectors.d.ts +8 -0
  25. package/dist/fingerprint/detectors.js +174 -0
  26. package/dist/fingerprint/index.d.ts +2 -0
  27. package/dist/fingerprint/index.js +41 -0
  28. package/dist/fingerprint/index.test.d.ts +1 -0
  29. package/dist/fingerprint/index.test.js +148 -0
  30. package/dist/graph/buildLookup.d.ts +10 -0
  31. package/dist/graph/buildLookup.js +32 -0
  32. package/dist/graph/edges/callEdges.d.ts +7 -0
  33. package/dist/graph/edges/callEdges.js +145 -0
  34. package/dist/graph/edges/eventEdges.d.ts +7 -0
  35. package/dist/graph/edges/eventEdges.js +203 -0
  36. package/dist/graph/edges/guardEdges.d.ts +3 -0
  37. package/dist/graph/edges/guardEdges.js +232 -0
  38. package/dist/graph/edges/hookEdges.d.ts +3 -0
  39. package/dist/graph/edges/hookEdges.js +54 -0
  40. package/dist/graph/edges/importEdges.d.ts +8 -0
  41. package/dist/graph/edges/importEdges.js +224 -0
  42. package/dist/graph/edges/propEdges.d.ts +3 -0
  43. package/dist/graph/edges/propEdges.js +142 -0
  44. package/dist/graph/edges/routeEdge.d.ts +3 -0
  45. package/dist/graph/edges/routeEdge.js +124 -0
  46. package/dist/graph/edges/stateEdges.d.ts +3 -0
  47. package/dist/graph/edges/stateEdges.js +206 -0
  48. package/dist/graph/edges/testEdges.d.ts +3 -0
  49. package/dist/graph/edges/testEdges.js +143 -0
  50. package/dist/graph/edges/utils.d.ts +2 -0
  51. package/dist/graph/edges/utils.js +25 -0
  52. package/dist/graph/index.d.ts +6 -0
  53. package/dist/graph/index.js +65 -0
  54. package/dist/graph/index.test.d.ts +1 -0
  55. package/dist/graph/index.test.js +542 -0
  56. package/dist/graph/thirdPartyLibs.d.ts +8 -0
  57. package/dist/graph/thirdPartyLibs.js +162 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +15 -0
  60. package/dist/jobs/index.d.ts +5 -0
  61. package/dist/jobs/index.js +11 -0
  62. package/dist/jobs/queue/interface.d.ts +13 -0
  63. package/dist/jobs/queue/interface.js +1 -0
  64. package/dist/jobs/queue/memory.d.ts +24 -0
  65. package/dist/jobs/queue/memory.js +291 -0
  66. package/dist/jobs/runner.d.ts +3 -0
  67. package/dist/jobs/runner.js +136 -0
  68. package/dist/jobs/types.d.ts +112 -0
  69. package/dist/jobs/types.js +33 -0
  70. package/dist/parser/directives.d.ts +4 -0
  71. package/dist/parser/directives.js +31 -0
  72. package/dist/parser/extractors/components.d.ts +5 -0
  73. package/dist/parser/extractors/components.js +240 -0
  74. package/dist/parser/extractors/functions.d.ts +4 -0
  75. package/dist/parser/extractors/functions.js +240 -0
  76. package/dist/parser/extractors/hooks.d.ts +4 -0
  77. package/dist/parser/extractors/hooks.js +128 -0
  78. package/dist/parser/extractors/stores.d.ts +3 -0
  79. package/dist/parser/extractors/stores.js +181 -0
  80. package/dist/parser/index.d.ts +14 -0
  81. package/dist/parser/index.js +168 -0
  82. package/dist/parser/index.test.d.ts +1 -0
  83. package/dist/parser/index.test.js +319 -0
  84. package/dist/parser/typeUtils.d.ts +9 -0
  85. package/dist/parser/typeUtils.js +46 -0
  86. package/dist/pipeline/index.d.ts +50 -0
  87. package/dist/pipeline/index.js +249 -0
  88. package/dist/scoring/connectionCounter.d.ts +28 -0
  89. package/dist/scoring/connectionCounter.js +134 -0
  90. package/dist/scoring/fileScorer.d.ts +2 -0
  91. package/dist/scoring/fileScorer.js +44 -0
  92. package/dist/scoring/index.d.ts +22 -0
  93. package/dist/scoring/index.js +130 -0
  94. package/dist/scoring/index.test.d.ts +1 -0
  95. package/dist/scoring/index.test.js +453 -0
  96. package/dist/scoring/nodeScorer.d.ts +3 -0
  97. package/dist/scoring/nodeScorer.js +108 -0
  98. package/dist/scoring/noiseFilter.d.ts +18 -0
  99. package/dist/scoring/noiseFilter.js +92 -0
  100. package/dist/storage/fileStorage.d.ts +117 -0
  101. package/dist/storage/fileStorage.js +616 -0
  102. package/dist/storage/index.d.ts +4 -0
  103. package/dist/storage/index.js +2 -0
  104. package/dist/storage/interface.d.ts +27 -0
  105. package/dist/storage/interface.js +1 -0
  106. package/dist/summarizer/checkpoint.d.ts +15 -0
  107. package/dist/summarizer/checkpoint.js +110 -0
  108. package/dist/summarizer/index.d.ts +2 -0
  109. package/dist/summarizer/index.js +281 -0
  110. package/dist/summarizer/mapreduce.d.ts +4 -0
  111. package/dist/summarizer/mapreduce.js +87 -0
  112. package/dist/summarizer/prompts.d.ts +22 -0
  113. package/dist/summarizer/prompts.js +205 -0
  114. package/dist/summarizer/providers/anthropic.d.ts +9 -0
  115. package/dist/summarizer/providers/anthropic.js +78 -0
  116. package/dist/summarizer/providers/gemini.d.ts +9 -0
  117. package/dist/summarizer/providers/gemini.js +79 -0
  118. package/dist/summarizer/providers/index.d.ts +3 -0
  119. package/dist/summarizer/providers/index.js +43 -0
  120. package/dist/summarizer/providers/ollama.d.ts +9 -0
  121. package/dist/summarizer/providers/ollama.js +23 -0
  122. package/dist/summarizer/providers/openRouter.d.ts +9 -0
  123. package/dist/summarizer/providers/openRouter.js +19 -0
  124. package/dist/summarizer/providers/openai.d.ts +9 -0
  125. package/dist/summarizer/providers/openai.js +72 -0
  126. package/dist/summarizer/providers/types.d.ts +32 -0
  127. package/dist/summarizer/providers/types.js +1 -0
  128. package/dist/summarizer/retry.d.ts +7 -0
  129. package/dist/summarizer/retry.js +51 -0
  130. package/dist/summarizer/topological.d.ts +3 -0
  131. package/dist/summarizer/topological.js +105 -0
  132. package/dist/summarizer/types.d.ts +57 -0
  133. package/dist/summarizer/types.js +17 -0
  134. package/dist/types.d.ts +78 -0
  135. package/dist/types.js +1 -0
  136. package/package.json +48 -0
@@ -0,0 +1,168 @@
1
+ import { Project } from "ts-morph";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { extractComponents } from "./extractors/components.js";
5
+ import { extractHooks } from "./extractors/hooks.js";
6
+ import { extractFunctions } from "./extractors/functions.js";
7
+ import { extractStores } from "./extractors/stores.js";
8
+ import { detectFileDirective } from "./directives.js";
9
+ import { createHash } from "crypto";
10
+ // Directories to skip entirely while walking
11
+ const IGNORE_DIRS = [
12
+ "node_modules",
13
+ "dist",
14
+ "build",
15
+ ".next",
16
+ "coverage",
17
+ "migrations",
18
+ ".git",
19
+ ];
20
+ // File patterns to skip
21
+ function shouldIgnoreFile(fileName) {
22
+ // In V1.0 I am now detecting test and story files as well because we have added an edge TESTS and 2 node types TEST and STORY
23
+ // if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(fileName)) return true;
24
+ // if (/\.stories\.(ts|tsx|js|jsx)$/.test(fileName)) return true;
25
+ if (/\.d\.ts$/.test(fileName))
26
+ return true;
27
+ if (/\.config\.(ts|js)$/.test(fileName))
28
+ return true;
29
+ return false;
30
+ }
31
+ function getFileNodeType(fileName) {
32
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(fileName))
33
+ return "TEST";
34
+ if (/\.stories\.(ts|tsx|js|jsx)$/.test(fileName))
35
+ return "STORY";
36
+ return "FILE";
37
+ }
38
+ // Recursively walks directory and adds valid source files to the project
39
+ function addFilesRecursively(dir, project) {
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(dir, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ // If we can't read a directory just skip it
46
+ return;
47
+ }
48
+ for (const entry of entries) {
49
+ const fullPath = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ // Skip ignored directories immediately
52
+ if (IGNORE_DIRS.includes(entry.name))
53
+ continue;
54
+ addFilesRecursively(fullPath, project);
55
+ }
56
+ else if (entry.isFile()) {
57
+ // Only process source files
58
+ if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
59
+ continue;
60
+ // Skip ignored file patterns
61
+ if (shouldIgnoreFile(entry.name))
62
+ continue;
63
+ project.addSourceFileAtPath(fullPath);
64
+ }
65
+ }
66
+ }
67
+ export function parseRepo(repoPath) {
68
+ // Set up ts-morph project
69
+ const project = new Project({
70
+ compilerOptions: {
71
+ allowJs: true, // support plain JS files
72
+ checkJs: false, // don't type check JS, just parse
73
+ jsx: 4, // support JSX (4 = React)
74
+ strict: false, // don't enforce strict mode on user's code
75
+ },
76
+ skipAddingFilesFromTsConfig: true,
77
+ });
78
+ // Walk directory and add files manually
79
+ // This approach works reliably on all platforms including Windows
80
+ addFilesRecursively(repoPath, project);
81
+ const sourceFiles = project.getSourceFiles();
82
+ const allNodes = [];
83
+ let skippedFiles = 0;
84
+ for (const file of sourceFiles) {
85
+ try {
86
+ const absFilePath = file.getFilePath();
87
+ const relativePath = path.relative(repoPath, absFilePath).replace(/\\/g, "/");
88
+ const fileType = getFileNodeType(path.basename(relativePath)); // returns either TEST / STORY / FILE type
89
+ // One FILE node per source file — represents the file itself in the graph
90
+ const fileNode = {
91
+ id: `file::${relativePath}`,
92
+ name: path.basename(relativePath),
93
+ type: fileType,
94
+ filePath: relativePath,
95
+ startLine: 1,
96
+ endLine: file.getEndLineNumber(),
97
+ parentFile: `file::${relativePath}`, //there is no parent file for file type node
98
+ metadata: {
99
+ nodeCount: 0,
100
+ childNodeIds: [],
101
+ language: absFilePath.endsWith('.ts') || absFilePath.endsWith('.tsx') ? 'typescript' :
102
+ absFilePath.endsWith('.js') || absFilePath.endsWith('.jsx') ? 'javascript' : 'unknown',
103
+ },
104
+ };
105
+ const fileDirective = detectFileDirective(file);
106
+ const components = extractComponents(file, fileDirective);
107
+ const hooks = extractHooks(file, fileDirective);
108
+ const functions = extractFunctions(file, fileDirective);
109
+ const stores = extractStores(file);
110
+ const extracted = [...components, ...hooks, ...functions, ...stores];
111
+ for (const node of extracted) {
112
+ // Normalize all extracted nodes to relative paths so every node in the
113
+ // graph uses the same coordinate system as the FILE nodes.
114
+ // Extractors store absolute ts-morph paths; we rewrite them here.
115
+ node.filePath = relativePath;
116
+ node.id = `${relativePath}::${node.name}`;
117
+ node.parentFile = `file::${relativePath}`;
118
+ if (node.rawCode) {
119
+ node.codeHash = createHash("sha256").update(node.rawCode).digest("hex").slice(0, 16);
120
+ }
121
+ }
122
+ if (fileType === "TEST" || fileType === "STORY") {
123
+ // Do not add child nodes for test/story files — they are test helpers,
124
+ fileNode.metadata.testCases = extracted.map(n => n.name);
125
+ fileNode.metadata.nodeCount = 0;
126
+ fileNode.metadata.childNodeIds = [];
127
+ // File hash based on all child code combined
128
+ const fileRawCode = extracted.map(n => n.rawCode ?? "").join("\n");
129
+ if (fileRawCode.trim()) {
130
+ fileNode.codeHash = createHash("sha256")
131
+ .update(fileRawCode).digest("hex").slice(0, 16);
132
+ }
133
+ allNodes.push(fileNode); // only the file node, no children
134
+ }
135
+ else {
136
+ fileNode.metadata.nodeCount = extracted.length;
137
+ fileNode.metadata.childNodeIds = extracted.map(n => n.id);
138
+ // File node hash — based on all child code combined
139
+ const fileRawCode = extracted.map(n => n.rawCode ?? "").join("\n");
140
+ if (fileRawCode.trim()) {
141
+ fileNode.codeHash = createHash("sha256").update(fileRawCode).digest("hex").slice(0, 16);
142
+ }
143
+ allNodes.push(fileNode, ...extracted);
144
+ }
145
+ }
146
+ catch (error) {
147
+ // Never let one bad file break the entire analysis
148
+ console.warn(`Skipped file due to error: ${file.getFilePath()}`);
149
+ skippedFiles++;
150
+ }
151
+ }
152
+ const componentCount = allNodes.filter((n) => n.type === "COMPONENT").length;
153
+ const hookCount = allNodes.filter((n) => n.type === "HOOK").length;
154
+ const functionCount = allNodes.filter((n) => n.type === "FUNCTION").length;
155
+ const storeCount = allNodes.filter((n) => n.type === "STATE_STORE").length;
156
+ return {
157
+ nodes: allNodes,
158
+ stats: {
159
+ totalFiles: sourceFiles.length,
160
+ totalNodes: allNodes.length,
161
+ componentCount,
162
+ hookCount,
163
+ functionCount,
164
+ storeCount,
165
+ skippedFiles,
166
+ },
167
+ };
168
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,319 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { parseRepo } from "./index.js";
5
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
6
+ function createFakeRepo(files) {
7
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-parser-test-"));
8
+ for (const [filePath, content] of Object.entries(files)) {
9
+ const fullPath = path.join(tmpDir, filePath);
10
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
11
+ fs.writeFileSync(fullPath, content);
12
+ }
13
+ return tmpDir;
14
+ }
15
+ function deleteFakeRepo(repoPath) {
16
+ fs.rmSync(repoPath, { recursive: true, force: true });
17
+ }
18
+ // ─── Tests ────────────────────────────────────────────────────────────────────
19
+ describe("parseRepo", () => {
20
+ // ─── Component Detection ───────────────────────────────────────────────────
21
+ it("should detect a function declaration component", () => {
22
+ const repoPath = createFakeRepo({
23
+ "src/Button.tsx": `
24
+ export function Button() {
25
+ return <button>Click me</button>;
26
+ }
27
+ `,
28
+ });
29
+ const result = parseRepo(repoPath);
30
+ const component = result.nodes.find((n) => n.name === "Button");
31
+ expect(component).toBeDefined();
32
+ expect(component?.type).toBe("COMPONENT");
33
+ deleteFakeRepo(repoPath);
34
+ });
35
+ it("should detect an arrow function component", () => {
36
+ const repoPath = createFakeRepo({
37
+ "src/Card.tsx": `
38
+ export const Card = () => {
39
+ return <div>Card</div>;
40
+ };
41
+ `,
42
+ });
43
+ const result = parseRepo(repoPath);
44
+ const component = result.nodes.find((n) => n.name === "Card");
45
+ expect(component).toBeDefined();
46
+ expect(component?.type).toBe("COMPONENT");
47
+ deleteFakeRepo(repoPath);
48
+ });
49
+ it("should detect hooks in component metadata", () => {
50
+ const repoPath = createFakeRepo({
51
+ "src/Counter.tsx": `
52
+ import { useState } from "react";
53
+ export function Counter() {
54
+ const [count, setCount] = useState(0);
55
+ return <div>{count}</div>;
56
+ }
57
+ `,
58
+ });
59
+ const result = parseRepo(repoPath);
60
+ const component = result.nodes.find((n) => n.name === "Counter");
61
+ expect(component?.metadata.hasState).toBe(true);
62
+ expect(component?.metadata.hooks).toContain("useState");
63
+ deleteFakeRepo(repoPath);
64
+ });
65
+ it("should not detect lowercase functions as components", () => {
66
+ const repoPath = createFakeRepo({
67
+ "src/utils.tsx": `
68
+ export function formatDate() {
69
+ return <div>not a component</div>;
70
+ }
71
+ `,
72
+ });
73
+ const result = parseRepo(repoPath);
74
+ const node = result.nodes.find((n) => n.name === "formatDate");
75
+ expect(node?.type).not.toBe("COMPONENT");
76
+ deleteFakeRepo(repoPath);
77
+ });
78
+ it("should detect React.memo wrapped component", () => {
79
+ const repoPath = createFakeRepo({
80
+ "src/MemoCard.tsx": `
81
+ export const MemoCard = React.memo(() => {
82
+ return <div>Memo</div>;
83
+ });
84
+ `,
85
+ });
86
+ const result = parseRepo(repoPath);
87
+ const component = result.nodes.find((n) => n.name === "MemoCard");
88
+ expect(component).toBeDefined();
89
+ expect(component?.metadata.isMemoized).toBe(true);
90
+ deleteFakeRepo(repoPath);
91
+ });
92
+ // ─── Hook Detection ────────────────────────────────────────────────────────
93
+ it("should detect a custom hook", () => {
94
+ const repoPath = createFakeRepo({
95
+ "src/useAuth.ts": `
96
+ export function useAuth() {
97
+ const [user, setUser] = useState(null);
98
+ return { user };
99
+ }
100
+ `,
101
+ });
102
+ const result = parseRepo(repoPath);
103
+ const hook = result.nodes.find((n) => n.name === "useAuth");
104
+ expect(hook).toBeDefined();
105
+ expect(hook?.type).toBe("HOOK");
106
+ deleteFakeRepo(repoPath);
107
+ });
108
+ it("should not create a HOOK node for built-in hooks", () => {
109
+ const repoPath = createFakeRepo({
110
+ "src/Component.tsx": `
111
+ export function MyComponent() {
112
+ const [x] = useState(0);
113
+ return <div>{x}</div>;
114
+ }
115
+ `,
116
+ });
117
+ const result = parseRepo(repoPath);
118
+ const hook = result.nodes.find((n) => n.name === "useState" && n.type === "HOOK");
119
+ expect(hook).toBeUndefined();
120
+ deleteFakeRepo(repoPath);
121
+ });
122
+ it("should detect async custom hook", () => {
123
+ const repoPath = createFakeRepo({
124
+ "src/useData.ts": `
125
+ export async function useData() {
126
+ const data = await fetch('/api/data');
127
+ return data.json();
128
+ }
129
+ `,
130
+ });
131
+ const result = parseRepo(repoPath);
132
+ const hook = result.nodes.find((n) => n.name === "useData");
133
+ expect(hook?.metadata.isAsync).toBe(true);
134
+ deleteFakeRepo(repoPath);
135
+ });
136
+ // ─── Function Detection ────────────────────────────────────────────────────
137
+ it("should detect a regular function", () => {
138
+ const repoPath = createFakeRepo({
139
+ "src/utils.ts": `
140
+ export function calculateTotal(items: any[]) {
141
+ return items.reduce((sum, item) => sum + item.price, 0);
142
+ }
143
+ `,
144
+ });
145
+ const result = parseRepo(repoPath);
146
+ const fn = result.nodes.find((n) => n.name === "calculateTotal");
147
+ expect(fn).toBeDefined();
148
+ expect(fn?.type).toBe("FUNCTION");
149
+ deleteFakeRepo(repoPath);
150
+ });
151
+ it("should detect async functions", () => {
152
+ const repoPath = createFakeRepo({
153
+ "src/api.ts": `
154
+ export async function fetchUser(id: string) {
155
+ const res = await fetch('/api/users/' + id);
156
+ return res.json();
157
+ }
158
+ `,
159
+ });
160
+ const result = parseRepo(repoPath);
161
+ const fn = result.nodes.find((n) => n.name === "fetchUser");
162
+ expect(fn?.metadata.isAsync).toBe(true);
163
+ deleteFakeRepo(repoPath);
164
+ });
165
+ it("should detect fetch api calls inside functions", () => {
166
+ const repoPath = createFakeRepo({
167
+ "src/api.ts": `
168
+ export async function getProducts() {
169
+ const res = await fetch('/api/products');
170
+ return res.json();
171
+ }
172
+ `,
173
+ });
174
+ const result = parseRepo(repoPath);
175
+ const fn = result.nodes.find((n) => n.name === "getProducts");
176
+ expect((fn?.metadata.apiCalls).length).toBeGreaterThan(0);
177
+ deleteFakeRepo(repoPath);
178
+ });
179
+ it("should detect error handling in functions", () => {
180
+ const repoPath = createFakeRepo({
181
+ "src/api.ts": `
182
+ export async function safeRequest() {
183
+ try {
184
+ const res = await fetch('/api/data');
185
+ return res.json();
186
+ } catch (error) {
187
+ console.error(error);
188
+ }
189
+ }
190
+ `,
191
+ });
192
+ const result = parseRepo(repoPath);
193
+ const fn = result.nodes.find((n) => n.name === "safeRequest");
194
+ expect(fn?.metadata.hasErrorHandling).toBe(true);
195
+ deleteFakeRepo(repoPath);
196
+ });
197
+ // ─── Store Detection ───────────────────────────────────────────────────────
198
+ it("should detect a zustand store", () => {
199
+ const repoPath = createFakeRepo({
200
+ "src/store.ts": `
201
+ const useCartStore = create((set) => ({
202
+ items: [],
203
+ addItem: (item) => set((state) => ({ items: [...state.items, item] })),
204
+ }));
205
+ `,
206
+ });
207
+ const result = parseRepo(repoPath);
208
+ const store = result.nodes.find((n) => n.name === "useCartStore");
209
+ expect(store).toBeDefined();
210
+ expect(store?.type).toBe("STATE_STORE");
211
+ expect(store?.metadata.storeType).toBe("zustand");
212
+ deleteFakeRepo(repoPath);
213
+ });
214
+ it("should detect zustand state shape and actions", () => {
215
+ const repoPath = createFakeRepo({
216
+ "src/store.ts": `
217
+ const useCartStore = create((set) => ({
218
+ items: [],
219
+ total: 0,
220
+ addItem: (item) => set((state) => ({ items: [...state.items, item] })),
221
+ clearCart: () => set({ items: [], total: 0 }),
222
+ }));
223
+ `,
224
+ });
225
+ const result = parseRepo(repoPath);
226
+ const store = result.nodes.find((n) => n.name === "useCartStore");
227
+ expect(store?.metadata.stateShape).toContain("items");
228
+ expect(store?.metadata.stateShape).toContain("total");
229
+ expect(store?.metadata.actions).toContain("addItem");
230
+ expect(store?.metadata.actions).toContain("clearCart");
231
+ deleteFakeRepo(repoPath);
232
+ });
233
+ it("should detect a redux slice", () => {
234
+ const repoPath = createFakeRepo({
235
+ "src/cartSlice.ts": `
236
+ const cartSlice = createSlice({
237
+ name: 'cart',
238
+ initialState: { items: [], total: 0 },
239
+ reducers: {
240
+ addItem: (state, action) => { state.items.push(action.payload); },
241
+ clearCart: (state) => { state.items = []; },
242
+ }
243
+ });
244
+ `,
245
+ });
246
+ const result = parseRepo(repoPath);
247
+ const store = result.nodes.find((n) => n.name === "cartSlice");
248
+ expect(store).toBeDefined();
249
+ expect(store?.type).toBe("STATE_STORE");
250
+ expect(store?.metadata.storeType).toBe("redux");
251
+ expect(store?.metadata.actions).toContain("addItem");
252
+ expect(store?.metadata.actions).toContain("clearCart");
253
+ deleteFakeRepo(repoPath);
254
+ });
255
+ it("should detect a react context store", () => {
256
+ const repoPath = createFakeRepo({
257
+ "src/AuthContext.ts": `
258
+ const AuthContext = createContext(null);
259
+ `,
260
+ });
261
+ const result = parseRepo(repoPath);
262
+ const store = result.nodes.find((n) => n.name === "AuthContext");
263
+ expect(store).toBeDefined();
264
+ expect(store?.type).toBe("STATE_STORE");
265
+ expect(store?.metadata.storeType).toBe("context");
266
+ deleteFakeRepo(repoPath);
267
+ });
268
+ // ─── Stats ─────────────────────────────────────────────────────────────────
269
+ it("should return correct file count in stats", () => {
270
+ const repoPath = createFakeRepo({
271
+ "src/Button.tsx": `export function Button() { return <button />; }`,
272
+ "src/useAuth.ts": `export function useAuth() { return {}; }`,
273
+ "src/utils.ts": `export function formatDate() { return ''; }`,
274
+ });
275
+ const result = parseRepo(repoPath);
276
+ expect(result.stats.totalFiles).toBe(3);
277
+ deleteFakeRepo(repoPath);
278
+ });
279
+ it("should count components correctly in stats", () => {
280
+ const repoPath = createFakeRepo({
281
+ "src/Button.tsx": `export function Button() { return <button />; }`,
282
+ "src/Card.tsx": `export function Card() { return <div />; }`,
283
+ });
284
+ const result = parseRepo(repoPath);
285
+ expect(result.stats.componentCount).toBe(2);
286
+ deleteFakeRepo(repoPath);
287
+ });
288
+ // ─── Noise Filtering ───────────────────────────────────────────────────────
289
+ it("should ignore node_modules", () => {
290
+ const repoPath = createFakeRepo({
291
+ "src/Button.tsx": `export function Button() { return <button />; }`,
292
+ "node_modules/react/index.js": `export function useState() {}`,
293
+ });
294
+ const result = parseRepo(repoPath);
295
+ const nodeModuleNode = result.nodes.find((n) => n.filePath.includes("node_modules"));
296
+ expect(nodeModuleNode).toBeUndefined();
297
+ deleteFakeRepo(repoPath);
298
+ });
299
+ it("should ignore test files", () => {
300
+ const repoPath = createFakeRepo({
301
+ "src/Button.tsx": `export function Button() { return <button />; }`,
302
+ "src/Button.test.tsx": `export function FakeButton() { return <button />; }`,
303
+ });
304
+ const result = parseRepo(repoPath);
305
+ const testNode = result.nodes.find((n) => n.filePath.includes(".test."));
306
+ expect(testNode).toBeUndefined();
307
+ deleteFakeRepo(repoPath);
308
+ });
309
+ it("should ignore config files", () => {
310
+ const repoPath = createFakeRepo({
311
+ "src/Button.tsx": `export function Button() { return <button />; }`,
312
+ "next.config.ts": `export default {};`,
313
+ });
314
+ const result = parseRepo(repoPath);
315
+ const configNode = result.nodes.find((n) => n.filePath.includes(".config."));
316
+ expect(configNode).toBeUndefined();
317
+ deleteFakeRepo(repoPath);
318
+ });
319
+ });
@@ -0,0 +1,9 @@
1
+ import { SourceFile } from "ts-morph";
2
+ export interface ParamInfo {
3
+ name: string;
4
+ type?: string;
5
+ }
6
+ export declare function extractParams(node: any): ParamInfo[];
7
+ export declare function extractReturnTypeAnnotation(node: any): string | undefined;
8
+ export declare function extractBareTypeNames(typeStrings: (string | undefined)[]): string[];
9
+ export declare function extractReferencedInterfaces(sourceFile: SourceFile, typeNames: string[]): Record<string, Record<string, string>>;
@@ -0,0 +1,46 @@
1
+ export function extractParams(node) {
2
+ const params = node.getParameters ? node.getParameters() : [];
3
+ return params.map((p) => ({
4
+ name: p.getName(),
5
+ type: p.getTypeNode()?.getText() ?? undefined,
6
+ }));
7
+ }
8
+ export function extractReturnTypeAnnotation(node) {
9
+ return node.getReturnTypeNode?.()?.getText() ?? undefined;
10
+ }
11
+ // Extract bare PascalCase type names from a set of type annotation strings.
12
+ // e.g. ["FetchOptions", "Promise<User>"] → ["FetchOptions", "User"]
13
+ export function extractBareTypeNames(typeStrings) {
14
+ const names = [];
15
+ for (const t of typeStrings) {
16
+ if (!t)
17
+ continue;
18
+ for (const m of t.matchAll(/\b([A-Z][A-Za-z0-9_]+)\b/g)) {
19
+ names.push(m[1]);
20
+ }
21
+ }
22
+ return [...new Set(names)];
23
+ }
24
+ // Looks up interface and type alias declarations in the CURRENT file only (O(1) depth).
25
+ // Returns a map of typeName → { propName: propType }.
26
+ export function extractReferencedInterfaces(sourceFile, typeNames) {
27
+ const result = {};
28
+ for (const typeName of typeNames) {
29
+ // Try interface declaration first
30
+ const iface = sourceFile.getInterface(typeName);
31
+ if (iface) {
32
+ const props = {};
33
+ for (const prop of iface.getProperties()) {
34
+ props[prop.getName()] = prop.getTypeNode()?.getText() ?? "unknown";
35
+ }
36
+ result[typeName] = props;
37
+ continue;
38
+ }
39
+ // Try type alias
40
+ const alias = sourceFile.getTypeAlias(typeName);
41
+ if (alias) {
42
+ result[typeName] = { _type: alias.getTypeNode()?.getText() ?? "unknown" };
43
+ }
44
+ }
45
+ return result;
46
+ }
@@ -0,0 +1,50 @@
1
+ import type { FilterThresholds } from "../scoring/noiseFilter.js";
2
+ import type { CodeNode, CodeEdge, ProjectFingerprint, RouteNode, BackendRouteNode } from "../types.js";
3
+ export type { FilterThresholds };
4
+ export interface GitInfo {
5
+ commitHash: string;
6
+ branch: string;
7
+ message: string;
8
+ hasGit: boolean;
9
+ }
10
+ export interface PipelineOptions {
11
+ thresholds?: FilterThresholds;
12
+ onStep?: (step: "fingerprint" | "filesystem" | "parse" | "edges" | "scoring") => void;
13
+ includedThirdPartyLibs?: string[];
14
+ }
15
+ export interface PipelineStats {
16
+ totalNodesBeforeFilter: number;
17
+ totalEdgesBeforeFilter: number;
18
+ totalNodesAfterFilter: number;
19
+ totalEdgesAfterFilter: number;
20
+ removedNodeCount: number;
21
+ removedEdgeCount: number;
22
+ averageNodeScore: number;
23
+ topScoringNodes: {
24
+ name: string;
25
+ score: number;
26
+ type: string;
27
+ }[];
28
+ topScoringFiles: {
29
+ name: string;
30
+ score: number;
31
+ filePath: string;
32
+ }[];
33
+ }
34
+ export interface PipelineResult {
35
+ graphId: string;
36
+ repoPath: string;
37
+ analyzedAt: string;
38
+ fingerprint: ProjectFingerprint;
39
+ routes: RouteNode[] | BackendRouteNode[];
40
+ nodes: CodeNode[];
41
+ edges: CodeEdge[];
42
+ allNodes: CodeNode[];
43
+ allEdges: CodeEdge[];
44
+ nodeScores: Record<string, number>;
45
+ stats: PipelineStats;
46
+ isGithubRepo: boolean;
47
+ gitInfo: GitInfo;
48
+ }
49
+ export declare function analyzePipeline(repoPath: string, isGithubRepo: boolean, options?: PipelineOptions): Promise<PipelineResult>;
50
+ export declare function refilterPipeline(stored: PipelineResult, thresholds: FilterThresholds): Pick<PipelineResult, "nodes" | "edges" | "stats">;