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,249 @@
1
+ import path from "path";
2
+ import { createHash } from "crypto";
3
+ import { execSync } from "child_process";
4
+ import { analyzeFingerprint } from "../fingerprint/index.js";
5
+ import { analyzeFilesystem } from "../filesystem/index.js";
6
+ import { parseRepo } from "../parser/index.js";
7
+ import { detectEdges } from "../graph/index.js";
8
+ import { buildThirdPartyNodes } from "../graph/thirdPartyLibs.js";
9
+ import { scoreAndFilter } from "../scoring/index.js";
10
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
11
+ // Deterministic graphId — same repo always produces same id
12
+ // This ensures multiple analyses of the same repo go into the same folder
13
+ function generateGraphId(repoPath, isGithubRepo) {
14
+ const normalized = isGithubRepo
15
+ ? repoPath.toLowerCase().trim() // normalize GitHub URL
16
+ : path.resolve(repoPath).toLowerCase(); // normalize local path
17
+ return createHash("sha256")
18
+ .update(normalized)
19
+ .digest("hex")
20
+ .slice(0, 16);
21
+ }
22
+ // Gets current git state of the repo
23
+ // Falls back gracefully if git is not initialized
24
+ function getGitInfo(repoPath) {
25
+ try {
26
+ const commitHash = execSync("git rev-parse HEAD", { cwd: repoPath })
27
+ .toString().trim();
28
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: repoPath })
29
+ .toString().trim();
30
+ const message = execSync("git log -1 --pretty=%s", { cwd: repoPath })
31
+ .toString().trim();
32
+ return { commitHash, branch, message, hasGit: true };
33
+ }
34
+ catch {
35
+ // No git, or no commits yet — use timestamp as version key
36
+ return {
37
+ commitHash: Date.now().toString(),
38
+ branch: "unknown",
39
+ message: "no git history",
40
+ hasGit: false,
41
+ };
42
+ }
43
+ }
44
+ function buildStats(scoringResult, allNodes) {
45
+ const topScoringFiles = allNodes
46
+ .filter((n) => n.type === "FILE")
47
+ .map((n) => ({
48
+ name: n.name,
49
+ score: scoringResult.nodeScores.get(n.id) ?? 0,
50
+ filePath: n.filePath,
51
+ }))
52
+ .sort((a, b) => b.score - a.score)
53
+ .slice(0, 10);
54
+ return {
55
+ ...scoringResult.stats,
56
+ topScoringFiles,
57
+ };
58
+ }
59
+ function mapToRecord(map) {
60
+ const record = {};
61
+ for (const [k, v] of map)
62
+ record[k] = v;
63
+ return record;
64
+ }
65
+ // routesToCodeNodes
66
+ //
67
+ // Converts RouteNode[] / BackendRouteNode[] into ROUTE CodeNodes so they
68
+ // participate in the graph as first-class nodes.
69
+ //
70
+ // Route-specific fields go into metadata — CodeNode schema stays clean.
71
+ // IDs follow the same filePath::name convention as all other nodes so
72
+ // the lookup maps in buildLookupMaps() work without any changes.
73
+ //
74
+ // Naming convention:
75
+ // Next.js page/layout/API: "GET /api/users" → id = "app/api/users/route.ts::GET /api/users"
76
+ // Backend (Express etc.): "POST /users" → id = "src/routes/users.ts::POST /users"
77
+ function routesToCodeNodes(routes, repoPath) {
78
+ const nodes = [];
79
+ for (const route of routes) {
80
+ // Make filePath relative — same as parser does for all other nodes
81
+ const relativeFilePath = path.relative(repoPath, route.filePath).replace(/\\/g, "/");
82
+ if (route.type === "BACKEND_ROUTE") {
83
+ // Each backend route = one node per HTTP method + path combination
84
+ const name = `${route.httpMethod} ${route.urlPath}`;
85
+ const id = `${relativeFilePath}::${name}`;
86
+ // create synthetic function type nodes for the inline handlers
87
+ let inlineHandlerId;
88
+ if (route.inlineHandler) {
89
+ inlineHandlerId = `${relativeFilePath}::${name}::handler`;
90
+ nodes.push({
91
+ id: inlineHandlerId,
92
+ name: `${name} handler`,
93
+ type: "FUNCTION",
94
+ filePath: relativeFilePath,
95
+ startLine: route.inlineHandler.startLine,
96
+ endLine: route.inlineHandler.endLine,
97
+ rawCode: route.inlineHandler.rawCode,
98
+ codeHash: createHash("sha256").update(route.inlineHandler.rawCode).digest("hex").slice(0, 16),
99
+ parentFile: `file::${relativeFilePath}`,
100
+ metadata: {
101
+ isHttpHandler: true,
102
+ httpMethod: route.httpMethod,
103
+ isInlineHandler: true,
104
+ },
105
+ });
106
+ }
107
+ nodes.push({
108
+ id,
109
+ name,
110
+ type: "ROUTE",
111
+ filePath: relativeFilePath,
112
+ startLine: 1,
113
+ endLine: 1,
114
+ parentFile: `file::${relativeFilePath}`,
115
+ metadata: {
116
+ urlPath: route.urlPath,
117
+ httpMethod: route.httpMethod,
118
+ isDynamic: route.isDynamic,
119
+ params: route.params ?? [],
120
+ framework: route.framework,
121
+ handlerName: route.handlerName, // used by routeEdges to resolve handler
122
+ routeKind: "backend",
123
+ },
124
+ });
125
+ }
126
+ else {
127
+ // Next.js RouteNode — one node per HTTP method for API routes,
128
+ // one node for page/layout/etc.
129
+ const httpMethods = route.httpMethods && route.httpMethods.length > 0
130
+ ? route.httpMethods
131
+ : route.type === "API_ROUTE"
132
+ ? ["GET", "PUT", "POST", "DELETE", "PATCH"] // fallback — we'll refine via routeEdges handler lookup
133
+ : [null]; // non-API routes (PAGE, LAYOUT etc.) have no method
134
+ for (const method of httpMethods) {
135
+ const name = method
136
+ ? `${method} ${route.urlPath}`
137
+ : route.urlPath;
138
+ const id = `${relativeFilePath}::${name}`;
139
+ nodes.push({
140
+ id,
141
+ name,
142
+ type: "ROUTE",
143
+ filePath: relativeFilePath,
144
+ startLine: 1,
145
+ endLine: 1,
146
+ parentFile: `file::${relativeFilePath}`,
147
+ metadata: {
148
+ urlPath: route.urlPath,
149
+ httpMethod: method ?? null,
150
+ isDynamic: route.isDynamic,
151
+ isCatchAll: route.isCatchAll,
152
+ isGroupRoute: route.isGroupRoute,
153
+ params: route.params ?? [],
154
+ routeNodeType: route.type, // PAGE | LAYOUT | API_ROUTE | etc.
155
+ layoutPath: route.layoutPath,
156
+ framework: "nextjs",
157
+ routeKind: "nextjs",
158
+ },
159
+ });
160
+ }
161
+ }
162
+ }
163
+ return nodes;
164
+ }
165
+ // ─── analyzePipeline ──────────────────────────────────────────────────────────
166
+ export async function analyzePipeline(repoPath, isGithubRepo, options) {
167
+ const absoluteRepoPath = path.resolve(repoPath);
168
+ const graphId = generateGraphId(repoPath, isGithubRepo); // stable, deterministic ID based on repo path
169
+ const gitInfo = getGitInfo(absoluteRepoPath);
170
+ const analyzedAt = new Date().toISOString();
171
+ console.log(`\nšŸ” devlens — analyzing ${absoluteRepoPath}`);
172
+ console.log(` Graph ID: ${graphId}`);
173
+ console.log(` Commit: ${gitInfo.commitHash} (${gitInfo.branch})`);
174
+ console.log(` Message: ${gitInfo.message}`);
175
+ // ── Step 1: Fingerprint ───────────────────────────────────────
176
+ console.log("\n[1/5] Fingerprinting project...");
177
+ const fingerprint = analyzeFingerprint(absoluteRepoPath);
178
+ console.log(` Framework: ${fingerprint.framework} | Language: ${fingerprint.language} | Type: ${fingerprint.projectType}`);
179
+ // ── Step 2: Filesystem / routes ───────────────────────────────
180
+ console.log("\n[2/5] Analyzing filesystem routes...");
181
+ const routes = analyzeFilesystem(absoluteRepoPath, fingerprint);
182
+ console.log(` Routes found: ${routes.length}`);
183
+ // Convert routes -> CodeNodes so they join the graph as nodes as well
184
+ // It is important to add here before the detection of the edges
185
+ let routeNodes = routesToCodeNodes(routes, absoluteRepoPath);
186
+ console.log(` Route nodes created: ${routeNodes.length}`);
187
+ // ── Step 3: Parse source files into nodes ─────────────────────
188
+ console.log("\n[3/5] Parsing source files...");
189
+ const parserResult = parseRepo(absoluteRepoPath);
190
+ console.log(` Files: ${parserResult.stats.totalFiles} | Nodes: ${parserResult.stats.totalNodes} | Skipped: ${parserResult.stats.skippedFiles}`);
191
+ // ── Step 3.5: Build third-party nodes ────────────────────────
192
+ const thirdPartyNodes = options?.includedThirdPartyLibs?.length
193
+ ? buildThirdPartyNodes(absoluteRepoPath, options.includedThirdPartyLibs)
194
+ : [];
195
+ if (thirdPartyNodes.length) {
196
+ console.log(` Third-party nodes: ${thirdPartyNodes.length}`);
197
+ }
198
+ // ── Step 4: Detect edges ──────────────────────────────────────
199
+ console.log("\n[4/5] Detecting edges...");
200
+ const edgeResult = detectEdges([...parserResult.nodes, ...routeNodes, ...thirdPartyNodes], routes, absoluteRepoPath, fingerprint);
201
+ // filter API_ROUTE nodes without handlers - because at the time of converting routes to code nodes, POST and GET both possibilties are taken for the API_ROUTE nodes, however it is possible that only one of them is being used for that route. Meaning only one handler and for the second method undefined handler.
202
+ routeNodes = routeNodes.filter(routeNode => {
203
+ if (routeNode.metadata.routeNodeType === "API_ROUTE") {
204
+ const hasHandler = edgeResult.edges.some(edge => edge.type === "HANDLES" && edge.from === routeNode.id);
205
+ return hasHandler;
206
+ }
207
+ return true;
208
+ });
209
+ const allNodes = [...parserResult.nodes, ...routeNodes, ...thirdPartyNodes, ...edgeResult.ghostNodes];
210
+ const allEdges = edgeResult.edges;
211
+ // ── Step 5: Score and filter ──────────────────────────────────
212
+ console.log("\n[5/5] Scoring and filtering...");
213
+ const scoringResult = scoreAndFilter(allNodes, allEdges, options?.thresholds);
214
+ const nodeScores = mapToRecord(scoringResult.nodeScores);
215
+ const stats = buildStats(scoringResult, allNodes);
216
+ // Embed score directly onto every node — allNodes and filteredNodes both.
217
+ // nodeScores map stays for diffCommits and refilterPipeline which need it,
218
+ // but consumers (frontend, Neo4j, summarizer) get score on the node itself.
219
+ for (const node of allNodes) {
220
+ node.score = nodeScores[node.id] ?? 0;
221
+ }
222
+ console.log(`\nāœ… Analysis complete — graph ${graphId} @ commit ${gitInfo.commitHash}`);
223
+ return {
224
+ graphId,
225
+ repoPath: absoluteRepoPath,
226
+ analyzedAt,
227
+ fingerprint,
228
+ routes,
229
+ nodes: scoringResult.filteredNodes,
230
+ edges: scoringResult.filteredEdges,
231
+ allNodes,
232
+ allEdges,
233
+ nodeScores,
234
+ stats,
235
+ isGithubRepo,
236
+ gitInfo,
237
+ };
238
+ }
239
+ // ─── refilterPipeline ─────────────────────────────────────────────────────────
240
+ export function refilterPipeline(stored, thresholds) {
241
+ const existingScores = new Map(Object.entries(stored.nodeScores));
242
+ const scoringResult = scoreAndFilter(stored.allNodes, stored.allEdges, thresholds, existingScores);
243
+ const stats = buildStats(scoringResult, stored.allNodes);
244
+ return {
245
+ nodes: scoringResult.filteredNodes,
246
+ edges: scoringResult.filteredEdges,
247
+ stats,
248
+ };
249
+ }
@@ -0,0 +1,28 @@
1
+ import type { CodeNode, CodeEdge } from "../types.js";
2
+ export interface ConnectionProfile {
3
+ incomingCalls: number;
4
+ outgoingCalls: number;
5
+ incomingReads: number;
6
+ incomingWrites: number;
7
+ incomingProps: number;
8
+ outgoingProps: number;
9
+ importedBy: number;
10
+ }
11
+ export interface ConnectionMaxima {
12
+ maxIncomingCalls: number;
13
+ maxOutgoingCalls: number;
14
+ maxIncomingReads: number;
15
+ maxIncomingWrites: number;
16
+ maxIncomingProps: number;
17
+ maxOutgoingProps: number;
18
+ maxImportedBy: number;
19
+ p75IncomingCalls: number;
20
+ p75OutgoingCalls: number;
21
+ p75IncomingReads: number;
22
+ p75IncomingProps: number;
23
+ }
24
+ export interface ConnectionCountResult {
25
+ profiles: Map<string, ConnectionProfile>;
26
+ maxima: ConnectionMaxima;
27
+ }
28
+ export declare function countConnections(nodes: CodeNode[], edges: CodeEdge[]): ConnectionCountResult;
@@ -0,0 +1,134 @@
1
+ function emptyProfile() {
2
+ return {
3
+ incomingCalls: 0,
4
+ outgoingCalls: 0,
5
+ incomingReads: 0,
6
+ incomingWrites: 0,
7
+ incomingProps: 0,
8
+ outgoingProps: 0,
9
+ importedBy: 0,
10
+ };
11
+ }
12
+ // Computes the 75th percentile of a number array
13
+ function percentile75(values) {
14
+ if (values.length === 0)
15
+ return 1;
16
+ // Build frequency map — O(n)
17
+ // key: connection count value, value: how many nodes have that count
18
+ const freq = new Map();
19
+ for (const v of values) {
20
+ freq.set(v, (freq.get(v) ?? 0) + 1);
21
+ }
22
+ // Sort only the UNIQUE keys — O(k log k) where k << n
23
+ // In practice k is tiny — most codebases have counts 1-20
24
+ // even if they have 500+ nodes
25
+ const uniqueKeys = Array.from(freq.keys()).sort((a, b) => a - b);
26
+ // Walk cumulative count until we reach the 75th percentile position
27
+ const target = Math.floor(values.length * 0.75);
28
+ let cumulative = 0;
29
+ for (const key of uniqueKeys) {
30
+ cumulative += freq.get(key);
31
+ if (cumulative >= target) {
32
+ return Math.max(1, key);
33
+ }
34
+ }
35
+ // Fallback — return the largest value
36
+ return Math.max(1, uniqueKeys[uniqueKeys.length - 1]);
37
+ }
38
+ export function countConnections(nodes, edges) {
39
+ const profiles = new Map();
40
+ // Initialize a profile for every node
41
+ for (const node of nodes) {
42
+ profiles.set(node.id, emptyProfile());
43
+ }
44
+ // ─── Pass 1 — Count edges ──────────────────────────────────────
45
+ for (const edge of edges) {
46
+ const fromProfile = profiles.get(edge.from);
47
+ const toProfile = profiles.get(edge.to);
48
+ switch (edge.type) {
49
+ case "CALLS":
50
+ if (fromProfile)
51
+ fromProfile.outgoingCalls += 1;
52
+ if (toProfile)
53
+ toProfile.incomingCalls += 1;
54
+ break;
55
+ case "READS_FROM":
56
+ if (toProfile)
57
+ toProfile.incomingReads += 1;
58
+ break;
59
+ case "WRITES_TO":
60
+ if (toProfile)
61
+ toProfile.incomingWrites += 1;
62
+ break;
63
+ case "PROP_PASS":
64
+ if (fromProfile)
65
+ fromProfile.outgoingProps += 1;
66
+ if (toProfile)
67
+ toProfile.incomingProps += 1;
68
+ break;
69
+ case "IMPORTS":
70
+ if (toProfile)
71
+ toProfile.importedBy += 1;
72
+ break;
73
+ }
74
+ }
75
+ // ─── Pass 2 — Find true maxima ────────────────────────────────
76
+ let maxIncomingCalls = 1;
77
+ let maxOutgoingCalls = 1;
78
+ let maxIncomingReads = 1;
79
+ let maxIncomingWrites = 1;
80
+ let maxIncomingProps = 1;
81
+ let maxOutgoingProps = 1;
82
+ let maxImportedBy = 1;
83
+ for (const profile of profiles.values()) {
84
+ if (profile.incomingCalls > maxIncomingCalls)
85
+ maxIncomingCalls = profile.incomingCalls;
86
+ if (profile.outgoingCalls > maxOutgoingCalls)
87
+ maxOutgoingCalls = profile.outgoingCalls;
88
+ if (profile.incomingReads > maxIncomingReads)
89
+ maxIncomingReads = profile.incomingReads;
90
+ if (profile.incomingWrites > maxIncomingWrites)
91
+ maxIncomingWrites = profile.incomingWrites;
92
+ if (profile.incomingProps > maxIncomingProps)
93
+ maxIncomingProps = profile.incomingProps;
94
+ if (profile.outgoingProps > maxOutgoingProps)
95
+ maxOutgoingProps = profile.outgoingProps;
96
+ if (profile.importedBy > maxImportedBy)
97
+ maxImportedBy = profile.importedBy;
98
+ }
99
+ // ─── Pass 3 — Compute 75th percentiles ────────────────────────
100
+ // Collect all non-zero values per signal type
101
+ // We only include non-zero values so nodes with no connections
102
+ // don't drag the percentile down to zero
103
+ const allIncomingCalls = [];
104
+ const allOutgoingCalls = [];
105
+ const allIncomingReads = [];
106
+ const allIncomingProps = [];
107
+ for (const profile of profiles.values()) {
108
+ if (profile.incomingCalls > 0)
109
+ allIncomingCalls.push(profile.incomingCalls);
110
+ if (profile.outgoingCalls > 0)
111
+ allOutgoingCalls.push(profile.outgoingCalls);
112
+ if (profile.incomingReads > 0)
113
+ allIncomingReads.push(profile.incomingReads);
114
+ if (profile.incomingProps > 0)
115
+ allIncomingProps.push(profile.incomingProps);
116
+ }
117
+ return {
118
+ profiles,
119
+ maxima: {
120
+ maxIncomingCalls,
121
+ maxOutgoingCalls,
122
+ maxIncomingReads,
123
+ maxIncomingWrites,
124
+ maxIncomingProps,
125
+ maxOutgoingProps,
126
+ maxImportedBy,
127
+ // 75th percentile — used for normalization in nodeScorer
128
+ p75IncomingCalls: percentile75(allIncomingCalls),
129
+ p75OutgoingCalls: percentile75(allOutgoingCalls),
130
+ p75IncomingReads: percentile75(allIncomingReads),
131
+ p75IncomingProps: percentile75(allIncomingProps),
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,2 @@
1
+ import type { CodeNode } from "../types.js";
2
+ export declare function scoreFile(fileNode: CodeNode, children: CodeNode[], nodeScores: Map<string, number>, importedBy: number): number;
@@ -0,0 +1,44 @@
1
+ // ─── Internal Gravity (G_int) ──────────────────────────────────────────────
2
+ // Formula: Σ(S²) / ΣS
3
+ // Favors one high scorer over many averages
4
+ function calcInternalGravity(childScores) {
5
+ if (childScores.length === 0)
6
+ return 0;
7
+ const sumOfSquares = childScores.reduce((acc, s) => acc + s * s, 0);
8
+ const sumOfScores = childScores.reduce((acc, s) => acc + s, 0);
9
+ if (sumOfScores === 0)
10
+ return 0;
11
+ return sumOfSquares / sumOfScores;
12
+ }
13
+ // ─── Reputation Boost (R_ext) ─────────────────────────────────────────────
14
+ // Formula: (10 - G_int) Ɨ (1 - 1/log10(importedBy + 10))
15
+ // Log-scaled import popularity
16
+ function calcReputationBoost(gInt, importedBy) {
17
+ const gap = 10 - gInt;
18
+ const multiplier = 1 - (1 / Math.log10(importedBy + 10));
19
+ return gap * multiplier;
20
+ }
21
+ // ─── Best Child Floor ──────────────────────────────────────────────────────
22
+ // fileScore >= bestChild Ɨ 0.90 (dilution protection)
23
+ const BEST_CHILD_FLOOR_RATIO = 0.90;
24
+ // ─── File Score: gravity + floor + reputation ───────────────────────────────
25
+ export function scoreFile(fileNode, children, nodeScores, importedBy) {
26
+ if (fileNode.type !== "FILE")
27
+ return 0;
28
+ // Empty files (types/barrels): reputation only
29
+ if (children.length === 0) {
30
+ const gInt = 0;
31
+ const rExt = calcReputationBoost(gInt, importedBy);
32
+ return Math.min(10, Math.max(0, rExt));
33
+ }
34
+ const childScores = children.map((n) => nodeScores.get(n.id) ?? 0);
35
+ // 1. Internal gravity
36
+ const gInt = calcInternalGravity(childScores);
37
+ // 2. Apply best-child floor
38
+ const maxChildScore = Math.max(...childScores);
39
+ const floor = maxChildScore * BEST_CHILD_FLOOR_RATIO;
40
+ const adjustedGInt = Math.max(gInt, floor);
41
+ // 3. Reputation boost
42
+ const rExt = calcReputationBoost(adjustedGInt, importedBy);
43
+ return Math.min(10, Math.max(0, adjustedGInt + rExt));
44
+ }
@@ -0,0 +1,22 @@
1
+ import type { CodeNode, CodeEdge } from "../types.js";
2
+ import { type FilterThresholds } from "./noiseFilter.js";
3
+ export interface ScoringResult {
4
+ filteredNodes: CodeNode[];
5
+ filteredEdges: CodeEdge[];
6
+ nodeScores: Map<string, number>;
7
+ stats: {
8
+ totalNodesBeforeFilter: number;
9
+ totalEdgesBeforeFilter: number;
10
+ totalNodesAfterFilter: number;
11
+ totalEdgesAfterFilter: number;
12
+ removedNodeCount: number;
13
+ removedEdgeCount: number;
14
+ averageNodeScore: number;
15
+ topScoringNodes: {
16
+ name: string;
17
+ score: number;
18
+ type: string;
19
+ }[];
20
+ };
21
+ }
22
+ export declare function scoreAndFilter(nodes: CodeNode[], edges: CodeEdge[], thresholds?: FilterThresholds, existingScores?: Map<string, number>): ScoringResult;
@@ -0,0 +1,130 @@
1
+ import { countConnections } from "./connectionCounter.js";
2
+ import { scoreNode } from "./nodeScorer.js";
3
+ import { scoreFile } from "./fileScorer.js";
4
+ import { filterNoise } from "./noiseFilter.js";
5
+ export function scoreAndFilter(nodes, edges, thresholds, existingScores // when provided, skip Passes 1-4
6
+ ) {
7
+ let nodeScores;
8
+ if (existingScores) {
9
+ // Re-filter only — reuse scores from a previous analysis run
10
+ nodeScores = existingScores;
11
+ }
12
+ else {
13
+ console.log(`\nšŸ“Š Scoring ${nodes.length} nodes...`);
14
+ // Pass 1 + 2 — Count connections and find maxima
15
+ const { profiles, maxima } = countConnections(nodes, edges);
16
+ // Build childrenByFile map — O(n)
17
+ // Maps fileNode.id → all child nodes in that file
18
+ const childrenByFile = new Map();
19
+ for (const node of nodes) {
20
+ if (node.type === "FILE")
21
+ continue;
22
+ if (!node.parentFile)
23
+ continue;
24
+ if (!childrenByFile.has(node.parentFile)) {
25
+ childrenByFile.set(node.parentFile, []);
26
+ }
27
+ childrenByFile.get(node.parentFile).push(node);
28
+ }
29
+ // Pass 3 — Score all non-FILE nodes
30
+ nodeScores = new Map();
31
+ for (const node of nodes) {
32
+ if (node.type === "FILE")
33
+ continue; // scored in pass 4
34
+ const profile = profiles.get(node.id) ?? {
35
+ incomingCalls: 0,
36
+ outgoingCalls: 0,
37
+ incomingReads: 0,
38
+ incomingWrites: 0,
39
+ incomingProps: 0,
40
+ outgoingProps: 0,
41
+ importedBy: 0,
42
+ };
43
+ const score = scoreNode(node, profile, maxima);
44
+ nodeScores.set(node.id, score);
45
+ }
46
+ // ─── Pass 4 — Score FILE nodes using child scores ─────────────
47
+ for (const node of nodes) {
48
+ if (node.type !== "FILE")
49
+ continue;
50
+ const children = childrenByFile.get(node.id) ?? [];
51
+ const profile = profiles.get(node.id) ?? {
52
+ incomingCalls: 0,
53
+ outgoingCalls: 0,
54
+ incomingReads: 0,
55
+ incomingWrites: 0,
56
+ incomingProps: 0,
57
+ outgoingProps: 0,
58
+ importedBy: 0,
59
+ };
60
+ const score = scoreFile(node, children, nodeScores, profile.importedBy);
61
+ nodeScores.set(node.id, score);
62
+ }
63
+ }
64
+ // Pass 4.5 — Score ROUTE nodes from their handler
65
+ //
66
+ // A route's significance is entirely determined by the handler it
67
+ // delegates to. We find the HANDLES edge from each route node and
68
+ // assign it the handler's score directly.
69
+ //
70
+ // If a route has multiple HANDLES edges (shouldn't happen but
71
+ // defensive) we take the max. If no handler is resolved, the
72
+ // route keeps its base type bonus score.
73
+ // Build a quick lookup: routeNodeId → handler scores via HANDLES edges
74
+ const handlesEdges = edges.filter(e => e.type === "HANDLES");
75
+ for (const node of nodes) {
76
+ if (node.type !== "ROUTE")
77
+ continue;
78
+ const handlerScores = handlesEdges
79
+ .filter(e => e.from === node.id)
80
+ .map(e => nodeScores.get(e.to) ?? 0);
81
+ if (handlerScores.length === 0)
82
+ continue;
83
+ const handlerScore = Math.max(...handlerScores);
84
+ nodeScores.set(node.id, handlerScore);
85
+ }
86
+ // ─── Pass 5 — Filter noise
87
+ const filterResult = filterNoise(nodes, edges, nodeScores, thresholds);
88
+ // ─── Build stats ──────────────────────────────────────────────
89
+ const allScores = Array.from(nodeScores.values());
90
+ const averageScore = allScores.length > 0
91
+ ? allScores.reduce((a, b) => a + b, 0) / allScores.length
92
+ : 0;
93
+ // Top 10 scoring nodes — useful for UI and debugging
94
+ const topScoringNodes = nodes
95
+ .filter((n) => n.type !== "FILE")
96
+ .map((n) => ({
97
+ name: n.name,
98
+ type: n.type,
99
+ score: nodeScores.get(n.id) ?? 0,
100
+ }))
101
+ .sort((a, b) => b.score - a.score)
102
+ .slice(0, 10);
103
+ // ─── Log summary ──────────────────────────────────────────────
104
+ console.log(` Nodes before filter: ${nodes.length}`);
105
+ console.log(` Nodes after filter: ${filterResult.nodes.length}`);
106
+ console.log(` Edges before filter: ${edges.length}`);
107
+ console.log(` Edges after filter: ${filterResult.edges.length}`);
108
+ console.log(` Removed nodes: ${filterResult.removedNodeCount}`);
109
+ console.log(` Removed edges: ${filterResult.removedEdgeCount}`);
110
+ console.log(` Average score: ${averageScore.toFixed(2)}`);
111
+ console.log(`\n šŸ† Top scoring nodes:`);
112
+ for (const n of topScoringNodes) {
113
+ console.log(` ${n.score.toFixed(2).padStart(5)} [${n.type}] ${n.name}`);
114
+ }
115
+ return {
116
+ filteredNodes: filterResult.nodes,
117
+ filteredEdges: filterResult.edges,
118
+ nodeScores,
119
+ stats: {
120
+ totalNodesBeforeFilter: nodes.length,
121
+ totalEdgesBeforeFilter: edges.length,
122
+ totalNodesAfterFilter: filterResult.nodes.length,
123
+ totalEdgesAfterFilter: filterResult.edges.length,
124
+ removedNodeCount: filterResult.removedNodeCount,
125
+ removedEdgeCount: filterResult.removedEdgeCount,
126
+ averageNodeScore: parseFloat(averageScore.toFixed(2)),
127
+ topScoringNodes,
128
+ },
129
+ };
130
+ }
@@ -0,0 +1 @@
1
+ export {};