Skip to content

Commit c6bdbaa

Browse files
authored
Merge pull request #25 from optave/feat/dev-publish-workflow
feat: dev publish workflow, parser refactor, and quality-of-life improvements
2 parents 6caf6ce + ac0b198 commit c6bdbaa

45 files changed

Lines changed: 3254 additions & 2100 deletions

Some content is hidden

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

.claude/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
session-edits.log

.claude/hooks/check-readme.sh

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
# Runs as a PreToolUse hook on Bash tool calls.
44

55
INPUT=$(cat)
6-
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
6+
COMMAND=$(echo "$INPUT" | node -e "
7+
let d='';
8+
process.stdin.on('data',c=>d+=c);
9+
process.stdin.on('end',()=>{
10+
const p=JSON.parse(d).tool_input?.command||'';
11+
if(p)process.stdout.write(p);
12+
});
13+
" 2>/dev/null) || true
714

815
# Only act on git commit commands
916
if ! echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
@@ -30,13 +37,16 @@ if [ "$NEEDS_CHECK" -gt 0 ]; then
3037
[ "$CLAUDE_STAGED" -eq 0 ] && MISSING="${MISSING:+$MISSING, }CLAUDE.md"
3138
[ "$ROADMAP_STAGED" -eq 0 ] && MISSING="${MISSING:+$MISSING, }ROADMAP.md"
3239

33-
jq -n --arg missing "$MISSING" '{
34-
hookSpecificOutput: {
35-
hookEventName: "PreToolUse",
36-
permissionDecision: "deny",
37-
permissionDecisionReason: ($missing + " not staged but source files were changed. Review whether these docs need updating — README.md (language support table, feature list, command docs), CLAUDE.md (architecture table, supported languages, key design decisions), and ROADMAP.md (phase status, new features, deliverables). If they truly do not need changes, re-run the commit with docs check acknowledged.")
38-
}
39-
}'
40+
node -e "
41+
const missing = process.argv[1];
42+
console.log(JSON.stringify({
43+
hookSpecificOutput: {
44+
hookEventName: 'PreToolUse',
45+
permissionDecision: 'deny',
46+
permissionDecisionReason: missing + ' not staged but source files were changed. Review whether these docs need updating — README.md (language support table, feature list, command docs), CLAUDE.md (architecture table, supported languages, key design decisions), and ROADMAP.md (phase status, new features, deliverables). If they truly do not need changes, re-run the commit with docs check acknowledged.'
47+
}
48+
}));
49+
" "$MISSING"
4050
exit 0
4151
fi
4252

.claude/hooks/enrich-context.sh

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ INPUT=$(cat)
1010

1111
# Extract file path based on tool type
1212
# Read tool uses tool_input.file_path, Grep uses tool_input.path
13-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
13+
FILE_PATH=$(echo "$INPUT" | node -e "
14+
let d='';
15+
process.stdin.on('data',c=>d+=c);
16+
process.stdin.on('end',()=>{
17+
const o=JSON.parse(d).tool_input||{};
18+
const p=o.file_path||o.path||'';
19+
if(p)process.stdout.write(p);
20+
});
21+
" 2>/dev/null) || true
1422

1523
# Guard: no file path found
1624
if [ -z "$FILE_PATH" ]; then
@@ -30,8 +38,9 @@ fi
3038

3139
# Convert absolute path to relative (strip project dir prefix)
3240
REL_PATH="$FILE_PATH"
33-
if [[ "$FILE_PATH" == "${CLAUDE_PROJECT_DIR}"* ]]; then
34-
REL_PATH="${FILE_PATH#"${CLAUDE_PROJECT_DIR}"/}"
41+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
42+
if [[ "$FILE_PATH" == "${PROJECT_DIR}"* ]]; then
43+
REL_PATH="${FILE_PATH#"${PROJECT_DIR}"/}"
3544
fi
3645
# Normalize backslashes to forward slashes (Windows compatibility)
3746
REL_PATH="${REL_PATH//\\//}"
@@ -50,13 +59,22 @@ if [ -z "$DEPS" ] || [ "$DEPS" = "null" ]; then
5059
fi
5160

5261
# Output as informational context (never deny)
53-
echo "$DEPS" | jq -c '{
54-
hookSpecificOutput: (
55-
"Codegraph context for " + (.file // "unknown") + ":\n" +
56-
" Imports: " + ((.results[0].imports // []) | length | tostring) + " files\n" +
57-
" Imported by: " + ((.results[0].importedBy // []) | length | tostring) + " files\n" +
58-
" Definitions: " + ((.results[0].definitions // []) | length | tostring) + " symbols"
59-
)
60-
}' 2>/dev/null || true
62+
echo "$DEPS" | node -e "
63+
let d='';
64+
process.stdin.on('data',c=>d+=c);
65+
process.stdin.on('end',()=>{
66+
try {
67+
const o=JSON.parse(d);
68+
const r=o.results?.[0]||{};
69+
const imports=(r.imports||[]).length;
70+
const importedBy=(r.importedBy||[]).length;
71+
const defs=(r.definitions||[]).length;
72+
const file=o.file||'unknown';
73+
console.log(JSON.stringify({
74+
hookSpecificOutput: 'Codegraph context for '+file+':\\n Imports: '+imports+' files\\n Imported by: '+importedBy+' files\\n Definitions: '+defs+' symbols'
75+
}));
76+
} catch(e) {}
77+
});
78+
" 2>/dev/null || true
6179

6280
exit 0

.claude/hooks/guard-git.sh

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
# guard-git.sh — PreToolUse hook for Bash tool calls
3+
# Blocks dangerous git commands that interfere with parallel sessions
4+
# and validates commits against the session edit log.
5+
6+
set -euo pipefail
7+
8+
INPUT=$(cat)
9+
10+
# Extract the command from tool_input JSON
11+
COMMAND=$(echo "$INPUT" | node -e "
12+
let d='';
13+
process.stdin.on('data',c=>d+=c);
14+
process.stdin.on('end',()=>{
15+
const p=JSON.parse(d).tool_input?.command||'';
16+
if(p)process.stdout.write(p);
17+
});
18+
" 2>/dev/null) || true
19+
20+
if [ -z "$COMMAND" ]; then
21+
exit 0
22+
fi
23+
24+
# Only act on git commands
25+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+'; then
26+
exit 0
27+
fi
28+
29+
deny() {
30+
local reason="$1"
31+
node -e "
32+
console.log(JSON.stringify({
33+
hookSpecificOutput: {
34+
hookEventName: 'PreToolUse',
35+
permissionDecision: 'deny',
36+
permissionDecisionReason: process.argv[1]
37+
}
38+
}));
39+
" "$reason"
40+
exit 0
41+
}
42+
43+
# --- Block dangerous commands ---
44+
45+
# git add . / git add -A / git add --all (broad staging)
46+
if echo "$COMMAND" | grep -qE '^\s*git\s+add\s+(\.\s*$|-A|--all)'; then
47+
deny "BLOCKED: 'git add .' / 'git add -A' stages ALL changes including other sessions' work. Stage specific files instead: git add <file1> <file2>"
48+
fi
49+
50+
# git reset (unstaging / hard reset)
51+
if echo "$COMMAND" | grep -qE '^\s*git\s+reset'; then
52+
deny "BLOCKED: 'git reset' can unstage or destroy other sessions' work. To unstage your own files, use: git restore --staged <file>"
53+
fi
54+
55+
# git checkout -- <file> (reverting files)
56+
if echo "$COMMAND" | grep -qE '^\s*git\s+checkout\s+--'; then
57+
deny "BLOCKED: 'git checkout -- <file>' reverts working tree changes and may destroy other sessions' edits. If you need to discard your own changes, be explicit about which files."
58+
fi
59+
60+
# git restore (reverting) — EXCEPT git restore --staged (safe unstaging)
61+
if echo "$COMMAND" | grep -qE '^\s*git\s+restore'; then
62+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+restore\s+--staged'; then
63+
deny "BLOCKED: 'git restore <file>' reverts working tree changes and may destroy other sessions' edits. To unstage files safely, use: git restore --staged <file>"
64+
fi
65+
fi
66+
67+
# git clean (delete untracked files)
68+
if echo "$COMMAND" | grep -qE '^\s*git\s+clean'; then
69+
deny "BLOCKED: 'git clean' deletes untracked files that may belong to other sessions."
70+
fi
71+
72+
# git stash (hides all changes)
73+
if echo "$COMMAND" | grep -qE '^\s*git\s+stash'; then
74+
deny "BLOCKED: 'git stash' hides all working tree changes including other sessions' work. In worktree mode, commit your changes directly instead."
75+
fi
76+
77+
# --- Commit validation against edit log ---
78+
79+
if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
80+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
81+
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
82+
83+
# If no edit log exists, allow (backward compat for sessions without tracking)
84+
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
85+
exit 0
86+
fi
87+
88+
# Get unique edited files from log
89+
EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u)
90+
91+
# Get staged files
92+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null) || true
93+
94+
if [ -z "$STAGED_FILES" ]; then
95+
exit 0
96+
fi
97+
98+
# Find staged files that weren't edited in this session
99+
UNEXPECTED=""
100+
while IFS= read -r staged_file; do
101+
if ! echo "$EDITED_FILES" | grep -qxF "$staged_file"; then
102+
UNEXPECTED="${UNEXPECTED:+$UNEXPECTED, }$staged_file"
103+
fi
104+
done <<< "$STAGED_FILES"
105+
106+
if [ -n "$UNEXPECTED" ]; then
107+
deny "BLOCKED: These staged files were NOT edited in this session: $UNEXPECTED. They may belong to another session. Commit only your files: git commit <your-files> -m \"msg\""
108+
fi
109+
fi
110+
111+
exit 0

.claude/hooks/rebuild-graph.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
# rebuild-graph.sh — PostToolUse hook for Edit and Write tools
3+
# Incrementally rebuilds the codegraph after source file edits.
4+
# Always exits 0 (informational only, never blocks).
5+
6+
set -euo pipefail
7+
8+
INPUT=$(cat)
9+
10+
# Extract file path using node (jq may not be available on Windows)
11+
FILE_PATH=$(echo "$INPUT" | node -e "
12+
let d='';
13+
process.stdin.on('data',c=>d+=c);
14+
process.stdin.on('end',()=>{
15+
const p=JSON.parse(d).tool_input?.file_path||'';
16+
if(p)process.stdout.write(p);
17+
});
18+
" 2>/dev/null) || true
19+
20+
if [ -z "$FILE_PATH" ]; then
21+
exit 0
22+
fi
23+
24+
# Only rebuild for source files codegraph tracks
25+
# Skip docs, configs, test fixtures, and non-code files
26+
case "$FILE_PATH" in
27+
*.js|*.ts|*.tsx|*.jsx|*.py|*.go|*.rs|*.java|*.cs|*.php|*.rb|*.tf|*.hcl)
28+
;;
29+
*)
30+
exit 0
31+
;;
32+
esac
33+
34+
# Skip test fixtures — they're copied to tmp dirs anyway
35+
if echo "$FILE_PATH" | grep -qE '(fixtures|__fixtures__|testdata)/'; then
36+
exit 0
37+
fi
38+
39+
# Guard: codegraph DB must exist (project has been built at least once)
40+
DB_PATH="${CLAUDE_PROJECT_DIR:-.}/.codegraph/graph.db"
41+
if [ ! -f "$DB_PATH" ]; then
42+
exit 0
43+
fi
44+
45+
# Run incremental build (skips unchanged files via hash check)
46+
if command -v codegraph &>/dev/null; then
47+
codegraph build "${CLAUDE_PROJECT_DIR:-.}" -d "$DB_PATH" 2>/dev/null || true
48+
else
49+
node "${CLAUDE_PROJECT_DIR}/src/cli.js" build "${CLAUDE_PROJECT_DIR:-.}" -d "$DB_PATH" 2>/dev/null || true
50+
fi
51+
52+
exit 0

.claude/hooks/track-edits.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
# track-edits.sh — PostToolUse hook for Edit and Write tools
3+
# Logs each edited file path to .claude/session-edits.log so that
4+
# guard-git.sh can validate commits against actually-edited files.
5+
# In worktrees each session gets its own log automatically.
6+
# Always exits 0 (informational only, never blocks).
7+
8+
set -euo pipefail
9+
10+
INPUT=$(cat)
11+
12+
# Extract file_path from tool_input JSON
13+
FILE_PATH=$(echo "$INPUT" | node -e "
14+
let d='';
15+
process.stdin.on('data',c=>d+=c);
16+
process.stdin.on('end',()=>{
17+
const p=JSON.parse(d).tool_input?.file_path||'';
18+
if(p)process.stdout.write(p);
19+
});
20+
" 2>/dev/null) || true
21+
22+
if [ -z "$FILE_PATH" ]; then
23+
exit 0
24+
fi
25+
26+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
27+
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
28+
29+
# Normalize to relative path with forward slashes
30+
REL_PATH=$(node -e "
31+
const path = require('path');
32+
const abs = path.resolve(process.argv[1]);
33+
const base = path.resolve(process.argv[2]);
34+
const rel = path.relative(base, abs).split(path.sep).join('/');
35+
process.stdout.write(rel);
36+
" "$FILE_PATH" "$PROJECT_DIR" 2>/dev/null) || true
37+
38+
if [ -z "$REL_PATH" ]; then
39+
exit 0
40+
fi
41+
42+
# Append timestamped entry
43+
mkdir -p "$(dirname "$LOG_FILE")"
44+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $REL_PATH" >> "$LOG_FILE"
45+
46+
exit 0

.claude/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"type": "command",
99
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-readme.sh\"",
1010
"timeout": 10
11+
},
12+
{
13+
"type": "command",
14+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git.sh\"",
15+
"timeout": 10
1116
}
1217
]
1318
},
@@ -31,6 +36,23 @@
3136
}
3237
]
3338
}
39+
],
40+
"PostToolUse": [
41+
{
42+
"matcher": "Edit|Write",
43+
"hooks": [
44+
{
45+
"type": "command",
46+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rebuild-graph.sh\"",
47+
"timeout": 30
48+
},
49+
{
50+
"type": "command",
51+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/track-edits.sh\"",
52+
"timeout": 5
53+
}
54+
]
55+
}
3456
]
3557
}
3658
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Embedding Regression
2+
3+
on:
4+
schedule:
5+
- cron: '0 6 * * 1' # Monday 6am UTC
6+
workflow_dispatch:
7+
pull_request:
8+
paths:
9+
- 'src/embedder.js'
10+
- 'tests/search/**'
11+
- 'package.json'
12+
13+
concurrency:
14+
group: embedding-regression-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
embedding-regression:
19+
runs-on: ubuntu-latest
20+
name: Embedding regression tests
21+
timeout-minutes: 15
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: 22
30+
31+
- name: Install dependencies
32+
run: npm install
33+
34+
- name: Cache HuggingFace models
35+
uses: actions/cache@v4
36+
with:
37+
path: ~/.cache/huggingface
38+
key: hf-models-minilm-v1
39+
40+
- name: Run embedding regression tests
41+
run: npx vitest run tests/search/embedding-regression.test.js

0 commit comments

Comments
 (0)