Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5baf3ad
perf: skip ensureWasmTrees when native engine provides complete data
carlos-alm Mar 5, 2026
d411ab6
fix: address PR #344 review comments — TODO for constant exclusion, c…
carlos-alm Mar 5, 2026
4317e0d
fix: add null guard on symbols.definitions in pre-parse check
carlos-alm Mar 5, 2026
c2bb5de
fix: treat empty cfg.blocks array as valid native CFG
carlos-alm Mar 5, 2026
7e6b489
style: format cfg check per biome rules
carlos-alm Mar 5, 2026
f0a5522
feat: add `sequence` command for Mermaid sequence diagrams
carlos-alm Mar 5, 2026
0201d31
perf: hoist db.prepare() calls and build O(1) lookup map in sequence
carlos-alm Mar 5, 2026
19a990a
fix: address sequence review comments — alias collision, truncation, …
carlos-alm Mar 5, 2026
0db9017
ci: retrigger workflows
carlos-alm Mar 5, 2026
ae4af09
Merge remote-tracking branch 'origin/main' into feat/sequence-command
carlos-alm Mar 5, 2026
995c816
fix: address round-3 review — deduplicate findBestMatch, guard trunca…
carlos-alm Mar 6, 2026
c97bdee
chore: add hook to block "generated with" in PR bodies
carlos-alm Mar 9, 2026
95c5c66
feat: add pre-commit hooks for cycle detection and dead export checks
carlos-alm Mar 9, 2026
f8ff775
fix: scope cycle and dead-export hooks to session-edited files
carlos-alm Mar 9, 2026
d535550
feat: add signature change warning hook with role-based risk levels
carlos-alm Mar 9, 2026
5f1dc81
merge main + address greptile review comments
carlos-alm Mar 9, 2026
e5580a2
fix: remove duplicate declarations and fix sequence.js indentation
carlos-alm Mar 9, 2026
633bc74
fix: remove trailing whitespace in sequence.js
carlos-alm Mar 9, 2026
544984b
Merge remote-tracking branch 'origin/chore/guard-pr-body' into chore/…
carlos-alm Mar 9, 2026
e90d3ec
fix: remove duplicate --unused option in exports CLI command
carlos-alm Mar 9, 2026
386e865
feat: add signature change warning hook with role-based risk levels
carlos-alm Mar 9, 2026
4c924da
fix: resolve merge conflicts and address greptile review comments
carlos-alm Mar 9, 2026
c104434
fix: resolve merge conflict in cli.js, hoist db.prepare() in signatur…
carlos-alm Mar 9, 2026
6b8d7cf
merge remote chore/guard-pr-body, resolve conflicts
carlos-alm Mar 9, 2026
2637d42
merge: resolve conflicts with main
carlos-alm Mar 9, 2026
7d17382
fix: address greptile review — flow kind regression and hook consistency
carlos-alm Mar 9, 2026
9e6192f
fix: surface triage query errors instead of misleading "no symbols" m…
carlos-alm Mar 9, 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
94 changes: 94 additions & 0 deletions .claude/hooks/check-cycles.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# check-cycles.sh — PreToolUse hook for Bash (git commit)
# Blocks commits if circular dependencies involve files edited in this session.

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

# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit\b'; then
exit 0
fi

# Guard: codegraph DB must exist
WORK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || WORK_ROOT="${CLAUDE_PROJECT_DIR:-.}"
if [ ! -f "$WORK_ROOT/.codegraph/graph.db" ]; then
exit 0
fi

# Guard: must have staged changes
STAGED=$(git diff --cached --name-only 2>/dev/null) || true
if [ -z "$STAGED" ]; then
exit 0
fi

# Load session edit log
LOG_FILE="$WORK_ROOT/.claude/session-edits.log"
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
exit 0
fi
EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u)

# Run check with cycles predicate on staged changes
RESULT=$(node "$WORK_ROOT/src/cli.js" check --staged --json -T 2>/dev/null) || true

if [ -z "$RESULT" ]; then
exit 0
fi

# Check if cycles predicate failed — but only block if a cycle involves
# a file that was edited in this session
CYCLES_FAILED=$(echo "$RESULT" | EDITED="$EDITED_FILES" node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
try {
const data=JSON.parse(d);
const cyclesPred=(data.predicates||[]).find(p=>p.name==='cycles');
if(!cyclesPred || cyclesPred.passed) return;
const edited=new Set(process.env.EDITED.split('\\n').filter(Boolean));
// Filter to cycles that involve at least one file we edited
const relevant=(cyclesPred.cycles||[]).filter(
cycle=>cycle.some(f=>edited.has(f))
);
if(relevant.length===0) return;
const summary=relevant.slice(0,5).map(c=>c.join(' -> ')).join('\\n ');
const extra=relevant.length>5?'\\n ... and '+(relevant.length-5)+' more':'';
process.stdout.write(summary+extra);
}catch{}
});
" 2>/dev/null) || true

if [ -n "$CYCLES_FAILED" ]; then
REASON="BLOCKED: Circular dependencies detected involving files you edited:
$CYCLES_FAILED
Fix the cycles before committing."

node -e "
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: process.argv[1]
}
}));
" "$REASON"
exit 0
fi

exit 0
103 changes: 103 additions & 0 deletions .claude/hooks/check-dead-exports.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# check-dead-exports.sh — PreToolUse hook for Bash (git commit)
# Blocks commits if any src/ file edited in THIS SESSION has exports with zero consumers.
# Uses the session edit log to scope checks to files you actually touched.

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

# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit\b'; then
exit 0
fi

# Guard: codegraph DB must exist
WORK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || WORK_ROOT="${CLAUDE_PROJECT_DIR:-.}"
if [ ! -f "$WORK_ROOT/.codegraph/graph.db" ]; then
exit 0
fi

# Guard: must have staged changes
STAGED=$(git diff --cached --name-only 2>/dev/null) || true
if [ -z "$STAGED" ]; then
exit 0
fi

# Load session edit log to scope checks to files we actually edited
LOG_FILE="$WORK_ROOT/.claude/session-edits.log"
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
exit 0
fi
EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u)

# Check each staged source file that was edited in this session
DEAD_EXPORTS=""

while IFS= read -r file; do
# Only check source files
case "$file" in
src/*.js|src/*.ts|src/*.tsx) ;;
*) continue ;;
esac

# Only check files edited in this session
if ! echo "$EDITED_FILES" | grep -qxF "$file"; then
continue
fi

RESULT=$(node "$WORK_ROOT/src/cli.js" exports "$file" --unused --json -T 2>/dev/null) || true
if [ -z "$RESULT" ]; then
continue
fi

# Extract unused export names
UNUSED=$(echo "$RESULT" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
try {
const data=JSON.parse(d);
const unused=(data.results||[]).filter(r=>r.consumerCount===0);
if(unused.length>0){
process.stdout.write(unused.map(u=>u.name+' ('+data.file+':'+u.line+')').join(', '));
}
}catch{}
});
" 2>/dev/null) || true

if [ -n "$UNUSED" ]; then
DEAD_EXPORTS="${DEAD_EXPORTS:+$DEAD_EXPORTS; }$UNUSED"
fi
done <<< "$STAGED"

if [ -n "$DEAD_EXPORTS" ]; then
REASON="BLOCKED: Dead exports (zero consumers) detected in files you edited: $DEAD_EXPORTS. Either add consumers, remove the exports, or verify these are intentionally public API."

node -e "
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: process.argv[1]
}
}));
" "$REASON"
exit 0
fi

exit 0
24 changes: 24 additions & 0 deletions .claude/hooks/guard-pr-body.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Block PR creation if the body contains "generated with" (case-insensitive)

set -euo pipefail

INPUT=$(cat)

# Extract just the command field to avoid false positives on the description field
cmd=$(echo "$INPUT" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard scans the entire gh pr create command string for generated with, not just the --body value. This means a PR whose title contains that phrase (e.g. --title "Report generated with nightly CI") would be incorrectly blocked, even though the body is clean.

Extract just the --body argument before testing:

Suggested change
process.stdin.on('end',()=>{
if echo "$cmd" | grep -oP "(?<=--body )['\"]?\K[^'\"\\n]+" | grep -qi 'generated with'; then

Or more robustly, parse the body via Node:

body=$(echo "$cmd" | node -e "
  let d=''; process.stdin.on('data',c=>d+=c);
  process.stdin.on('end',()=>{
    const m=d.match(/--body\s+['\"]([^'\"]*)['\"]/) || d.match(/--body\s+(\S+)/);
    if(m) process.stdout.write(m[1]);
  });
" 2>/dev/null) || true
if echo "\$body" | grep -qi 'generated with'; then

This avoids false positives from other flag values.

const p=JSON.parse(d).tool_input?.command||'';
if(p)process.stdout.write(p);
});
" 2>/dev/null) || true

echo "$cmd" | grep -qi 'gh pr create' || exit 0

# Block if body contains "generated with"
if echo "$cmd" | grep -qi 'generated with'; then
echo "BLOCK: Remove any 'Generated with ...' line from the PR body." >&2
exit 2
fi
126 changes: 126 additions & 0 deletions .claude/hooks/warn-signature-changes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# warn-signature-changes.sh — PreToolUse hook for Bash (git commit)
# Warns when staged changes modify function signatures, highlighting risk
# level based on the symbol's role (core > utility > others).
# 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

# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit\b'; then
exit 0
fi

# Guard: codegraph DB must exist
WORK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || WORK_ROOT="${CLAUDE_PROJECT_DIR:-.}"
if [ ! -f "$WORK_ROOT/.codegraph/graph.db" ]; then
exit 0
fi

# Guard: must have staged changes
STAGED=$(git diff --cached --name-only 2>/dev/null) || true
if [ -z "$STAGED" ]; then
exit 0
fi

# Run check --staged to get signature violations, then enrich with role + caller count
WARNING=$(node -e "
const path = require('path');
const { checkData } = require(path.join(process.argv[1], 'src/check.js'));
const { openReadonlyOrFail } = require(path.join(process.argv[1], 'src/db.js'));

const result = checkData(undefined, { staged: true, noTests: true });
if (!result || result.error) process.exit(0);

const sigPred = (result.predicates || []).find(p => p.name === 'signatures');
if (!sigPred || sigPred.passed || !sigPred.violations.length) process.exit(0);

const db = openReadonlyOrFail();
const lines = [];

const stmtNode = db.prepare(
'SELECT id, role FROM nodes WHERE name = ? AND file = ? AND line = ?'
);
const stmtCallers = db.prepare(
'SELECT DISTINCT n.id FROM edges e JOIN nodes n ON e.source_id = n.id WHERE e.target_id = ? AND e.kind = \\'calls\\''
);

for (const v of sigPred.violations) {
const node = stmtNode.get(v.name, v.file, v.line);
const role = node?.role || 'unknown';

let callerCount = 0;
if (node) {
const visited = new Set([node.id]);
let frontier = [node.id];
for (let d = 0; d < 3; d++) {
const next = [];
for (const fid of frontier) {
const callers = stmtCallers.all(fid);
for (const c of callers) {
if (!visited.has(c.id)) {
visited.add(c.id);
next.push(c.id);
callerCount++;
}
}
}
frontier = next;
if (!frontier.length) break;
}
}

const risk = role === 'core' ? 'HIGH' : role === 'utility' ? 'MEDIUM' : 'low';
lines.push(risk + ': ' + v.name + ' (' + v.kind + ') [' + role + '] at ' + v.file + ':' + v.line + ' — ' + callerCount + ' transitive callers');
}

db.close();

if (lines.length > 0) {
process.stdout.write(lines.join('\\n'));
}
" "$WORK_ROOT" 2>/dev/null) || true

if [ -z "$WARNING" ]; then
exit 0
fi

# Escape for JSON
ESCAPED=$(printf '%s' "$WARNING" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)));
" 2>/dev/null) || true

if [ -z "$ESCAPED" ]; then
exit 0
fi

# Inject as additionalContext — informational, never blocks
node -e "
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
additionalContext: '[codegraph] Signature changes detected in staged files:\\n' + JSON.parse(process.argv[1])
}
}));
" "$ESCAPED" 2>/dev/null || true

exit 0
20 changes: 20 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,30 @@
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git.sh\"",
"timeout": 10
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-pr-body.sh\"",
"timeout": 10
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/show-diff-impact.sh\"",
"timeout": 15
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-cycles.sh\"",
"timeout": 15
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-dead-exports.sh\"",
"timeout": 30
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/warn-signature-changes.sh\"",
"timeout": 15
}
]
},
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ All notable changes to this project will be documented in this file. See [commit
### Performance

* **ast:** use single transaction for AST node insertion — astMs drops from ~3600ms to ~350ms (native) and ~547ms (WASM), reducing overall native build from 24.9 to 8.5 ms/file ([#333](https://github.com/optave/codegraph/pull/333))
* **builder:** skip `ensureWasmTrees` when native engine provides complete CFG + dataflow data ([#344](https://github.com/optave/codegraph/pull/344))

### Bug Fixes

* **native:** fix function-scoped `const` declarations being incorrectly extracted as top-level constants ([#344](https://github.com/optave/codegraph/pull/344))

## [3.0.2](https://github.com/optave/codegraph/compare/v3.0.1...v3.0.2) (2026-03-04)

Expand Down
Loading
Loading