From e58626c31f0834793d42e7398d76560045e980f5 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:21:44 -0600 Subject: [PATCH 1/4] refactor(native): extract magic numbers to named constants Extract hardcoded magic numbers to named constants in constants.rs: - Louvain: MAX_LEVELS=50, MAX_PASSES=20, MIN_GAIN=1e-12, DEFAULT_SEED=42 - Dataflow: TRUNCATION_LIMIT=120 - Build pipeline: FAST_PATH_MAX_CHANGED_FILES=5, FAST_PATH_MIN_EXISTING_FILES=20 Also extract DEFAULT_RANDOM_SEED=42 in TS louvain.ts. --- crates/codegraph-core/src/build_pipeline.rs | 3 ++- crates/codegraph-core/src/constants.rs | 27 +++++++++++++++++++ crates/codegraph-core/src/dataflow.rs | 14 +++++----- crates/codegraph-core/src/graph_algorithms.rs | 9 ++++--- src/graph/algorithms/louvain.ts | 7 +++-- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/codegraph-core/src/build_pipeline.rs b/crates/codegraph-core/src/build_pipeline.rs index b565fe57..09e745e9 100644 --- a/crates/codegraph-core/src/build_pipeline.rs +++ b/crates/codegraph-core/src/build_pipeline.rs @@ -18,6 +18,7 @@ use crate::change_detection; use crate::config::{BuildConfig, BuildOpts, BuildPathAliases}; +use crate::constants::{FAST_PATH_MAX_CHANGED_FILES, FAST_PATH_MIN_EXISTING_FILES}; use crate::file_collector; use crate::import_edges::{self, ImportEdgeContext}; use crate::import_resolution; @@ -492,7 +493,7 @@ pub fn run_pipeline( // reverse-dep files added for edge rebuilding, which inflates the count // and would skip the fast path even for single-file incremental builds. let use_fast_path = - !change_result.is_full_build && parse_changes.len() <= 5 && existing_file_count > 20; + !change_result.is_full_build && parse_changes.len() <= FAST_PATH_MAX_CHANGED_FILES && existing_file_count > FAST_PATH_MIN_EXISTING_FILES; if use_fast_path { structure::update_changed_file_metrics( diff --git a/crates/codegraph-core/src/constants.rs b/crates/codegraph-core/src/constants.rs index d1156147..5c7f4569 100644 --- a/crates/codegraph-core/src/constants.rs +++ b/crates/codegraph-core/src/constants.rs @@ -1,3 +1,30 @@ /// Maximum recursion depth for AST traversal to prevent stack overflow /// on deeply nested trees. Used by extractors, complexity, CFG, and dataflow. pub const MAX_WALK_DEPTH: usize = 200; + +// ─── Louvain community detection ──────────────────────────────────── + +/// Maximum number of coarsening levels in the Louvain algorithm. +pub const LOUVAIN_MAX_LEVELS: usize = 50; + +/// Maximum number of local-move passes per level before stopping. +pub const LOUVAIN_MAX_PASSES: usize = 20; + +/// Minimum modularity gain to accept a node move (avoids floating-point noise). +pub const LOUVAIN_MIN_GAIN: f64 = 1e-12; + +/// Default random seed for deterministic community detection. +pub const DEFAULT_RANDOM_SEED: u32 = 42; + +// ─── Dataflow analysis ────────────────────────────────────────────── + +/// Maximum character length for truncated dataflow expressions. +pub const DATAFLOW_TRUNCATION_LIMIT: usize = 120; + +// ─── Build pipeline ───────────────────────────────────────────────── + +/// Maximum number of changed files eligible for the incremental fast path. +pub const FAST_PATH_MAX_CHANGED_FILES: usize = 5; + +/// Minimum existing file count required before the fast path is considered. +pub const FAST_PATH_MIN_EXISTING_FILES: usize = 20; diff --git a/crates/codegraph-core/src/dataflow.rs b/crates/codegraph-core/src/dataflow.rs index af736be0..f22313ac 100644 --- a/crates/codegraph-core/src/dataflow.rs +++ b/crates/codegraph-core/src/dataflow.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use tree_sitter::{Node, Tree}; -use crate::constants::MAX_WALK_DEPTH; +use crate::constants::{DATAFLOW_TRUNCATION_LIMIT, MAX_WALK_DEPTH}; use crate::types::{ DataflowArgFlow, DataflowAssignment, DataflowMutation, DataflowParam, DataflowResult, DataflowReturn, @@ -1196,7 +1196,7 @@ fn handle_var_declarator( var_name: n.clone(), caller_func: Some(func_name.clone()), source_call_name: callee.clone(), - expression: truncate(node_text(node, source), 120), + expression: truncate(node_text(node, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); scope @@ -1209,7 +1209,7 @@ fn handle_var_declarator( var_name: var_name.clone(), caller_func: Some(func_name), source_call_name: callee.clone(), - expression: truncate(node_text(node, source), 120), + expression: truncate(node_text(node, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); scope.locals.insert(var_name, LocalSource::CallReturn { callee }); @@ -1245,7 +1245,7 @@ fn handle_assignment( func_name: Some(func_name.clone()), receiver_name: receiver, binding_type: binding.as_ref().map(|b| b.binding_type.clone()), - mutating_expr: truncate(node_text(node, source), 120), + mutating_expr: truncate(node_text(node, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); } @@ -1264,7 +1264,7 @@ fn handle_assignment( var_name: var_name.clone(), caller_func: Some(func_name), source_call_name: callee.clone(), - expression: truncate(node_text(node, source), 120), + expression: truncate(node_text(node, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); if let Some(scope) = scope_stack.last_mut() { @@ -1340,7 +1340,7 @@ fn handle_call_expr( arg_name: Some(tracked.clone()), binding_type: binding.as_ref().map(|b| b.binding_type.clone()), confidence: conf, - expression: truncate(node_text(&arg_raw, source), 120), + expression: truncate(node_text(&arg_raw, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); } @@ -1442,7 +1442,7 @@ fn handle_expr_stmt_mutation( func_name, receiver_name: recv, binding_type: binding.as_ref().map(|b| b.binding_type.clone()), - mutating_expr: truncate(node_text(&expr, source), 120), + mutating_expr: truncate(node_text(&expr, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); } diff --git a/crates/codegraph-core/src/graph_algorithms.rs b/crates/codegraph-core/src/graph_algorithms.rs index 78dbf448..f2dc9889 100644 --- a/crates/codegraph-core/src/graph_algorithms.rs +++ b/crates/codegraph-core/src/graph_algorithms.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet, VecDeque}; +use crate::constants::{DEFAULT_RANDOM_SEED, LOUVAIN_MAX_LEVELS, LOUVAIN_MAX_PASSES, LOUVAIN_MIN_GAIN}; use crate::types::GraphEdge; use napi_derive::napi; @@ -242,7 +243,7 @@ pub fn louvain_communities( &edges, &node_ids, resolution.unwrap_or(1.0), - random_seed.unwrap_or(42), + random_seed.unwrap_or(DEFAULT_RANDOM_SEED), ) } @@ -314,7 +315,7 @@ fn louvain_impl( // edges, inflating the penalty term and causing under-merging at coarser levels. let total_m2: f64 = 2.0 * total_weight; - for _level in 0..50 { + for _level in 0..LOUVAIN_MAX_LEVELS { if cur_edges.is_empty() { break; } @@ -337,7 +338,7 @@ fn louvain_impl( } let mut any_moved = false; - for _pass in 0..20 { + for _pass in 0..LOUVAIN_MAX_PASSES { let mut pass_moved = false; for &node in &order { let node_comm = level_comm[node]; @@ -368,7 +369,7 @@ fn louvain_impl( } } - if best_comm != node_comm && best_gain > 1e-12 { + if best_comm != node_comm && best_gain > LOUVAIN_MIN_GAIN { comm_total[node_comm] -= node_deg; comm_total[best_comm] += node_deg; level_comm[node] = best_comm; diff --git a/src/graph/algorithms/louvain.ts b/src/graph/algorithms/louvain.ts index f1c610b3..6cece3f5 100644 --- a/src/graph/algorithms/louvain.ts +++ b/src/graph/algorithms/louvain.ts @@ -12,6 +12,9 @@ import type { CodeGraph } from '../model.js'; import type { DetectClustersResult } from './leiden/index.js'; import { detectClusters } from './leiden/index.js'; +/** Default random seed for deterministic community detection. */ +const DEFAULT_RANDOM_SEED = 42; + export interface LouvainOptions { resolution?: number; maxLevels?: number; @@ -42,7 +45,7 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}): } const edges = graph.toEdgeArray(); const nodeIds = graph.nodeIds(); - const result = native.louvainCommunities(edges, nodeIds, resolution, 42); + const result = native.louvainCommunities(edges, nodeIds, resolution, DEFAULT_RANDOM_SEED); const assignments = new Map(); for (const entry of result.assignments) { assignments.set(entry.node, entry.community); @@ -57,7 +60,7 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}): function louvainJS(graph: CodeGraph, opts: LouvainOptions, resolution: number): LouvainResult { const result: DetectClustersResult = detectClusters(graph, { resolution, - randomSeed: 42, + randomSeed: DEFAULT_RANDOM_SEED, directed: false, ...(opts.maxLevels != null && { maxLevels: opts.maxLevels }), ...(opts.maxLocalPasses != null && { maxLocalPasses: opts.maxLocalPasses }), From a2e1cdadc9dadb486e91cb18bb1f51be3dca924b Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:36:22 -0600 Subject: [PATCH 2/4] refactor(native): extract shared barrel resolution into common module --- .../codegraph-core/src/barrel_resolution.rs | 191 ++++++++++++++++++ crates/codegraph-core/src/edge_builder.rs | 73 ++----- crates/codegraph-core/src/import_edges.rs | 67 +++--- crates/codegraph-core/src/lib.rs | 1 + 4 files changed, 237 insertions(+), 95 deletions(-) create mode 100644 crates/codegraph-core/src/barrel_resolution.rs diff --git a/crates/codegraph-core/src/barrel_resolution.rs b/crates/codegraph-core/src/barrel_resolution.rs new file mode 100644 index 00000000..ad7e48f0 --- /dev/null +++ b/crates/codegraph-core/src/barrel_resolution.rs @@ -0,0 +1,191 @@ +//! Shared barrel-file resolution logic. +//! +//! Both `edge_builder.rs` (napi-driven) and `import_edges.rs` (SQLite-driven) +//! need to recursively resolve a symbol through barrel reexport chains. +//! This module extracts the common algorithm so both callers share a single +//! implementation. + +use std::collections::HashSet; + +/// Minimal view of a single reexport entry, borrowed from the caller's data. +pub struct ReexportRef<'a> { + pub source: &'a str, + pub names: &'a [String], + pub wildcard_reexport: bool, +} + +/// Trait that abstracts over the different context types in `edge_builder` and +/// `import_edges`. Each implementor provides access to its own reexport map +/// and definition index so the resolution algorithm stays generic. +pub trait BarrelContext { + /// Return the reexport entries for `barrel_path`, or `None` if the path + /// has no reexports. + fn reexports_for(&self, barrel_path: &str) -> Option>>; + + /// Return `true` if `file_path` contains a definition named `symbol`. + fn has_definition(&self, file_path: &str, symbol: &str) -> bool; +} + +/// Recursively resolve a symbol through barrel reexport chains. +/// +/// Mirrors `resolveBarrelExport()` in `resolve-imports.ts`. +/// The caller provides a `visited` set to prevent infinite loops on circular +/// reexport chains. +pub fn resolve_barrel_export<'a, C: BarrelContext>( + ctx: &'a C, + barrel_path: &str, + symbol_name: &str, + visited: &mut HashSet, +) -> Option { + if visited.contains(barrel_path) { + return None; + } + visited.insert(barrel_path.to_string()); + + let reexports = ctx.reexports_for(barrel_path)?; + + for re in &reexports { + // Named reexports (non-wildcard) + if !re.names.is_empty() && !re.wildcard_reexport { + if re.names.iter().any(|n| n == symbol_name) { + if ctx.has_definition(re.source, symbol_name) { + return Some(re.source.to_string()); + } + let deeper = resolve_barrel_export(ctx, re.source, symbol_name, visited); + if deeper.is_some() { + return deeper; + } + // Fallback: return source even if no definition found + return Some(re.source.to_string()); + } + continue; + } + + // Wildcard or empty-names reexports + if re.wildcard_reexport || re.names.is_empty() { + if ctx.has_definition(re.source, symbol_name) { + return Some(re.source.to_string()); + } + let deeper = resolve_barrel_export(ctx, re.source, symbol_name, visited); + if deeper.is_some() { + return deeper; + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct TestContext { + reexports: HashMap, bool)>>, + definitions: HashMap>, + } + + impl BarrelContext for TestContext { + fn reexports_for(&self, barrel_path: &str) -> Option>> { + self.reexports.get(barrel_path).map(|entries| { + entries + .iter() + .map(|(source, names, wildcard)| ReexportRef { + source: source.as_str(), + names: names.as_slice(), + wildcard_reexport: *wildcard, + }) + .collect() + }) + } + + fn has_definition(&self, file_path: &str, symbol: &str) -> bool { + self.definitions + .get(file_path) + .map_or(false, |defs| defs.contains(symbol)) + } + } + + #[test] + fn resolves_named_reexport() { + let mut reexports = HashMap::new(); + reexports.insert( + "src/index.ts".to_string(), + vec![("src/utils.ts".to_string(), vec!["foo".to_string()], false)], + ); + let mut definitions = HashMap::new(); + definitions.insert( + "src/utils.ts".to_string(), + HashSet::from(["foo".to_string()]), + ); + + let ctx = TestContext { reexports, definitions }; + let mut visited = HashSet::new(); + let result = resolve_barrel_export(&ctx, "src/index.ts", "foo", &mut visited); + assert_eq!(result.as_deref(), Some("src/utils.ts")); + } + + #[test] + fn resolves_wildcard_reexport() { + let mut reexports = HashMap::new(); + reexports.insert( + "src/index.ts".to_string(), + vec![("src/utils.ts".to_string(), vec![], true)], + ); + let mut definitions = HashMap::new(); + definitions.insert( + "src/utils.ts".to_string(), + HashSet::from(["bar".to_string()]), + ); + + let ctx = TestContext { reexports, definitions }; + let mut visited = HashSet::new(); + let result = resolve_barrel_export(&ctx, "src/index.ts", "bar", &mut visited); + assert_eq!(result.as_deref(), Some("src/utils.ts")); + } + + #[test] + fn resolves_transitive_chain() { + let mut reexports = HashMap::new(); + reexports.insert( + "src/index.ts".to_string(), + vec![("src/mid.ts".to_string(), vec![], true)], + ); + reexports.insert( + "src/mid.ts".to_string(), + vec![("src/deep.ts".to_string(), vec!["baz".to_string()], false)], + ); + let mut definitions = HashMap::new(); + definitions.insert( + "src/deep.ts".to_string(), + HashSet::from(["baz".to_string()]), + ); + + let ctx = TestContext { reexports, definitions }; + let mut visited = HashSet::new(); + let result = resolve_barrel_export(&ctx, "src/index.ts", "baz", &mut visited); + assert_eq!(result.as_deref(), Some("src/deep.ts")); + } + + #[test] + fn prevents_circular_reexport() { + let mut reexports = HashMap::new(); + reexports.insert( + "src/a.ts".to_string(), + vec![("src/b.ts".to_string(), vec![], true)], + ); + reexports.insert( + "src/b.ts".to_string(), + vec![("src/a.ts".to_string(), vec![], true)], + ); + + let ctx = TestContext { + reexports, + definitions: HashMap::new(), + }; + let mut visited = HashSet::new(); + let result = resolve_barrel_export(&ctx, "src/a.ts", "missing", &mut visited); + assert_eq!(result, None); + } +} diff --git a/crates/codegraph-core/src/edge_builder.rs b/crates/codegraph-core/src/edge_builder.rs index 8d03dbd0..3a4194ac 100644 --- a/crates/codegraph-core/src/edge_builder.rs +++ b/crates/codegraph-core/src/edge_builder.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use napi_derive::napi; +use crate::barrel_resolution::{self, BarrelContext, ReexportRef}; use crate::import_resolution; /// Kind sets for hierarchy edge resolution -- mirrors the JS constants in @@ -466,55 +467,25 @@ impl<'a> ImportEdgeContext<'a> { } } -/// Recursively resolve a symbol through barrel reexport chains. -/// Mirrors `resolveBarrelExport()` in resolve-imports.ts. -fn resolve_barrel_export<'a>( - ctx: &'a ImportEdgeContext<'a>, - barrel_path: &'a str, - symbol_name: &str, - visited: &mut HashSet<&'a str>, -) -> Option<&'a str> { - if visited.contains(barrel_path) { - return None; +impl<'a> BarrelContext for ImportEdgeContext<'a> { + fn reexports_for(&self, barrel_path: &str) -> Option>> { + self.reexport_map.get(barrel_path).map(|entries| { + entries + .iter() + .map(|re| ReexportRef { + source: re.source.as_str(), + names: &re.names, + wildcard_reexport: re.wildcard_reexport, + }) + .collect() + }) } - visited.insert(barrel_path); - let reexports = ctx.reexport_map.get(barrel_path)?; - - for re in reexports.iter() { - // Named reexports (non-wildcard) - if !re.names.is_empty() && !re.wildcard_reexport { - if re.names.iter().any(|n| n == symbol_name) { - if let Some(defs) = ctx.file_defs.get(re.source.as_str()) { - if defs.contains(symbol_name) { - return Some(re.source.as_str()); - } - let deeper = resolve_barrel_export(ctx, re.source.as_str(), symbol_name, visited); - if deeper.is_some() { - return deeper; - } - } - // Fallback: return source even if no definition found - return Some(re.source.as_str()); - } - continue; - } - - // Wildcard or empty-names reexports - if re.wildcard_reexport || re.names.is_empty() { - if let Some(defs) = ctx.file_defs.get(re.source.as_str()) { - if defs.contains(symbol_name) { - return Some(re.source.as_str()); - } - let deeper = resolve_barrel_export(ctx, re.source.as_str(), symbol_name, visited); - if deeper.is_some() { - return deeper; - } - } - } + fn has_definition(&self, file_path: &str, symbol: &str) -> bool { + self.file_defs + .get(file_path) + .map_or(false, |defs| defs.contains(symbol)) } - - None } /// Build import and barrel-through edges in Rust. @@ -583,7 +554,7 @@ pub fn build_import_edges( // Barrel resolution: if not reexport and target is a barrel file if !imp.reexport && ctx.barrel_set.contains(resolved_path) { - let mut resolved_sources: HashSet<&str> = HashSet::new(); + let mut resolved_sources: HashSet = HashSet::new(); for name in &imp.names { let clean_name = if name.starts_with("* as ") || name.starts_with("*\tas ") { // Strip "* as " or "*\tas " prefix (both exactly 5 bytes) @@ -594,12 +565,11 @@ pub fn build_import_edges( }; let mut visited = HashSet::new(); - let actual = resolve_barrel_export(&ctx, resolved_path, clean_name, &mut visited); + let actual = barrel_resolution::resolve_barrel_export(&ctx, resolved_path, clean_name, &mut visited); if let Some(actual_source) = actual { - if actual_source != resolved_path && !resolved_sources.contains(actual_source) { - resolved_sources.insert(actual_source); - if let Some(&actual_node_id) = ctx.file_node_map.get(actual_source) { + if actual_source != resolved_path && !resolved_sources.contains(&actual_source) { + if let Some(&actual_node_id) = ctx.file_node_map.get(actual_source.as_str()) { let barrel_kind = match edge_kind { "imports-type" => "imports-type", "dynamic-imports" => "dynamic-imports", @@ -613,6 +583,7 @@ pub fn build_import_edges( dynamic: 0, }); } + resolved_sources.insert(actual_source); } } } diff --git a/crates/codegraph-core/src/import_edges.rs b/crates/codegraph-core/src/import_edges.rs index 1dfcd4d0..8d3966a7 100644 --- a/crates/codegraph-core/src/import_edges.rs +++ b/crates/codegraph-core/src/import_edges.rs @@ -4,6 +4,7 @@ //! the barrel detection from `resolve-imports.ts:isBarrelFile()`, and the //! recursive barrel export resolution from `resolveBarrelExport()`. +use crate::barrel_resolution::{self, BarrelContext, ReexportRef}; use crate::import_resolution; use crate::types::{FileSymbols, PathAliases}; use rusqlite::Connection; @@ -79,58 +80,36 @@ impl ImportEdgeContext { } /// Recursively resolve a barrel export to its actual source file. + /// + /// Delegates to the shared [`barrel_resolution::resolve_barrel_export`] algorithm. pub fn resolve_barrel_export( &self, barrel_path: &str, symbol_name: &str, visited: &mut HashSet, ) -> Option { - if visited.contains(barrel_path) { - return None; - } - visited.insert(barrel_path.to_string()); + barrel_resolution::resolve_barrel_export(self, barrel_path, symbol_name, visited) + } +} - let reexports = self.reexport_map.get(barrel_path)?; - for re in reexports { - // Named reexport (not wildcard) - if !re.names.is_empty() && !re.wildcard_reexport { - if re.names.iter().any(|n| n == symbol_name) { - if let Some(target_symbols) = self.file_symbols.get(&re.source) { - let has_def = target_symbols - .definitions - .iter() - .any(|d| d.name == symbol_name); - if has_def { - return Some(re.source.clone()); - } - let deeper = self.resolve_barrel_export(&re.source, symbol_name, visited); - if deeper.is_some() { - return deeper; - } - } - return Some(re.source.clone()); - } - continue; - } +impl BarrelContext for ImportEdgeContext { + fn reexports_for(&self, barrel_path: &str) -> Option>> { + self.reexport_map.get(barrel_path).map(|entries| { + entries + .iter() + .map(|re| ReexportRef { + source: re.source.as_str(), + names: &re.names, + wildcard_reexport: re.wildcard_reexport, + }) + .collect() + }) + } - // Wildcard reexport or unnamed - if re.wildcard_reexport || re.names.is_empty() { - if let Some(target_symbols) = self.file_symbols.get(&re.source) { - let has_def = target_symbols - .definitions - .iter() - .any(|d| d.name == symbol_name); - if has_def { - return Some(re.source.clone()); - } - let deeper = self.resolve_barrel_export(&re.source, symbol_name, visited); - if deeper.is_some() { - return deeper; - } - } - } - } - None + fn has_definition(&self, file_path: &str, symbol: &str) -> bool { + self.file_symbols + .get(file_path) + .map_or(false, |s| s.definitions.iter().any(|d| d.name == symbol)) } } diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index 1b16b029..5fbe317d 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -1,5 +1,6 @@ pub mod analysis; pub mod ast_db; +pub mod barrel_resolution; pub mod build_pipeline; pub mod change_detection; pub mod cfg; From 2fbf78dcac71c923606420b1a34cd73dd099dd02 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:24:44 -0600 Subject: [PATCH 3/4] fix(native): correct type mismatch and elide redundant lifetime Change FAST_PATH_MIN_EXISTING_FILES from usize to i64 to match the return type of structure::get_existing_file_count, fixing the Rust compile error. Also elide the unnecessary named lifetime on resolve_barrel_export since the return type is fully owned. --- crates/codegraph-core/src/barrel_resolution.rs | 4 ++-- crates/codegraph-core/src/constants.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/codegraph-core/src/barrel_resolution.rs b/crates/codegraph-core/src/barrel_resolution.rs index ad7e48f0..beeeed03 100644 --- a/crates/codegraph-core/src/barrel_resolution.rs +++ b/crates/codegraph-core/src/barrel_resolution.rs @@ -31,8 +31,8 @@ pub trait BarrelContext { /// Mirrors `resolveBarrelExport()` in `resolve-imports.ts`. /// The caller provides a `visited` set to prevent infinite loops on circular /// reexport chains. -pub fn resolve_barrel_export<'a, C: BarrelContext>( - ctx: &'a C, +pub fn resolve_barrel_export( + ctx: &C, barrel_path: &str, symbol_name: &str, visited: &mut HashSet, diff --git a/crates/codegraph-core/src/constants.rs b/crates/codegraph-core/src/constants.rs index 5c7f4569..21f7fe28 100644 --- a/crates/codegraph-core/src/constants.rs +++ b/crates/codegraph-core/src/constants.rs @@ -27,4 +27,5 @@ pub const DATAFLOW_TRUNCATION_LIMIT: usize = 120; pub const FAST_PATH_MAX_CHANGED_FILES: usize = 5; /// Minimum existing file count required before the fast path is considered. -pub const FAST_PATH_MIN_EXISTING_FILES: usize = 20; +/// Typed as `i64` to match the SQLite row-count return type in `structure::get_existing_file_count`. +pub const FAST_PATH_MIN_EXISTING_FILES: i64 = 20; From d800cc1a6b05694b4882db07710f64f06ae92cb7 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:13:52 -0600 Subject: [PATCH 4/4] docs: add explanatory note for 3.9.0 fnDeps regression and missing versions Address Greptile review feedback: - Add Note (3.9.0) explaining the ~180% fnDeps regression as codebase growth from 23 new language extractors added in 3.7.0-3.8.0 - Document that native being ~2% slower than WASM for fnDeps is within measurement noise - Explain absence of 3.8.0/3.8.1 query benchmark rows (data removed due to pre-fix measurement) --- generated/benchmarks/QUERY-BENCHMARKS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/generated/benchmarks/QUERY-BENCHMARKS.md b/generated/benchmarks/QUERY-BENCHMARKS.md index 1db24fd2..b9b10e08 100644 --- a/generated/benchmarks/QUERY-BENCHMARKS.md +++ b/generated/benchmarks/QUERY-BENCHMARKS.md @@ -79,6 +79,8 @@ Latencies are median over 5 runs. Hub target = most-connected node. +**Note (3.9.0):** The ↑177-184% fnDeps regression (9.7ms → 27ms) reflects substantial codebase growth between 3.7.0 and 3.9.0 — many new language extractors were added across 3.7.0-3.8.0 (Elixir, Lua, Dart, Zig, Haskell, OCaml, F#, Gleam, Clojure, Julia, R, Erlang, C, C++, Kotlin, Swift, Scala, Bash, Solidity, Objective-C, CUDA, Groovy, Verilog), significantly increasing the `buildGraph` hub node's edge count. The `findCallersBatch` path was also refactored in 3.8.1 (PR #815). fnImpact and diffImpact grew only 8-14%, consistent with normal expansion. The native engine being marginally slower than WASM for fnDeps (27.4ms vs 26.9ms, ~2%) is within measurement noise and not a meaningful inversion. Versions 3.8.0 and 3.8.1 are absent because their query benchmark data was removed — v3.8.1 was measured before the `findCallersBatch` fix and showed artificially inflated fnDeps latencies; v3.8.0 had no separate query benchmark run. + **Note (3.6.0):** Native deltas are relative to 3.4.1 (the last version with native data; 3.5.0 was wasm-only). The mid-query target changed from `db` (3.5.0) to `node`, which affects diffImpact scope and explains the ↑41% WASM diffImpact jump (6.4ms → 9ms). fnDeps/fnImpact growth of 6-10% is consistent with codebase expansion across two releases. **Note (3.5.0):** This version has WASM-only data (`native: null`) because the native engine crashed during `insertNodes` in the graph build phase. The root cause is a napi-rs serialization bug: parameter and child nodes with undefined `visibility` fields marshal as `null` at the JS-Rust boundary, which fails conversion into the Rust `Option` type in `InsertNodesDefinition.visibility`. The mid-query target also changed from `noTests` to `db`, which may affect diffImpact scope. Query latencies for 3.5.0 are therefore not directly comparable to prior versions that include both engine rows. This will be fixed in the next release.