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.
- package/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +155 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
package/dist/pipeline/index.js
CHANGED
|
@@ -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
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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,
|
package/dist/scoring/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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]");
|