Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3892e7d
chore: gitignore napi-generated artifacts in crates/codegraph-core
carlos-alm Jun 13, 2026
ef8ea4f
chore(tests): remove unused biome suppression in visitor.test.ts
carlos-alm Jun 13, 2026
a372b82
fix(titan-run): sync --start-from enum and phase-timestamp list with …
carlos-alm Jun 13, 2026
9a52c7c
fix(hooks): track Bash file modifications via before/after git status…
carlos-alm Jun 13, 2026
85a26df
chore(native): remove dead code (unused var, method, variant, fields)
carlos-alm Jun 13, 2026
184d221
refactor(native): extract emit_pts_alias_edges params into PtsAliasCt…
carlos-alm Jun 13, 2026
909e1df
fix(wasm): sort call targets by confidence before emit to match nativ…
carlos-alm Jun 13, 2026
66fc899
fix(bench): add 2 warmup runs and raise INCREMENTAL_RUNS to 5 for inc…
carlos-alm Jun 13, 2026
84e1a5f
ci(bench): add per-PR perf canary for extractor/graph/native changes
carlos-alm Jun 13, 2026
d07b358
fix(perf): plumb symbolsOnly through parseFilesWasmInline to skip ana…
carlos-alm Jun 13, 2026
3db5d8c
fix(perf): scope runPostNativeCha to changed files on incremental builds
carlos-alm Jun 13, 2026
8b3aa3d
fix(native): add post-pass phase timings to result.phases
carlos-alm Jun 13, 2026
fd4ffd1
fix(perf): correct INLINE_BACKFILL_THRESHOLD docstring; raise thresho…
carlos-alm Jun 13, 2026
498ee21
fix(perf): guard post-native passes against unnecessary work on 1-fil…
carlos-alm Jun 13, 2026
61a9839
chore(types): remove dead protoMethodsMs field and stale comment
carlos-alm Jun 13, 2026
5f5d4d2
fix: class-scope field annotation typeMap keys to prevent cross-class…
carlos-alm Jun 13, 2026
bd5292e
Merge remote-tracking branch 'origin/main' into fix/field-typemap-cla…
carlos-alm Jun 13, 2026
cff36c6
fix(perf): update stale parseFilesWasmForBackfill docstring to refere…
carlos-alm Jun 13, 2026
06a911e
fix(resolver): check class-scoped typeMap key before bare fallback fo…
carlos-alm Jun 13, 2026
84f6c7c
test(1458): add Rust multi-class field collision unit test and end-to…
carlos-alm Jun 13, 2026
76a4380
fix(test): use TypeScript parser for field annotation collision test
carlos-alm Jun 13, 2026
1bb912a
fix: resolve merge conflicts with main
carlos-alm Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .claude/hooks/snapshot-pre-bash.sh
Original file line number Diff line number Diff line change
@@ -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
119 changes: 119 additions & 0 deletions .claude/hooks/track-bash-writes.sh
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down Expand Up @@ -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\"",
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/titan-run/SKILL.md
Original file line number Diff line number Diff line change
@@ -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: <path (default: .)> <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind|parity> <--gauntlet-batch-size 5> <--yes>
argument-hint: <path (default: .)> <--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
---

Expand Down Expand Up @@ -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['<PHASE>']=s.phaseTimestamps['<PHASE>']||{};s.phaseTimestamps['<PHASE>'].completedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));"
```

Replace `<PHASE>` 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 `<PHASE>` 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
Expand Down
111 changes: 111 additions & 0 deletions .github/workflows/perf-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Perf Canary

# Lightweight per-PR build-time regression gate for PRs that touch the
# extractor, graph-builder, or native Rust layers — the parts of the codebase
# that caused the v3.12.0 regressions (+1827% 1-file rebuild, +98% full build).
#
# Only the incremental-benchmark suite is run (full build + no-op + 1-file
# rebuild for both engines). The regression guard uses BENCH_CANARY=1 mode,
# which applies a 50% threshold instead of the full suite's 25% — enough
# to catch catastrophic regressions while tolerating CI runner variance.
#
# This is intentionally separate from the full pre-publish-benchmark job in
# ci.yml, which runs unconditionally on every PR and measures the complete
# suite. The canary completes in roughly 5–10 minutes; the full suite takes
# 20–60 minutes.

on:
pull_request:
paths:
- "src/extractors/**"
- "src/domain/graph/**"
- "crates/**"
- "scripts/benchmark.ts"
- "scripts/incremental-benchmark.ts"
- "scripts/lib/bench-config.ts"
- "scripts/lib/fork-engine.ts"

concurrency:
group: perf-canary-${{ github.ref }}
cancel-in-progress: true

jobs:
perf-canary:
name: Perf canary (incremental tiers)
runs-on: ubuntu-latest
env:
CODEGRAPH_FAST_SKIP_DIAG: "1"

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-node@v6
with:
node-version: "22"
cache: "npm"

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: crates/codegraph-core

- name: Install napi-rs CLI
timeout-minutes: 5
run: npm install -g @napi-rs/cli@3

- name: Build native addon
working-directory: crates/codegraph-core
run: napi build --release

- name: Install dependencies
timeout-minutes: 20
shell: bash
run: |
for attempt in 1 2 3; do
npm install && break
if [ "$attempt" -lt 3 ]; then
echo "::warning::npm install attempt $attempt failed, retrying in 15s..."
sleep 15
else
echo "::error::npm install failed after 3 attempts"
exit 1
fi
done

- name: Install native addon over published binary
run: node scripts/ci-install-native.mjs

# Build dist/ so benchmarks load the same compiled JS that ships to npm,
# matching the methodology used by the full pre-publish-benchmark gate.
- name: Build TypeScript
run: npm run build

- name: Run incremental benchmark
timeout-minutes: 15
run: |
STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')")
node $STRIP_FLAG --import ./scripts/ts-resolve-loader.js scripts/incremental-benchmark.ts --version dev --dist > incremental-canary-result.json

- name: Update incremental report
run: |
STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')")
node $STRIP_FLAG scripts/update-incremental-report.ts incremental-canary-result.json

- name: Regression guard (50% threshold)
env:
RUN_REGRESSION_GUARD: "1"
BENCH_CANARY: "1"
run: npm run test:regression-guard

- name: Upload canary result
if: always()
uses: actions/upload-artifact@v7
with:
name: incremental-canary-result
path: incremental-canary-result.json
if-no-files-found: warn
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions crates/codegraph-core/src/ast_analysis/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,10 +659,6 @@ impl<'a> CfgBuilder<'a> {
}
}

fn start_line_of(&self, block_idx: u32) -> Option<u32> {
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<Node<'b>> {
let kind = node.kind();
Expand Down
Loading
Loading