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.
- package/LICENSE +674 -0
- package/dist/clustering/index.d.ts +27 -0
- package/dist/clustering/index.js +149 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +78 -0
- package/dist/config/providers/file.d.ts +19 -0
- package/dist/config/providers/file.js +215 -0
- package/dist/config/providers/request.d.ts +2 -0
- package/dist/config/providers/request.js +72 -0
- package/dist/config/types.d.ts +46 -0
- package/dist/config/types.js +81 -0
- package/dist/config/writer.d.ts +29 -0
- package/dist/config/writer.js +103 -0
- package/dist/filesystem/appRouter.d.ts +2 -0
- package/dist/filesystem/appRouter.js +126 -0
- package/dist/filesystem/backendRoutes.d.ts +2 -0
- package/dist/filesystem/backendRoutes.js +161 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +28 -0
- package/dist/filesystem/index.test.d.ts +1 -0
- package/dist/filesystem/index.test.js +178 -0
- package/dist/filesystem/pagesRouter.d.ts +2 -0
- package/dist/filesystem/pagesRouter.js +109 -0
- package/dist/fingerprint/detectors.d.ts +8 -0
- package/dist/fingerprint/detectors.js +174 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +41 -0
- package/dist/fingerprint/index.test.d.ts +1 -0
- package/dist/fingerprint/index.test.js +148 -0
- package/dist/graph/buildLookup.d.ts +10 -0
- package/dist/graph/buildLookup.js +32 -0
- package/dist/graph/edges/callEdges.d.ts +7 -0
- package/dist/graph/edges/callEdges.js +145 -0
- package/dist/graph/edges/eventEdges.d.ts +7 -0
- package/dist/graph/edges/eventEdges.js +203 -0
- package/dist/graph/edges/guardEdges.d.ts +3 -0
- package/dist/graph/edges/guardEdges.js +232 -0
- package/dist/graph/edges/hookEdges.d.ts +3 -0
- package/dist/graph/edges/hookEdges.js +54 -0
- package/dist/graph/edges/importEdges.d.ts +8 -0
- package/dist/graph/edges/importEdges.js +224 -0
- package/dist/graph/edges/propEdges.d.ts +3 -0
- package/dist/graph/edges/propEdges.js +142 -0
- package/dist/graph/edges/routeEdge.d.ts +3 -0
- package/dist/graph/edges/routeEdge.js +124 -0
- package/dist/graph/edges/stateEdges.d.ts +3 -0
- package/dist/graph/edges/stateEdges.js +206 -0
- package/dist/graph/edges/testEdges.d.ts +3 -0
- package/dist/graph/edges/testEdges.js +143 -0
- package/dist/graph/edges/utils.d.ts +2 -0
- package/dist/graph/edges/utils.js +25 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +65 -0
- package/dist/graph/index.test.d.ts +1 -0
- package/dist/graph/index.test.js +542 -0
- package/dist/graph/thirdPartyLibs.d.ts +8 -0
- package/dist/graph/thirdPartyLibs.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/index.js +11 -0
- package/dist/jobs/queue/interface.d.ts +13 -0
- package/dist/jobs/queue/interface.js +1 -0
- package/dist/jobs/queue/memory.d.ts +24 -0
- package/dist/jobs/queue/memory.js +291 -0
- package/dist/jobs/runner.d.ts +3 -0
- package/dist/jobs/runner.js +136 -0
- package/dist/jobs/types.d.ts +112 -0
- package/dist/jobs/types.js +33 -0
- package/dist/parser/directives.d.ts +4 -0
- package/dist/parser/directives.js +31 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.js +240 -0
- package/dist/parser/extractors/functions.d.ts +4 -0
- package/dist/parser/extractors/functions.js +240 -0
- package/dist/parser/extractors/hooks.d.ts +4 -0
- package/dist/parser/extractors/hooks.js +128 -0
- package/dist/parser/extractors/stores.d.ts +3 -0
- package/dist/parser/extractors/stores.js +181 -0
- package/dist/parser/index.d.ts +14 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.test.d.ts +1 -0
- package/dist/parser/index.test.js +319 -0
- package/dist/parser/typeUtils.d.ts +9 -0
- package/dist/parser/typeUtils.js +46 -0
- package/dist/pipeline/index.d.ts +50 -0
- package/dist/pipeline/index.js +249 -0
- package/dist/scoring/connectionCounter.d.ts +28 -0
- package/dist/scoring/connectionCounter.js +134 -0
- package/dist/scoring/fileScorer.d.ts +2 -0
- package/dist/scoring/fileScorer.js +44 -0
- package/dist/scoring/index.d.ts +22 -0
- package/dist/scoring/index.js +130 -0
- package/dist/scoring/index.test.d.ts +1 -0
- package/dist/scoring/index.test.js +453 -0
- package/dist/scoring/nodeScorer.d.ts +3 -0
- package/dist/scoring/nodeScorer.js +108 -0
- package/dist/scoring/noiseFilter.d.ts +18 -0
- package/dist/scoring/noiseFilter.js +92 -0
- package/dist/storage/fileStorage.d.ts +117 -0
- package/dist/storage/fileStorage.js +616 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/interface.d.ts +27 -0
- package/dist/storage/interface.js +1 -0
- package/dist/summarizer/checkpoint.d.ts +15 -0
- package/dist/summarizer/checkpoint.js +110 -0
- package/dist/summarizer/index.d.ts +2 -0
- package/dist/summarizer/index.js +281 -0
- package/dist/summarizer/mapreduce.d.ts +4 -0
- package/dist/summarizer/mapreduce.js +87 -0
- package/dist/summarizer/prompts.d.ts +22 -0
- package/dist/summarizer/prompts.js +205 -0
- package/dist/summarizer/providers/anthropic.d.ts +9 -0
- package/dist/summarizer/providers/anthropic.js +78 -0
- package/dist/summarizer/providers/gemini.d.ts +9 -0
- package/dist/summarizer/providers/gemini.js +79 -0
- package/dist/summarizer/providers/index.d.ts +3 -0
- package/dist/summarizer/providers/index.js +43 -0
- package/dist/summarizer/providers/ollama.d.ts +9 -0
- package/dist/summarizer/providers/ollama.js +23 -0
- package/dist/summarizer/providers/openRouter.d.ts +9 -0
- package/dist/summarizer/providers/openRouter.js +19 -0
- package/dist/summarizer/providers/openai.d.ts +9 -0
- package/dist/summarizer/providers/openai.js +72 -0
- package/dist/summarizer/providers/types.d.ts +32 -0
- package/dist/summarizer/providers/types.js +1 -0
- package/dist/summarizer/retry.d.ts +7 -0
- package/dist/summarizer/retry.js +51 -0
- package/dist/summarizer/topological.d.ts +3 -0
- package/dist/summarizer/topological.js +105 -0
- package/dist/summarizer/types.d.ts +57 -0
- package/dist/summarizer/types.js +17 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
import { scoreAndFilter } from "./index.js";
|
|
6
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
+
function makeNode(overrides) {
|
|
8
|
+
return {
|
|
9
|
+
type: "FUNCTION",
|
|
10
|
+
filePath: "src/test.ts",
|
|
11
|
+
startLine: 1,
|
|
12
|
+
endLine: 10,
|
|
13
|
+
parentFile: "file::src/test.ts",
|
|
14
|
+
metadata: {},
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function makeFileNode(filePath) {
|
|
19
|
+
return {
|
|
20
|
+
id: `file::${filePath}`,
|
|
21
|
+
name: filePath.split("/").pop(),
|
|
22
|
+
type: "FILE",
|
|
23
|
+
filePath,
|
|
24
|
+
startLine: 0,
|
|
25
|
+
endLine: 100,
|
|
26
|
+
parentFile: `file::${filePath}`,
|
|
27
|
+
metadata: { nodeCount: 0 },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeEdge(from, to, type) {
|
|
31
|
+
return { from, to, type, metadata: {} };
|
|
32
|
+
}
|
|
33
|
+
// ─── connectionCounter ────────────────────────────────────────────────────────
|
|
34
|
+
describe("countConnections", () => {
|
|
35
|
+
it("should count incoming and outgoing CALLS correctly", () => {
|
|
36
|
+
const nodes = [
|
|
37
|
+
makeNode({ id: "a", name: "funcA" }),
|
|
38
|
+
makeNode({ id: "b", name: "funcB" }),
|
|
39
|
+
makeNode({ id: "c", name: "funcC" }),
|
|
40
|
+
];
|
|
41
|
+
const edges = [
|
|
42
|
+
makeEdge("a", "b", "CALLS"),
|
|
43
|
+
makeEdge("a", "c", "CALLS"),
|
|
44
|
+
];
|
|
45
|
+
const { profiles } = countConnections(nodes, edges);
|
|
46
|
+
expect(profiles.get("a").outgoingCalls).toBe(2);
|
|
47
|
+
expect(profiles.get("b").incomingCalls).toBe(1);
|
|
48
|
+
expect(profiles.get("c").incomingCalls).toBe(1);
|
|
49
|
+
});
|
|
50
|
+
it("should count READS_FROM and WRITES_TO on the target node", () => {
|
|
51
|
+
const nodes = [
|
|
52
|
+
makeNode({ id: "store", name: "useCartStore", type: "STATE_STORE" }),
|
|
53
|
+
makeNode({ id: "readerA", name: "ComponentA", type: "COMPONENT" }),
|
|
54
|
+
makeNode({ id: "readerB", name: "ComponentB", type: "COMPONENT" }),
|
|
55
|
+
makeNode({ id: "writer", name: "ComponentC", type: "COMPONENT" }),
|
|
56
|
+
];
|
|
57
|
+
const edges = [
|
|
58
|
+
makeEdge("readerA", "store", "READS_FROM"),
|
|
59
|
+
makeEdge("readerB", "store", "READS_FROM"),
|
|
60
|
+
makeEdge("writer", "store", "WRITES_TO"),
|
|
61
|
+
];
|
|
62
|
+
const { profiles } = countConnections(nodes, edges);
|
|
63
|
+
expect(profiles.get("store").incomingReads).toBe(2);
|
|
64
|
+
expect(profiles.get("store").incomingWrites).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
it("should count PROP_PASS incoming and outgoing", () => {
|
|
67
|
+
const nodes = [
|
|
68
|
+
makeNode({ id: "parent", name: "Parent", type: "COMPONENT" }),
|
|
69
|
+
makeNode({ id: "child", name: "Child", type: "COMPONENT" }),
|
|
70
|
+
];
|
|
71
|
+
const edges = [
|
|
72
|
+
makeEdge("parent", "child", "PROP_PASS"),
|
|
73
|
+
];
|
|
74
|
+
const { profiles } = countConnections(nodes, edges);
|
|
75
|
+
expect(profiles.get("parent").outgoingProps).toBe(1);
|
|
76
|
+
expect(profiles.get("child").incomingProps).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
it("should count IMPORTS as importedBy on the target FILE node", () => {
|
|
79
|
+
const nodes = [
|
|
80
|
+
makeFileNode("src/a.ts"),
|
|
81
|
+
makeFileNode("src/b.ts"),
|
|
82
|
+
makeFileNode("src/c.ts"),
|
|
83
|
+
];
|
|
84
|
+
const edges = [
|
|
85
|
+
makeEdge("file::src/a.ts", "file::src/b.ts", "IMPORTS"),
|
|
86
|
+
makeEdge("file::src/c.ts", "file::src/b.ts", "IMPORTS"),
|
|
87
|
+
];
|
|
88
|
+
const { profiles } = countConnections(nodes, edges);
|
|
89
|
+
expect(profiles.get("file::src/b.ts").importedBy).toBe(2);
|
|
90
|
+
expect(profiles.get("file::src/a.ts").importedBy).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
it("should compute p75 maxima that are at least 1", () => {
|
|
93
|
+
const nodes = [
|
|
94
|
+
makeNode({ id: "a", name: "a" }),
|
|
95
|
+
makeNode({ id: "b", name: "b" }),
|
|
96
|
+
];
|
|
97
|
+
const edges = [];
|
|
98
|
+
const { maxima } = countConnections(nodes, edges);
|
|
99
|
+
expect(maxima.p75IncomingCalls).toBeGreaterThanOrEqual(1);
|
|
100
|
+
expect(maxima.p75OutgoingCalls).toBeGreaterThanOrEqual(1);
|
|
101
|
+
});
|
|
102
|
+
it("should give a node with more connections a higher p75 signal", () => {
|
|
103
|
+
const nodes = Array.from({ length: 10 }, (_, i) => makeNode({ id: `n${i}`, name: `func${i}` }));
|
|
104
|
+
// n0 is called by 8 nodes — highest incoming
|
|
105
|
+
const edges = Array.from({ length: 8 }, (_, i) => makeEdge(`n${i + 1}`, "n0", "CALLS"));
|
|
106
|
+
const { profiles, maxima } = countConnections(nodes, edges);
|
|
107
|
+
expect(profiles.get("n0").incomingCalls).toBe(8);
|
|
108
|
+
expect(maxima.p75IncomingCalls).toBeGreaterThanOrEqual(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// ─── nodeScorer ───────────────────────────────────────────────────────────────
|
|
112
|
+
describe("scoreNode", () => {
|
|
113
|
+
const defaultMaxima = {
|
|
114
|
+
maxIncomingCalls: 10,
|
|
115
|
+
maxOutgoingCalls: 10,
|
|
116
|
+
maxIncomingReads: 10,
|
|
117
|
+
maxIncomingWrites: 10,
|
|
118
|
+
maxIncomingProps: 10,
|
|
119
|
+
maxOutgoingProps: 10,
|
|
120
|
+
maxImportedBy: 10,
|
|
121
|
+
p75IncomingCalls: 3,
|
|
122
|
+
p75OutgoingCalls: 3,
|
|
123
|
+
p75IncomingReads: 3,
|
|
124
|
+
p75IncomingProps: 3,
|
|
125
|
+
};
|
|
126
|
+
const emptyProfile = {
|
|
127
|
+
incomingCalls: 0,
|
|
128
|
+
outgoingCalls: 0,
|
|
129
|
+
incomingReads: 0,
|
|
130
|
+
incomingWrites: 0,
|
|
131
|
+
incomingProps: 0,
|
|
132
|
+
outgoingProps: 0,
|
|
133
|
+
importedBy: 0,
|
|
134
|
+
};
|
|
135
|
+
it("should return 5.0 for GHOST nodes regardless of profile", () => {
|
|
136
|
+
const ghost = makeNode({ id: "g", name: "event:payment", type: "GHOST" });
|
|
137
|
+
const score = scoreNode(ghost, emptyProfile, defaultMaxima);
|
|
138
|
+
expect(score).toBe(5.0);
|
|
139
|
+
});
|
|
140
|
+
it("should return 0 for FILE nodes", () => {
|
|
141
|
+
const file = makeFileNode("src/payment.ts");
|
|
142
|
+
const score = scoreNode(file, emptyProfile, defaultMaxima);
|
|
143
|
+
expect(score).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
it("should score within 0-10 range", () => {
|
|
146
|
+
const node = makeNode({ id: "n", name: "processPayment", endLine: 60 });
|
|
147
|
+
const score = scoreNode(node, emptyProfile, defaultMaxima);
|
|
148
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
149
|
+
expect(score).toBeLessThanOrEqual(10);
|
|
150
|
+
});
|
|
151
|
+
it("should score a complex function higher than a trivial one", () => {
|
|
152
|
+
const complex = makeNode({
|
|
153
|
+
id: "complex", name: "processPayment", endLine: 80,
|
|
154
|
+
metadata: { apiCalls: ["/api/stripe"], hasErrorHandling: true },
|
|
155
|
+
});
|
|
156
|
+
const trivial = makeNode({
|
|
157
|
+
id: "trivial", name: "getLabel", endLine: 3,
|
|
158
|
+
metadata: {},
|
|
159
|
+
});
|
|
160
|
+
const complexScore = scoreNode(complex, emptyProfile, defaultMaxima);
|
|
161
|
+
const trivialScore = scoreNode(trivial, emptyProfile, defaultMaxima);
|
|
162
|
+
expect(complexScore).toBeGreaterThan(trivialScore);
|
|
163
|
+
});
|
|
164
|
+
it("should apply utility name penalty", () => {
|
|
165
|
+
const utilNode = makeNode({ id: "u", name: "formatDate", endLine: 10 });
|
|
166
|
+
const normalNode = makeNode({ id: "n", name: "processPayment", endLine: 10 });
|
|
167
|
+
const utilScore = scoreNode(utilNode, emptyProfile, defaultMaxima);
|
|
168
|
+
const normalScore = scoreNode(normalNode, emptyProfile, defaultMaxima);
|
|
169
|
+
expect(utilScore).toBeLessThan(normalScore);
|
|
170
|
+
});
|
|
171
|
+
it("should apply isolation penalty to tiny disconnected nodes", () => {
|
|
172
|
+
const isolated = makeNode({ id: "i", name: "doThing", endLine: 3 });
|
|
173
|
+
const connected = makeNode({ id: "c", name: "doThing", endLine: 3 });
|
|
174
|
+
const connectedProfile = { ...emptyProfile, incomingCalls: 2 };
|
|
175
|
+
const isolatedScore = scoreNode(isolated, emptyProfile, defaultMaxima);
|
|
176
|
+
const connectedScore = scoreNode(connected, connectedProfile, defaultMaxima);
|
|
177
|
+
expect(isolatedScore).toBeLessThan(connectedScore);
|
|
178
|
+
});
|
|
179
|
+
it("should give STATE_STORE a higher type bonus than FUNCTION", () => {
|
|
180
|
+
const store = makeNode({
|
|
181
|
+
id: "s", name: "useCartStore", type: "STATE_STORE", endLine: 20,
|
|
182
|
+
});
|
|
183
|
+
const func = makeNode({
|
|
184
|
+
id: "f", name: "processCart", type: "FUNCTION", endLine: 20,
|
|
185
|
+
});
|
|
186
|
+
const storeScore = scoreNode(store, emptyProfile, defaultMaxima);
|
|
187
|
+
const funcScore = scoreNode(func, emptyProfile, defaultMaxima);
|
|
188
|
+
expect(storeScore).toBeGreaterThan(funcScore);
|
|
189
|
+
});
|
|
190
|
+
it("should score a node with API calls higher than one without", () => {
|
|
191
|
+
const withApi = makeNode({
|
|
192
|
+
id: "a", name: "fetchData", endLine: 30,
|
|
193
|
+
metadata: { apiCalls: ["/api/users"] },
|
|
194
|
+
});
|
|
195
|
+
const withoutApi = makeNode({
|
|
196
|
+
id: "b", name: "fetchData", endLine: 30,
|
|
197
|
+
metadata: {},
|
|
198
|
+
});
|
|
199
|
+
const withApiScore = scoreNode(withApi, emptyProfile, defaultMaxima);
|
|
200
|
+
const withoutApiScore = scoreNode(withoutApi, emptyProfile, defaultMaxima);
|
|
201
|
+
expect(withApiScore).toBeGreaterThan(withoutApiScore);
|
|
202
|
+
});
|
|
203
|
+
it("should score a heavily connected node higher than an isolated one", () => {
|
|
204
|
+
const node = makeNode({ id: "n", name: "processPayment", endLine: 30 });
|
|
205
|
+
const isolatedProfile = { ...emptyProfile };
|
|
206
|
+
const connectedProfile = { ...emptyProfile, incomingCalls: 5, outgoingCalls: 3 };
|
|
207
|
+
const isolatedScore = scoreNode(node, isolatedProfile, defaultMaxima);
|
|
208
|
+
const connectedScore = scoreNode(node, connectedProfile, defaultMaxima);
|
|
209
|
+
expect(connectedScore).toBeGreaterThan(isolatedScore);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ─── fileScorer ───────────────────────────────────────────────────────────────
|
|
213
|
+
describe("scoreFile", () => {
|
|
214
|
+
it("should return 0 for non-FILE nodes", () => {
|
|
215
|
+
const func = makeNode({ id: "f", name: "processPayment" });
|
|
216
|
+
const scores = new Map();
|
|
217
|
+
expect(scoreFile(func, [], scores, 0)).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
it("should score higher when best child scores higher", () => {
|
|
220
|
+
const fileNode = makeFileNode("src/payment.ts");
|
|
221
|
+
const childA = makeNode({ id: "a", name: "funcA", parentFile: fileNode.id });
|
|
222
|
+
const childB = makeNode({ id: "b", name: "funcB", parentFile: fileNode.id });
|
|
223
|
+
const lowScores = new Map([["a", 3.0], ["b", 2.0]]);
|
|
224
|
+
const highScores = new Map([["a", 8.0], ["b", 2.0]]);
|
|
225
|
+
const lowScore = scoreFile(fileNode, [childA, childB], lowScores, 0);
|
|
226
|
+
const highScore = scoreFile(fileNode, [childA, childB], highScores, 0);
|
|
227
|
+
expect(highScore).toBeGreaterThan(lowScore);
|
|
228
|
+
});
|
|
229
|
+
it("should boost score when imported by more files", () => {
|
|
230
|
+
const fileNode = makeFileNode("src/utils.ts");
|
|
231
|
+
const child = makeNode({ id: "c", name: "helper", parentFile: fileNode.id });
|
|
232
|
+
const scores = new Map([["c", 4.0]]);
|
|
233
|
+
const lowImport = scoreFile(fileNode, [child], scores, 0);
|
|
234
|
+
const highImport = scoreFile(fileNode, [child], scores, 20);
|
|
235
|
+
expect(highImport).toBeGreaterThan(lowImport);
|
|
236
|
+
});
|
|
237
|
+
it("should apply best child floor — file should not score below 90% of best child", () => {
|
|
238
|
+
const fileNode = makeFileNode("src/payment.ts");
|
|
239
|
+
const children = [
|
|
240
|
+
makeNode({ id: "a", name: "criticalFn", parentFile: fileNode.id }),
|
|
241
|
+
makeNode({ id: "b", name: "helperOne", parentFile: fileNode.id }),
|
|
242
|
+
makeNode({ id: "c", name: "helperTwo", parentFile: fileNode.id }),
|
|
243
|
+
makeNode({ id: "d", name: "helperThree", parentFile: fileNode.id }),
|
|
244
|
+
];
|
|
245
|
+
// One critical node, many helpers dragging G_int down
|
|
246
|
+
const scores = new Map([
|
|
247
|
+
["a", 8.0],
|
|
248
|
+
["b", 1.0],
|
|
249
|
+
["c", 1.0],
|
|
250
|
+
["d", 1.0],
|
|
251
|
+
]);
|
|
252
|
+
const fileScore = scoreFile(fileNode, children, scores, 0);
|
|
253
|
+
// File should score at least 90% of best child (8.0 × 0.90 = 7.2)
|
|
254
|
+
expect(fileScore).toBeGreaterThanOrEqual(7.2);
|
|
255
|
+
});
|
|
256
|
+
it("should give empty file a score based only on reputation", () => {
|
|
257
|
+
const fileNode = makeFileNode("src/types.ts");
|
|
258
|
+
const scores = new Map();
|
|
259
|
+
const noImports = scoreFile(fileNode, [], scores, 0);
|
|
260
|
+
const manyImports = scoreFile(fileNode, [], scores, 30);
|
|
261
|
+
expect(noImports).toBeLessThan(manyImports);
|
|
262
|
+
expect(noImports).toBeCloseTo(0, 0);
|
|
263
|
+
});
|
|
264
|
+
it("should never exceed 10", () => {
|
|
265
|
+
const fileNode = makeFileNode("src/critical.ts");
|
|
266
|
+
const child = makeNode({ id: "c", name: "godFunction", parentFile: fileNode.id });
|
|
267
|
+
const scores = new Map([["c", 10.0]]);
|
|
268
|
+
const fileScore = scoreFile(fileNode, [child], scores, 1000);
|
|
269
|
+
expect(fileScore).toBeLessThanOrEqual(10);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
// ─── noiseFilter ─────────────────────────────────────────────────────────────
|
|
273
|
+
describe("filterNoise", () => {
|
|
274
|
+
it("should remove nodes below threshold", () => {
|
|
275
|
+
const nodes = [
|
|
276
|
+
makeNode({ id: "important", name: "processPayment" }),
|
|
277
|
+
makeNode({ id: "noise", name: "formatDate" }),
|
|
278
|
+
];
|
|
279
|
+
const edges = [];
|
|
280
|
+
const scores = new Map([
|
|
281
|
+
["important", 7.0],
|
|
282
|
+
["noise", 1.0],
|
|
283
|
+
]);
|
|
284
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
285
|
+
expect(result.nodes.find((n) => n.id === "important")).toBeDefined();
|
|
286
|
+
expect(result.nodes.find((n) => n.id === "noise")).toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
it("should always keep STATE_STORE nodes regardless of score", () => {
|
|
289
|
+
const store = makeNode({
|
|
290
|
+
id: "store", name: "useCartStore", type: "STATE_STORE",
|
|
291
|
+
});
|
|
292
|
+
const scores = new Map([["store", 0.5]]);
|
|
293
|
+
const result = filterNoise([store], [], scores, { nodeMinScore: 3.0 });
|
|
294
|
+
expect(result.nodes.find((n) => n.id === "store")).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
it("should always keep GHOST nodes regardless of score", () => {
|
|
297
|
+
const ghost = makeNode({ id: "g", name: "event:payment", type: "GHOST" });
|
|
298
|
+
const scores = new Map([["g", 0.1]]);
|
|
299
|
+
const result = filterNoise([ghost], [], scores, { nodeMinScore: 3.0 });
|
|
300
|
+
expect(result.nodes.find((n) => n.id === "g")).toBeDefined();
|
|
301
|
+
});
|
|
302
|
+
it("should remove edges where both nodes were removed", () => {
|
|
303
|
+
const nodes = [
|
|
304
|
+
makeNode({ id: "a", name: "funcA" }),
|
|
305
|
+
makeNode({ id: "b", name: "funcB" }),
|
|
306
|
+
];
|
|
307
|
+
const edges = [makeEdge("a", "b", "CALLS")];
|
|
308
|
+
const scores = new Map([["a", 1.0], ["b", 1.0]]);
|
|
309
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
310
|
+
expect(result.edges.length).toBe(0);
|
|
311
|
+
});
|
|
312
|
+
it("should keep edges where at least one node survived", () => {
|
|
313
|
+
const nodes = [
|
|
314
|
+
makeNode({ id: "important", name: "processPayment" }),
|
|
315
|
+
makeNode({ id: "noise", name: "formatDate" }),
|
|
316
|
+
];
|
|
317
|
+
const edges = [makeEdge("important", "noise", "CALLS")];
|
|
318
|
+
const scores = new Map([["important", 7.0], ["noise", 1.0]]);
|
|
319
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
320
|
+
// Edge removed because "noise" node was removed
|
|
321
|
+
expect(result.edges.length).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
it("should always keep GUARDS edges", () => {
|
|
324
|
+
const nodes = [
|
|
325
|
+
makeNode({ id: "mw", name: "authMiddleware" }),
|
|
326
|
+
makeNode({ id: "route", name: "adminRoute" }),
|
|
327
|
+
];
|
|
328
|
+
const edges = [makeEdge("mw", "route", "GUARDS")];
|
|
329
|
+
const scores = new Map([["mw", 1.0], ["route", 1.0]]);
|
|
330
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
331
|
+
expect(result.edges.find((e) => e.type === "GUARDS")).toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
it("should always keep READS_FROM edges", () => {
|
|
334
|
+
const nodes = [
|
|
335
|
+
makeNode({ id: "comp", name: "CartButton", type: "COMPONENT" }),
|
|
336
|
+
makeNode({ id: "store", name: "useCartStore", type: "STATE_STORE" }),
|
|
337
|
+
];
|
|
338
|
+
const edges = [makeEdge("comp", "store", "READS_FROM")];
|
|
339
|
+
const scores = new Map([["comp", 1.0], ["store", 1.0]]);
|
|
340
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
341
|
+
expect(result.edges.find((e) => e.type === "READS_FROM")).toBeDefined();
|
|
342
|
+
});
|
|
343
|
+
it("should rescue FILE node if it has a kept child", () => {
|
|
344
|
+
const fileNode = makeFileNode("src/payment.ts");
|
|
345
|
+
const important = makeNode({
|
|
346
|
+
id: "imp", name: "processPayment", parentFile: fileNode.id,
|
|
347
|
+
});
|
|
348
|
+
const nodes = [fileNode, important];
|
|
349
|
+
const edges = [];
|
|
350
|
+
const scores = new Map([
|
|
351
|
+
[fileNode.id, 1.0], // file scores below threshold
|
|
352
|
+
["imp", 7.0], // but child is important
|
|
353
|
+
]);
|
|
354
|
+
const result = filterNoise(nodes, edges, scores, {
|
|
355
|
+
nodeMinScore: 3.0,
|
|
356
|
+
fileMinScore: 2.0,
|
|
357
|
+
});
|
|
358
|
+
// File should be rescued because its child survived
|
|
359
|
+
expect(result.nodes.find((n) => n.id === fileNode.id)).toBeDefined();
|
|
360
|
+
});
|
|
361
|
+
it("should respect UI threshold overrides", () => {
|
|
362
|
+
const nodes = [
|
|
363
|
+
makeNode({ id: "a", name: "funcA" }),
|
|
364
|
+
makeNode({ id: "b", name: "funcB" }),
|
|
365
|
+
];
|
|
366
|
+
const scores = new Map([["a", 5.0], ["b", 3.5]]);
|
|
367
|
+
// Strict threshold — only a survives
|
|
368
|
+
const strict = filterNoise(nodes, [], scores, { nodeMinScore: 4.0 });
|
|
369
|
+
expect(strict.nodes.length).toBe(1);
|
|
370
|
+
// Loose threshold — both survive
|
|
371
|
+
const loose = filterNoise(nodes, [], scores, { nodeMinScore: 2.0 });
|
|
372
|
+
expect(loose.nodes.length).toBe(2);
|
|
373
|
+
});
|
|
374
|
+
it("should report correct removed counts", () => {
|
|
375
|
+
const nodes = [
|
|
376
|
+
makeNode({ id: "a", name: "funcA" }),
|
|
377
|
+
makeNode({ id: "b", name: "funcB" }),
|
|
378
|
+
makeNode({ id: "c", name: "funcC" }),
|
|
379
|
+
];
|
|
380
|
+
const edges = [makeEdge("a", "b", "CALLS")];
|
|
381
|
+
const scores = new Map([["a", 7.0], ["b", 1.0], ["c", 1.0]]);
|
|
382
|
+
const result = filterNoise(nodes, edges, scores, { nodeMinScore: 3.0 });
|
|
383
|
+
expect(result.removedNodeCount).toBe(2);
|
|
384
|
+
expect(result.removedEdgeCount).toBe(1);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// ─── Full pipeline ────────────────────────────────────────────────────────────
|
|
388
|
+
describe("scoreAndFilter", () => {
|
|
389
|
+
it("should return filtered nodes and edges", () => {
|
|
390
|
+
const nodes = [
|
|
391
|
+
makeFileNode("src/payment.ts"),
|
|
392
|
+
makeNode({
|
|
393
|
+
id: "proc", name: "processPayment",
|
|
394
|
+
endLine: 60, parentFile: "file::src/payment.ts",
|
|
395
|
+
metadata: { apiCalls: ["/api/stripe"], hasErrorHandling: true },
|
|
396
|
+
}),
|
|
397
|
+
makeNode({
|
|
398
|
+
id: "fmt", name: "formatDate",
|
|
399
|
+
endLine: 3, parentFile: "file::src/payment.ts",
|
|
400
|
+
metadata: {},
|
|
401
|
+
}),
|
|
402
|
+
];
|
|
403
|
+
const edges = [makeEdge("proc", "fmt", "CALLS")];
|
|
404
|
+
const result = scoreAndFilter(nodes, edges);
|
|
405
|
+
expect(result.filteredNodes).toBeDefined();
|
|
406
|
+
expect(result.filteredEdges).toBeDefined();
|
|
407
|
+
expect(result.nodeScores).toBeDefined();
|
|
408
|
+
expect(result.stats).toBeDefined();
|
|
409
|
+
});
|
|
410
|
+
it("should score complex nodes higher than trivial ones", () => {
|
|
411
|
+
const nodes = [
|
|
412
|
+
makeFileNode("src/payment.ts"),
|
|
413
|
+
makeNode({
|
|
414
|
+
id: "complex", name: "processPayment",
|
|
415
|
+
endLine: 80, parentFile: "file::src/payment.ts",
|
|
416
|
+
metadata: { apiCalls: ["/api/stripe"], hasErrorHandling: true },
|
|
417
|
+
}),
|
|
418
|
+
makeNode({
|
|
419
|
+
id: "trivial", name: "formatDate",
|
|
420
|
+
endLine: 3, parentFile: "file::src/payment.ts",
|
|
421
|
+
metadata: {},
|
|
422
|
+
}),
|
|
423
|
+
];
|
|
424
|
+
const result = scoreAndFilter(nodes, []);
|
|
425
|
+
const complexScore = result.nodeScores.get("complex") ?? 0;
|
|
426
|
+
const trivialScore = result.nodeScores.get("trivial") ?? 0;
|
|
427
|
+
expect(complexScore).toBeGreaterThan(trivialScore);
|
|
428
|
+
});
|
|
429
|
+
it("should include top scoring nodes in stats", () => {
|
|
430
|
+
const nodes = [
|
|
431
|
+
makeFileNode("src/payment.ts"),
|
|
432
|
+
makeNode({
|
|
433
|
+
id: "n", name: "processPayment",
|
|
434
|
+
endLine: 50, parentFile: "file::src/payment.ts",
|
|
435
|
+
metadata: { apiCalls: ["/api/stripe"] },
|
|
436
|
+
}),
|
|
437
|
+
];
|
|
438
|
+
const result = scoreAndFilter(nodes, []);
|
|
439
|
+
expect(result.stats.topScoringNodes.length).toBeGreaterThan(0);
|
|
440
|
+
expect(result.stats.topScoringNodes[0]).toHaveProperty("name");
|
|
441
|
+
expect(result.stats.topScoringNodes[0]).toHaveProperty("score");
|
|
442
|
+
});
|
|
443
|
+
it("should respect threshold overrides from UI", () => {
|
|
444
|
+
const nodes = [
|
|
445
|
+
makeFileNode("src/payment.ts"),
|
|
446
|
+
makeNode({ id: "a", name: "funcA", endLine: 50, parentFile: "file::src/payment.ts" }),
|
|
447
|
+
makeNode({ id: "b", name: "funcB", endLine: 3, parentFile: "file::src/payment.ts" }),
|
|
448
|
+
];
|
|
449
|
+
const strict = scoreAndFilter(nodes, [], { nodeMinScore: 8.0 });
|
|
450
|
+
const loose = scoreAndFilter(nodes, [], { nodeMinScore: 1.0 });
|
|
451
|
+
expect(strict.filteredNodes.length).toBeLessThan(loose.filteredNodes.length);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Utility name patterns — low-value nodes
|
|
2
|
+
const UTILITY_PATTERN = /^(format|get|set|is|has|cn|clsx|classNames|util|helper|parse|convert|transform|sanitize|normalize|encode|decode|map|filter|reduce|sort|group|chunk|flatten|merge|pick|omit|debounce|throttle|memoize|curry|compose|pipe)/i;
|
|
3
|
+
// Logarithmic normalization using 75th percentile (top 25% ~1.0)
|
|
4
|
+
function logNorm(count, p75) {
|
|
5
|
+
if (p75 <= 0 || count <= 0)
|
|
6
|
+
return 0;
|
|
7
|
+
return Math.min(1.0, Math.log10(count + 1) / Math.log10(p75 + 1));
|
|
8
|
+
}
|
|
9
|
+
// Complexity bucket (0-4): lines + API calls + error handling + outgoing calls
|
|
10
|
+
function calcComplexity(node, profile) {
|
|
11
|
+
const lines = node.endLine - node.startLine;
|
|
12
|
+
let lineSignal;
|
|
13
|
+
if (lines < 5)
|
|
14
|
+
lineSignal = 0.1;
|
|
15
|
+
else if (lines < 15)
|
|
16
|
+
lineSignal = 0.3;
|
|
17
|
+
else if (lines < 30)
|
|
18
|
+
lineSignal = 0.5;
|
|
19
|
+
else if (lines < 60)
|
|
20
|
+
lineSignal = 0.7;
|
|
21
|
+
else if (lines < 100)
|
|
22
|
+
lineSignal = 0.9;
|
|
23
|
+
else
|
|
24
|
+
lineSignal = 1.0;
|
|
25
|
+
const apiCalls = node.metadata.apiCalls;
|
|
26
|
+
const apiSignal = apiCalls && apiCalls.length > 0 ? 1.0 : 0.0;
|
|
27
|
+
const errorSignal = node.metadata.hasErrorHandling === true ? 1.0 : 0.0;
|
|
28
|
+
let callDepthSignal;
|
|
29
|
+
const outgoing = profile.outgoingCalls;
|
|
30
|
+
if (outgoing === 0)
|
|
31
|
+
callDepthSignal = 0.0;
|
|
32
|
+
else if (outgoing <= 2)
|
|
33
|
+
callDepthSignal = 0.3;
|
|
34
|
+
else if (outgoing <= 4)
|
|
35
|
+
callDepthSignal = 0.6;
|
|
36
|
+
else if (outgoing <= 6)
|
|
37
|
+
callDepthSignal = 0.8;
|
|
38
|
+
else
|
|
39
|
+
callDepthSignal = 1.0;
|
|
40
|
+
return (lineSignal + apiSignal + errorSignal + callDepthSignal);
|
|
41
|
+
}
|
|
42
|
+
// Connections bucket (0-3): log-norm signals w/ dominant amplification + isolation penalty
|
|
43
|
+
function calcConnections(node, profile, maxima) {
|
|
44
|
+
const incomingCallsSignal = logNorm(profile.incomingCalls, maxima.p75IncomingCalls);
|
|
45
|
+
const outgoingCallsSignal = logNorm(profile.outgoingCalls, maxima.p75OutgoingCalls);
|
|
46
|
+
const stateDependency = profile.incomingReads + profile.incomingWrites;
|
|
47
|
+
const stateSignal = logNorm(stateDependency, maxima.p75IncomingReads);
|
|
48
|
+
const propSignal = logNorm(profile.incomingProps, maxima.p75IncomingProps);
|
|
49
|
+
const totalConnectivity = profile.incomingCalls +
|
|
50
|
+
profile.outgoingCalls +
|
|
51
|
+
profile.incomingReads +
|
|
52
|
+
profile.incomingWrites +
|
|
53
|
+
profile.incomingProps;
|
|
54
|
+
const isolationPenalty = totalConnectivity === 0 ? -1.0 : 0.0;
|
|
55
|
+
const primarySignal = Math.max(incomingCallsSignal, outgoingCallsSignal, stateSignal, propSignal);
|
|
56
|
+
const secondarySum = incomingCallsSignal + outgoingCallsSignal + stateSignal + propSignal
|
|
57
|
+
- primarySignal;
|
|
58
|
+
const raw = primarySignal * 2.5 + Math.min(0.5, secondarySum) + isolationPenalty;
|
|
59
|
+
return Math.min(3.0, Math.max(0, raw));
|
|
60
|
+
}
|
|
61
|
+
// Type bonus (0-2)
|
|
62
|
+
function calcTypeBonus(node) {
|
|
63
|
+
switch (node.type) {
|
|
64
|
+
case "STATE_STORE": return 2.0;
|
|
65
|
+
case "COMPONENT": return 0.75;
|
|
66
|
+
case "HOOK": return 0.75;
|
|
67
|
+
case "FUNCTION": return 0.5;
|
|
68
|
+
case "ROUTE": return 2;
|
|
69
|
+
case "TEST": return 0.3; // low bonus because test files are secondary, do not play role in the logical part
|
|
70
|
+
case "STORY": return 0.3;
|
|
71
|
+
default: return 0.0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Noise penalty (0 to -2): utility names + tiny isolated functions
|
|
75
|
+
function calcNoisePenalty(node, profile) {
|
|
76
|
+
let penalty = 0.0;
|
|
77
|
+
if (UTILITY_PATTERN.test(node.name)) {
|
|
78
|
+
penalty -= 1.0;
|
|
79
|
+
}
|
|
80
|
+
const lines = node.endLine - node.startLine;
|
|
81
|
+
const totalIncoming = profile.incomingCalls +
|
|
82
|
+
profile.incomingReads +
|
|
83
|
+
profile.incomingWrites +
|
|
84
|
+
profile.incomingProps;
|
|
85
|
+
if (lines < 5 && totalIncoming === 0) {
|
|
86
|
+
penalty -= 1.0;
|
|
87
|
+
}
|
|
88
|
+
return penalty;
|
|
89
|
+
}
|
|
90
|
+
// Main scorer: base(1) + complexity(4) + connections(3) + type(2) + noise(-2) → 0-10
|
|
91
|
+
export function scoreNode(node, profile, maxima) {
|
|
92
|
+
if (node.type === "GHOST")
|
|
93
|
+
return 5.0;
|
|
94
|
+
if (node.type === "FILE")
|
|
95
|
+
return 0;
|
|
96
|
+
if (node.type === "THIRD_PARTY") {
|
|
97
|
+
// Score by how many local files import it (fan-in), +0.5 flat bonus.
|
|
98
|
+
const connectionScore = calcConnections(node, profile, maxima);
|
|
99
|
+
return Math.min(10, connectionScore + 0.5);
|
|
100
|
+
}
|
|
101
|
+
const base = 1.0;
|
|
102
|
+
const complexity = calcComplexity(node, profile);
|
|
103
|
+
const connections = calcConnections(node, profile, maxima);
|
|
104
|
+
const typeBonus = calcTypeBonus(node);
|
|
105
|
+
const noise = calcNoisePenalty(node, profile);
|
|
106
|
+
const raw = base + complexity + connections + typeBonus + noise;
|
|
107
|
+
return Math.min(10, Math.max(0, raw));
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CodeNode, CodeEdge } from "../types.js";
|
|
2
|
+
export interface FilterResult {
|
|
3
|
+
nodes: CodeNode[];
|
|
4
|
+
edges: CodeEdge[];
|
|
5
|
+
removedNodeCount: number;
|
|
6
|
+
removedEdgeCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_THRESHOLDS: {
|
|
9
|
+
NODE_MIN_SCORE: number;
|
|
10
|
+
FILE_MIN_SCORE: number;
|
|
11
|
+
GHOST_MIN_SCORE: number;
|
|
12
|
+
};
|
|
13
|
+
export interface FilterThresholds {
|
|
14
|
+
nodeMinScore?: number;
|
|
15
|
+
fileMinScore?: number;
|
|
16
|
+
ghostMinScore?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function filterNoise(nodes: CodeNode[], edges: CodeEdge[], nodeScores: Map<string, number>, thresholds?: FilterThresholds): FilterResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Default Thresholds — used when UI does not overrides, exposed as constants so contributors can tune them
|
|
2
|
+
export const DEFAULT_THRESHOLDS = {
|
|
3
|
+
// Non-FILE nodes below this score are removed
|
|
4
|
+
NODE_MIN_SCORE: 0,
|
|
5
|
+
// FILE nodes below this score are removed
|
|
6
|
+
// Lower than node threshold because files can be important
|
|
7
|
+
// even if their children scored low (e.g. constants files)
|
|
8
|
+
FILE_MIN_SCORE: 0,
|
|
9
|
+
// Ghost nodes are never removed — they represent real connections
|
|
10
|
+
GHOST_MIN_SCORE: 0,
|
|
11
|
+
};
|
|
12
|
+
// Edge types that are never removed regardless of node scores
|
|
13
|
+
const PROTECTED_EDGE_TYPES = new Set([
|
|
14
|
+
"GUARDS",
|
|
15
|
+
"READS_FROM",
|
|
16
|
+
"WRITES_TO",
|
|
17
|
+
"HANDLES",
|
|
18
|
+
"TESTS",
|
|
19
|
+
]);
|
|
20
|
+
// Node types that are never removed regardless of score
|
|
21
|
+
const PROTECTED_NODE_TYPES = new Set([
|
|
22
|
+
"STATE_STORE",
|
|
23
|
+
"GHOST",
|
|
24
|
+
"ROUTE",
|
|
25
|
+
"STORY",
|
|
26
|
+
"TEST",
|
|
27
|
+
"THIRD_PARTY",
|
|
28
|
+
]);
|
|
29
|
+
export function filterNoise(nodes, edges, nodeScores, thresholds // ← optional, UI can override
|
|
30
|
+
) {
|
|
31
|
+
const originalNodeCount = nodes.length;
|
|
32
|
+
const originalEdgeCount = edges.length;
|
|
33
|
+
// Merge UI overrides with defaults
|
|
34
|
+
// UI only needs to pass what it wants to change
|
|
35
|
+
const activeThresholds = {
|
|
36
|
+
nodeMinScore: thresholds?.nodeMinScore ?? DEFAULT_THRESHOLDS.NODE_MIN_SCORE,
|
|
37
|
+
fileMinScore: thresholds?.fileMinScore ?? DEFAULT_THRESHOLDS.FILE_MIN_SCORE,
|
|
38
|
+
ghostMinScore: thresholds?.ghostMinScore ?? DEFAULT_THRESHOLDS.GHOST_MIN_SCORE,
|
|
39
|
+
};
|
|
40
|
+
// ─── Step 1 — Determine which nodes to keep ───────────────────
|
|
41
|
+
const keepNodeIds = new Set();
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
const score = nodeScores.get(node.id) ?? 0;
|
|
44
|
+
// Protected node types are always kept
|
|
45
|
+
if (PROTECTED_NODE_TYPES.has(node.type)) {
|
|
46
|
+
keepNodeIds.add(node.id);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// FILE nodes use their own threshold
|
|
50
|
+
if (node.type === "FILE") {
|
|
51
|
+
if (score >= activeThresholds.fileMinScore) {
|
|
52
|
+
keepNodeIds.add(node.id);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// All other nodes use the standard threshold
|
|
57
|
+
if (score >= activeThresholds.nodeMinScore) {
|
|
58
|
+
keepNodeIds.add(node.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ─── Step 2 — Rescue FILE nodes with kept children ────────────
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if (node.type !== "FILE")
|
|
64
|
+
continue;
|
|
65
|
+
if (keepNodeIds.has(node.id))
|
|
66
|
+
continue;
|
|
67
|
+
const hasKeptChild = nodes.some((n) => n.parentFile === node.id && keepNodeIds.has(n.id));
|
|
68
|
+
if (hasKeptChild)
|
|
69
|
+
keepNodeIds.add(node.id);
|
|
70
|
+
}
|
|
71
|
+
// ─── Step 3 — Filter nodes ────────────────────────────────────
|
|
72
|
+
const filteredNodes = nodes.filter((n) => keepNodeIds.has(n.id));
|
|
73
|
+
for (const node of filteredNodes) {
|
|
74
|
+
// clean resolvedCalls...we are not cleaning metadata.calls because the calls are passed to the LLM for context. So js specific calls or built in method calls are important for context understanding context
|
|
75
|
+
const resolvedCalls = node.metadata.resolvedCalls;
|
|
76
|
+
if (resolvedCalls?.length) {
|
|
77
|
+
node.metadata.resolvedCalls = resolvedCalls.filter(r => keepNodeIds.has(r.nodeId));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ─── Step 4 — Filter edges ────────────────────────────────────
|
|
81
|
+
const filteredEdges = edges.filter((edge) => {
|
|
82
|
+
if (PROTECTED_EDGE_TYPES.has(edge.type))
|
|
83
|
+
return true;
|
|
84
|
+
return keepNodeIds.has(edge.from) && keepNodeIds.has(edge.to);
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
nodes: filteredNodes,
|
|
88
|
+
edges: filteredEdges,
|
|
89
|
+
removedNodeCount: originalNodeCount - filteredNodes.length,
|
|
90
|
+
removedEdgeCount: originalEdgeCount - filteredEdges.length,
|
|
91
|
+
};
|
|
92
|
+
}
|