devlensio 0.6.2 → 1.0.1

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 (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +155 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +30 -42
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. package/package.json +22 -4
@@ -1,12 +1,8 @@
1
1
  import path from "path";
2
2
  import { createHash } from "crypto";
3
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
4
  import { scoreAndFilter } from "../scoring/index.js";
5
+ import { runExtractor } from "../extractors/runner.js";
10
6
  // Helpers
11
7
  // Deterministic graphId — same repo always produces same id
12
8
  // This ensures multiple analyses of the same repo go into the same folder
@@ -74,7 +70,7 @@ function mapToRecord(map) {
74
70
  // Naming convention:
75
71
  // Next.js page/layout/API: "GET /api/users" → id = "app/api/users/route.ts::GET /api/users"
76
72
  // Backend (Express etc.): "POST /users" → id = "src/routes/users.ts::POST /users"
77
- function routesToCodeNodes(routes, repoPath) {
73
+ export function routesToCodeNodes(routes, repoPath) {
78
74
  const nodes = [];
79
75
  for (const route of routes) {
80
76
  // Make filePath relative — same as parser does for all other nodes
@@ -198,41 +194,33 @@ export async function analyzePipeline(repoPath, isGithubRepo, options) {
198
194
  console.log(` Graph ID: ${graphId}`);
199
195
  console.log(` Commit: ${gitInfo.commitHash} (${gitInfo.branch})`);
200
196
  console.log(` Message: ${gitInfo.message}`);
201
- // Step 1: Fingerprint
202
- console.log("\n[1/5] Fingerprinting project...");
203
- const fingerprint = analyzeFingerprint(absoluteRepoPath);
204
- console.log(` Framework: ${fingerprint.framework} | Language: ${fingerprint.language} | Type: ${fingerprint.projectType}`);
205
- // Step 2: Filesystem / routes
206
- console.log("\n[2/5] Analyzing filesystem routes...");
207
- const routes = analyzeFilesystem(absoluteRepoPath, fingerprint);
208
- console.log(` Routes found: ${routes.length}`);
209
- // Convert routes -> CodeNodes so they join the graph as nodes as well
210
- // It is important to add here before the detection of the edges
211
- let routeNodes = routesToCodeNodes(routes, absoluteRepoPath);
212
- console.log(` Route nodes created: ${routeNodes.length}`);
213
- // Step 3: Parse source files into nodes
214
- console.log("\n[3/5] Parsing source files...");
215
- const parserResult = parseRepo(absoluteRepoPath);
216
- console.log(` Files: ${parserResult.stats.totalFiles} | Nodes: ${parserResult.stats.totalNodes} | Skipped: ${parserResult.stats.skippedFiles}`);
217
- // Step 3.5: Build third-party nodes
218
- const thirdPartyNodes = options?.includedThirdPartyLibs?.length
219
- ? buildThirdPartyNodes(absoluteRepoPath, options.includedThirdPartyLibs)
220
- : [];
221
- if (thirdPartyNodes.length) {
222
- console.log(` Third-party nodes: ${thirdPartyNodes.length}`);
223
- }
224
- // Step 4: Detect edges
225
- console.log("\n[4/5] Detecting edges...");
226
- const edgeResult = detectEdges([...parserResult.nodes, ...routeNodes, ...thirdPartyNodes], routes, absoluteRepoPath, fingerprint);
227
- routeNodes = routeNodes.filter(routeNode => {
228
- if (routeNode.metadata.routeNodeType === "API_ROUTE") {
229
- const hasHandler = edgeResult.edges.some(edge => edge.type === "HANDLES" && edge.from === routeNode.id);
230
- return hasHandler;
231
- }
232
- return true;
197
+ // ── Steps 1-4: Run the extractor (inline for JS/TS, subprocess for others)
198
+ //
199
+ // The extractor handles fingerprinting, filesystem/route detection, parsing,
200
+ // and edge detection. It returns nodes, edges, routes, and fingerprint
201
+ // all in one ExtractorResult.
202
+ //
203
+ // For JS/TS: runs inline via ts-morph (same code as before, no subprocess)
204
+ // For Python/Java/Go/Rust: spawns a child process
205
+ const extractorResult = await runExtractor(absoluteRepoPath, {
206
+ includeThirdPartyLibs: options?.includedThirdPartyLibs,
207
+ onStep: options?.onStep,
233
208
  });
234
- const allNodes = [...parserResult.nodes, ...routeNodes, ...thirdPartyNodes, ...edgeResult.ghostNodes];
235
- const allEdges = edgeResult.edges;
209
+ console.log(` Framework: ${extractorResult.fingerprint.framework} | ` +
210
+ `Language: ${extractorResult.fingerprint.language} | ` +
211
+ `Type: ${extractorResult.fingerprint.projectType}`);
212
+ console.log(` Files: ${extractorResult.stats.totalFiles} | ` +
213
+ `Nodes: ${extractorResult.stats.totalNodes} | ` +
214
+ `Skipped: ${extractorResult.stats.skippedFiles}`);
215
+ console.log(` Edges: ${extractorResult.edges.length}`);
216
+ if (extractorResult.errors.length > 0) {
217
+ console.warn(` ⚠ ${extractorResult.errors.length} files had errors:`);
218
+ for (const err of extractorResult.errors.slice(0, 5)) {
219
+ console.warn(` - ${err.file}: ${err.error}`);
220
+ }
221
+ }
222
+ const allNodes = extractorResult.nodes;
223
+ const allEdges = extractorResult.edges;
236
224
  // Step 5: Score and filter
237
225
  console.log("\n[5/5] Scoring and filtering...");
238
226
  const scoringResult = scoreAndFilter(allNodes, allEdges, options?.thresholds);
@@ -249,8 +237,8 @@ export async function analyzePipeline(repoPath, isGithubRepo, options) {
249
237
  graphId,
250
238
  repoPath: absoluteRepoPath,
251
239
  analyzedAt,
252
- fingerprint,
253
- routes,
240
+ fingerprint: extractorResult.fingerprint,
241
+ routes: extractorResult.routes ?? [],
254
242
  nodes: scoringResult.filteredNodes,
255
243
  edges: scoringResult.filteredEdges,
256
244
  allNodes,
@@ -2,8 +2,16 @@ import { countConnections } from "./connectionCounter.js";
2
2
  import { scoreNode } from "./nodeScorer.js";
3
3
  import { scoreFile } from "./fileScorer.js";
4
4
  import { filterNoise } from "./noiseFilter.js";
5
+ import { pruneDisconnected } from "./pruneDisconnected.js";
5
6
  export function scoreAndFilter(nodes, edges, thresholds, existingScores // when provided, skip Passes 1-4
6
7
  ) {
8
+ // Pass 0 — Connected-only pruning (engine-wide). Drop zero-edge
9
+ // STRUCT/INTERFACE/ENUM/PACKAGE/TRAIT nodes so floating type declarations
10
+ // never reach the graph. Runs before scoring so profiles/scores only ever
11
+ // cover surviving nodes; idempotent for the refilter path.
12
+ const pruned = pruneDisconnected(nodes, edges);
13
+ nodes = pruned.nodes;
14
+ edges = pruned.edges;
7
15
  let nodeScores;
8
16
  if (existingScores) {
9
17
  // Re-filter only — reuse scores from a previous analysis run
@@ -451,3 +451,46 @@ describe("scoreAndFilter", () => {
451
451
  expect(strict.filteredNodes.length).toBeLessThan(loose.filteredNodes.length);
452
452
  });
453
453
  });
454
+ // ─── CLASS / METHOD scoring ──────────────────────────────────────────────────
455
+ describe("CLASS / METHOD node scoring", () => {
456
+ const maxima = {
457
+ maxIncomingCalls: 10, maxOutgoingCalls: 10, maxIncomingReads: 10,
458
+ maxIncomingWrites: 10, maxIncomingProps: 10, maxOutgoingProps: 10,
459
+ maxImportedBy: 10, p75IncomingCalls: 3, p75OutgoingCalls: 3,
460
+ p75IncomingReads: 3, p75IncomingProps: 3,
461
+ };
462
+ const emptyProfile = {
463
+ incomingCalls: 0, outgoingCalls: 0, incomingReads: 0,
464
+ incomingWrites: 0, incomingProps: 0, outgoingProps: 0, importedBy: 0,
465
+ };
466
+ it("gives CLASS nodes a higher type bonus than FUNCTION nodes", () => {
467
+ const classNode = makeNode({ id: "c", name: "UserService", type: "CLASS", endLine: 50 });
468
+ const fnNode = makeNode({ id: "f", name: "doThing", type: "FUNCTION", endLine: 50 });
469
+ expect(scoreNode(classNode, emptyProfile, maxima))
470
+ .toBeGreaterThan(scoreNode(fnNode, emptyProfile, maxima));
471
+ });
472
+ it("gives METHOD nodes a modest bonus below FUNCTION", () => {
473
+ const methodNode = makeNode({ id: "m", name: "UserService.save", type: "METHOD", endLine: 50 });
474
+ const fnNode = makeNode({ id: "f", name: "doThing", type: "FUNCTION", endLine: 50 });
475
+ expect(scoreNode(methodNode, emptyProfile, maxima))
476
+ .toBeLessThan(scoreNode(fnNode, emptyProfile, maxima));
477
+ expect(scoreNode(methodNode, emptyProfile, maxima)).toBeGreaterThan(0);
478
+ });
479
+ it("keeps CLASS and METHOD nodes under default filter thresholds", () => {
480
+ const nodes = [
481
+ makeFileNode("src/svc.ts"),
482
+ makeNode({
483
+ id: "src/svc.ts::UserService", name: "UserService", type: "CLASS",
484
+ endLine: 50, parentFile: "file::src/svc.ts",
485
+ }),
486
+ makeNode({
487
+ id: "src/svc.ts::UserService.save", name: "UserService.save", type: "METHOD",
488
+ endLine: 40, parentFile: "file::src/svc.ts",
489
+ }),
490
+ ];
491
+ const result = scoreAndFilter(nodes, []);
492
+ const ids = result.filteredNodes.map((n) => n.id);
493
+ expect(ids).toContain("src/svc.ts::UserService");
494
+ expect(ids).toContain("src/svc.ts::UserService.save");
495
+ });
496
+ });
@@ -65,6 +65,8 @@ function calcTypeBonus(node) {
65
65
  case "COMPONENT": return 0.75;
66
66
  case "HOOK": return 0.75;
67
67
  case "FUNCTION": return 0.5;
68
+ case "CLASS": return 1.0;
69
+ case "METHOD": return 0.4;
68
70
  case "ROUTE": return 2;
69
71
  case "TEST": return 0.3; // low bonus because test files are secondary, do not play role in the logical part
70
72
  case "STORY": return 0.3;
@@ -0,0 +1,8 @@
1
+ import type { CodeNode, CodeEdge } from "../types.js";
2
+ export declare const PRUNABLE_TYPES: Set<string>;
3
+ export interface PruneResult {
4
+ nodes: CodeNode[];
5
+ edges: CodeEdge[];
6
+ removedNodeCount: number;
7
+ }
8
+ export declare function pruneDisconnected(nodes: CodeNode[], edges: CodeEdge[]): PruneResult;
@@ -0,0 +1,66 @@
1
+ // ─── Connected-only node pruning (engine-wide) ─────────────────────────────
2
+ //
3
+ // Type-declaration nodes (STRUCT/INTERFACE/ENUM/PACKAGE/TRAIT) are emitted by
4
+ // the language extractors as declarations, but a node with ZERO edge
5
+ // participation renders as a floating dot — pure noise. This pass drops those
6
+ // nodes from every language uniformly (Java INTERFACE/ENUM, Rust STRUCT/ENUM/
7
+ // TRAIT, Go STRUCT/INTERFACE/ENUM; Python/JS are unaffected — they don't emit
8
+ // these types, and CLASS stays always-on per the JS scope decision).
9
+ //
10
+ // Rule: a prunable node survives iff it is referenced by ≥1 edge (either
11
+ // direction). A Go ENUM implementing an interface or a Rust ENUM with an impl
12
+ // block is referenced → kept. Floating serializers/validators/enum constants →
13
+ // dropped.
14
+ //
15
+ // Idempotent: refiltering an already-pruned stored graph is a no-op.
16
+ export const PRUNABLE_TYPES = new Set([
17
+ "STRUCT",
18
+ "INTERFACE",
19
+ "ENUM",
20
+ "PACKAGE",
21
+ "TRAIT",
22
+ ]);
23
+ export function pruneDisconnected(nodes, edges) {
24
+ const referenced = new Set();
25
+ for (const e of edges) {
26
+ referenced.add(e.from);
27
+ referenced.add(e.to);
28
+ }
29
+ const removed = new Set();
30
+ const kept = [];
31
+ for (const node of nodes) {
32
+ if (PRUNABLE_TYPES.has(node.type) && !referenced.has(node.id)) {
33
+ removed.add(node.id);
34
+ continue;
35
+ }
36
+ kept.push(node);
37
+ }
38
+ if (removed.size === 0) {
39
+ return { nodes: kept, edges, removedNodeCount: 0 };
40
+ }
41
+ // Clean FILE parents: drop pruned ids from childNodeIds + nodeCount so the
42
+ // frontend drill-down never references a node that isn't in the graph.
43
+ for (const node of kept) {
44
+ if (node.type === "FILE") {
45
+ const meta = node.metadata;
46
+ const childIds = meta.childNodeIds;
47
+ if (Array.isArray(childIds)) {
48
+ const clean = childIds.filter((id) => !removed.has(id));
49
+ if (clean.length !== childIds.length) {
50
+ meta.childNodeIds = clean;
51
+ meta.nodeCount = clean.length;
52
+ }
53
+ }
54
+ }
55
+ // Any node may carry resolvedCalls pointing at a pruned declaration
56
+ // (e.g. a call that resolved to an enum member) — drop the dangling refs.
57
+ const resolvedCalls = node.metadata.resolvedCalls;
58
+ if (Array.isArray(resolvedCalls)) {
59
+ const cleanCalls = resolvedCalls.filter((r) => !removed.has(r.nodeId ?? ""));
60
+ if (cleanCalls.length !== resolvedCalls.length) {
61
+ node.metadata.resolvedCalls = cleanCalls;
62
+ }
63
+ }
64
+ }
65
+ return { nodes: kept, edges, removedNodeCount: removed.size };
66
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { pruneDisconnected, PRUNABLE_TYPES } from "./pruneDisconnected.js";
3
+ // ─── Helpers ──────────────────────────────────────────────────────────────
4
+ function makeNode(overrides) {
5
+ return {
6
+ type: "FUNCTION",
7
+ filePath: "src/test.go",
8
+ startLine: 1,
9
+ endLine: 10,
10
+ parentFile: "file::src/test.go",
11
+ metadata: {},
12
+ ...overrides,
13
+ };
14
+ }
15
+ function makeFileNode(filePath, childNodeIds = []) {
16
+ return {
17
+ id: `file::${filePath}`,
18
+ name: filePath.split("/").pop(),
19
+ type: "FILE",
20
+ filePath,
21
+ startLine: 0,
22
+ endLine: 100,
23
+ parentFile: `file::${filePath}`,
24
+ metadata: { nodeCount: childNodeIds.length, childNodeIds },
25
+ };
26
+ }
27
+ function makeEdge(from, to, type = "CALLS") {
28
+ return { from, to, type, metadata: {} };
29
+ }
30
+ // ─── pruneDisconnected ─────────────────────────────────────────────────────
31
+ describe("pruneDisconnected", () => {
32
+ test("PRUNABLE_TYPES covers the 5 declaration types", () => {
33
+ expect([...PRUNABLE_TYPES].sort()).toEqual(["ENUM", "INTERFACE", "PACKAGE", "STRUCT", "TRAIT"]);
34
+ });
35
+ test("drops zero-edge STRUCT/INTERFACE/ENUM/PACKAGE/TRAIT nodes", () => {
36
+ const nodes = [
37
+ makeFileNode("a.go", ["a.go::Keep", "a.go::Drop"]),
38
+ makeNode({ id: "a.go::Keep", name: "Keep", type: "STRUCT", parentFile: "file::a.go" }),
39
+ makeNode({ id: "a.go::Drop", name: "Drop", type: "STRUCT", parentFile: "file::a.go" }),
40
+ makeNode({ id: "a.go::Iface", name: "Iface", type: "INTERFACE", parentFile: "file::a.go" }),
41
+ makeNode({ id: "a.go::Color", name: "Color", type: "ENUM", parentFile: "file::a.go" }),
42
+ makeNode({ id: "pkg::example.com/mod", name: "mod", type: "PACKAGE", filePath: "" }),
43
+ makeNode({ id: "a.go::Trait", name: "Trait", type: "TRAIT", parentFile: "file::a.go" }),
44
+ makeNode({ id: "a.go::fn", name: "fn", type: "FUNCTION", parentFile: "file::a.go" }),
45
+ ];
46
+ // Keep STRUCT is a CALLS target; everything else floats
47
+ const edges = [makeEdge("a.go::fn", "a.go::Keep")];
48
+ const { nodes: kept, removedNodeCount } = pruneDisconnected(nodes, edges);
49
+ expect(removedNodeCount).toBe(5);
50
+ const ids = kept.map((n) => n.id);
51
+ expect(ids).toContain("a.go::Keep");
52
+ expect(ids).toContain("a.go::fn");
53
+ expect(ids).not.toContain("a.go::Drop");
54
+ expect(ids).not.toContain("a.go::Iface");
55
+ expect(ids).not.toContain("a.go::Color");
56
+ expect(ids).not.toContain("pkg::example.com/mod");
57
+ expect(ids).not.toContain("a.go::Trait");
58
+ });
59
+ test("connected prunable nodes survive (edge in EITHER direction)", () => {
60
+ const nodes = [
61
+ makeNode({ id: "s.go::Impl", name: "Impl", type: "STRUCT", parentFile: "file::s.go" }),
62
+ makeNode({ id: "s.go::Iface", name: "Iface", type: "INTERFACE", parentFile: "file::s.go" }),
63
+ makeNode({ id: "s.go::Color", name: "Color", type: "ENUM", parentFile: "file::s.go" }),
64
+ ];
65
+ // STRUCT implements INTERFACE (source), ENUM is the CALLS target
66
+ const edges = [
67
+ makeEdge("s.go::Impl", "s.go::Iface", "IMPLEMENTS"),
68
+ makeEdge("s.go::fn", "s.go::Color"),
69
+ ];
70
+ const { nodes: kept, removedNodeCount } = pruneDisconnected(nodes, edges);
71
+ expect(removedNodeCount).toBe(0);
72
+ expect(kept.map((n) => n.id).sort()).toEqual(["s.go::Color", "s.go::Iface", "s.go::Impl"]);
73
+ });
74
+ test("cleans childNodeIds + nodeCount on FILE parents", () => {
75
+ const nodes = [
76
+ makeFileNode("a.go", ["a.go::Keep", "a.go::Drop", "a.go::fn"]),
77
+ makeNode({ id: "a.go::Keep", name: "Keep", type: "STRUCT", parentFile: "file::a.go" }),
78
+ makeNode({ id: "a.go::Drop", name: "Drop", type: "ENUM", parentFile: "file::a.go" }),
79
+ makeNode({ id: "a.go::fn", name: "fn", type: "FUNCTION", parentFile: "file::a.go" }),
80
+ ];
81
+ const edges = [makeEdge("a.go::fn", "a.go::Keep")];
82
+ const { nodes: kept } = pruneDisconnected(nodes, edges);
83
+ const file = kept.find((n) => n.type === "FILE");
84
+ expect(file.metadata.childNodeIds).toEqual(["a.go::Keep", "a.go::fn"]);
85
+ expect(file.metadata.nodeCount).toBe(2);
86
+ });
87
+ test("cleans resolvedCalls entries pointing at pruned nodes", () => {
88
+ const nodes = [
89
+ makeNode({
90
+ id: "a.go::fn", name: "fn", type: "FUNCTION", parentFile: "file::a.go",
91
+ metadata: { resolvedCalls: [{ name: "Color.Red", nodeId: "a.go::Color" }] },
92
+ }),
93
+ makeNode({ id: "a.go::Color", name: "Color", type: "ENUM", parentFile: "file::a.go" }),
94
+ ];
95
+ const { nodes: kept } = pruneDisconnected(nodes, []);
96
+ const fn = kept.find((n) => n.id === "a.go::fn");
97
+ expect(fn.metadata.resolvedCalls).toEqual([]);
98
+ });
99
+ test("idempotent — pruning an already-pruned graph is a no-op", () => {
100
+ const nodes = [
101
+ makeFileNode("a.go", ["a.go::fn"]),
102
+ makeNode({ id: "a.go::fn", name: "fn", type: "FUNCTION", parentFile: "file::a.go" }),
103
+ makeNode({ id: "a.go::Color", name: "Color", type: "ENUM", parentFile: "file::a.go" }),
104
+ ];
105
+ const edges = [makeEdge("a.go::fn", "a.go::Color")]; // enum connected → kept
106
+ const first = pruneDisconnected(nodes, edges);
107
+ const second = pruneDisconnected(first.nodes, first.edges);
108
+ expect(second.removedNodeCount).toBe(0);
109
+ expect(second.nodes.map((n) => n.id)).toEqual(first.nodes.map((n) => n.id));
110
+ });
111
+ test("CLASS/METHOD/FUNCTION/ROUTE/FILE/TEST are never pruned", () => {
112
+ const nodes = [
113
+ makeNode({ id: "c.go::K", name: "K", type: "CLASS", parentFile: "file::c.go" }),
114
+ makeNode({ id: "c.go::m", name: "m", type: "METHOD", parentFile: "file::c.go" }),
115
+ makeNode({ id: "c.go::f", name: "f", type: "FUNCTION", parentFile: "file::c.go" }),
116
+ makeNode({ id: "c.go::r", name: "r", type: "ROUTE", parentFile: "file::c.go" }),
117
+ makeNode({ id: "file::c_test.go", name: "c_test.go", type: "TEST", parentFile: "file::c_test.go" }),
118
+ ];
119
+ const { nodes: kept, removedNodeCount } = pruneDisconnected(nodes, []);
120
+ expect(removedNodeCount).toBe(0);
121
+ expect(kept).toHaveLength(5);
122
+ });
123
+ });
@@ -8,7 +8,7 @@ export interface RouteIndex {
8
8
  byFilePath: Map<string, RouteNode | BackendRouteNode>;
9
9
  }
10
10
  export declare function buildEdgeIndex(edges: CodeEdge[]): EdgeIndex;
11
- export declare function buildRouteIndex(routes: RouteNode[] | BackendRouteNode[]): RouteIndex;
11
+ export declare function buildRouteIndex(routes: (RouteNode | BackendRouteNode)[]): RouteIndex;
12
12
  export interface PromptContext {
13
13
  node: CodeNode;
14
14
  allNodes: Map<string, CodeNode>;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type Language = "javascript" | "typescript" | "python" | "unknown";
2
- export type Framework = "nextjs" | "react" | "express" | "fastify" | "koa" | "hono" | "elysia" | "bun" | "unknown";
1
+ export type Language = "javascript" | "typescript" | "python" | "java" | "go" | "rust" | "unknown";
2
+ export type Framework = "nextjs" | "react" | "express" | "fastify" | "koa" | "hono" | "elysia" | "bun" | "django" | "flask" | "fastapi" | "spring-boot" | "quarkus" | "gin" | "echo" | "fiber" | "net-http" | "actix-web" | "rocket" | "axum" | "unknown";
3
3
  export type FrontendFramework = "nextjs" | "react";
4
4
  export type BackendFramework = "express" | "fastify" | "koa" | "hono" | "elysia" | "bun";
5
5
  export type RouterType = "app" | "pages" | "app+pages" | "react-router" | "none";
@@ -38,7 +38,7 @@ export interface BackendRouteNode {
38
38
  filePath: string;
39
39
  httpMethod: string;
40
40
  handlerName?: string;
41
- framework: BackendFramework;
41
+ framework: Framework;
42
42
  isDynamic: boolean;
43
43
  params?: string[];
44
44
  inlineHandler?: {
@@ -47,7 +47,7 @@ export interface BackendRouteNode {
47
47
  endLine: number;
48
48
  };
49
49
  }
50
- export type NodeType = "COMPONENT" | "HOOK" | "FUNCTION" | "STATE_STORE" | "UTILITY" | "FILE" | "GHOST" | "ROUTE" | "TEST" | "STORY" | "THIRD_PARTY";
50
+ export type NodeType = "COMPONENT" | "HOOK" | "FUNCTION" | "STATE_STORE" | "UTILITY" | "FILE" | "GHOST" | "ROUTE" | "TEST" | "STORY" | "THIRD_PARTY" | "CLASS" | "METHOD" | "INTERFACE" | "ENUM" | "STRUCT" | "MODULE" | "TRAIT" | "IMPL_BLOCK" | "PACKAGE";
51
51
  export interface CodeNode {
52
52
  id: string;
53
53
  name: string;
@@ -70,7 +70,7 @@ export interface CodeNode {
70
70
  score?: Number;
71
71
  metadata: Record<string, unknown>;
72
72
  }
73
- export type EdgeType = "CALLS" | "IMPORTS" | "READS_FROM" | "WRITES_TO" | "PROP_PASS" | "EMITS" | "LISTENS" | "WRAPPED_BY" | "GUARDS" | "HANDLES" | "TESTS" | "USES" | "NEXTJS_API_CALL" | "NAVIGATES_TO";
73
+ export type EdgeType = "CALLS" | "IMPORTS" | "READS_FROM" | "WRITES_TO" | "PROP_PASS" | "EMITS" | "LISTENS" | "WRAPPED_BY" | "GUARDS" | "HANDLES" | "TESTS" | "USES" | "NEXTJS_API_CALL" | "NAVIGATES_TO" | "IMPLEMENTS" | "EXTENDS" | "EXPORTS" | "THROWS";
74
74
  export interface CodeEdge {
75
75
  from: string;
76
76
  to: string;
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // DevLens Go extractor — cross-compiled static binaries (Node script, so it
3
+ // runs on Windows too; never bash).
4
+ //
5
+ // The Go extractor ships as per-platform static binaries inside the devlensio
6
+ // tarball (files whitelist). This builds all targets with CGO_ENABLED=0 (pure
7
+ // static). Node is guaranteed on every machine that installs an npm package;
8
+ // the Go toolchain is a build-time dep (same class as JDK for java's build.mjs).
9
+ //
10
+ // Usage: node build.mjs (idempotent — run from this directory)
11
+
12
+ import { execFileSync } from "node:child_process";
13
+ import { mkdirSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url));
18
+
19
+ // Target matrix — the platforms the registry resolves per process.platform.
20
+ const TARGETS = [
21
+ ["linux", "amd64"],
22
+ ["linux", "arm64"],
23
+ ["darwin", "amd64"],
24
+ ["darwin", "arm64"],
25
+ ["windows", "amd64"],
26
+ ];
27
+
28
+ function run(cmd, args, opts = {}) {
29
+ execFileSync(cmd, args, { stdio: "inherit", ...opts });
30
+ }
31
+
32
+ const go = process.env.GO || "go";
33
+ for (const [goos, goarch] of TARGETS) {
34
+ const outDir = join(HERE, "bin", `${goos}-${goarch}`);
35
+ mkdirSync(outDir, { recursive: true });
36
+ const exe = goos === "windows" ? "devlens_go_extractor.exe" : "devlens_go_extractor";
37
+ console.log(`==> building ${goos}/${goarch}`);
38
+ run(go, ["build", "-o", join(outDir, exe), "."], {
39
+ cwd: HERE,
40
+ env: { ...process.env, GOOS: goos, GOARCH: goarch, CGO_ENABLED: "0" },
41
+ });
42
+ }
43
+ console.log("==> Done: extractors/go/bin/<goos>-<goarch>/devlens_go_extractor[.exe]");