Skip to content

Commit 03c8070

Browse files
authored
Merge branch 'main' into fix/incremental-edge-gap-533
2 parents ca7ba2a + ffb628c commit 03c8070

9 files changed

Lines changed: 246 additions & 50 deletions

File tree

.github/workflows/publish.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
echo "npm_tag=$NPM_TAG" >> "$GITHUB_OUTPUT"
9494
9595
build-native:
96-
needs: preflight
96+
needs: [preflight, compute-version]
9797
strategy:
9898
fail-fast: false
9999
matrix:
@@ -177,6 +177,16 @@ jobs:
177177
sudo ln -sf /lib/x86_64-linux-gnu/libgcc_s.so.1 "${GNU_LIB}/libgcc_s.so"
178178
sudo ln -sf /lib/x86_64-linux-gnu/libgcc_s.so.1 "${GNU_LIB}/libgcc_s.so.1"
179179
180+
- name: Sync Cargo.toml version
181+
env:
182+
VERSION: ${{ needs.compute-version.outputs.version }}
183+
shell: bash
184+
run: |
185+
[[ -n "$VERSION" ]] || { echo "::error::VERSION is empty — compute-version output missing"; exit 1; }
186+
CARGO="crates/codegraph-core/Cargo.toml"
187+
awk -v v="$VERSION" '!done && /^version =/{$0="version = \""v"\""; done=1}1' "$CARGO" > "${CARGO}.tmp"
188+
mv "${CARGO}.tmp" "$CARGO"
189+
180190
- name: Install napi-rs CLI
181191
run: npm install -g @napi-rs/cli@3
182192

crates/codegraph-core/src/edge_builder.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,17 @@ pub fn build_call_edges(
339339
for cls in &file_input.classes {
340340
let source_row = nodes_by_name_and_file
341341
.get(&(cls.name.as_str(), rel_path.as_str()))
342-
.and_then(|v| v.iter().find(|n| n.kind == "class"));
342+
.and_then(|v| v.iter().find(|n| {
343+
n.kind == "class" || n.kind == "struct" || n.kind == "record" || n.kind == "enum"
344+
}));
343345

344346
if let Some(source) = source_row {
345347
if let Some(ref extends_name) = cls.extends {
346348
let targets = nodes_by_name
347349
.get(extends_name.as_str())
348-
.map(|v| v.iter().filter(|n| n.kind == "class").collect::<Vec<_>>())
350+
.map(|v| v.iter().filter(|n| {
351+
n.kind == "class" || n.kind == "struct" || n.kind == "trait" || n.kind == "record"
352+
}).collect::<Vec<_>>())
349353
.unwrap_or_default();
350354
for t in targets {
351355
edges.push(ComputedEdge {
@@ -362,7 +366,7 @@ pub fn build_call_edges(
362366
.get(implements_name.as_str())
363367
.map(|v| {
364368
v.iter()
365-
.filter(|n| n.kind == "interface" || n.kind == "class")
369+
.filter(|n| n.kind == "interface" || n.kind == "class" || n.kind == "trait")
366370
.collect::<Vec<_>>()
367371
})
368372
.unwrap_or_default();

generated/benchmarks/QUERY-BENCHMARKS.md

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Latencies are median over 5 runs. Hub target = most-connected node.
55

66
| Version | Engine | fnDeps d1 | fnDeps d3 | fnDeps d5 | fnImpact d1 | fnImpact d3 | fnImpact d5 | diffImpact |
77
|---------|--------|----------:|----------:|----------:|------------:|------------:|------------:|-----------:|
8+
| 3.3.0 | native | 1.4 ↑56% | 1.3 ↑44% | 1.3 ↑44% | 1.3 ↑44% | 1.3 ↑44% | 1.3 ↑44% | 8.8ms ↑28% |
9+
| 3.3.0 | wasm | 1.3 ↑30% | 1.3 ↑30% | 1.2 ↑33% | 1.3 ↑44% | 1.3 ↑44% | 1.3 ↑44% | 8.5ms ↑39% |
810
| 3.1.4 | native | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 6.9ms ↓17% |
911
| 3.1.4 | wasm | 1 ↑11% | 1 ↑25% | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 0.9 ↑12% | 6.1ms ↓22% |
1012
| 3.1.3 | native | 0.8 ~ | 0.8 ~ | 0.8 ~ | 0.8 ~ | 0.8 ~ | 0.8 ~ | 8.3ms ↓2% |
@@ -30,42 +32,90 @@ Latencies are median over 5 runs. Hub target = most-connected node.
3032

3133
### Latest results
3234

33-
**Version:** 3.1.4 | **Date:** 2026-03-16
35+
**Version:** 3.3.0 | **Date:** 2026-03-19
3436

3537
#### Native (Rust)
3638

37-
**Targets:** hub=`src/db.js`, mid=`previous`, leaf=`docs`
39+
**Targets:** hub=`src/types.ts`, mid=`db`, leaf=`docs`
3840

3941
| Metric | Value |
4042
|--------|------:|
41-
| fnDeps depth 1 | 0.9ms |
42-
| fnDeps depth 3 | 0.9ms |
43-
| fnDeps depth 5 | 0.9ms |
44-
| fnImpact depth 1 | 0.9ms |
45-
| fnImpact depth 3 | 0.9ms |
46-
| fnImpact depth 5 | 0.9ms |
47-
| diffImpact latency | 6.9ms |
43+
| fnDeps depth 1 | 1.4ms |
44+
| fnDeps depth 3 | 1.3ms |
45+
| fnDeps depth 5 | 1.3ms |
46+
| fnImpact depth 1 | 1.3ms |
47+
| fnImpact depth 3 | 1.3ms |
48+
| fnImpact depth 5 | 1.3ms |
49+
| diffImpact latency | 8.8ms |
4850
| diffImpact affected functions | 0 |
4951
| diffImpact affected files | 0 |
5052

5153
#### WASM
5254

53-
**Targets:** hub=`src/db.js`, mid=`previous`, leaf=`docs`
55+
**Targets:** hub=`src/types.ts`, mid=`functionNodeId`, leaf=`docs`
5456

5557
| Metric | Value |
5658
|--------|------:|
57-
| fnDeps depth 1 | 1ms |
58-
| fnDeps depth 3 | 1ms |
59-
| fnDeps depth 5 | 0.9ms |
60-
| fnImpact depth 1 | 0.9ms |
61-
| fnImpact depth 3 | 0.9ms |
62-
| fnImpact depth 5 | 0.9ms |
63-
| diffImpact latency | 6.1ms |
59+
| fnDeps depth 1 | 1.3ms |
60+
| fnDeps depth 3 | 1.3ms |
61+
| fnDeps depth 5 | 1.2ms |
62+
| fnImpact depth 1 | 1.3ms |
63+
| fnImpact depth 3 | 1.3ms |
64+
| fnImpact depth 5 | 1.3ms |
65+
| diffImpact latency | 8.5ms |
6466
| diffImpact affected functions | 0 |
6567
| diffImpact affected files | 0 |
6668

6769
<!-- QUERY_BENCHMARK_DATA
6870
[
71+
{
72+
"version": "3.3.0",
73+
"date": "2026-03-19",
74+
"wasm": {
75+
"targets": {
76+
"hub": "src/types.ts",
77+
"mid": "functionNodeId",
78+
"leaf": "docs"
79+
},
80+
"fnDeps": {
81+
"depth1Ms": 1.3,
82+
"depth3Ms": 1.3,
83+
"depth5Ms": 1.2
84+
},
85+
"fnImpact": {
86+
"depth1Ms": 1.3,
87+
"depth3Ms": 1.3,
88+
"depth5Ms": 1.3
89+
},
90+
"diffImpact": {
91+
"latencyMs": 8.5,
92+
"affectedFunctions": 0,
93+
"affectedFiles": 0
94+
}
95+
},
96+
"native": {
97+
"targets": {
98+
"hub": "src/types.ts",
99+
"mid": "db",
100+
"leaf": "docs"
101+
},
102+
"fnDeps": {
103+
"depth1Ms": 1.4,
104+
"depth3Ms": 1.3,
105+
"depth5Ms": 1.3
106+
},
107+
"fnImpact": {
108+
"depth1Ms": 1.3,
109+
"depth3Ms": 1.3,
110+
"depth5Ms": 1.3
111+
},
112+
"diffImpact": {
113+
"latencyMs": 8.8,
114+
"affectedFunctions": 0,
115+
"affectedFiles": 0
116+
}
117+
}
118+
},
69119
{
70120
"version": "3.1.4",
71121
"date": "2026-03-16",

scripts/lib/bench-config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ export async function resolveBenchmarkSource() {
134134
console.error(`Warning: failed to install native package: ${err.message}`);
135135
}
136136

137+
// @huggingface/transformers is a devDependency (lazy-loaded for embeddings).
138+
// It is not installed as a transitive dep in npm mode, so install it
139+
// explicitly so the embedding benchmark workers can import it.
140+
try {
141+
const localPkg = JSON.parse(
142+
fs.readFileSync(path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', '..', 'package.json'), 'utf8'),
143+
);
144+
const hfVersion = localPkg.devDependencies?.['@huggingface/transformers'];
145+
if (hfVersion) {
146+
console.error(`Installing @huggingface/transformers@${hfVersion} for embedding benchmarks...`);
147+
execFileSync('npm', ['install', `@huggingface/transformers@${hfVersion}`, '--no-audit', '--no-fund', '--no-save'], {
148+
cwd: tmpDir,
149+
stdio: 'pipe',
150+
timeout: 120_000,
151+
});
152+
console.error('Installed @huggingface/transformers');
153+
}
154+
} catch (err) {
155+
console.error(`Warning: failed to install @huggingface/transformers: ${err.message}`);
156+
}
157+
137158
const srcDir = path.join(pkgDir, 'src');
138159

139160
if (!fs.existsSync(srcDir)) {

scripts/query-benchmark.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,32 @@ function round1(n) {
111111
return Math.round(n * 10) / 10;
112112
}
113113

114+
// Pinned hub targets — stable function names that exist across versions.
115+
// Auto-selecting the most-connected node makes version-to-version comparison
116+
// meaningless when barrel/type files get added or removed.
117+
const PINNED_HUB_CANDIDATES = ['buildGraph', 'openDb', 'loadConfig'];
118+
114119
function selectTargets() {
115120
const db = new Database(dbPath, { readonly: true });
121+
try {
122+
123+
// Try pinned candidates first for a stable hub across versions
124+
let hub = null;
125+
for (const candidate of PINNED_HUB_CANDIDATES) {
126+
const row = db
127+
.prepare(
128+
`SELECT n.name FROM nodes n
129+
JOIN edges e ON e.source_id = n.id OR e.target_id = n.id
130+
WHERE n.name = ? AND n.file NOT LIKE '%test%' AND n.file NOT LIKE '%spec%'
131+
LIMIT 1`,
132+
)
133+
.get(candidate);
134+
if (row) {
135+
hub = row.name;
136+
break;
137+
}
138+
}
139+
116140
const rows = db
117141
.prepare(
118142
`SELECT n.name, COUNT(e.id) AS cnt
@@ -123,14 +147,19 @@ function selectTargets() {
123147
ORDER BY cnt DESC`,
124148
)
125149
.all();
126-
db.close();
127150

128151
if (rows.length === 0) throw new Error('No nodes with edges found in graph');
129152

130-
const hub = rows[0].name;
153+
// Fall back to most-connected if no pinned candidate found
154+
if (!hub) hub = rows[0].name;
155+
131156
const mid = rows[Math.floor(rows.length / 2)].name;
132157
const leaf = rows[rows.length - 1].name;
133158
return { hub, mid, leaf };
159+
160+
} finally {
161+
db.close();
162+
}
134163
}
135164

136165
function benchDepths(fn, name, depths) {

scripts/update-embedding-report.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ if (arg) {
2626
}
2727
const entry = JSON.parse(jsonText);
2828

29+
// Guard: reject empty benchmark results (all workers crashed or no symbols indexed)
30+
if (!entry.symbols || !entry.models || Object.keys(entry.models).length === 0) {
31+
console.error(
32+
`Embedding benchmark produced empty results (symbols=${entry.symbols}, models=${Object.keys(entry.models || {}).length}). ` +
33+
'Skipping report update to avoid overwriting valid data. Check benchmark worker logs.',
34+
);
35+
process.exit(1);
36+
}
37+
2938
// ── Paths ────────────────────────────────────────────────────────────────
3039
const reportPath = path.join(root, 'generated', 'benchmarks', 'EMBEDDING-BENCHMARKS.md');
3140

src/domain/analysis/impact.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ import { findMatchingNodes } from './symbol-lookup.js';
2424

2525
const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
2626

27+
/**
28+
* Check whether the graph contains any 'implements' edges.
29+
* Cached per db handle so the query runs at most once per connection.
30+
*/
31+
const _hasImplementsCache = new WeakMap();
32+
function hasImplementsEdges(db) {
33+
if (_hasImplementsCache.has(db)) return _hasImplementsCache.get(db);
34+
const row = db.prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1").get();
35+
const result = !!row;
36+
_hasImplementsCache.set(db, result);
37+
return result;
38+
}
39+
2740
/**
2841
* BFS traversal to find transitive callers of a node.
2942
* When an interface/trait node is encountered (either as the start node or
@@ -40,14 +53,17 @@ export function bfsTransitiveCallers(
4053
startId,
4154
{ noTests = false, maxDepth = 3, includeImplementors = true, onVisit } = {},
4255
) {
56+
// Skip all implementor lookups when the graph has no implements edges
57+
const resolveImplementors = includeImplementors && hasImplementsEdges(db);
58+
4359
const visited = new Set([startId]);
4460
const levels = {};
4561
let frontier = [startId];
4662

4763
// Seed: if start node is an interface/trait, include its implementors at depth 1.
4864
// Implementors go into a separate list so their callers appear at depth 2, not depth 1.
4965
const implNextFrontier = [];
50-
if (includeImplementors) {
66+
if (resolveImplementors) {
5167
const startNode = findNodeById(db, startId);
5268
if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
5369
const impls = findImplementors(db, startId);
@@ -88,7 +104,7 @@ export function bfsTransitiveCallers(
88104

89105
// If a caller is an interface/trait, also pull in its implementors
90106
// Implementors are one extra hop away, so record at d+1
91-
if (includeImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
107+
if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
92108
const impls = findImplementors(db, c.id);
93109
for (const impl of impls) {
94110
if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {

0 commit comments

Comments
 (0)