Skip to content

Commit 783819d

Browse files
authored
Merge pull request #490 from DashBot-0001/opt/verified-wins
perf+robustness: eight verified hot-path fixes for large graphs (tour-gen O(n²)→O(n), single-parse, louvain crash, …)
2 parents eb33a5f + 921526c commit 783819d

19 files changed

Lines changed: 351 additions & 65 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "understand-anything",
33
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
4-
"version": "2.9.1",
4+
"version": "2.9.2",
55
"author": {
66
"name": "Egonex"
77
},

.copilot-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "understand-anything",
33
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
4-
"version": "2.9.1",
4+
"version": "2.9.2",
55
"author": {
66
"name": "Egonex"
77
},

.cursor-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "understand-anything",
33
"displayName": "Understand Anything",
44
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
5-
"version": "2.9.1",
5+
"version": "2.9.2",
66
"author": {
77
"name": "Egonex"
88
},

understand-anything-plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "understand-anything",
33
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
4-
"version": "2.9.1",
4+
"version": "2.9.2",
55
"author": {
66
"name": "Egonex"
77
},

understand-anything-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@understand-anything/skill",
3-
"version": "2.9.1",
3+
"version": "2.9.2",
44
"type": "module",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

understand-anything-plugin/packages/core/src/__tests__/plugin-registry.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,38 @@ describe("PluginRegistry", () => {
112112
expect(result).toBeNull();
113113
});
114114

115+
it("analyzeFileFull delegates when the plugin implements it", () => {
116+
const registry = new PluginRegistry();
117+
const plugin = createMockPlugin("ts-plugin", ["typescript"]);
118+
plugin.analyzeFileFull = () => ({
119+
structure: {
120+
...emptyAnalysis,
121+
functions: [{ name: "hello", lineRange: [1, 5], params: [] }],
122+
},
123+
callGraph: [{ caller: "hello", callee: "world", lineNumber: 2 }],
124+
});
125+
registry.register(plugin);
126+
127+
const result = registry.analyzeFileFull("src/test.ts", "const x = 1;");
128+
expect(result).not.toBeNull();
129+
expect(result!.structure.functions).toHaveLength(1);
130+
expect(result!.callGraph).toHaveLength(1);
131+
});
132+
133+
it("analyzeFileFull returns null when the plugin lacks the method (caller falls back)", () => {
134+
const registry = new PluginRegistry();
135+
registry.register(createMockPlugin("ts-plugin", ["typescript"]));
136+
const result = registry.analyzeFileFull("src/test.ts", "const x = 1;");
137+
expect(result).toBeNull();
138+
});
139+
140+
it("analyzeFileFull returns null for unsupported files", () => {
141+
const registry = new PluginRegistry();
142+
registry.register(createMockPlugin("ts-plugin", ["typescript"]));
143+
const result = registry.analyzeFileFull("main.py", "print('hello')");
144+
expect(result).toBeNull();
145+
});
146+
115147
it("unregister rebuilds language map correctly", () => {
116148
const registry = new PluginRegistry();
117149
const plugin1 = createMockPlugin("plugin1", ["typescript", "javascript"]);

understand-anything-plugin/packages/core/src/analyzer/layer-detector.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,28 @@ function matchFileToLayer(filePath: string): string | null {
104104
*/
105105
export function detectLayers(graph: KnowledgeGraph): Layer[] {
106106
const layerMap = new Map<string, string[]>(); // layerName -> nodeIds
107+
// file nodes without filePath go to "Core" *after* the main pass, so a
108+
// single sweep over graph.nodes replaces the previous two full passes while
109+
// preserving the original ordering (all with-path entries first, then
110+
// path-less ones) and the Map key-insertion order.
111+
const corePathless: string[] = [];
107112

108113
for (const node of graph.nodes) {
109114
if (node.type !== "file") continue;
110-
if (!node.filePath) continue;
115+
if (!node.filePath) {
116+
corePathless.push(node.id);
117+
continue;
118+
}
111119

112120
const layerName = matchFileToLayer(node.filePath) ?? "Core";
113121
const existing = layerMap.get(layerName) ?? [];
114122
existing.push(node.id);
115123
layerMap.set(layerName, existing);
116124
}
117125

118-
// Also catch file nodes without filePath
119-
for (const node of graph.nodes) {
120-
if (node.type !== "file") continue;
121-
if (node.filePath) continue;
122-
126+
if (corePathless.length > 0) {
123127
const existing = layerMap.get("Core") ?? [];
124-
existing.push(node.id);
128+
for (const id of corePathless) existing.push(id);
125129
layerMap.set("Core", existing);
126130
}
127131

understand-anything-plugin/packages/core/src/analyzer/tour-generator.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,11 @@ export function generateHeuristicTour(graph: KnowledgeGraph): TourStep[] {
165165
}
166166

167167
const topoOrder: string[] = [];
168-
while (queue.length > 0) {
169-
const current = queue.shift()!;
168+
// Index cursor instead of queue.shift(): shift() is O(n) (re-indexes the
169+
// whole array) → O(n²) over the BFS. A head pointer makes each dequeue O(1).
170+
let head = 0;
171+
while (head < queue.length) {
172+
const current = queue[head++];
170173
topoOrder.push(current);
171174

172175
for (const neighbor of adjacency.get(current) ?? []) {
@@ -178,10 +181,15 @@ export function generateHeuristicTour(graph: KnowledgeGraph): TourStep[] {
178181
}
179182
}
180183

181-
// Add any nodes not reached by topological sort (isolated nodes or cycles)
184+
// Add any nodes not reached by topological sort (isolated nodes or cycles).
185+
// `topoOrder.includes()` per node was O(n²) over the full node set; a Set
186+
// membership test makes it O(n). Mirror the array-grows semantics by adding
187+
// to the set on push so a duplicate node id is still de-duplicated.
188+
const inTopo = new Set(topoOrder);
182189
for (const node of codeNodes) {
183-
if (!topoOrder.includes(node.id)) {
190+
if (!inTopo.has(node.id)) {
184191
topoOrder.push(node.id);
192+
inTopo.add(node.id);
185193
}
186194
}
187195

understand-anything-plugin/packages/core/src/embedding-search.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ export function cosineSimilarity(a: number[], b: number[]): number {
2929
return dot / (magA * magB);
3030
}
3131

32+
/**
33+
* Cosine similarity when the query vector's magnitude is already known.
34+
* The query is constant across an entire search() sweep, so recomputing its
35+
* magnitude (and re-squaring every query component) per candidate node is
36+
* pure waste. Same arithmetic, same order as cosineSimilarity → bit-identical
37+
* results, but it skips the per-node magA loop.
38+
*/
39+
function cosineSimilarityWithQueryMag(
40+
query: number[],
41+
queryMag: number,
42+
vec: number[],
43+
): number {
44+
if (queryMag === 0) return 0;
45+
let dot = 0;
46+
let magB = 0;
47+
for (let i = 0; i < query.length; i++) {
48+
dot += query[i] * vec[i];
49+
magB += vec[i] * vec[i];
50+
}
51+
magB = Math.sqrt(magB);
52+
if (magB === 0) return 0;
53+
return dot / (queryMag * magB);
54+
}
55+
3256
/**
3357
* Semantic search engine using vector embeddings.
3458
* Stores pre-computed embeddings for graph nodes and performs
@@ -61,13 +85,24 @@ export class SemanticSearchEngine {
6185

6286
const scored: Array<{ nodeId: string; score: number }> = [];
6387

88+
// Hoist the query magnitude out of the per-node loop — it's invariant.
89+
let queryMag = 0;
90+
for (let i = 0; i < queryEmbedding.length; i++) {
91+
queryMag += queryEmbedding[i] * queryEmbedding[i];
92+
}
93+
queryMag = Math.sqrt(queryMag);
94+
6495
for (const node of this.nodes) {
6596
if (typeFilter && !typeFilter.includes(node.type)) continue;
6697

6798
const embedding = this.embeddings.get(node.id);
6899
if (!embedding) continue;
69100

70-
const similarity = cosineSimilarity(queryEmbedding, embedding);
101+
const similarity = cosineSimilarityWithQueryMag(
102+
queryEmbedding,
103+
queryMag,
104+
embedding,
105+
);
71106
if (similarity >= threshold) {
72107
scored.push({ nodeId: node.id, score: 1 - similarity });
73108
}

understand-anything-plugin/packages/core/src/plugins/registry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ export class PluginRegistry {
7171
return plugin.extractCallGraph(filePath, content);
7272
}
7373

74+
/**
75+
* Single-parse fast path: returns both structure and call graph from one
76+
* parse when the resolved plugin supports it, else null so the caller can
77+
* fall back to separate analyzeFile + extractCallGraph calls.
78+
*/
79+
analyzeFileFull(
80+
filePath: string,
81+
content: string,
82+
): { structure: StructuralAnalysis; callGraph: CallGraphEntry[] } | null {
83+
const plugin = this.getPluginForFile(filePath);
84+
if (!plugin?.analyzeFileFull) return null;
85+
return plugin.analyzeFileFull(filePath, content);
86+
}
87+
7488
getPlugins(): AnalyzerPlugin[] {
7589
return [...this.plugins];
7690
}

0 commit comments

Comments
 (0)