Skip to content

Commit 3df0ac4

Browse files
committed
fix: resolve merge conflicts with main (docs check acknowledged)
Impact: 1 functions changed, 2 affected
2 parents 2810b4c + cac0e79 commit 3df0ac4

108 files changed

Lines changed: 3042 additions & 563 deletions

File tree

Some content is hidden

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

.claude/hooks/track-edits.sh

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ if [ -z "$FILE_PATH" ]; then
2323
exit 0
2424
fi
2525

26+
# Normalize Windows-style separators before any dirname/git splitting below —
27+
# dirname (GNU coreutils) only splits on '/', so a backslash-delimited path
28+
# would make it silently no-op and return ".". Only touch paths that
29+
# unambiguously look like a Windows absolute path (drive letter prefix), so a
30+
# POSIX path containing a literal backslash in a filename is left untouched.
31+
# Uses grep/tr rather than a bash case pattern/parameter expansion so the
32+
# match is byte-based, not affected by the shell's active locale.
33+
if printf '%s' "$FILE_PATH" | grep -qE '^[A-Za-z]:[\\/]'; then
34+
FILE_PATH=$(printf '%s' "$FILE_PATH" | tr '\\' '/')
35+
fi
36+
2637
# Resolve the git worktree that actually owns the edited file, rather than
2738
# the hook process's own ambient cwd. Edit/Write tool calls carry only an
2839
# absolute file_path with no associated "current directory" state, so the
@@ -32,9 +43,9 @@ fi
3243
#
3344
# Walk up to the nearest existing ancestor directory first: Write can target
3445
# a not-yet-created nested directory, and `git -C` requires an existing path.
35-
SEARCH_DIR=$(dirname -- "$FILE_PATH")
46+
SEARCH_DIR=$(dirname "$FILE_PATH")
3647
while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do
37-
SEARCH_DIR=$(dirname -- "$SEARCH_DIR")
48+
SEARCH_DIR=$(dirname "$SEARCH_DIR")
3849
done
3950

4051
PROJECT_DIR=""
@@ -46,11 +57,39 @@ if [ -z "$PROJECT_DIR" ]; then
4657
fi
4758
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
4859

49-
# Normalize to relative path with forward slashes
60+
# Normalize to relative path with forward slashes. Canonicalize both sides
61+
# via realpath first (walking up to the nearest existing ancestor, since
62+
# Write can target a not-yet-created nested path) — on Windows the same
63+
# directory can be reported as either its long form or its auto-generated
64+
# 8.3 short-name alias (e.g. "runneradmin" vs "RUNNER~1") depending on which
65+
# API produced it, and a naive string-based path.relative() treats those as
66+
# unrelated trees, producing a long chain of spurious '../' segments.
5067
REL_PATH=$(node -e "
68+
const fs = require('fs');
5169
const path = require('path');
52-
const abs = path.resolve(process.argv[1]);
53-
const base = path.resolve(process.argv[2]);
70+
71+
function realpathWalkUp(p) {
72+
let dir = path.resolve(p);
73+
let tail = '';
74+
while (true) {
75+
try {
76+
// .native calls the OS's own canonicalization (GetFinalPathNameByHandleW
77+
// on Windows), which resolves 8.3 short-name aliases; the plain JS
78+
// fallback only walks symlinks component-by-component and would leave
79+
// a short-name alias like "RUNNER~1" untouched.
80+
const real = fs.realpathSync.native(dir);
81+
return tail ? path.join(real, tail) : real;
82+
} catch {
83+
const parent = path.dirname(dir);
84+
if (parent === dir) return path.resolve(p);
85+
tail = tail ? path.join(path.basename(dir), tail) : path.basename(dir);
86+
dir = parent;
87+
}
88+
}
89+
}
90+
91+
const abs = realpathWalkUp(process.argv[1]);
92+
const base = realpathWalkUp(process.argv[2]);
5493
const rel = path.relative(base, abs).split(path.sep).join('/');
5594
process.stdout.write(rel);
5695
" "$FILE_PATH" "$PROJECT_DIR" 2>/dev/null) || true

.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`.

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ installs two git hooks:
5858

5959
## Project Structure
6060

61-
Source is TypeScript under `src/`, compiled via `tsup`/`tsc`; the native engine
61+
Source is TypeScript under `src/`, compiled via `tsc`; the native engine
6262
lives in `crates/codegraph-core/` (Rust, via napi-rs) and mirrors the `src/`
6363
tree module-for-module. `src/` is organized by layer, not by language:
6464

@@ -327,9 +327,10 @@ configuration system, credential resolution, MCP isolation model) see
327327

328328
## WASM Grammars
329329

330-
`.wasm` grammar files are **not** committed to git — they're built from
330+
Most `.wasm` grammar files are **not** committed to git — they're built from
331331
`devDependencies` into `grammars/` automatically via the `prepare` npm script
332-
(`npm run build:wasm`), which runs on every `npm install`. Each git worktree
332+
(`npm run build:wasm`), which runs on every `npm install`; pinned exceptions
333+
such as `grammars/tree-sitter-erlang.wasm` remain tracked. Each git worktree
333334
therefore needs its own `npm install` before its `grammars/` directory is
334335
populated (see "Working in multiple git worktrees?" above; `npm run doctor`
335336
detects a missing or incomplete `grammars/`).

crates/codegraph-core/src/ast_analysis/complexity.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,22 @@ mod tests {
17481748
assert_eq!(m.cyclomatic, 3);
17491749
}
17501750

1751+
#[test]
1752+
fn lua_method_declaration() {
1753+
// Greptile follow-up to #1782: colon-syntax method declarations
1754+
// (`function Obj:method(x)`) have a `method_index_expression` name
1755+
// field but are still `function_declaration` nodes, so `function_nodes`
1756+
// already covers them — this pins that native/TS parity explicitly.
1757+
// Mirrors the TS test 'method declaration (colon syntax) is
1758+
// recognized as a function'.
1759+
let m = compute_lua(
1760+
"local Obj = {}\nfunction Obj:method(x)\n if x > 0 then\n return x\n end\nend",
1761+
);
1762+
assert_eq!(m.cognitive, 1);
1763+
assert_eq!(m.cyclomatic, 2);
1764+
assert_eq!(m.max_nesting, 1);
1765+
}
1766+
17511767
#[test]
17521768
fn lua_nested_if() {
17531769
let m = compute_lua(

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,10 +895,13 @@ impl NativeDatabase {
895895
let insert_ok = insert_nodes::do_insert_nodes(conn, &batches, &removed_files)
896896
.inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_nodes failed: {e}"))
897897
.is_ok();
898+
if !insert_ok {
899+
return Ok(false);
900+
}
898901
let hashes_ok = insert_nodes::commit_file_hashes(conn, &file_hashes)
899902
.inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_nodes hash commit failed: {e}"))
900903
.is_ok();
901-
Ok(insert_ok && hashes_ok)
904+
Ok(hashes_ok)
902905
}
903906

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

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

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,11 @@ impl<'a> EdgeContext<'a> {
206206
/// (a cross-file consumer added in an untouched file won't be seen until the
207207
/// next full rebuild) — the same scoping trade-off already accepted
208208
/// elsewhere in this codebase's incremental classification
209-
/// (`has_active_file_siblings`, exported-via-reexport, and median fan-in/out
210-
/// all recompute from the affected file set only, not the whole graph, in
211-
/// `graph/classifiers/roles.rs`'s incremental path). Mirrors
209+
/// (`has_active_file_siblings` and exported-via-reexport both recompute from
210+
/// the affected file set only, not the whole graph, in
211+
/// `graph/classifiers/roles.rs`'s incremental path — median fan-in/out is a
212+
/// separate case, deliberately kept as a whole-graph statistic even on the
213+
/// incremental path, for classification-threshold consistency). Mirrors
212214
/// `collectInvokedPropertyNames` in `src/domain/graph/builder/call-resolver.ts`.
213215
fn collect_invoked_property_names(files: &[FileEdgeInput]) -> HashSet<&str> {
214216
let mut names = HashSet::new();
@@ -831,13 +833,14 @@ fn process_file<'a>(
831833
// of these positions is as likely to be a plain data reference
832834
// (`{ name: SOME_CONSTANT }`) as a real function/class, so drop any
833835
// other-kind match rather than fabricating a "calls" edge to a
834-
// constant. `class` is included alongside function/method because
835-
// `instanceof`'s right operand is always a class/constructor
836-
// (#1784) — unlike the original #1771 object-literal case, which is
837-
// function/method only. Applied once here (after all
838-
// resolve_call_targets tiers), mirroring the
839-
// `dynamicKind === 'value-ref'` filter in resolveFallbackTargets
840-
// (stages/build-edges.ts).
836+
// constant. `class` was added because `instanceof`'s right operand
837+
// is always a class/constructor (#1784). The filter is keyed on
838+
// `dynamic_kind`, not on which site produced the call, so the #1771
839+
// object-literal and #1776 Lua sites also gain class-kind
840+
// resolution as a side effect — not because either idiom commonly
841+
// names a class. Applied once here (after all resolve_call_targets
842+
// tiers), mirroring the `dynamicKind === 'value-ref'` filter in
843+
// resolveFallbackTargets (stages/build-edges.ts).
841844
if call.dynamic_kind.as_deref() == Some("value-ref") {
842845
targets.retain(|t| t.kind == "function" || t.kind == "method" || t.kind == "class");
843846
// #1895: object-literal-property value-refs (key_expr set — see
@@ -888,7 +891,7 @@ fn process_file<'a>(
888891
}
889892
}
890893

891-
emit_hierarchy_edges(ctx, file_input, fc.rel_path, &fc.imported_names, edges);
894+
emit_hierarchy_edges(ctx, file_input, fc.rel_path, &fc.imported_names, &fc.imported_original_names, edges);
892895
}
893896

894897
/// Callable definition kinds — only function/method bodies act as enclosing
@@ -1622,7 +1625,11 @@ fn emit_receiver_edge(
16221625
/// the graph regardless of file or language, producing false cross-file
16231626
/// (even cross-language) hierarchy edges for common type names. Priority:
16241627
/// 1. Same-file declaration, when `name` is not itself an import artifact.
1625-
/// 2. The file's actually-resolved import for `name` (barrel-traced).
1628+
/// 2. The file's actually-resolved import for `name` (barrel-traced). For a
1629+
/// renamed import (`import { Base as MyBase }`), the imported file stores
1630+
/// the symbol under its original exported name, not the local alias — so
1631+
/// `imported_original_name` resolves `MyBase` back to `Base` before the
1632+
/// lookup, mirroring `resolve_call_targets` (#1730).
16261633
/// 3. Last resort: a same-language-family global-by-name match (#1783),
16271634
/// first candidate only — a heritage clause names exactly one type.
16281635
fn resolve_hierarchy_targets<'a>(
@@ -1631,6 +1638,7 @@ fn resolve_hierarchy_targets<'a>(
16311638
rel_path: &str,
16321639
imported_names: &HashMap<&str, &str>,
16331640
target_kinds: &[&str],
1641+
imported_original_names: &HashMap<&str, &str>,
16341642
) -> Vec<&'a NodeInfo> {
16351643
let samefile_all: Vec<&NodeInfo> = ctx.nodes_by_name_and_file
16361644
.get(&(name, rel_path))
@@ -1643,8 +1651,9 @@ fn resolve_hierarchy_targets<'a>(
16431651
}
16441652

16451653
if let Some(imported_from) = imported_names.get(name) {
1654+
let target_name = imported_original_names.get(name).copied().unwrap_or(name);
16461655
let imported_candidates: Vec<&NodeInfo> = ctx.nodes_by_name_and_file
1647-
.get(&(name, *imported_from))
1656+
.get(&(target_name, *imported_from))
16481657
.cloned().unwrap_or_default()
16491658
.into_iter()
16501659
.filter(|n| target_kinds.contains(&n.kind.as_str()))
@@ -1665,6 +1674,7 @@ fn resolve_hierarchy_targets<'a>(
16651674
fn emit_hierarchy_edges(
16661675
ctx: &EdgeContext, file_input: &FileEdgeInput, rel_path: &str,
16671676
imported_names: &HashMap<&str, &str>,
1677+
imported_original_names: &HashMap<&str, &str>,
16681678
edges: &mut Vec<ComputedEdge>,
16691679
) {
16701680
for cls in &file_input.classes {
@@ -1675,7 +1685,7 @@ fn emit_hierarchy_edges(
16751685
let Some(source) = source_row else { continue };
16761686

16771687
if let Some(ref extends_name) = cls.extends {
1678-
let targets = resolve_hierarchy_targets(ctx, extends_name, rel_path, imported_names, EXTENDS_TARGET_KINDS);
1688+
let targets = resolve_hierarchy_targets(ctx, extends_name, rel_path, imported_names, EXTENDS_TARGET_KINDS, imported_original_names);
16791689
for t in targets {
16801690
edges.push(ComputedEdge {
16811691
source_id: source.id, target_id: t.id,
@@ -1685,7 +1695,7 @@ fn emit_hierarchy_edges(
16851695
}
16861696
}
16871697
if let Some(ref implements_name) = cls.implements {
1688-
let targets = resolve_hierarchy_targets(ctx, implements_name, rel_path, imported_names, IMPLEMENTS_TARGET_KINDS);
1698+
let targets = resolve_hierarchy_targets(ctx, implements_name, rel_path, imported_names, IMPLEMENTS_TARGET_KINDS, imported_original_names);
16891699
for t in targets {
16901700
edges.push(ComputedEdge {
16911701
source_id: source.id, target_id: t.id,
@@ -1697,6 +1707,7 @@ fn emit_hierarchy_edges(
16971707
}
16981708
}
16991709

1710+
17001711
// ── Import edges (native) ──────────────────────────────────────────────
17011712

17021713
#[napi(object)]
@@ -1945,6 +1956,16 @@ fn is_named_reexport(imp: &ImportInfo) -> bool {
19451956
imp.reexport && !imp.wildcard_reexport
19461957
}
19471958

1959+
/// True for a genuine wildcard re-export (`export * from 'Y'`). Emitted as a
1960+
/// distinct file-level marker edge (`reexports-wildcard`) alongside the
1961+
/// generic `reexports` edge so the query layer can tell a target reached
1962+
/// only by named specifiers apart from one that's also reached by a
1963+
/// wildcard — even when a *different* statement in the same file names
1964+
/// specific symbols from that exact target (#1849 review).
1965+
fn is_wildcard_reexport(imp: &ImportInfo) -> bool {
1966+
imp.reexport && imp.wildcard_reexport
1967+
}
1968+
19481969
/// For a `type` import or a named re-export targeting a barrel or resolved
19491970
/// file, emit one symbol-level edge per named symbol so the target symbols
19501971
/// receive fan-in credit and aren't misclassified as dead code
@@ -2113,6 +2134,15 @@ fn process_single_import(
21132134
}
21142135
if is_named_reexport(imp) {
21152136
emit_named_symbol_edges(edges, file_input, imp, resolved_path, "reexports", ctx);
2137+
} else if is_wildcard_reexport(imp) {
2138+
edges.push(ComputedEdge {
2139+
source_id: file_input.file_node_id,
2140+
target_id: target_node_id,
2141+
kind: "reexports-wildcard".to_string(),
2142+
confidence: 1.0,
2143+
dynamic: 0,
2144+
dynamic_kind: None,
2145+
});
21162146
}
21172147
emit_barrel_through_edges(edges, file_input, imp, resolved_path, edge_kind, ctx);
21182148
}
@@ -2269,9 +2299,12 @@ mod import_edge_tests {
22692299

22702300
#[test]
22712301
fn wildcard_reexport_emits_no_symbol_level_edge() {
2272-
// `export * from './utils'` carries no specific names, so only the
2273-
// file-level `reexports` edge is emitted — the query layer falls
2274-
// back to the target's full export list for genuine wildcards.
2302+
// `export * from './utils'` carries no specific names, so no
2303+
// symbol-level edge is emitted. It does get the dedicated
2304+
// `reexports-wildcard` file-level marker (alongside the generic
2305+
// `reexports` edge) so the query layer can always apply full-export
2306+
// semantics for genuine wildcards, even when a *different* statement
2307+
// to the same target also names specific symbols (#1849 review).
22752308
let files = vec![make_file("src/index.ts", 1, vec![
22762309
ImportInfo {
22772310
source: "./utils".to_string(),
@@ -2302,9 +2335,64 @@ mod import_edge_tests {
23022335
"/root".to_string(),
23032336
Some(symbol_nodes),
23042337
);
2305-
assert_eq!(edges.len(), 1);
2338+
assert_eq!(edges.len(), 2);
23062339
assert_eq!(edges[0].kind, "reexports");
23072340
assert_eq!(edges[0].target_id, 2);
2341+
assert_eq!(edges[1].kind, "reexports-wildcard");
2342+
assert_eq!(edges[1].target_id, 2);
2343+
}
2344+
2345+
#[test]
2346+
fn named_and_wildcard_reexport_of_same_target_both_marked() {
2347+
// `export { foo } from './utils'` AND `export * from './utils'` in
2348+
// the same file, both targeting utils.ts. The wildcard's full-export
2349+
// semantics must stay independently signalled (via the dedicated
2350+
// `reexports-wildcard` marker) rather than being suppressed by the
2351+
// named specifier's symbol-level edge — otherwise the query layer
2352+
// would report only `foo` and silently drop every other export of
2353+
// utils.ts that the wildcard was meant to surface (#1849 review).
2354+
let files = vec![make_file("src/index.ts", 1, vec![
2355+
make_import("./utils", vec!["foo"], true, false, false),
2356+
ImportInfo {
2357+
source: "./utils".to_string(),
2358+
names: vec![],
2359+
reexport: true,
2360+
type_only: false,
2361+
dynamic_import: false,
2362+
wildcard_reexport: true,
2363+
type_only_names: vec![],
2364+
renamed_imports: vec![],
2365+
},
2366+
], vec![])];
2367+
let resolved = vec![make_resolved("/root/src/index.ts", "./utils", "src/utils.ts")];
2368+
let node_ids = vec![make_node_entry("src/index.ts", 1), make_node_entry("src/utils.ts", 2)];
2369+
let symbol_nodes = vec![SymbolNodeEntry {
2370+
name: "foo".to_string(),
2371+
file: "src/utils.ts".to_string(),
2372+
node_id: 99,
2373+
kind: "function".to_string(),
2374+
}];
2375+
2376+
let edges = build_import_edges(
2377+
files,
2378+
resolved,
2379+
vec![],
2380+
node_ids,
2381+
vec![],
2382+
"/root".to_string(),
2383+
Some(symbol_nodes),
2384+
);
2385+
assert_eq!(edges.len(), 4);
2386+
// Named statement: file-level `reexports` + symbol-level `reexports` to foo.
2387+
assert_eq!(edges[0].kind, "reexports");
2388+
assert_eq!(edges[0].target_id, 2);
2389+
assert_eq!(edges[1].kind, "reexports");
2390+
assert_eq!(edges[1].target_id, 99);
2391+
// Wildcard statement: file-level `reexports` + the `reexports-wildcard` marker.
2392+
assert_eq!(edges[2].kind, "reexports");
2393+
assert_eq!(edges[2].target_id, 2);
2394+
assert_eq!(edges[3].kind, "reexports-wildcard");
2395+
assert_eq!(edges[3].target_id, 2);
23082396
}
23092397

23102398
#[test]

0 commit comments

Comments
 (0)