Skip to content

Commit 8ad73c9

Browse files
committed
fix: resolve merge conflicts with main
docs check acknowledged — conflict resolution only, no new functionality. Impact: 46 functions changed, 183 affected
2 parents aec5d2b + 9e41888 commit 8ad73c9

53 files changed

Lines changed: 1332 additions & 272 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/titan-grind/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Forge shapes the metal. Grind smooths the rough edges. Your goal: find helpers t
109109

110110
12. **Capture dead-symbol baseline** (only if `grind.deadSymbolBaseline` is null):
111111
```bash
112-
codegraph roles --role dead -T --json | node -e "const d=[];process.stdin.on('data',c=>d.push(c));process.stdin.on('end',()=>{const data=JSON.parse(Buffer.concat(d));console.log(JSON.stringify({total:data.count,byRole:data.summary}));})"
112+
codegraph roles --role dead -T --json | node -e "const d=[];process.stdin.on('data',c=>d.push(c));process.stdin.on('end',()=>{try{const data=JSON.parse(Buffer.concat(d));console.log(JSON.stringify({total:data.count??0,byRole:data.summary??{}}));}catch(e){console.error('Failed to parse roles --json output: '+e.message);process.exit(1);}})"
113113
```
114114
`codegraph roles --json` returns `{ count, summary, symbols }` (not a bare array) — `summary` is already the per-role breakdown (e.g. `dead-leaf`, `dead-entry`, `dead-ffi`, `dead-unresolved`), so no manual reduce is needed.
115115
Store the total in `grind.deadSymbolBaseline`. Write `titan-state.json` immediately.
@@ -580,7 +580,7 @@ After all targets in the phase are processed:
580580
581581
```bash
582582
codegraph build
583-
codegraph roles --role dead -T --json | node -e "const d=[];process.stdin.on('data',c=>d.push(c));process.stdin.on('end',()=>{const data=JSON.parse(Buffer.concat(d));console.log(JSON.stringify({total:data.count,byRole:data.summary}));})"
583+
codegraph roles --role dead -T --json | node -e "const d=[];process.stdin.on('data',c=>d.push(c));process.stdin.on('end',()=>{try{const data=JSON.parse(Buffer.concat(d));console.log(JSON.stringify({total:data.count??0,byRole:data.summary??{}}));}catch(e){console.error('Failed to parse roles --json output: '+e.message);process.exit(1);}})"
584584
```
585585
586586
Store in `grind.deadSymbolCurrent`. Write `titan-state.json`.

crates/codegraph-core/src/db/connection.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,10 +880,13 @@ impl NativeDatabase {
880880
let insert_ok = insert_nodes::do_insert_nodes(conn, &batches, &removed_files)
881881
.inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_nodes failed: {e}"))
882882
.is_ok();
883+
if !insert_ok {
884+
return Ok(false);
885+
}
883886
let hashes_ok = insert_nodes::commit_file_hashes(conn, &file_hashes)
884887
.inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_nodes hash commit failed: {e}"))
885888
.is_ok();
886-
Ok(insert_ok && hashes_ok)
889+
Ok(hashes_ok)
887890
}
888891

889892
/// Bulk-insert edge rows using chunked multi-value INSERT statements.

crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,16 @@ fn is_named_reexport(imp: &ImportInfo) -> bool {
16171617
imp.reexport && !imp.wildcard_reexport
16181618
}
16191619

1620+
/// True for a genuine wildcard re-export (`export * from 'Y'`). Emitted as a
1621+
/// distinct file-level marker edge (`reexports-wildcard`) alongside the
1622+
/// generic `reexports` edge so the query layer can tell a target reached
1623+
/// only by named specifiers apart from one that's also reached by a
1624+
/// wildcard — even when a *different* statement in the same file names
1625+
/// specific symbols from that exact target (#1849 review).
1626+
fn is_wildcard_reexport(imp: &ImportInfo) -> bool {
1627+
imp.reexport && imp.wildcard_reexport
1628+
}
1629+
16201630
/// For a `type` import or a named re-export targeting a barrel or resolved
16211631
/// file, emit one symbol-level edge per named symbol so the target symbols
16221632
/// receive fan-in credit and aren't misclassified as dead code
@@ -1751,6 +1761,15 @@ fn process_single_import(
17511761
}
17521762
if is_named_reexport(imp) {
17531763
emit_named_symbol_edges(edges, file_input, imp, resolved_path, "reexports", ctx);
1764+
} else if is_wildcard_reexport(imp) {
1765+
edges.push(ComputedEdge {
1766+
source_id: file_input.file_node_id,
1767+
target_id: target_node_id,
1768+
kind: "reexports-wildcard".to_string(),
1769+
confidence: 1.0,
1770+
dynamic: 0,
1771+
dynamic_kind: None,
1772+
});
17541773
}
17551774
emit_barrel_through_edges(edges, file_input, imp, resolved_path, edge_kind, ctx);
17561775
}
@@ -1857,9 +1876,12 @@ mod import_edge_tests {
18571876

18581877
#[test]
18591878
fn wildcard_reexport_emits_no_symbol_level_edge() {
1860-
// `export * from './utils'` carries no specific names, so only the
1861-
// file-level `reexports` edge is emitted — the query layer falls
1862-
// back to the target's full export list for genuine wildcards.
1879+
// `export * from './utils'` carries no specific names, so no
1880+
// symbol-level edge is emitted. It does get the dedicated
1881+
// `reexports-wildcard` file-level marker (alongside the generic
1882+
// `reexports` edge) so the query layer can always apply full-export
1883+
// semantics for genuine wildcards, even when a *different* statement
1884+
// to the same target also names specific symbols (#1849 review).
18631885
let files = vec![make_file("src/index.ts", 1, vec![
18641886
ImportInfo {
18651887
source: "./utils".to_string(),
@@ -1887,9 +1909,61 @@ mod import_edge_tests {
18871909
"/root".to_string(),
18881910
Some(symbol_nodes),
18891911
);
1890-
assert_eq!(edges.len(), 1);
1912+
assert_eq!(edges.len(), 2);
18911913
assert_eq!(edges[0].kind, "reexports");
18921914
assert_eq!(edges[0].target_id, 2);
1915+
assert_eq!(edges[1].kind, "reexports-wildcard");
1916+
assert_eq!(edges[1].target_id, 2);
1917+
}
1918+
1919+
#[test]
1920+
fn named_and_wildcard_reexport_of_same_target_both_marked() {
1921+
// `export { foo } from './utils'` AND `export * from './utils'` in
1922+
// the same file, both targeting utils.ts. The wildcard's full-export
1923+
// semantics must stay independently signalled (via the dedicated
1924+
// `reexports-wildcard` marker) rather than being suppressed by the
1925+
// named specifier's symbol-level edge — otherwise the query layer
1926+
// would report only `foo` and silently drop every other export of
1927+
// utils.ts that the wildcard was meant to surface (#1849 review).
1928+
let files = vec![make_file("src/index.ts", 1, vec![
1929+
make_import("./utils", vec!["foo"], true, false, false),
1930+
ImportInfo {
1931+
source: "./utils".to_string(),
1932+
names: vec![],
1933+
reexport: true,
1934+
type_only: false,
1935+
dynamic_import: false,
1936+
wildcard_reexport: true,
1937+
},
1938+
], vec![])];
1939+
let resolved = vec![make_resolved("/root/src/index.ts", "./utils", "src/utils.ts")];
1940+
let node_ids = vec![make_node_entry("src/index.ts", 1), make_node_entry("src/utils.ts", 2)];
1941+
let symbol_nodes = vec![SymbolNodeEntry {
1942+
name: "foo".to_string(),
1943+
file: "src/utils.ts".to_string(),
1944+
node_id: 99,
1945+
}];
1946+
1947+
let edges = build_import_edges(
1948+
files,
1949+
resolved,
1950+
vec![],
1951+
node_ids,
1952+
vec![],
1953+
"/root".to_string(),
1954+
Some(symbol_nodes),
1955+
);
1956+
assert_eq!(edges.len(), 4);
1957+
// Named statement: file-level `reexports` + symbol-level `reexports` to foo.
1958+
assert_eq!(edges[0].kind, "reexports");
1959+
assert_eq!(edges[0].target_id, 2);
1960+
assert_eq!(edges[1].kind, "reexports");
1961+
assert_eq!(edges[1].target_id, 99);
1962+
// Wildcard statement: file-level `reexports` + the `reexports-wildcard` marker.
1963+
assert_eq!(edges[2].kind, "reexports");
1964+
assert_eq!(edges[2].target_id, 2);
1965+
assert_eq!(edges[3].kind, "reexports-wildcard");
1966+
assert_eq!(edges[3].target_id, 2);
18931967
}
18941968

18951969
#[test]

crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,17 @@ fn is_named_reexport(imp: &crate::types::Import) -> bool {
266266
imp.reexport.unwrap_or(false) && !imp.wildcard_reexport.unwrap_or(false)
267267
}
268268

269+
/// True for a genuine wildcard re-export (`export * from 'Y'`). Emitted as a
270+
/// distinct file-level marker edge (`reexports-wildcard`) alongside the
271+
/// generic `reexports` edge so the query layer can tell a target reached
272+
/// only by named specifiers apart from one that's also reached by a
273+
/// wildcard — even when a *different* statement in the same file names
274+
/// specific symbols from that exact target (#1849 review). Mirrors
275+
/// `is_wildcard_reexport` in build_edges.rs (FFI fallback path).
276+
fn is_wildcard_reexport(imp: &crate::types::Import) -> bool {
277+
imp.reexport.unwrap_or(false) && imp.wildcard_reexport.unwrap_or(false)
278+
}
279+
269280
/// Walk type-only imports and named re-exports in `ctx.file_symbols` and
270281
/// return the distinct `(name, file)` pairs that `build_import_edges` will
271282
/// need to look up. Resolves barrel files the same way the edge-building
@@ -450,6 +461,14 @@ fn emit_edges_for_import(
450461
ctx,
451462
symbol_node_ids,
452463
);
464+
} else if is_wildcard_reexport(imp) {
465+
edges.push(EdgeRow {
466+
source_id: file_node_id,
467+
target_id,
468+
kind: "reexports-wildcard".to_string(),
469+
confidence: 1.0,
470+
dynamic: 0,
471+
});
453472
}
454473
emit_barrel_through_rows(
455474
edges,

crates/codegraph-core/src/domain/graph/resolve.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ fn ancestor_chain(dir: &str) -> Vec<String> {
259259
chain
260260
}
261261

262+
// directory_distance is on the hot path for every call-edge confidence
263+
// score, invoked from inside compute_confidence's rayon `.par_iter()` caller
264+
// (line ~330 below). The same directory pairs recur constantly across a
265+
// build, so memoizing avoids rebuilding both ancestor chains and the lookup
266+
// map every call. Thread-local (not a shared Mutex/DashMap) because rayon's
267+
// worker pool is reused across the whole build — each worker accumulates its
268+
// own useful cache with zero lock contention, at the cost of some redundant
269+
// computation the first time a given pair is seen on each thread.
270+
// distance(a, b) === distance(b, a) (symmetric tree distance), so the key is
271+
// order-independent to halve the effective cache size per thread (#1769
272+
// perf regression).
273+
thread_local! {
274+
static DIRECTORY_DISTANCE_CACHE: std::cell::RefCell<std::collections::HashMap<(String, String), usize>> =
275+
std::cell::RefCell::new(std::collections::HashMap::new());
276+
}
277+
262278
/// Directory-tree distance between two directories: hops up from `a` to the
263279
/// nearest ancestor shared with `b`, plus hops down from there to `b`.
264280
///
@@ -271,14 +287,24 @@ fn ancestor_chain(dir: &str) -> Vec<String> {
271287
/// so e.g. a file in `graph/algorithms/*.rs` calling a method declared in
272288
/// the shallower `graph/model.rs` was scored as maximally distant (issue #1769).
273289
fn directory_distance(a: &str, b: &str) -> usize {
290+
let key = if a <= b { (a.to_string(), b.to_string()) } else { (b.to_string(), a.to_string()) };
291+
if let Some(cached) = DIRECTORY_DISTANCE_CACHE.with(|c| c.borrow().get(&key).copied()) {
292+
return cached;
293+
}
294+
274295
let chain_a = ancestor_chain(a);
275296
let chain_b = ancestor_chain(b);
297+
let index_in_b: std::collections::HashMap<&str, usize> =
298+
chain_b.iter().enumerate().map(|(j, d)| (d.as_str(), j)).collect();
299+
let mut dist = usize::MAX;
276300
for (i, dir_a) in chain_a.iter().enumerate() {
277-
if let Some(j) = chain_b.iter().position(|dir_b| dir_b == dir_a) {
278-
return i + j;
301+
if let Some(&j) = index_in_b.get(dir_a.as_str()) {
302+
dist = i + j;
303+
break;
279304
}
280305
}
281-
usize::MAX
306+
DIRECTORY_DISTANCE_CACHE.with(|c| c.borrow_mut().insert(key, dist));
307+
dist
282308
}
283309

284310
/// Compute proximity-based confidence for call resolution.

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,6 +2333,11 @@ const HTTP_VERB_CALLEES: &[&str] = &[
23332333
/// separate `CALLBACK_ACCEPTING_CALLEES` entry needed); only the arg at its
23342334
/// listed index is eligible.
23352335
///
2336+
/// Invariant: this map and `CALLBACK_ACCEPTING_CALLEES` must stay disjoint.
2337+
/// A callee name present in both would have its any-position intent silently
2338+
/// narrowed to the single listed index (positional wins — see the gate in
2339+
/// `extract_callback_reference_calls`), with no error or warning.
2340+
///
23362341
/// Name-based, not receiver-typed, so it can't distinguish `Array.from(x,
23372342
/// mapFn)` from an unrelated `.from(x, y)` shaped differently (e.g.
23382343
/// `Buffer.from(data, encoding)`) — that residual risk is far narrower than
@@ -4569,6 +4574,39 @@ mod tests {
45694574
);
45704575
}
45714576

4577+
#[test]
4578+
fn applies_array_from_positional_gate_to_member_expression_args_too() {
4579+
// Mirrors the TS test of the same intent: the old member_expression
4580+
// guard was an explicit `&& memberExprArgsAllowed` inline check; the
4581+
// positional restructuring moved that responsibility to the shared
4582+
// early-return above the loop. `Array.from(arr, obj.mapper)` exercises
4583+
// that a member_expression at the positional index (1) is still
4584+
// emitted with its receiver, while one at index 0 is not.
4585+
let s = parse_js("Array.from(arr, obj.mapper);");
4586+
assert!(
4587+
s.calls.iter().any(|c| c.dynamic == Some(true) && c.name == "mapper" && c.receiver.as_deref() == Some("obj")),
4588+
"Array.from(arr, obj.mapper) must emit mapper with receiver obj; got: {:?}",
4589+
s.calls,
4590+
);
4591+
assert!(
4592+
!s.calls.iter().any(|c| c.dynamic == Some(true) && c.name == "arr"),
4593+
"Array.from(arr, obj.mapper) must not emit `arr` (index 0); got: {:?}",
4594+
s.calls,
4595+
);
4596+
4597+
let s2 = parse_js("Array.from(obj.arrayLike, mapCallback);");
4598+
assert!(
4599+
!s2.calls.iter().any(|c| c.dynamic == Some(true) && c.name == "arrayLike"),
4600+
"Array.from(obj.arrayLike, mapCallback) must not emit `arrayLike` (index 0); got: {:?}",
4601+
s2.calls,
4602+
);
4603+
assert!(
4604+
s2.calls.iter().any(|c| c.dynamic == Some(true) && c.name == "mapCallback"),
4605+
"Array.from(obj.arrayLike, mapCallback) must emit mapCallback; got: {:?}",
4606+
s2.calls,
4607+
);
4608+
}
4609+
45724610
#[test]
45734611
fn no_dynamic_call_for_dynamic_import_arg() {
45744612
// Parity with TS walk path: callback-reference extraction must be skipped

0 commit comments

Comments
 (0)