From 3892e7dcddd5173d4d83cc6839ce0d888aa36226 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:31:28 -0600 Subject: [PATCH 1/8] chore: gitignore napi-generated artifacts in crates/codegraph-core --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index aa62bcb59..2606dd88f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ dist/ coverage/ .env grammars/*.wasm +crates/codegraph-core/index.js +crates/codegraph-core/index.d.ts +crates/codegraph-core/*.node .claude/session-edits.log .claude/worktrees/ generated/DEPENDENCIES.md From ef8ea4fb31f4c073529c8772fa3aa9288a992fd6 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:32:19 -0600 Subject: [PATCH 2/8] chore(tests): remove unused biome suppression in visitor.test.ts --- tests/unit/visitor.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/visitor.test.ts b/tests/unit/visitor.test.ts index 992b4307a..62dc5b3e6 100644 --- a/tests/unit/visitor.test.ts +++ b/tests/unit/visitor.test.ts @@ -4,7 +4,6 @@ import { describe, expect, it } from 'vitest'; // We need a tree-sitter tree to test. Use the JS parser. -// biome-ignore lint/suspicious/noExplicitAny: tree-sitter parser type is complex and not worth typing for tests let parse: any; async function ensureParser() { From a372b82593ddfecd591c17133ef378872ffbec13 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:33:09 -0600 Subject: [PATCH 3/8] fix(titan-run): sync --start-from enum and phase-timestamp list with actual phases --- .claude/skills/titan-run/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/titan-run/SKILL.md b/.claude/skills/titan-run/SKILL.md index 51e22569f..04c3fdc92 100644 --- a/.claude/skills/titan-run/SKILL.md +++ b/.claude/skills/titan-run/SKILL.md @@ -1,7 +1,7 @@ --- name: titan-run description: Run the full Titan Paradigm pipeline end-to-end by dispatching each phase to sub-agents with fresh context windows. Orchestrates recon → gauntlet → sync → forge → grind (+ repo-provided parity audit) automatically. -argument-hint: <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind|parity> <--gauntlet-batch-size 5> <--yes> +argument-hint: <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind|parity|close> <--gauntlet-batch-size 5> <--yes> allowed-tools: Agent, Read, Bash, Glob, Write, Edit --- @@ -50,7 +50,7 @@ You are the **orchestrator** for the full Titan Paradigm pipeline. Your job is t node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('.codegraph/titan/titan-state.json','utf8'));s.phaseTimestamps=s.phaseTimestamps||{};s.phaseTimestamps['']=s.phaseTimestamps['']||{};s.phaseTimestamps[''].completedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));" ``` - Replace `` with `recon`, `gauntlet`, `sync`, `forge`, `parity`, or `close`. **Run the start command immediately before dispatching each phase's first sub-agent, and the completion command immediately after post-phase validation passes.** If resuming a phase (e.g., gauntlet loop iteration 2+), do NOT overwrite `startedAt` — only set it if it doesn't already exist. + Replace `` with `recon`, `gauntlet`, `sync`, `forge`, `grind`, `parity`, or `close`. **Run the start command immediately before dispatching each phase's first sub-agent, and the completion command immediately after post-phase validation passes.** If resuming a phase (e.g., gauntlet loop iteration 2+), do NOT overwrite `startedAt` — only set it if it doesn't already exist. **Timestamp validation:** After recording `completedAt` for any phase, verify `startedAt < completedAt`: ```bash From 9a52c7cc5eea2cba016d14ed3928e07128fad4e3 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:36:04 -0600 Subject: [PATCH 4/8] fix(hooks): track Bash file modifications via before/after git status diff Adds snapshot-pre-bash.sh (PreToolUse Bash) + track-bash-writes.sh (PostToolUse Bash): the pre-hook captures git status --porcelain to a per-worktree temp file before each Bash call; the post-hook diffs the before/after state and appends newly modified or created files to .claude/session-edits.log. This closes the gap where files written by sed -i, printf redirects, tee, heredocs, or build tools (Cargo.lock, lockfiles) were never recorded, causing guard-git.sh to emit false-positive BLOCKED errors. Closes #1457 --- .claude/hooks/snapshot-pre-bash.sh | 54 +++++++++++++ .claude/hooks/track-bash-writes.sh | 119 +++++++++++++++++++++++++++++ .claude/settings.json | 10 +++ 3 files changed, 183 insertions(+) create mode 100755 .claude/hooks/snapshot-pre-bash.sh create mode 100755 .claude/hooks/track-bash-writes.sh diff --git a/.claude/hooks/snapshot-pre-bash.sh b/.claude/hooks/snapshot-pre-bash.sh new file mode 100755 index 000000000..a91caebbb --- /dev/null +++ b/.claude/hooks/snapshot-pre-bash.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# snapshot-pre-bash.sh — PreToolUse hook for Bash tool calls +# Snapshots `git status --porcelain` to a temp file before each Bash call so +# that track-bash-writes.sh (PostToolUse) can diff the before/after state and +# log files newly modified by the command to .claude/session-edits.log. +# Always exits 0 (informational only, never blocks). + +set -euo pipefail + +INPUT=$(cat) + +# Extract the command from tool_input JSON +COMMAND=$(echo "$INPUT" | node -e " + let d=''; + process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + const p=JSON.parse(d).tool_input?.command||''; + if(p)process.stdout.write(p); + }); +" 2>/dev/null) || true + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Skip read-only commands that can never write files — reduces snapshot overhead +# for the most common Bash calls (ls, cat, grep, git log, git status, etc.). +# sed is intentionally NOT in this list because `sed -i` modifies files in-place. +if echo "$COMMAND" | grep -qE '^\s*(ls|cat|head|tail|grep|find|git\s+(log|status|diff|show|branch|remote|fetch|rev-parse|stash\s+list|ls-files|blame|describe|tag|config\s+--get)|gh\s+(pr|issue|repo)\s+(view|list|status)|echo|printf|pwd|which|node\s+-e|node\s+-p|npx\s+--version|wc|sort|uniq|awk)\b'; then + exit 0 +fi + +# Resolve the project root (worktree-aware — each worktree has its own .claude/) +PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" + +# Key the snapshot file to the project root so parallel worktrees don't collide. +# Use a simple hash of the path — just enough to be unique per worktree. +PROJECT_HASH=$(echo "$PROJECT_DIR" | node -e " + const crypto = require('crypto'); + let d=''; + process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + process.stdout.write(crypto.createHash('sha1').update(d.trim()).digest('hex').slice(0,8)); + }); +" 2>/dev/null) || PROJECT_HASH="default" + +SNAPSHOT_FILE="/tmp/claude-bash-snapshot-${PROJECT_HASH}.txt" + +# Capture current git status --porcelain. +# Lines look like: "XY filename" or "XY orig -> dest" (rename). +# We only care about the status marker and path — porcelain is stable across git versions. +git -C "$PROJECT_DIR" status --porcelain 2>/dev/null > "$SNAPSHOT_FILE" || true + +exit 0 diff --git a/.claude/hooks/track-bash-writes.sh b/.claude/hooks/track-bash-writes.sh new file mode 100755 index 000000000..e5d1ded98 --- /dev/null +++ b/.claude/hooks/track-bash-writes.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# track-bash-writes.sh — PostToolUse hook for Bash tool calls +# Compares `git status --porcelain` against the snapshot taken by +# snapshot-pre-bash.sh (PreToolUse) to detect files newly modified or +# created by the Bash command, then appends them to .claude/session-edits.log +# so that guard-git.sh can validate commits correctly. +# Always exits 0 (informational only, never blocks). + +set -euo pipefail + +INPUT=$(cat) + +# Extract the command from tool_input JSON +COMMAND=$(echo "$INPUT" | node -e " + let d=''; + process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + const p=JSON.parse(d).tool_input?.command||''; + if(p)process.stdout.write(p); + }); +" 2>/dev/null) || true + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Resolve the project root (worktree-aware — each worktree has its own .claude/) +PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" + +# Reproduce the same project hash used by snapshot-pre-bash.sh +PROJECT_HASH=$(echo "$PROJECT_DIR" | node -e " + const crypto = require('crypto'); + let d=''; + process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + process.stdout.write(crypto.createHash('sha1').update(d.trim()).digest('hex').slice(0,8)); + }); +" 2>/dev/null) || PROJECT_HASH="default" + +SNAPSHOT_FILE="/tmp/claude-bash-snapshot-${PROJECT_HASH}.txt" + +# If there is no snapshot (hook was not installed yet, or the pre-hook was +# skipped for a read-only command) we have no baseline — exit cleanly. +if [ ! -f "$SNAPSHOT_FILE" ]; then + exit 0 +fi + +# Capture current state after the command ran +AFTER=$(git -C "$PROJECT_DIR" status --porcelain 2>/dev/null) || true + +# Read the before-state +BEFORE=$(cat "$SNAPSHOT_FILE") || true + +# Clean up the snapshot so it doesn't pollute the next command's pre-hook +rm -f "$SNAPSHOT_FILE" + +# Build the set of paths that existed (as dirty) before the command ran. +# porcelain format: "XY path" or "XY original -> new" (rename). +# We extract every path token after the two-char status code. +parse_paths() { + local status_output="$1" + echo "$status_output" | awk ' + /^[ MADRCU?!]{2} / { + # Drop the two-char status + space + rest = substr($0, 4) + # Handle rename: "old -> new" + if (index(rest, " -> ") > 0) { + n = split(rest, parts, " -> ") + for (i = 1; i <= n; i++) { + p = parts[i] + gsub(/^"/, "", p); gsub(/"$/, "", p) + if (p != "") print p + } + } else { + gsub(/^"/, "", rest); gsub(/"$/, "", rest) + if (rest != "") print rest + } + } + ' +} + +BEFORE_PATHS=$(parse_paths "$BEFORE" | sort) +AFTER_PATHS=$(parse_paths "$AFTER" | sort) + +if [ -z "$AFTER_PATHS" ]; then + exit 0 +fi + +# Find paths present in AFTER but not in BEFORE — these were newly dirtied +# (modified, created, or renamed-to) by the Bash command. +NEW_PATHS=$(comm -13 <(echo "$BEFORE_PATHS") <(echo "$AFTER_PATHS")) || true + +if [ -z "$NEW_PATHS" ]; then + exit 0 +fi + +# Also exclude paths that were already tracked by track-edits.sh or other hooks +# (i.e. already in the session-edits.log) so we don't double-log. +LOG_FILE="$PROJECT_DIR/.claude/session-edits.log" +ALREADY_LOGGED="" +if [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then + ALREADY_LOGGED=$(awk '{print $2}' "$LOG_FILE" | sort -u) +fi + +mkdir -p "$(dirname "$LOG_FILE")" +TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +while IFS= read -r rel_path; do + if [ -z "$rel_path" ]; then + continue + fi + # Skip if already in the log from a prior hook (Edit/Write/track-moves) + if [ -n "$ALREADY_LOGGED" ] && echo "$ALREADY_LOGGED" | grep -qxF "$rel_path"; then + continue + fi + echo "$TS $rel_path" >> "$LOG_FILE" +done <<< "$NEW_PATHS" + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index b3acd6d1b..7ab746809 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,6 +7,11 @@ { "matcher": "Bash", "hooks": [ + { + "type": "command", + "command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/snapshot-pre-bash.sh\"", + "timeout": 5 + }, { "type": "command", "command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/check-readme.sh\"", @@ -79,6 +84,11 @@ { "matcher": "Bash", "hooks": [ + { + "type": "command", + "command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/track-bash-writes.sh\"", + "timeout": 5 + }, { "type": "command", "command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/track-moves.sh\"", From 85a26df4f1a06db752548b3f5e3d299ec5f46806 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:39:52 -0600 Subject: [PATCH 5/8] chore(native): remove dead code (unused var, method, variant, fields) - clojure.rs: annotate lifetime-anchor assignment to silence false-positive - cfg.rs: remove never-called start_line_of method - complexity.rs: remove never-constructed NotHandled variant; convert irrefutable if-let patterns to plain let destructures - dataflow.rs: remove never-read callee fields from CallReturn/Destructured - incremental.rs: remove never-read lang field from CacheEntry cargo check and cargo clippy both clean after these changes. --- crates/codegraph-core/src/ast_analysis/cfg.rs | 4 --- .../src/ast_analysis/complexity.rs | 36 ++++++++----------- .../src/ast_analysis/dataflow.rs | 10 +++--- .../src/domain/graph/builder/incremental.rs | 3 +- .../codegraph-core/src/extractors/clojure.rs | 4 +++ 5 files changed, 25 insertions(+), 32 deletions(-) diff --git a/crates/codegraph-core/src/ast_analysis/cfg.rs b/crates/codegraph-core/src/ast_analysis/cfg.rs index 226a31362..fb784d40f 100644 --- a/crates/codegraph-core/src/ast_analysis/cfg.rs +++ b/crates/codegraph-core/src/ast_analysis/cfg.rs @@ -659,10 +659,6 @@ impl<'a> CfgBuilder<'a> { } } - fn start_line_of(&self, block_idx: u32) -> Option { - self.blocks.iter().find(|b| b.index == block_idx).and_then(|b| b.start_line) - } - /// Get statement children from a block or statement list. fn get_statements<'b>(&self, node: &Node<'b>) -> Vec> { let kind = node.kind(); diff --git a/crates/codegraph-core/src/ast_analysis/complexity.rs b/crates/codegraph-core/src/ast_analysis/complexity.rs index fdd572512..9827b091f 100644 --- a/crates/codegraph-core/src/ast_analysis/complexity.rs +++ b/crates/codegraph-core/src/ast_analysis/complexity.rs @@ -516,8 +516,6 @@ fn walk_children( enum BranchAction { /// Node handled — walk children at the given nesting delta, then return. Handled { cognitive_delta: u32, cyclomatic_delta: u32, nesting_delta: u32 }, - /// Not a special branch pattern — fall through to normal processing. - NotHandled, } /// Classify a branch node (one where `rules.is_branch(kind)` is true). @@ -675,14 +673,12 @@ fn walk( // Branch/control flow nodes (skip keyword leaf tokens) if rules.is_branch(kind) && node.child_count() > 0 { - if let BranchAction::Handled { cognitive_delta, cyclomatic_delta, nesting_delta } = - classify_branch(node, kind, rules, nesting_level) - { - *cognitive += cognitive_delta; - *cyclomatic += cyclomatic_delta; - walk_children(node, nesting_level + nesting_delta, false, rules, cognitive, cyclomatic, max_nesting, depth); - return; - } + let BranchAction::Handled { cognitive_delta, cyclomatic_delta, nesting_delta } = + classify_branch(node, kind, rules, nesting_level); + *cognitive += cognitive_delta; + *cyclomatic += cyclomatic_delta; + walk_children(node, nesting_level + nesting_delta, false, rules, cognitive, cyclomatic, max_nesting, depth); + return; } // Pattern C plain else (Go/Java) @@ -1323,17 +1319,15 @@ fn walk_all( // Branch/control flow nodes (skip keyword leaf tokens) if c_rules.is_branch(kind) && node.child_count() > 0 { - if let BranchAction::Handled { cognitive_delta, cyclomatic_delta, nesting_delta } = - classify_branch(node, kind, c_rules, nesting_level) - { - *cognitive += cognitive_delta; - *cyclomatic += cyclomatic_delta; - walk_all_children( - node, source, nesting_level + nesting_delta, false, skip_h, - c_rules, h_rules, cognitive, cyclomatic, max_nesting, operators, operands, - ); - return; - } + let BranchAction::Handled { cognitive_delta, cyclomatic_delta, nesting_delta } = + classify_branch(node, kind, c_rules, nesting_level); + *cognitive += cognitive_delta; + *cyclomatic += cyclomatic_delta; + walk_all_children( + node, source, nesting_level + nesting_delta, false, skip_h, + c_rules, h_rules, cognitive, cyclomatic, max_nesting, operators, operands, + ); + return; } // Pattern C plain else (Go/Java) diff --git a/crates/codegraph-core/src/ast_analysis/dataflow.rs b/crates/codegraph-core/src/ast_analysis/dataflow.rs index ddb4a11a1..5a897c0b9 100644 --- a/crates/codegraph-core/src/ast_analysis/dataflow.rs +++ b/crates/codegraph-core/src/ast_analysis/dataflow.rs @@ -882,8 +882,8 @@ fn collect_identifiers(node: &Node, out: &mut Vec, rules: &DataflowRules #[derive(Debug, Clone)] enum LocalSource { - CallReturn { callee: String }, - Destructured { callee: String }, + CallReturn, + Destructured, } struct ScopeFrame { @@ -1200,7 +1200,7 @@ fn handle_var_declarator( }); scope .locals - .insert(n.clone(), LocalSource::Destructured { callee: callee.clone() }); + .insert(n.clone(), LocalSource::Destructured); } } else { let var_name = node_text(&name_n, source).to_string(); @@ -1211,7 +1211,7 @@ fn handle_var_declarator( expression: truncate(node_text(node, source), DATAFLOW_TRUNCATION_LIMIT), line: node_line(node), }); - scope.locals.insert(var_name, LocalSource::CallReturn { callee }); + scope.locals.insert(var_name, LocalSource::CallReturn); } } @@ -1267,7 +1267,7 @@ fn handle_assignment( line: node_line(node), }); if let Some(scope) = scope_stack.last_mut() { - scope.locals.insert(var_name, LocalSource::CallReturn { callee }); + scope.locals.insert(var_name, LocalSource::CallReturn); } } } diff --git a/crates/codegraph-core/src/domain/graph/builder/incremental.rs b/crates/codegraph-core/src/domain/graph/builder/incremental.rs index 35fa04345..4985904ed 100644 --- a/crates/codegraph-core/src/domain/graph/builder/incremental.rs +++ b/crates/codegraph-core/src/domain/graph/builder/incremental.rs @@ -10,7 +10,6 @@ use crate::types::FileSymbols; struct CacheEntry { tree: Tree, - lang: LanguageKind, } /// Cache of parse trees for incremental parsing. @@ -51,7 +50,7 @@ impl ParseTreeCache { let symbols = extract_symbols(lang, &tree, source_bytes, &file_path); - self.entries.insert(file_path, CacheEntry { tree, lang }); + self.entries.insert(file_path, CacheEntry { tree }); Some(symbols) } diff --git a/crates/codegraph-core/src/extractors/clojure.rs b/crates/codegraph-core/src/extractors/clojure.rs index b5160474f..7263ecf1a 100644 --- a/crates/codegraph-core/src/extractors/clojure.rs +++ b/crates/codegraph-core/src/extractors/clojure.rs @@ -51,6 +51,10 @@ fn walk_clojure( return; } + // `next_ns_owned` holds the String so that `next_ns` can borrow it as + // `&str` for the duration of this stack frame. The assignment looks + // "never read" to the compiler but the borrow on the next line reads it. + #[allow(unused_assignments)] let mut next_ns_owned: Option = None; let next_ns: Option<&str> = if node.kind() == "list_lit" { match handle_list_form(node, source, symbols, current_ns) { From 184d22167f2f58a7a569538af6b616a1c11a3744 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:41:46 -0600 Subject: [PATCH 6/8] refactor(native): extract emit_pts_alias_edges params into PtsAliasCtx struct --- .../graph/builder/stages/build_edges.rs | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs b/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs index 3475adebe..39108e3d8 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs @@ -360,50 +360,55 @@ fn resolve_via_points_to<'a>( } } +/// Per-call-site inputs for `emit_pts_alias_edges`. +/// Groups the lookup parameters so the function stays within the argument-count limit. +struct PtsAliasCtx<'a> { + pts: &'a HashMap>, + lookup_name: &'a str, + call_line: u32, + caller_id: u32, + caller_name: &'a str, + is_dynamic: u32, + rel_path: &'a str, + imported_names: &'a HashMap<&'a str, &'a str>, + type_map: &'a HashMap<&'a str, (&'a str, f64)>, +} + /// Resolve each pts alias of `lookup_name` and emit hop-penalised call edges. /// Shared by the no-receiver gate and the receiver-key (`rest.prop()`) fallback; /// mirrors the alias-emission loops in buildFileCallEdges (build-edges.ts). -#[allow(clippy::too_many_arguments)] fn emit_pts_alias_edges<'a>( ctx: &EdgeContext<'a>, - pts: &HashMap>, - lookup_name: &str, - call_line: u32, - caller_id: u32, - caller_name: &str, - is_dynamic: u32, - rel_path: &str, - imported_names: &HashMap<&str, &str>, - type_map: &HashMap<&str, (&str, f64)>, + alias_ctx: &PtsAliasCtx<'_>, seen_edges: &HashSet, pts_edge_map: &mut HashMap, edges: &mut Vec, ) { - for alias in resolve_via_points_to(lookup_name, pts) { - let alias_imported_from = imported_names.get(alias).copied(); + for alias in resolve_via_points_to(alias_ctx.lookup_name, alias_ctx.pts) { + let alias_imported_from = alias_ctx.imported_names.get(alias).copied(); let alias_call = CallInfo { name: alias.to_string(), - line: call_line, + line: alias_ctx.call_line, dynamic: Some(true), receiver: None, }; let mut alias_targets = resolve_call_targets( - ctx, &alias_call, rel_path, alias_imported_from, type_map, caller_name, + ctx, &alias_call, alias_ctx.rel_path, alias_imported_from, alias_ctx.type_map, alias_ctx.caller_name, ); - sort_targets_by_confidence(&mut alias_targets, rel_path, alias_imported_from); + sort_targets_by_confidence(&mut alias_targets, alias_ctx.rel_path, alias_imported_from); for t in &alias_targets { - let edge_key = ((caller_id as u64) << 32) | (t.id as u64); - if t.id != caller_id && !seen_edges.contains(&edge_key) && !pts_edge_map.contains_key(&edge_key) { - let conf = resolve::compute_confidence(rel_path, &t.file, alias_imported_from) + let edge_key = ((alias_ctx.caller_id as u64) << 32) | (t.id as u64); + if t.id != alias_ctx.caller_id && !seen_edges.contains(&edge_key) && !pts_edge_map.contains_key(&edge_key) { + let conf = resolve::compute_confidence(alias_ctx.rel_path, &t.file, alias_imported_from) - PROPAGATION_HOP_PENALTY; if conf > 0.0 { pts_edge_map.insert(edge_key, edges.len()); edges.push(ComputedEdge { - source_id: caller_id, + source_id: alias_ctx.caller_id, target_id: t.id, kind: "calls".to_string(), confidence: conf, - dynamic: is_dynamic, + dynamic: alias_ctx.is_dynamic, }); } } @@ -593,8 +598,21 @@ fn process_file<'a>( }; if let Some(lookup_name) = lookup_name { emit_pts_alias_edges( - ctx, pts, &lookup_name, call.line, caller_id, caller_name, is_dynamic, - rel_path, &imported_names, &type_map, &seen_edges, &mut pts_edge_map, edges, + ctx, + &PtsAliasCtx { + pts, + lookup_name: &lookup_name, + call_line: call.line, + caller_id, + caller_name, + is_dynamic, + rel_path, + imported_names: &imported_names, + type_map: &type_map, + }, + &seen_edges, + &mut pts_edge_map, + edges, ); } } @@ -609,8 +627,21 @@ fn process_file<'a>( let receiver_key = format!("{}.{}", receiver, call.name); if pts.contains_key(receiver_key.as_str()) { emit_pts_alias_edges( - ctx, pts, &receiver_key, call.line, caller_id, caller_name, is_dynamic, - rel_path, &imported_names, &type_map, &seen_edges, &mut pts_edge_map, edges, + ctx, + &PtsAliasCtx { + pts, + lookup_name: &receiver_key, + call_line: call.line, + caller_id, + caller_name, + is_dynamic, + rel_path, + imported_names: &imported_names, + type_map: &type_map, + }, + &seen_edges, + &mut pts_edge_map, + edges, ); } } From 909e1df55b58fee7cc7d5942e1132be648fd7169 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 18:43:24 -0600 Subject: [PATCH 7/8] fix(wasm): sort call targets by confidence before emit to match native engine --- src/domain/graph/builder/stages/build-edges.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/domain/graph/builder/stages/build-edges.ts b/src/domain/graph/builder/stages/build-edges.ts index 3c0f2e3c3..88027aee0 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -1107,6 +1107,19 @@ function buildFileCallEdges( } } + // Sort targets by confidence descending before emitting edges. + // For multi-target calls with duplicate (source_id, target_id) pairs the + // stored confidence depends on which duplicate is processed last — sorting + // here guarantees the highest-confidence target wins on dedup, matching the + // native engine's sort_targets_by_confidence call in build_edges.rs. + if (targets.length > 1) { + targets = [...targets].sort( + (a, b) => + computeConfidence(relPath, b.file, importedFrom ?? null) - + computeConfidence(relPath, a.file, importedFrom ?? null), + ); + } + for (const t of targets) { const edgeKey = `${caller.id}|${t.id}`; if (t.id !== caller.id) { From a0892c9bedaac6b77db7bcc1bf24022579e763b9 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 19:57:45 -0600 Subject: [PATCH 8/8] fix(wasm): sort aliasTargets by confidence in pts fallback paths The Phase 8.3 and Phase 8.3f pts alias loops iterated aliasTargets without sorting first. The native engine's emit_pts_alias_edges calls sort_targets_by_confidence before the alias loop (build_edges.rs:398), ensuring the highest-confidence target wins the ptsEdgeRows dedup. Apply the same descending-confidence sort to both TS pts fallback paths to close this parity gap. --- .../graph/builder/stages/build-edges.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/domain/graph/builder/stages/build-edges.ts b/src/domain/graph/builder/stages/build-edges.ts index 88027aee0..64f5827f8 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -1201,7 +1201,15 @@ function buildFileCallEdges( importedNames, typeMap as Map, ); - for (const t of aliasTargets) { + const sortedAliasTargets = + aliasTargets.length > 1 + ? [...aliasTargets].sort( + (a, b) => + computeConfidence(relPath, b.file, aliasFrom ?? null) - + computeConfidence(relPath, a.file, aliasFrom ?? null), + ) + : aliasTargets; + for (const t of sortedAliasTargets) { const edgeKey = `${caller.id}|${t.id}`; if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) { const conf = @@ -1237,7 +1245,15 @@ function buildFileCallEdges( importedNames, typeMap as Map, ); - for (const t of aliasTargets) { + const sortedAliasTargets = + aliasTargets.length > 1 + ? [...aliasTargets].sort( + (a, b) => + computeConfidence(relPath, b.file, aliasFrom ?? null) - + computeConfidence(relPath, a.file, aliasFrom ?? null), + ) + : aliasTargets; + for (const t of sortedAliasTargets) { const edgeKey = `${caller.id}|${t.id}`; if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) { const conf =