-
Notifications
You must be signed in to change notification settings - Fork 17
fix: break circular dependency cycle and remove dead queryName export #378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
5baf3ad
d411ab6
4317e0d
c2bb5de
7e6b489
f0a5522
0201d31
19a990a
0db9017
ae4af09
995c816
c97bdee
95c5c66
f8ff775
d535550
3bdc5ee
3ca9a6d
5a415c4
8251248
bc03c9c
ae0725b
5efbc4c
3cd8130
4cce192
a72be9d
80e1a9e
2ebe158
9f65cd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 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||[]; | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||
| # Block PR creation if the body contains "generated with" (case-insensitive) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| 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 check gh pr create commands | ||||||||||||||||||||||||||||||||
| echo "$COMMAND" | grep -qi 'gh pr create' || exit 0 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Block if body contains "generated with" | ||||||||||||||||||||||||||||||||
| if echo "$COMMAND" | grep -qi 'generated with'; then | ||||||||||||||||||||||||||||||||
| echo "BLOCK: Remove any 'Generated with ...' line from the PR body." >&2 | ||||||||||||||||||||||||||||||||
| exit 2 | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent block mechanism: The sibling hooks Consider aligning to the same pattern used by the other blocking hooks:
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — replaced stderr + exit 2 with structured JSON \ pattern matching sibling hooks. Also added \ content check. |
||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent stdin vs. All the other new hooks ( input="$CLAUDE_TOOL_INPUT"
echo "$input" | grep -qi 'gh pr create' || exit 0While Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — |
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| #!/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=$(echo "" | node --input-type=module -e " | ||
| import path from 'path'; | ||
| const workRoot = process.argv[2]; | ||
| const { checkData } = await import(path.join(workRoot, 'src/check.js')); | ||
| const { openReadonlyOrFail } = await import(path.join(workRoot, '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 = []; | ||
|
|
||
| for (const v of sigPred.violations) { | ||
| // Get role from DB | ||
| const node = db.prepare( | ||
| 'SELECT role FROM nodes WHERE name = ? AND file = ? AND line = ?' | ||
| ).get(v.name, v.file, v.line); | ||
| const role = node?.role || 'unknown'; | ||
|
|
||
| // Count transitive callers (BFS, depth 3) | ||
| const defNode = db.prepare( | ||
| 'SELECT id FROM nodes WHERE name = ? AND file = ? AND line = ?' | ||
| ).get(v.name, v.file, v.line); | ||
|
|
||
| let callerCount = 0; | ||
| if (defNode) { | ||
| const visited = new Set([defNode.id]); | ||
| let frontier = [defNode.id]; | ||
| for (let d = 0; d < 3; d++) { | ||
| const next = []; | ||
| for (const fid of frontier) { | ||
| const callers = 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\\'' | ||
| ).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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glob pattern silently skips all subdirectory source files
The
casepatternsrc/*.js|src/*.ts|src/*.tsxonly matches files directly insidesrc/(e.g.,src/queries.js). Inbash case,*does not cross directory boundaries, so any file in a subdirectory —src/extractors/go.js,src/ast-analysis/rules/javascript.js,src/db/connection.js, etc. — will hit the*) continue ;;branch and be silently skipped.The repo has at least three subdirectories under
src/(extractors/,ast-analysis/,db/), so a significant portion of the codebase is never checked for dead exports.To match nested paths, replace the
caseguard with a regex or glob-based check:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — replaced the
caseglob pattern with a bash regex (^src/.*\.(js|ts|tsx)$) that correctly matches files in subdirectories likesrc/extractors/andsrc/ast-analysis/.