diff --git a/.claude/hooks/bash-guard.sh b/.claude/hooks/bash-guard.sh
new file mode 100755
index 000000000..97f5aa093
--- /dev/null
+++ b/.claude/hooks/bash-guard.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# PreToolUse hook: Block Bash calls that should use dedicated tools.
+# Exit 0 = allow, Exit 2 = block (message on stderr).
+
+INPUT=$(cat 2>/dev/null || true)
+COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
+
+[ -z "$COMMAND" ] && exit 0
+
+# Strip leading env vars (FOO=bar cmd ...) and whitespace to get the actual command
+STRIPPED=$(echo "$COMMAND" | sed 's/^[[:space:]]*\([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]*\)*//')
+FIRST_CMD=$(echo "$STRIPPED" | awk '{print $1}')
+
+case "$FIRST_CMD" in
+ grep|egrep|fgrep|rg)
+ echo "BLOCKED: Use the Grep tool instead of \`$FIRST_CMD\`. It provides better output and permissions handling." >&2
+ exit 2
+ ;;
+ find)
+ echo "BLOCKED: Use the Glob tool instead of \`find\`. Glob is faster and returns results sorted by modification time." >&2
+ exit 2
+ ;;
+ cat|head|tail)
+ echo "BLOCKED: Use the Read tool instead of \`$FIRST_CMD\`. Read provides line numbers and supports images/PDFs." >&2
+ exit 2
+ ;;
+ awk)
+ echo "BLOCKED: Use the Grep tool or Read tool instead of \`awk\`." >&2
+ exit 2
+ ;;
+ sed)
+ if echo "$COMMAND" | grep -qE '(^|[[:space:]])sed[[:space:]]+-i'; then
+ echo "BLOCKED: Use the Edit tool instead of \`sed -i\`. Edit tracks changes properly." >&2
+ exit 2
+ fi
+ ;;
+esac
+
+# echo with file redirection (echo "..." > file)
+if echo "$STRIPPED" | grep -qE '^echo\b.*[[:space:]]>'; then
+ echo "BLOCKED: Use the Write tool instead of \`echo >\`. Write provides proper file creation." >&2
+ exit 2
+fi
+
+exit 0
diff --git a/.claude/hooks/post-compact.sh b/.claude/hooks/post-compact.sh
new file mode 100755
index 000000000..265ebb899
--- /dev/null
+++ b/.claude/hooks/post-compact.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# PreCompact hook: Inject state preservation guidance before context compaction.
+
+cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
+
+STATE=""
+
+BRANCH=$(git branch --show-current 2>/dev/null)
+[ -n "$BRANCH" ] && STATE="${STATE}Branch: ${BRANCH}\n"
+
+DIRTY=$(git status --porcelain 2>/dev/null)
+if [ -n "$DIRTY" ]; then
+ COUNT=$(echo "$DIRTY" | wc -l | tr -d ' ')
+ STATE="${STATE}Uncommitted files (${COUNT}):\n${DIRTY}\n"
+fi
+
+UPSTREAM=$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null)
+if [ -n "$UPSTREAM" ]; then
+ AHEAD=$(git rev-list --count "${UPSTREAM}..HEAD" 2>/dev/null)
+ [ "$AHEAD" -gt 0 ] 2>/dev/null && STATE="${STATE}Unpushed commits: ${AHEAD}\n"
+fi
+
+RECENT=$(git log --oneline -5 2>/dev/null)
+[ -n "$RECENT" ] && STATE="${STATE}Recent commits:\n${RECENT}\n"
+
+LATEST_HANDOFF=$(ls -t "$CLAUDE_PROJECT_DIR/.claude/handoffs/"*.md 2>/dev/null | head -1)
+if [ -n "$LATEST_HANDOFF" ] && [ -f "$LATEST_HANDOFF" ]; then
+ HANDOFF_CONTENT=$(head -40 "$LATEST_HANDOFF" 2>/dev/null)
+ [ -n "$HANDOFF_CONTENT" ] && STATE="${STATE}\nHandoff context:\n${HANDOFF_CONTENT}\n"
+fi
+
+STATE="${STATE}\nProject conventions to preserve:\n"
+STATE="${STATE}- Python 3.9+, uv for all tooling, ruff + mypy via prek\n"
+STATE="${STATE}- Verification: uv run prek (single command for lint/format/types)\n"
+STATE="${STATE}- Pre-push: uv run prek run --from-ref origin/\n"
+STATE="${STATE}- Conventional commits: fix:, feat:, refactor:, test:, chore:\n"
+STATE="${STATE}- Result type: Success(value) / Failure(error), check with is_successful()\n"
+STATE="${STATE}- Language singleton: set_current_language() / current_language()\n"
+STATE="${STATE}- libcst for code transforms, ast for read-only analysis\n"
+
+[ -z "$STATE" ] && exit 0
+
+EXPANDED=$(printf '%b' "$STATE")
+jq -n --arg msg "PRESERVE the following session state through compaction:
+$EXPANDED" '{"systemMessage": $msg}'
+
+exit 0
diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh
index 714b7fafb..ace0f73a8 100755
--- a/.claude/hooks/post-edit-lint.sh
+++ b/.claude/hooks/post-edit-lint.sh
@@ -1,5 +1,4 @@
#!/usr/bin/env bash
-# Everyone is on macOS so this should be fine, we don't account for Windows
set -euo pipefail
input=$(cat)
@@ -10,6 +9,5 @@ if [[ -z "$file_path" || ! -f "$file_path" ]]; then
fi
if [[ "$file_path" == *.py ]]; then
- # First run auto-fixes formatting; second run catches real lint errors
uv run prek --files "$file_path" 2>/dev/null || uv run prek --files "$file_path"
fi
diff --git a/.claude/hooks/require-read.sh b/.claude/hooks/require-read.sh
new file mode 100755
index 000000000..c12d30f80
--- /dev/null
+++ b/.claude/hooks/require-read.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# PreToolUse hook: Block Write/Edit on existing files that haven't been Read first.
+# Exit 0 = allow, Exit 2 = block (message on stderr).
+
+INPUT=$(cat 2>/dev/null || true)
+FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
+
+[ -z "$FILE_PATH" ] && exit 0
+
+# New files don't need prior reads
+[ ! -f "$FILE_PATH" ] && exit 0
+
+TRACKER="$CLAUDE_PROJECT_DIR/.claude/.read-tracker"
+
+if [ ! -f "$TRACKER" ]; then
+ echo "BLOCKED: Read \`$(basename "$FILE_PATH")\` first before modifying it." >&2
+ exit 2
+fi
+
+if grep -qxF "$FILE_PATH" "$TRACKER"; then
+ exit 0
+fi
+
+echo "BLOCKED: Read \`$(basename "$FILE_PATH")\` first before modifying it." >&2
+exit 2
diff --git a/.claude/hooks/status-line.sh b/.claude/hooks/status-line.sh
new file mode 100755
index 000000000..71de759da
--- /dev/null
+++ b/.claude/hooks/status-line.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# Status line: derive context from git state.
+
+input=$(cat)
+project_dir=$(echo "$input" | jq -r '.workspace.project_dir')
+
+user=$(whoami)
+branch=$(git -C "$project_dir" branch --show-current 2>/dev/null)
+
+changed=$(git -C "$project_dir" diff --name-only HEAD 2>/dev/null)
+[ -z "$changed" ] && changed=$(git -C "$project_dir" diff --name-only 2>/dev/null)
+[ -z "$changed" ] && changed=$(git -C "$project_dir" diff --name-only --cached 2>/dev/null)
+
+if [ -n "$changed" ]; then
+ area=$(echo "$changed" | sed 's|/.*||' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}')
+else
+ area=""
+fi
+
+context=""
+case "$area" in
+ codeflash)
+ subsystem=$(echo "$changed" | grep '^codeflash/' | sed 's|^codeflash/||; s|/.*||' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}')
+ [ -n "$subsystem" ] && context="editing $subsystem" ;;
+ tests)
+ target=$(echo "$changed" | grep '^tests/' | sed 's|^tests/||; s|/.*||' | sort -u | head -1)
+ [ -n "$target" ] && context="testing $target" ;;
+ .claude)
+ context="configuring claude" ;;
+esac
+
+if [ -z "$context" ] && [ -n "$branch" ]; then
+ case "$branch" in
+ feat/*|cf-*) context="building: ${branch#feat/}" ;;
+ fix/*) context="fixing: ${branch#fix/}" ;;
+ refactor/*) context="refactoring: ${branch#refactor/}" ;;
+ test/*) context="testing: ${branch#test/}" ;;
+ chore/*) context="chore: ${branch#chore/}" ;;
+ esac
+fi
+
+dirty=""
+if [ -n "$(git -C "$project_dir" status --porcelain 2>/dev/null)" ]; then
+ dirty=" *"
+fi
+
+status="$user | codeflash"
+[ -n "$context" ] && status="$status | $context"
+[ -n "$branch" ] && status="$status | $branch$dirty"
+echo "$status"
diff --git a/.claude/hooks/track-read.sh b/.claude/hooks/track-read.sh
new file mode 100755
index 000000000..d89c17110
--- /dev/null
+++ b/.claude/hooks/track-read.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# PostToolUse hook: Track Read calls for the require-read guard.
+
+INPUT=$(cat 2>/dev/null || true)
+FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
+
+[ -z "$FILE_PATH" ] && exit 0
+
+TRACKER="$CLAUDE_PROJECT_DIR/.claude/.read-tracker"
+grep -qxF "$FILE_PATH" "$TRACKER" 2>/dev/null || echo "$FILE_PATH" >> "$TRACKER"
+exit 0
diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md
index bcc8c6c34..c4dcb5871 100644
--- a/.claude/rules/code-style.md
+++ b/.claude/rules/code-style.md
@@ -4,10 +4,11 @@
- **Python**: 3.9+ syntax
- **Package management**: Always use `uv`, never `pip`
- **Tooling**: Ruff for linting/formatting, mypy strict mode, prek for pre-commit checks
-- **Comments**: Minimal - only explain "why", not "what"
-- **Docstrings**: Do not add docstrings to new or changed code unless the user explicitly asks for them — not even one-liners. The codebase intentionally keeps functions self-documenting through clear naming and type annotations
-- **Types**: Match the type annotation style of surrounding code — the codebase uses annotations, so add them in new code
-- **Naming**: NEVER use leading underscores (`_function_name`) - Python has no true private functions, use public names
+- **Comments**: Minimal — only explain "why", not "what"
+- **Docstrings**: Do not add docstrings unless the user explicitly asks
+- **Types**: Match the type annotation style of surrounding code
+- **Naming**: No leading underscores (`_function_name`) — Python has no true private functions
- **Paths**: Always use absolute paths
-- **Encoding**: Always pass `encoding="utf-8"` to `open()`, `read_text()`, `write_text()`, etc. in new or changed code — Windows defaults to `cp1252` which breaks on non-ASCII content. Don't flag pre-existing code that lacks it unless you're already modifying that line.
-- **Verification**: Use `uv run prek` to verify code — it handles ruff, ty, mypy in one pass. Don't run `ruff`, `mypy`, or `python -c "import ..."` separately; `prek` is the single verification command
+- **Encoding**: Always pass `encoding="utf-8"` to `open()`, `read_text()`, `write_text()` in new or changed code
+- **Verification**: Use `uv run prek` — it handles ruff, ty, mypy in one pass. Don't run them separately
+- **Code transforms**: Use `libcst` for code modification/transformation. `ast` is acceptable for read-only analysis
diff --git a/.claude/rules/debugging.md b/.claude/rules/debugging.md
new file mode 100644
index 000000000..42050cb37
--- /dev/null
+++ b/.claude/rules/debugging.md
@@ -0,0 +1,19 @@
+# Debugging
+
+## Root cause first
+
+When encountering a bug, investigate the root cause. Don't patch symptoms. If you're about to add a try/except, a fallback default, or a defensive check — ask whether the real fix is upstream.
+
+## Isolated testing
+
+Prefer running individual test functions over full suites. Only run the full suite when explicitly asked or before pushing.
+
+- Single function: `uv run pytest tests/test_foo.py::TestBar::test_baz -v`
+- Single module: `uv run pytest tests/test_foo.py -v`
+- Full suite: only when asked, or before `git push`
+
+When debugging a specific endpoint or integration, test it directly instead of running the entire pipeline end-to-end.
+
+## Subprocess failures
+
+When a subprocess fails, always log stdout and stderr. "Exit code 1" with no output is useless.
diff --git a/.claude/rules/git.md b/.claude/rules/git.md
index edac5ff61..c5678ffc1 100644
--- a/.claude/rules/git.md
+++ b/.claude/rules/git.md
@@ -1,19 +1,35 @@
-# Git Commits & Pull Requests
+# Git
## Commits
+
- Never commit, amend, or push without explicit permission
-- Don't commit intermediate states — wait until the full implementation is complete, reviewed, and explicitly approved before committing. If the user corrects direction mid-implementation, incorporate the correction before any commit
-- Always create a new branch from `main` before starting any new work — never commit directly to `main` or reuse an existing feature branch for unrelated changes
-- Use conventional commit format: `fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`
-- Keep commits atomic - one logical change per commit
-- Commit message body should be concise (1-2 sentences max)
-- Merge for simple syncs, rebase when branches have diverged significantly
-- When committing to an external/third-party repo, follow that repo's own conventions for versioning, changelog, and CI
-- Pre-commit: Run `uv run prek` before committing — fix any issues before creating the commit
-- Pre-push: Run `uv run prek run --from-ref origin/` to check all changed files against the PR base — this matches CI behavior and catches issues that per-commit prek misses. To detect the base branch: `gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main`
+- Don't commit intermediate states — wait until the full implementation is complete and approved
+- Always create a new branch from `main` — never commit directly to `main`
+- Conventional format: `fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`
+- First line: imperative verb + what changed, under 72 characters
+- Body for *why*, not *what* — the diff shows what changed
+- One purpose per commit: a bug fix, a new function, a refactor — not all three
+- A commit that adds a function also adds its tests and exports — that's one logical change
+
+## Sizing
+
+- Too small: renaming a variable in one commit, updating its references in another
+- Right size: adding a function with its tests, `__init__` export, and usage update
+- Too large: implementing an entire subsystem in one commit
+
+## Pre-commit / Pre-push
+
+- Pre-commit: Run `uv run prek` before committing
+- Pre-push: Run `uv run prek run --from-ref origin/` to check all changed files against the PR base
## Pull Requests
-- PR titles should use conventional format
-- Keep the PR body short and straight to the point
+
+- PR titles use conventional format
+- Keep the PR body short and to the point
- If related to a Linear issue, include `CF-#` in the body
-- Branch naming: `cf-#-title` (lowercase, hyphenated), no other prefixes/suffixes
+- Branch naming: `cf-#-title` (lowercase, hyphenated)
+
+## Branch Hygiene
+
+- Delete feature branches locally after merging (`git branch -d `)
+- Use `/clean_gone` to prune local branches whose remote tracking branch has been deleted
diff --git a/.claude/rules/github.md b/.claude/rules/github.md
new file mode 100644
index 000000000..8c2495193
--- /dev/null
+++ b/.claude/rules/github.md
@@ -0,0 +1,5 @@
+# GitHub Interactions
+
+ALWAYS use MCP GitHub tools (`mcp__github__*`) for GitHub operations. Check for a matching MCP tool first — only fall back to `gh` via Bash when no MCP tool exists for the operation.
+
+This also applies to other MCP-connected services (Linear, Granola). MCP first, CLI second.
diff --git a/.claude/rules/language-patterns.md b/.claude/rules/language-patterns.md
index 34d61e605..8e9b166a7 100644
--- a/.claude/rules/language-patterns.md
+++ b/.claude/rules/language-patterns.md
@@ -6,8 +6,8 @@ paths:
# Language Support Patterns
- Current language is a module-level singleton in `languages/current.py` — use `set_current_language()` / `current_language()`, never pass language as a parameter through call chains
-- Use `get_language_support(identifier)` from `languages/registry.py` to get a `LanguageSupport` instance — never import language classes directly
-- New language support classes must use the `@register_language` decorator to register with the extension and language registries
-- `languages/__init__.py` uses `__getattr__` for lazy imports to avoid circular dependencies — follow this pattern when adding new exports
-- Prefer `LanguageSupport` protocol dispatch over `is_python()`/`is_javascript()` guards — remaining guards are being migrated to protocol methods
+- Use `get_language_support(identifier)` from `languages/registry.py` — never import language classes directly
+- New language support classes must use the `@register_language` decorator
+- `languages/__init__.py` uses `__getattr__` for lazy imports to avoid circular dependencies
+- Prefer `LanguageSupport` protocol dispatch over `is_python()`/`is_javascript()` guards
- `is_javascript()` returns `True` for both JavaScript and TypeScript (still used in ~15 call sites pending migration)
diff --git a/.claude/rules/sessions.md b/.claude/rules/sessions.md
new file mode 100644
index 000000000..0d47b8285
--- /dev/null
+++ b/.claude/rules/sessions.md
@@ -0,0 +1,27 @@
+# Session Discipline
+
+## Scope
+
+One task per session. Don't mix implementation with communication drafting, transcript search, or strategic planning.
+
+## Duration
+
+Cap sessions at 2-3 hours. Use `/handoff` at natural breakpoints rather than letting auto-compaction degrade context.
+
+- After 1 compaction: consider wrapping up the current task and handing off
+- After 3 compactions: stop, and tell the user to start a fresh session
+- Never continue past 5 compactions — context is too degraded
+
+## Context preservation
+
+When compacting, preserve: modified files list, current branch, test commands used, key decisions made. Use subagents for exploration to keep main context clean.
+
+## No polling
+
+Never poll background tasks. No `wc -l`, no `tail -f`, no `sleep` loops. Use `run_in_background` and wait for the completion notification.
+
+## File read budget
+
+If you've read the same file 3+ times in a session, either:
+- The session is too long and compaction destroyed your context — write a handoff
+- You're not retaining key information — write it down in your response before it compacts away
diff --git a/.claude/rules/source-code.md b/.claude/rules/source-code.md
deleted file mode 100644
index 297daa6ae..000000000
--- a/.claude/rules/source-code.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-paths:
- - "codeflash/**/*.py"
----
-
-# Source Code Rules
-
-- Use `libcst` for code modification/transformation to preserve formatting. `ast` is acceptable for read-only analysis and parsing.
diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md
index 0d8471a3c..a56f3ca40 100644
--- a/.claude/rules/testing.md
+++ b/.claude/rules/testing.md
@@ -4,13 +4,14 @@ paths:
- "codeflash/**/*test*.py"
---
-# Testing Conventions
+# Testing
-- Code context extraction and replacement tests must always assert for full string equality, no substring matching.
-- Use pytest's `tmp_path` fixture for temp directories — do not use `tempfile.mkdtemp()`, `tempfile.TemporaryDirectory()`, or `NamedTemporaryFile`. Some existing tests still use `tempfile` but new tests must use `tmp_path`.
-- Always call `.resolve()` on Path objects before passing them to functions under test — this ensures absolute paths and resolves symlinks. Example: `source_file = (tmp_path / "example.py").resolve()`
-- Use `.as_posix()` when converting resolved paths to strings (normalizes to forward slashes).
-- Any new feature or bug fix that can be tested automatically must have test cases.
-- If changes affect existing test expectations, update the tests accordingly. Tests must always pass after changes.
-- The pytest plugin patches `time`, `random`, `uuid`, and `datetime` for deterministic test execution — never assume real randomness or real time in verification tests.
-- `conftest.py` uses an autouse fixture that calls `reset_current_language()` — tests always start with Python as the default language.
+- Full string equality for context extraction/replacement tests — no substring matching
+- Use pytest's `tmp_path` fixture — not `tempfile.mkdtemp()` or `NamedTemporaryFile`
+- Always call `.resolve()` on Path objects before passing to functions under test
+- Use `.as_posix()` when converting resolved paths to strings
+- New features and bug fixes must have test cases
+- The pytest plugin patches `time`, `random`, `uuid`, `datetime` for deterministic execution
+- `conftest.py` autouse fixture calls `reset_current_language()` — tests start with Python as default
+- Prefer running individual tests over full suites: `uv run pytest tests/test_foo.py::TestBar::test_baz -v`
+- Only run the full suite when explicitly asked or before pushing
diff --git a/.claude/rules/workflow.md b/.claude/rules/workflow.md
index c76f77eed..7f8b5a6c1 100644
--- a/.claude/rules/workflow.md
+++ b/.claude/rules/workflow.md
@@ -1,13 +1,17 @@
# Workflow
## Code Changes
-- Before making any changes, outline your approach in 3-5 numbered steps. Include which repo/branch you'll work in, what commands you'll run, and what success looks like. Wait for approval before starting
+
+Before making any changes, outline your approach in 3-5 numbered steps. Include which branch you'll work on, what commands you'll run, and what success looks like. Wait for approval before starting.
## Response Style
-- When listing items (PRs, functions, optimization targets), always provide the complete list ordered by priority on the first attempt. Do not give partial lists
+
+When listing items (PRs, functions, optimization targets), provide the complete list ordered by priority on the first attempt. No partial lists.
## Commands
-- When running long-running commands (benchmarks, profiling, optimizers like codeflash), always run them in the foreground. Do not use background processes
+
+Long-running commands (benchmarks, profiling, optimizers) always run in the foreground. Do not use background processes.
## Debugging
-- When claiming something is a pre-existing issue (e.g., test failures on main), verify by checking out main and running the tests before making that claim
+
+When claiming something is a pre-existing issue (e.g., test failures on main), verify by checking out main and running the tests before making that claim.
diff --git a/.claude/settings.json b/.claude/settings.json
index 0ca931576..771760ec4 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,16 +1,89 @@
{
+ "attribution": {
+ "commit": "",
+ "pr": ""
+ },
+ "includeCoAuthoredBy": false,
+ "permissions": {
+ "allow": [
+ "Bash(git status*)",
+ "Bash(git diff*)",
+ "Bash(git log*)",
+ "Bash(git branch*)",
+ "Bash(git show*)",
+ "Bash(git fetch*)",
+ "Bash(git checkout*)",
+ "Bash(uv run*)",
+ "Bash(uv sync*)",
+ "Bash(uv pip*)",
+ "Bash(prek*)",
+ "Bash(make*)",
+ "Bash(gh *)"
+ ]
+ },
"hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/bash-guard.sh",
+ "timeout": 5
+ }
+ ]
+ },
+ {
+ "matcher": "Write",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/require-read.sh",
+ "timeout": 5
+ }
+ ]
+ }
+ ],
"PostToolUse": [
+ {
+ "matcher": "Read",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-read.sh",
+ "timeout": 5
+ }
+ ]
+ },
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
- "command": ".claude/hooks/post-edit-lint.sh",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-lint.sh",
"timeout": 30
}
]
}
+ ],
+ "PreCompact": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-compact.sh",
+ "timeout": 10
+ }
+ ]
+ }
]
+ },
+ "statusLine": {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/status-line.sh"
+ },
+ "enableAllProjectMcpServers": true,
+ "env": {
+ "ENABLE_LSP_TOOL": "1"
}
}
diff --git a/.claude/skills/fix-mypy.md b/.claude/skills/fix-mypy.md
index 1a9432bf3..6e43c4d3b 100644
--- a/.claude/skills/fix-mypy.md
+++ b/.claude/skills/fix-mypy.md
@@ -7,6 +7,6 @@ uv run mypy --non-interactive --config-file pyproject.toml
```
- Fix type annotation issues: missing return types, incorrect types, Optional/None unions, import errors for type hints
-- Do NOT add `# type: ignore` comments — always fix the root cause
+- Do NOT add `# type: ignore` comments -- always fix the root cause
- Do NOT fix type errors that require logic changes, complex generic type rework, or anything that could change runtime behavior
-- Files in `mypy_allowlist.txt` are checked in CI — ensure they remain error-free
+- Files in `mypy_allowlist.txt` are checked in CI -- ensure they remain error-free
diff --git a/.claude/skills/fix-prek.md b/.claude/skills/fix-prek.md
index f681512ec..32652153b 100644
--- a/.claude/skills/fix-prek.md
+++ b/.claude/skills/fix-prek.md
@@ -5,5 +5,5 @@ When prek (pre-commit) checks fail:
1. Run `uv run prek run` to see failures (local, checks staged files)
2. In CI, the equivalent is `uv run prek run --from-ref origin/main`
3. prek runs ruff format, ruff check, and mypy on changed files
-4. Fix issues in order: formatting → lint → type errors
+4. Fix issues in order: formatting -> lint -> type errors
5. Re-run `uv run prek run` to verify all checks pass
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b6f14199e..a030c3e81 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -36,6 +36,7 @@ jobs:
env:
PR_BODY: ${{ github.event.pull_request.body }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
+ AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
run: |
# Skip for bots (dependabot, renovate, github-actions)
if [[ "$PR_AUTHOR" == *"[bot]"* || "$PR_AUTHOR" == "dependabot" ]]; then
@@ -43,6 +44,12 @@ jobs:
exit 0
fi
+ # Skip for org members
+ if [[ "$AUTHOR_ASSOCIATION" == "MEMBER" ]]; then
+ echo "Org member ($PR_AUTHOR) — skipping linked issue check."
+ exit 0
+ fi
+
if [ -z "$PR_BODY" ]; then
echo "::error::PR body is empty. Every PR must link an issue or discussion."
echo "Use 'Closes #', 'Fixes #', 'Relates to #', or include a discussion URL."
@@ -124,7 +131,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
@@ -157,7 +164,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.13"
enable-cache: true
@@ -173,7 +180,7 @@ jobs:
- name: Upload coverage report
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage.xml
@@ -193,7 +200,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
@@ -230,6 +237,7 @@ jobs:
if: >-
fromJSON(needs.determine-changes.outputs.flags).e2e == 'true'
&& github.event_name != 'push'
+ && github.actor != 'dependabot[bot]'
strategy:
fail-fast: false
matrix:
@@ -289,7 +297,7 @@ jobs:
pr_state: ${{ github.event.pull_request.state }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: 3.11.6
enable-cache: true
@@ -333,6 +341,7 @@ jobs:
if: >-
fromJSON(needs.determine-changes.outputs.flags).e2e_js == 'true'
&& github.event_name != 'push'
+ && github.actor != 'dependabot[bot]'
strategy:
fail-fast: false
matrix:
@@ -345,10 +354,12 @@ jobs:
script: end_to_end_test_js_esm_async.py
js_project_dir: code_to_optimize/js/code_to_optimize_js_esm
expected_improvement: 10
+ allow_failure: true
- name: js-ts-class
script: end_to_end_test_js_ts_class.py
js_project_dir: code_to_optimize/js/code_to_optimize_ts
expected_improvement: 30
+ continue-on-error: ${{ matrix.allow_failure || false }}
environment: ${{ ((github.event_name == 'workflow_dispatch' && github.actor != 'misrasaurabh1' && github.actor != 'KRRT7') || (contains(toJSON(github.event.pull_request.files.*.filename), '.github/workflows/') && github.event.pull_request.user.login != 'misrasaurabh1' && github.event.pull_request.user.login != 'KRRT7')) && 'external-trusted-contributors' || '' }}
runs-on: ubuntu-latest
env:
@@ -397,7 +408,7 @@ jobs:
npm install
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: 3.11.6
enable-cache: true
@@ -414,6 +425,7 @@ jobs:
if: >-
fromJSON(needs.determine-changes.outputs.flags).e2e_java == 'true'
&& github.event_name != 'push'
+ && github.actor != 'dependabot[bot]'
strategy:
fail-fast: false
matrix:
@@ -466,7 +478,7 @@ jobs:
cache: maven
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: 3.11.6
enable-cache: true
@@ -476,7 +488,7 @@ jobs:
- name: Cache codeflash-runtime JAR
id: runtime-jar-cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.m2/repository/io/codeflash
key: codeflash-runtime-${{ hashFiles('codeflash-java-runtime/pom.xml', 'codeflash-java-runtime/src/**') }}
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index c012a94d1..b804f4cd8 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -57,7 +57,7 @@ jobs:
ref: ${{ github.event.pull_request.head.ref || github.ref }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
@@ -319,7 +319,7 @@ jobs:
ref: ${{ steps.pr-ref.outputs.ref }}
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
diff --git a/.github/workflows/codeflash-optimize.yaml b/.github/workflows/codeflash-optimize.yaml
index ab08aa1f8..4eb4e4f1c 100644
--- a/.github/workflows/codeflash-optimize.yaml
+++ b/.github/workflows/codeflash-optimize.yaml
@@ -31,7 +31,7 @@ jobs:
fetch-depth: 0
- name: 🐍 Set up Python 3.11 for CLI
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: 3.11.6
enable-cache: true
diff --git a/.github/workflows/java-e2e.yaml b/.github/workflows/java-e2e.yaml
index 0bfc979b6..1fcf50461 100644
--- a/.github/workflows/java-e2e.yaml
+++ b/.github/workflows/java-e2e.yaml
@@ -41,7 +41,7 @@ jobs:
cache: maven
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
python-version: 3.11.6
enable-cache: true
@@ -51,7 +51,7 @@ jobs:
- name: Cache codeflash-runtime JAR
id: runtime-jar-cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.m2/repository/io/codeflash
key: codeflash-runtime-${{ hashFiles('codeflash-java-runtime/pom.xml', 'codeflash-java-runtime/src/**') }}
diff --git a/.github/workflows/label-workflow-changes.yml b/.github/workflows/label-workflow-changes.yml
index 3bc8d6c54..72f886318 100644
--- a/.github/workflows/label-workflow-changes.yml
+++ b/.github/workflows/label-workflow-changes.yml
@@ -13,7 +13,7 @@ jobs:
pull-requests: write
steps:
- name: Label PR with workflow changes
- uses: actions/github-script@v7
+ uses: actions/github-script@v9
with:
script: |
const labelName = 'workflow-modified';
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index f2b6a3ccd..5b2c32981 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -110,7 +110,7 @@ jobs:
- name: Install uv
if: steps.should_run.outputs.run == 'true' && steps.check_tag.outputs.exists == 'false'
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
@@ -124,7 +124,7 @@ jobs:
- name: Create GitHub Release
if: steps.should_run.outputs.run == 'true' && steps.check_tag.outputs.exists == 'false'
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.extract_version.outputs.tag }}
name: ${{ matrix.release_name_prefix }} ${{ steps.extract_version.outputs.tag }}
diff --git a/.gitignore b/.gitignore
index 1a4e87d22..53d52f044 100644
--- a/.gitignore
+++ b/.gitignore
@@ -266,11 +266,12 @@ WARP.MD
.tessl/
tessl.json
-# Claude Code - track shared rules, ignore local config
+# Claude Code - track shared config, ignore local state
.claude/*
!.claude/rules/
-!.claude/settings.json
!.claude/hooks/
+!.claude/skills/
+!.claude/settings.json
**/node_modules/**
**/dist-nuitka/**
**/.npmrc
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3ef52eacc..6f0861c36 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,8 +1,15 @@
repos:
-- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.8
- hooks:
- # Run the linter.
- - id: ruff-check
- # Run the formatter.
- - id: ruff-format
\ No newline at end of file
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.15.8
+ hooks:
+ - id: ruff-check
+ - id: ruff-format
+
+ - repo: local
+ hooks:
+ - id: mypy
+ name: mypy
+ entry: uv run mypy --non-interactive --config-file pyproject.toml
+ language: system
+ types: [python]
+ require_serial: true
diff --git a/CLAUDE.md b/CLAUDE.md
index f56a1276c..253a1d6d5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -7,19 +7,29 @@ CodeFlash is an AI-powered code optimizer that automatically improves performanc
## Optimization Pipeline
```
-Discovery → Ranking → Context Extraction → Test Gen + Optimization → Baseline → Candidate Evaluation → PR
+Discovery -> Ranking -> Context Extraction -> Test Gen + Optimization -> Baseline -> Candidate Evaluation -> PR
```
See `.claude/rules/architecture.md` for directory mapping and entry points.
-# Instructions
-- **Bug fix workflow** — follow these steps in order, do not skip ahead:
- 1. Read the relevant code to understand the bug
- 2. Write a test that reproduces the bug (run it to confirm it fails)
- 3. Spawn subagents (using the Agent tool) to attempt the fix — each subagent should apply a fix and run the test to prove it passes
- 4. Review the subagent results, pick the best fix, and apply it
- 5. Never jump straight to writing a fix yourself — always go through steps 1-4
-- Everything that can be tested should have tests.
+## Setup
+
+```bash
+uv sync # Install all dependencies
+uv run prek install # Install git pre-commit hooks (ruff + mypy)
+```
+
+## Bug Fix Workflow
+
+Follow these steps in order, do not skip ahead:
+
+1. Read the relevant code to understand the bug
+2. Write a test that reproduces the bug (run it to confirm it fails)
+3. Spawn subagents (using the Agent tool) to attempt the fix — each subagent should apply a fix and run the test to prove it passes
+4. Review the subagent results, pick the best fix, and apply it
+5. Never jump straight to writing a fix yourself — always go through steps 1-4
+
+Everything that can be tested should have tests.
diff --git a/codeflash/languages/javascript/replay_test.py b/codeflash/languages/javascript/replay_test.py
new file mode 100644
index 000000000..01b525189
--- /dev/null
+++ b/codeflash/languages/javascript/replay_test.py
@@ -0,0 +1,244 @@
+from __future__ import annotations
+
+import json
+import re
+import sqlite3
+import textwrap
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+
+@dataclass
+class JavaScriptFunctionModule:
+ function_name: str
+ file_name: Path
+ module_name: str
+ class_name: Optional[str] = None
+ line_no: Optional[int] = None
+
+
+def get_next_arg_and_return(
+ trace_file: str, function_name: str, file_name: str, class_name: Optional[str] = None, num_to_get: int = 25
+) -> Generator[Any]:
+ db = sqlite3.connect(trace_file)
+ cur = db.cursor()
+
+ try:
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = {row[0] for row in cur.fetchall()}
+
+ if "function_calls" in tables:
+ if class_name:
+ cursor = cur.execute(
+ "SELECT args FROM function_calls WHERE function = ? AND filename = ? AND classname = ? AND type = 'call' ORDER BY time_ns ASC LIMIT ?",
+ (function_name, file_name, class_name, num_to_get),
+ )
+ else:
+ cursor = cur.execute(
+ "SELECT args FROM function_calls WHERE function = ? AND filename = ? AND type = 'call' ORDER BY time_ns ASC LIMIT ?",
+ (function_name, file_name, num_to_get),
+ )
+
+ while (val := cursor.fetchone()) is not None:
+ args_data = val[0]
+ if isinstance(args_data, bytes):
+ yield args_data
+ else:
+ yield args_data
+
+ elif "traces" in tables:
+ if class_name:
+ cursor = cur.execute(
+ "SELECT args FROM traces WHERE function = ? AND file = ? ORDER BY id ASC LIMIT ?",
+ (function_name, file_name, num_to_get),
+ )
+ else:
+ cursor = cur.execute(
+ "SELECT args FROM traces WHERE function = ? AND file = ? ORDER BY id ASC LIMIT ?",
+ (function_name, file_name, num_to_get),
+ )
+
+ while (val := cursor.fetchone()) is not None:
+ yield val[0]
+
+ finally:
+ db.close()
+
+
+def get_function_alias(module: str, function_name: str, class_name: Optional[str] = None) -> str:
+ module_alias = re.sub(r"[^a-zA-Z0-9]", "_", module).strip("_")
+
+ if class_name:
+ return f"{module_alias}_{class_name}_{function_name}"
+ return f"{module_alias}_{function_name}"
+
+
+def create_javascript_replay_test(
+ trace_file: str,
+ functions: list[JavaScriptFunctionModule],
+ max_run_count: int = 100,
+ framework: str = "jest",
+ project_root: Optional[Path] = None,
+) -> str:
+ is_vitest = framework.lower() == "vitest"
+
+ imports = []
+
+ if is_vitest:
+ imports.append("import { describe, test } from 'vitest';")
+
+ imports.append("const { getNextArg } = require('codeflash/replay');")
+ imports.append("")
+
+ for func in functions:
+ if func.function_name in ("__init__", "constructor"):
+ continue
+
+ alias = get_function_alias(func.module_name, func.function_name, func.class_name)
+
+ if func.class_name:
+ imports.append(f"const {{ {func.class_name}: {alias}_class }} = require('./{func.module_name}');")
+ else:
+ imports.append(f"const {{ {func.function_name}: {alias} }} = require('./{func.module_name}');")
+
+ imports.append("")
+
+ functions_to_test = [f.function_name for f in functions if f.function_name not in ("__init__", "constructor")]
+ metadata = f"""const traceFilePath = '{trace_file}';
+const functions = {json.dumps(functions_to_test)};
+"""
+
+ test_cases = []
+
+ for func in functions:
+ if func.function_name in ("__init__", "constructor"):
+ continue
+
+ alias = get_function_alias(func.module_name, func.function_name, func.class_name)
+ test_name = f"{func.class_name}.{func.function_name}" if func.class_name else func.function_name
+
+ if func.class_name:
+ class_arg = f"'{func.class_name}'"
+ test_body = textwrap.dedent(f"""
+describe('Replay: {test_name}', () => {{
+ const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name.as_posix()}', {max_run_count}, {class_arg});
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
+ const instance = new {alias}_class();
+ instance.{func.function_name}(...args);
+ }});
+}});
+""")
+ else:
+ test_body = textwrap.dedent(f"""
+describe('Replay: {test_name}', () => {{
+ const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name.as_posix()}', {max_run_count});
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
+ {alias}(...args);
+ }});
+}});
+""")
+
+ test_cases.append(test_body)
+
+ return "\n".join(
+ [
+ "// Auto-generated replay test by Codeflash",
+ "// Do not edit this file directly",
+ "",
+ *imports,
+ metadata,
+ *test_cases,
+ ]
+ )
+
+
+def get_traced_functions_from_db(trace_file: Path) -> list[JavaScriptFunctionModule]:
+ if not trace_file.exists():
+ return []
+
+ try:
+ conn = sqlite3.connect(trace_file)
+ cursor = conn.cursor()
+
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = {row[0] for row in cursor.fetchall()}
+
+ functions = []
+
+ if "function_calls" in tables:
+ cursor.execute(
+ "SELECT DISTINCT function, filename, classname, line_number FROM function_calls WHERE type = 'call'"
+ )
+ for row in cursor.fetchall():
+ func_name = row[0]
+ file_name = row[1]
+ class_name = row[2]
+ line_number = row[3]
+
+ module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
+ module_path = module_path.removeprefix("./")
+
+ functions.append(
+ JavaScriptFunctionModule(
+ function_name=func_name,
+ file_name=Path(file_name),
+ module_name=module_path,
+ class_name=class_name,
+ line_no=line_number,
+ )
+ )
+
+ elif "traces" in tables:
+ cursor.execute("SELECT DISTINCT function, file FROM traces")
+ for row in cursor.fetchall():
+ func_name = row[0]
+ file_name = row[1]
+
+ module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
+ module_path = module_path.removeprefix("./")
+
+ functions.append(
+ JavaScriptFunctionModule(
+ function_name=func_name, file_name=Path(file_name), module_name=module_path
+ )
+ )
+
+ conn.close()
+ return functions
+
+ except Exception:
+ return []
+
+
+def create_replay_test_file(
+ trace_file: Path,
+ output_path: Path,
+ framework: str = "jest",
+ max_run_count: int = 100,
+ project_root: Optional[Path] = None,
+) -> Optional[Path]:
+ functions = get_traced_functions_from_db(trace_file)
+
+ if not functions:
+ return None
+
+ content = create_javascript_replay_test(
+ trace_file=str(trace_file),
+ functions=functions,
+ max_run_count=max_run_count,
+ framework=framework,
+ project_root=project_root,
+ )
+
+ try:
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text(content, encoding="utf-8")
+ return output_path
+ except Exception:
+ return None
diff --git a/codeflash/languages/javascript/support.py b/codeflash/languages/javascript/support.py
index 2faeb76a3..b0605b19b 100644
--- a/codeflash/languages/javascript/support.py
+++ b/codeflash/languages/javascript/support.py
@@ -1702,8 +1702,9 @@ class JavaScriptSupport:
) -> str:
"""Add behavior instrumentation to capture inputs/outputs.
- For JavaScript, this wraps functions to capture their arguments
- and return values.
+ For JavaScript, instrumentation is handled at runtime by the Babel tracer plugin
+ (babel-tracer-plugin.js) via trace-runner.js. This method returns the source
+ unchanged since no source-level transformation is needed.
Args:
source: Source code to instrument.
@@ -1711,21 +1712,11 @@ class JavaScriptSupport:
output_file: Optional output file for traces.
Returns:
- Instrumented source code.
+ Source code unchanged (Babel handles instrumentation at runtime).
"""
- if not functions:
- return source
-
- from codeflash.languages.javascript.tracer import JavaScriptTracer
-
- # Use first function's file path if output_file not specified
- if output_file is None:
- file_path = functions[0].file_path
- output_file = file_path.parent / ".codeflash" / "traces.db"
-
- tracer = JavaScriptTracer(output_file)
- return tracer.instrument_source(source, functions[0].file_path, list(functions))
+ # JavaScript tracing is done at runtime via Babel plugin, not source transformation
+ return source
def instrument_for_benchmarking(self, test_source: str, target_function: FunctionToOptimize) -> str:
"""Add timing instrumentation to test code.
diff --git a/codeflash/languages/javascript/tracer.py b/codeflash/languages/javascript/tracer.py
index 2f5791ee0..107c456ce 100644
--- a/codeflash/languages/javascript/tracer.py
+++ b/codeflash/languages/javascript/tracer.py
@@ -1,35 +1,56 @@
"""Function tracing instrumentation for JavaScript.
-This module provides functionality to wrap JavaScript functions to capture their
-inputs, outputs, and execution behavior. This is used for generating replay tests
-and verifying optimization correctness.
+This module provides functionality to parse JavaScript function traces and generate
+replay tests. Tracing is performed via Babel AST transformation using the
+babel-tracer-plugin.js and trace-runner.js in the npm package.
+
+The tracer uses Babel plugin for AST transformation which:
+- Works with both CommonJS and ESM
+- Handles async functions, arrow functions, methods correctly
+- Preserves source maps and formatting
+
+Database Schema (matches Python tracer):
+- function_calls: Main trace data (type, function, classname, filename, line_number, time_ns, args)
+- metadata: Key-value metadata about the trace session
"""
from __future__ import annotations
import json
import logging
+import re
import sqlite3
-from typing import TYPE_CHECKING, Any
+import textwrap
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from pathlib import Path
- from codeflash.discovery.functions_to_optimize import FunctionToOptimize
-
logger = logging.getLogger(__name__)
-class JavaScriptTracer:
- """Instruments JavaScript code to capture function inputs and outputs.
+@dataclass
+class JavaScriptFunctionInfo:
+ function_name: str
+ file_name: str
+ module_path: str
+ class_name: Optional[str] = None
+ line_number: Optional[int] = None
- Similar to Python's tracing system, this wraps functions to record:
- - Input arguments
- - Return values
- - Exceptions thrown
- - Execution time
+
+class JavaScriptTracer:
+ """Parses JavaScript function traces and generates replay tests.
+
+ Tracing is performed via Babel AST transformation (trace-runner.js).
+ This class handles:
+ - Parsing trace results from SQLite database
+ - Extracting traced function information
+ - Generating replay test files for Jest/Vitest
"""
+ SCHEMA_VERSION = "1.0.0"
+
def __init__(self, output_db: Path) -> None:
"""Initialize the tracer.
@@ -38,322 +59,15 @@ class JavaScriptTracer:
"""
self.output_db = output_db
- self.tracer_var = "__codeflash_tracer__"
-
- def instrument_source(self, source: str, file_path: Path, functions: list[FunctionToOptimize]) -> str:
- """Instrument JavaScript source code with function tracing.
-
- Wraps specified functions to capture their inputs and outputs.
-
- Args:
- source: Original JavaScript source code.
- file_path: Path to the source file.
- functions: List of functions to instrument.
-
- Returns:
- Instrumented source code with tracing.
-
- """
- if not functions:
- return source
-
- # Add tracer initialization at the top
- tracer_init = self._generate_tracer_init()
-
- # Add instrumentation to each function
- lines = source.splitlines(keepends=True)
-
- # Process functions in reverse order to preserve line numbers
- for func in sorted(functions, key=lambda f: f.starting_line, reverse=True):
- instrumented = self._instrument_function(func, lines, file_path)
- start_idx = func.starting_line - 1
- end_idx = func.ending_line
- lines = lines[:start_idx] + instrumented + lines[end_idx:]
-
- instrumented_source = "".join(lines)
-
- # Add tracer save at the end
- tracer_save = self._generate_tracer_save()
-
- return tracer_init + "\n" + instrumented_source + "\n" + tracer_save
-
- def _generate_tracer_init(self) -> str:
- """Generate JavaScript code for tracer initialization."""
- return f"""
-// Codeflash function tracer initialization
-const {self.tracer_var} = {{
- traces: [],
- callId: 0,
-
- serialize: function(value) {{
- try {{
- // Handle special cases
- if (value === undefined) return {{ __type__: 'undefined' }};
- if (value === null) return null;
- if (typeof value === 'function') return {{ __type__: 'function', name: value.name }};
- if (typeof value === 'symbol') return {{ __type__: 'symbol', value: value.toString() }};
- if (value instanceof Error) return {{
- __type__: 'error',
- name: value.name,
- message: value.message,
- stack: value.stack
- }};
- if (typeof value === 'bigint') return {{ __type__: 'bigint', value: value.toString() }};
- if (value instanceof Date) return {{ __type__: 'date', value: value.toISOString() }};
- if (value instanceof RegExp) return {{ __type__: 'regexp', value: value.toString() }};
- if (value instanceof Map) return {{
- __type__: 'map',
- value: Array.from(value.entries()).map(([k, v]) => [this.serialize(k), this.serialize(v)])
- }};
- if (value instanceof Set) return {{
- __type__: 'set',
- value: Array.from(value).map(v => this.serialize(v))
- }};
-
- // Handle circular references with a simple check
- return JSON.parse(JSON.stringify(value));
- }} catch (e) {{
- return {{ __type__: 'unserializable', error: e.message }};
- }}
- }},
-
- wrap: function(originalFunc, funcName, filePath) {{
- const self = this;
-
- if (originalFunc.constructor.name === 'AsyncFunction') {{
- return async function(...args) {{
- const callId = self.callId++;
- const start = process.hrtime.bigint();
- let result, error;
-
- try {{
- result = await originalFunc.apply(this, args);
- }} catch (e) {{
- error = e;
- }}
-
- const end = process.hrtime.bigint();
-
- self.traces.push({{
- call_id: callId,
- function: funcName,
- file: filePath,
- args: args.map(a => self.serialize(a)),
- result: error ? null : self.serialize(result),
- error: error ? self.serialize(error) : null,
- runtime_ns: (end - start).toString(),
- timestamp: Date.now()
- }});
-
- if (error) throw error;
- return result;
- }};
- }}
-
- return function(...args) {{
- const callId = self.callId++;
- const start = process.hrtime.bigint();
- let result, error;
-
- try {{
- result = originalFunc.apply(this, args);
- }} catch (e) {{
- error = e;
- }}
-
- const end = process.hrtime.bigint();
-
- self.traces.push({{
- call_id: callId,
- function: funcName,
- file: filePath,
- args: args.map(a => self.serialize(a)),
- result: error ? null : self.serialize(result),
- error: error ? self.serialize(error) : null,
- runtime_ns: (end - start).toString(),
- timestamp: Date.now()
- }});
-
- if (error) throw error;
- return result;
- }};
- }},
-
- saveToDb: function() {{
- const sqlite3 = require('sqlite3').verbose();
- const fs = require('fs');
- const path = require('path');
-
- const dbPath = '{self.output_db.as_posix()}';
- const dbDir = path.dirname(dbPath);
-
- if (!fs.existsSync(dbDir)) {{
- fs.mkdirSync(dbDir, {{ recursive: true }});
- }}
-
- const db = new sqlite3.Database(dbPath);
-
- db.serialize(() => {{
- // Create table
- db.run(`
- CREATE TABLE IF NOT EXISTS traces (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- call_id INTEGER,
- function TEXT,
- file TEXT,
- args TEXT,
- result TEXT,
- error TEXT,
- runtime_ns TEXT,
- timestamp INTEGER
- )
- `);
-
- // Insert traces
- const stmt = db.prepare(`
- INSERT INTO traces (call_id, function, file, args, result, error, runtime_ns, timestamp)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `);
-
- for (const trace of this.traces) {{
- stmt.run(
- trace.call_id,
- trace.function,
- trace.file,
- JSON.stringify(trace.args),
- JSON.stringify(trace.result),
- JSON.stringify(trace.error),
- trace.runtime_ns,
- trace.timestamp
- );
- }}
-
- stmt.finalize();
- }});
-
- db.close();
- }},
-
- saveToJson: function() {{
- const fs = require('fs');
- const path = require('path');
-
- const jsonPath = '{self.output_db.with_suffix(".json").as_posix()}';
- const jsonDir = path.dirname(jsonPath);
-
- if (!fs.existsSync(jsonDir)) {{
- fs.mkdirSync(jsonDir, {{ recursive: true }});
- }}
-
- fs.writeFileSync(jsonPath, JSON.stringify(this.traces, null, 2));
- }}
-}};
-"""
-
- def _generate_tracer_save(self) -> str:
- """Generate JavaScript code to save tracer results."""
- return f"""
-// Save tracer results on process exit
-process.on('exit', () => {{
- try {{
- {self.tracer_var}.saveToJson();
- // Try SQLite, but don't fail if sqlite3 is not installed
- try {{
- {self.tracer_var}.saveToDb();
- }} catch (e) {{
- // SQLite not available, JSON is sufficient
- }}
- }} catch (e) {{
- console.error('Failed to save traces:', e);
- }}
-}});
-"""
-
- def _instrument_function(self, func: FunctionToOptimize, lines: list[str], file_path: Path) -> list[str]:
- """Instrument a single function with tracing.
-
- Args:
- func: Function to instrument.
- lines: Source lines.
- file_path: Path to source file.
-
- Returns:
- Instrumented function lines.
-
- """
- func_lines = lines[func.starting_line - 1 : func.ending_line]
- func_text = "".join(func_lines)
-
- # Detect function pattern
- func_name = func.function_name
- is_arrow = "=>" in func_text.split("\n")[0]
- is_method = func.is_method
- is_async = func.is_async
-
- # Generate wrapper code based on function type
- if is_arrow:
- # For arrow functions: const foo = (a, b) => { ... }
- # Replace with: const foo = __codeflash_tracer__.wrap((a, b) => { ... }, 'foo', 'file.js')
- return self._wrap_arrow_function(func_lines, func_name, file_path)
- if is_method:
- # For methods: methodName(a, b) { ... }
- # Wrap the method body
- return self._wrap_method(func_lines, func_name, file_path, is_async)
- # For regular functions: function foo(a, b) { ... }
- # Wrap the entire function
- return self._wrap_regular_function(func_lines, func_name, file_path, is_async)
-
- def _wrap_arrow_function(self, func_lines: list[str], func_name: str, file_path: Path) -> list[str]:
- """Wrap an arrow function with tracing."""
- # Find the assignment line
- first_line = func_lines[0]
- indent = len(first_line) - len(first_line.lstrip())
- indent_str = " " * indent
-
- # Insert wrapper call
- func_text = "".join(func_lines).rstrip()
-
- # Find the '=' and wrap everything after it
- if "=" in func_text:
- parts = func_text.split("=", 1)
- wrapped = f"{parts[0]}= {self.tracer_var}.wrap({parts[1]}, '{func_name}', '{file_path.as_posix()}');\n"
- return [wrapped]
-
- return func_lines
-
- def _wrap_method(self, func_lines: list[str], func_name: str, file_path: Path, is_async: bool) -> list[str]:
- """Wrap a class method with tracing."""
- # For methods, we wrap by reassigning them after definition
- # This is complex, so for now we'll return unwrapped
- # TODO: Implement method wrapping
- logger.warning("Method wrapping not fully implemented for %s", func_name)
- return func_lines
-
- def _wrap_regular_function(
- self, func_lines: list[str], func_name: str, file_path: Path, is_async: bool
- ) -> list[str]:
- """Wrap a regular function declaration with tracing."""
- # Replace: function foo(a, b) { ... }
- # With: const __original_foo = function foo(a, b) { ... }; const foo = __codeflash_tracer__.wrap(__original_foo, 'foo', 'file.js');
-
- func_text = "".join(func_lines).rstrip()
- first_line = func_lines[0]
- indent = len(first_line) - len(first_line.lstrip())
- indent_str = " " * indent
-
- wrapped = (
- f"{indent_str}const __original_{func_name}__ = {func_text};\n"
- f"{indent_str}const {func_name} = {self.tracer_var}.wrap(__original_{func_name}__, '{func_name}', '{file_path.as_posix()}');\n"
- )
-
- return [wrapped]
@staticmethod
def parse_results(trace_file: Path) -> list[dict[str, Any]]:
"""Parse tracing results from output file.
+ Supports both the new function_calls schema and legacy traces schema.
+
Args:
- trace_file: Path to traces JSON file.
+ trace_file: Path to traces file (SQLite or JSON).
Returns:
List of trace records.
@@ -363,36 +77,59 @@ process.on('exit', () => {{
if json_file.exists():
try:
- with json_file.open("r") as f:
- return json.load(f)
+ with json_file.open("r", encoding="utf-8") as f:
+ data: list[dict[str, Any]] = json.load(f)
+ return data
except Exception as e:
logger.exception("Failed to parse trace JSON: %s", e)
return []
- # Try SQLite database
if not trace_file.exists():
return []
try:
conn = sqlite3.connect(trace_file)
cursor = conn.cursor()
- cursor.execute("SELECT * FROM traces ORDER BY id")
+
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = {row[0] for row in cursor.fetchall()}
traces = []
- for row in cursor.fetchall():
- traces.append(
- {
- "id": row[0],
- "call_id": row[1],
- "function": row[2],
- "file": row[3],
- "args": json.loads(row[4]),
- "result": json.loads(row[5]),
- "error": json.loads(row[6]) if row[6] != "null" else None,
- "runtime_ns": int(row[7]),
- "timestamp": row[8],
- }
+
+ if "function_calls" in tables:
+ cursor.execute(
+ "SELECT type, function, classname, filename, line_number, "
+ "last_frame_address, time_ns, args FROM function_calls ORDER BY time_ns"
)
+ for row in cursor.fetchall():
+ traces.append(
+ {
+ "type": row[0],
+ "function": row[1],
+ "classname": row[2],
+ "filename": row[3],
+ "line_number": row[4],
+ "last_frame_address": row[5],
+ "time_ns": row[6],
+ "args": json.loads(row[7]) if row[7] else [],
+ }
+ )
+ elif "traces" in tables:
+ cursor.execute("SELECT * FROM traces ORDER BY id")
+ for row in cursor.fetchall():
+ traces.append(
+ {
+ "id": row[0],
+ "call_id": row[1],
+ "function": row[2],
+ "file": row[3],
+ "args": json.loads(row[4]) if row[4] else [],
+ "result": json.loads(row[5]) if row[5] else None,
+ "error": json.loads(row[6]) if row[6] and row[6] != "null" else None,
+ "runtime_ns": int(row[7]) if row[7] else 0,
+ "timestamp": row[8] if len(row) > 8 else None,
+ }
+ )
conn.close()
return traces
@@ -400,3 +137,145 @@ process.on('exit', () => {{
except Exception as e:
logger.exception("Failed to parse trace database: %s", e)
return []
+
+ @staticmethod
+ def get_traced_functions(trace_file: Path) -> list[JavaScriptFunctionInfo]:
+ if not trace_file.exists():
+ return []
+
+ try:
+ conn = sqlite3.connect(trace_file)
+ cursor = conn.cursor()
+
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = {row[0] for row in cursor.fetchall()}
+
+ functions = []
+
+ if "function_calls" in tables:
+ cursor.execute(
+ "SELECT DISTINCT function, filename, classname, line_number FROM function_calls WHERE type = 'call'"
+ )
+ for row in cursor.fetchall():
+ func_name = row[0]
+ file_name = row[1]
+ class_name = row[2]
+ line_number = row[3]
+
+ module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
+ module_path = module_path.removeprefix("./")
+
+ functions.append(
+ JavaScriptFunctionInfo(
+ function_name=func_name,
+ file_name=file_name,
+ module_path=module_path,
+ class_name=class_name,
+ line_number=line_number,
+ )
+ )
+
+ conn.close()
+ return functions
+
+ except Exception as e:
+ logger.exception("Failed to get traced functions: %s", e)
+ return []
+
+ def create_replay_test(
+ self,
+ trace_file: Path,
+ output_path: Path,
+ framework: str = "jest",
+ max_run_count: int = 100,
+ project_root: Optional[Path] = None,
+ ) -> Optional[str]:
+ functions = self.get_traced_functions(trace_file)
+ if not functions:
+ logger.warning("No traced functions found in %s", trace_file)
+ return None
+
+ is_vitest = framework.lower() == "vitest"
+
+ imports = []
+ if is_vitest:
+ imports.append("import { describe, test } from 'vitest';")
+
+ imports.append("const { getNextArg } = require('codeflash/replay');")
+ imports.append("")
+
+ for func in functions:
+ alias = self._get_function_alias(func.module_path, func.function_name, func.class_name)
+ if func.class_name:
+ imports.append(f"const {{ {func.class_name}: {alias}_class }} = require('./{func.module_path}');")
+ else:
+ imports.append(f"const {{ {func.function_name}: {alias} }} = require('./{func.module_path}');")
+
+ imports.append("")
+
+ trace_path = trace_file.as_posix()
+ metadata = [
+ f"const traceFilePath = '{trace_path}';",
+ f"const functions = {json.dumps([f.function_name for f in functions])};",
+ "",
+ ]
+
+ test_cases = []
+ for func in functions:
+ alias = self._get_function_alias(func.module_path, func.function_name, func.class_name)
+ test_name = f"{func.class_name}.{func.function_name}" if func.class_name else func.function_name
+ class_arg = f"'{func.class_name}'" if func.class_name else "null"
+
+ if func.class_name:
+ test_cases.append(
+ textwrap.dedent(f"""
+describe('Replay: {test_name}', () => {{
+ const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name}', {max_run_count}, {class_arg});
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
+ const instance = new {alias}_class();
+ instance.{func.function_name}(...args);
+ }});
+}});
+""")
+ )
+ else:
+ test_cases.append(
+ textwrap.dedent(f"""
+describe('Replay: {test_name}', () => {{
+ const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name}', {max_run_count});
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
+ {alias}(...args);
+ }});
+}});
+""")
+ )
+
+ content = "\n".join(
+ [
+ "// Auto-generated replay test by Codeflash",
+ "// Do not edit this file directly",
+ "",
+ *imports,
+ *metadata,
+ *test_cases,
+ ]
+ )
+
+ try:
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text(content, encoding="utf-8")
+ logger.info("Generated replay test: %s", output_path)
+ return str(output_path)
+ except Exception as e:
+ logger.exception("Failed to write replay test: %s", e)
+ return None
+
+ @staticmethod
+ def _get_function_alias(module_path: str, function_name: str, class_name: Optional[str] = None) -> str:
+ module_alias = re.sub(r"[^a-zA-Z0-9]", "_", module_path).strip("_")
+
+ if class_name:
+ return f"{module_alias}_{class_name}_{function_name}"
+ return f"{module_alias}_{function_name}"
diff --git a/codeflash/languages/javascript/tracer_runner.py b/codeflash/languages/javascript/tracer_runner.py
new file mode 100644
index 000000000..8b4ca5adb
--- /dev/null
+++ b/codeflash/languages/javascript/tracer_runner.py
@@ -0,0 +1,231 @@
+from __future__ import annotations
+
+import json
+import logging
+import os
+import shutil
+import subprocess
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING:
+ from argparse import Namespace
+
+logger = logging.getLogger(__name__)
+
+
+def find_node_executable() -> Optional[Path]:
+ node_path = shutil.which("node")
+ if node_path:
+ return Path(node_path)
+
+ npx_path = shutil.which("npx")
+ if npx_path:
+ return Path(npx_path)
+
+ return None
+
+
+def find_trace_runner() -> Optional[Path]:
+ cwd = Path.cwd()
+
+ local_path = cwd / "node_modules" / "codeflash" / "runtime" / "trace-runner.js"
+ if local_path.exists():
+ return local_path
+
+ try:
+ result = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True, check=True)
+ global_modules = Path(result.stdout.strip())
+ global_path = global_modules / "codeflash" / "runtime" / "trace-runner.js"
+ if global_path.exists():
+ return global_path
+ except Exception:
+ pass
+
+ bundled_path = Path(__file__).parent.parent.parent.parent / "packages" / "codeflash" / "runtime" / "trace-runner.js"
+ if bundled_path.exists():
+ return bundled_path
+
+ return None
+
+
+def run_javascript_tracer(args: Namespace, config: dict[str, Any], project_root: Path) -> dict[str, Any]:
+ result: dict[str, Any] = {"success": False, "trace_file": None, "replay_test_file": None, "error": None}
+
+ node_path = find_node_executable()
+ if not node_path:
+ result["error"] = "Node.js not found. Please install Node.js to use JavaScript tracing."
+ logger.error(result["error"])
+ return result
+
+ trace_runner_path = find_trace_runner()
+ if not trace_runner_path:
+ result["error"] = "trace-runner.js not found. Please install the codeflash npm package."
+ logger.error(result["error"])
+ return result
+
+ outfile = getattr(args, "outfile", None) or "codeflash.trace.sqlite"
+ trace_file = Path(outfile).resolve()
+
+ env = os.environ.copy()
+ env["CODEFLASH_TRACE_DB"] = str(trace_file)
+ env["CODEFLASH_PROJECT_ROOT"] = str(project_root)
+
+ max_count = getattr(args, "max_function_count", 256)
+ env["CODEFLASH_MAX_FUNCTION_COUNT"] = str(max_count)
+
+ timeout = getattr(args, "tracer_timeout", None)
+ if timeout:
+ env["CODEFLASH_TRACER_TIMEOUT"] = str(timeout)
+
+ only_functions = getattr(args, "only_functions", None)
+ if only_functions:
+ env["CODEFLASH_FUNCTIONS"] = json.dumps(only_functions)
+
+ cmd = [str(node_path), str(trace_runner_path)]
+
+ cmd.extend(["--trace-db", str(trace_file)])
+ cmd.extend(["--project-root", str(project_root)])
+
+ if max_count:
+ cmd.extend(["--max-function-count", str(max_count)])
+
+ if timeout:
+ cmd.extend(["--timeout", str(timeout)])
+
+ if only_functions:
+ cmd.extend(["--functions", json.dumps(only_functions)])
+
+ is_module = getattr(args, "module", False)
+ script_args = []
+
+ if hasattr(args, "script_args"):
+ script_args = args.script_args
+ elif hasattr(args, "unknown_args"):
+ script_args = args.unknown_args
+
+ if is_module and script_args and script_args[0] == "jest":
+ cmd.append("--jest")
+ cmd.append("--")
+ cmd.extend(script_args[1:])
+ elif is_module and script_args and script_args[0] == "vitest":
+ cmd.append("--vitest")
+ cmd.append("--")
+ cmd.extend(script_args[1:])
+ elif script_args:
+ cmd.extend(script_args)
+
+ logger.info("Running JavaScript tracer: %s", " ".join(cmd))
+
+ try:
+ process = subprocess.run(cmd, cwd=project_root, env=env, capture_output=False, check=False)
+
+ if process.returncode != 0:
+ result["error"] = f"Tracing failed with exit code {process.returncode}"
+ logger.error(result["error"])
+ return result
+
+ except Exception as e:
+ result["error"] = f"Failed to run tracer: {e}"
+ logger.exception(result["error"])
+ return result
+
+ if not trace_file.exists():
+ result["error"] = f"Trace file not created: {trace_file}"
+ logger.error(result["error"])
+ return result
+
+ result["success"] = True
+ result["trace_file"] = str(trace_file)
+
+ trace_only = getattr(args, "trace_only", False)
+ if not trace_only:
+ replay_test_path = generate_replay_test(trace_file=trace_file, project_root=project_root, config=config)
+ if replay_test_path:
+ result["replay_test_file"] = str(replay_test_path)
+ logger.info("Generated replay test: %s", replay_test_path)
+
+ return result
+
+
+def generate_replay_test(
+ trace_file: Path, project_root: Path, config: dict[str, Any], output_path: Optional[Path] = None
+) -> Optional[Path]:
+ from codeflash.languages.javascript.replay_test import create_replay_test_file
+
+ framework = detect_test_framework(project_root, config)
+
+ if output_path is None:
+ tests_root = config.get("tests_root", "tests")
+ tests_dir = project_root / tests_root
+ output_path = tests_dir / "codeflash_replay.test.js"
+
+ return create_replay_test_file(
+ trace_file=trace_file,
+ output_path=output_path,
+ framework=framework,
+ max_run_count=100,
+ project_root=project_root,
+ )
+
+
+def detect_test_framework(project_root: Path, config: dict[str, Any]) -> str:
+ if "test_framework" in config:
+ framework: str = config["test_framework"]
+ return framework
+
+ vitest_configs = ["vitest.config.js", "vitest.config.ts", "vitest.config.mjs"]
+ for conf in vitest_configs:
+ if (project_root / conf).exists():
+ return "vitest"
+
+ jest_configs = ["jest.config.js", "jest.config.ts", "jest.config.mjs", "jest.config.json"]
+ for conf in jest_configs:
+ if (project_root / conf).exists():
+ return "jest"
+
+ package_json = project_root / "package.json"
+ if package_json.exists():
+ try:
+ with package_json.open(encoding="utf-8") as f:
+ pkg = json.load(f)
+ test_script = pkg.get("scripts", {}).get("test", "")
+ if "vitest" in test_script:
+ return "vitest"
+ if "jest" in test_script:
+ return "jest"
+
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
+ if "vitest" in deps:
+ return "vitest"
+ if "jest" in deps:
+ return "jest"
+ except Exception:
+ pass
+
+ return "jest"
+
+
+def check_javascript_tracer_available() -> bool:
+ if not find_node_executable():
+ return False
+
+ if not find_trace_runner():
+ return False
+
+ return True
+
+
+def get_tracer_requirements_message() -> str:
+ missing = []
+
+ if not find_node_executable():
+ missing.append("Node.js (v18+)")
+
+ if not find_trace_runner():
+ missing.append("codeflash npm package (npm install codeflash)")
+
+ if not missing:
+ return "All requirements met for JavaScript tracing."
+
+ return "Missing requirements for JavaScript tracing:\n- " + "\n- ".join(missing)
diff --git a/codeflash/tracer.py b/codeflash/tracer.py
index 4a7d24585..834c7842d 100644
--- a/codeflash/tracer.py
+++ b/codeflash/tracer.py
@@ -149,6 +149,11 @@ def main(args: Namespace | None = None) -> ArgumentParser:
parser.add_argument(
"--limit", type=int, default=None, help="Limit the number of test files to process (for -m pytest mode)"
)
+ parser.add_argument(
+ "--language",
+ help="Language to trace (python, javascript, typescript). Auto-detected if not specified.",
+ default=None,
+ )
if args is not None:
parsed_args = args
@@ -182,6 +187,13 @@ def main(args: Namespace | None = None) -> ArgumentParser:
outfile = parsed_args.outfile
config, found_config_path = parse_config_file(parsed_args.codeflash_config)
project_root = project_root_from_module_root(Path(config["module_root"]), found_config_path)
+
+ language = getattr(parsed_args, "language", None) or config.get("language", "python")
+ if language in ("javascript", "typescript"):
+ return run_javascript_tracer_main(
+ parsed_args=parsed_args, config=config, project_root=project_root, unknown_args=unknown_args
+ )
+
if len(unknown_args) > 0:
args_dict = {
"functions": parsed_args.only_functions,
@@ -332,6 +344,87 @@ def main(args: Namespace | None = None) -> ArgumentParser:
return parser
+def run_javascript_tracer_main(
+ parsed_args: Namespace, config: dict, project_root: Path, unknown_args: list[str]
+) -> ArgumentParser:
+ from codeflash.languages.javascript.tracer_runner import (
+ check_javascript_tracer_available,
+ detect_test_framework,
+ get_tracer_requirements_message,
+ run_javascript_tracer,
+ )
+
+ if not check_javascript_tracer_available():
+ console.print("[bold red]Error:[/] JavaScript tracer requirements not met.")
+ console.print(get_tracer_requirements_message())
+ sys.exit(1)
+
+ trace_only = getattr(parsed_args, "trace_only", False)
+ only_functions = getattr(parsed_args, "only_functions", None)
+ max_function_count = getattr(parsed_args, "max_function_count", 256)
+ timeout = getattr(parsed_args, "tracer_timeout", None)
+
+ framework = detect_test_framework(project_root, config)
+ logger.info("JavaScript tracer: framework=%s, project_root=%s", framework, project_root)
+
+ trace_db_path = get_run_tmp_file(Path("js_trace.sqlite"))
+
+ script_or_test_args = unknown_args if unknown_args else []
+
+ replay_test_path = run_javascript_tracer(
+ script_args=script_or_test_args,
+ trace_db_path=trace_db_path,
+ project_root=project_root,
+ functions=only_functions,
+ max_function_count=max_function_count,
+ timeout=int(timeout) if timeout else 0,
+ framework=framework,
+ )
+
+ if replay_test_path and not trace_only:
+ from codeflash.cli_cmds.cli import parse_args as cli_parse_args
+ from codeflash.cli_cmds.cli import process_pyproject_config
+ from codeflash.cli_cmds.console import paneled_text
+ from codeflash.cli_cmds.console_constants import CODEFLASH_LOGO
+ from codeflash.languages import Language, set_current_language
+ from codeflash.optimization import optimizer
+ from codeflash.telemetry import posthog_cf
+ from codeflash.telemetry.sentry import init_sentry
+
+ language = getattr(parsed_args, "language", None) or config.get("language", "javascript")
+ if language == "typescript":
+ set_current_language(Language.TYPESCRIPT)
+ else:
+ set_current_language(Language.JAVASCRIPT)
+
+ sys.argv = ["codeflash", "--replay-test", str(replay_test_path)]
+ args = cli_parse_args()
+ paneled_text(
+ CODEFLASH_LOGO,
+ panel_args={"title": "https://codeflash.ai", "expand": False},
+ text_args={"style": "bold gold3"},
+ )
+
+ args = process_pyproject_config(args)
+ args.previous_checkpoint_functions = None
+ init_sentry(enabled=not args.disable_telemetry, exclude_errors=True)
+ posthog_cf.initialize_posthog(enabled=not args.disable_telemetry)
+
+ args.effort = EffortLevel.HIGH.value
+ optimizer.run_with_args(args)
+
+ # Clean up
+ trace_db_path.unlink(missing_ok=True)
+ if replay_test_path:
+ Path(replay_test_path).unlink(missing_ok=True)
+ elif replay_test_path:
+ console.print(f"[bold green]Trace complete.[/] Replay test: {replay_test_path}")
+ else:
+ console.print("[bold yellow]Warning:[/] No functions were traced.")
+
+ return ArgumentParser()
+
+
def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser:
"""Run the Java two-stage tracer (JFR + argument capture) and optionally optimize."""
from codeflash.cli_cmds.cli import parse_args, process_pyproject_config
diff --git a/packages/codeflash/package.json b/packages/codeflash/package.json
index 68fc947cf..d96500397 100644
--- a/packages/codeflash/package.json
+++ b/packages/codeflash/package.json
@@ -6,7 +6,8 @@
"types": "runtime/index.d.ts",
"bin": {
"codeflash": "./bin/codeflash.js",
- "codeflash-setup": "./bin/codeflash-setup.js"
+ "codeflash-setup": "./bin/codeflash-setup.js",
+ "codeflash-trace": "./runtime/trace-runner.js"
},
"publishConfig": {
"access": "public"
@@ -36,6 +37,18 @@
"./jest-reporter": {
"require": "./runtime/jest-reporter.js",
"import": "./runtime/jest-reporter.js"
+ },
+ "./tracer": {
+ "require": "./runtime/tracer.js",
+ "import": "./runtime/tracer.js"
+ },
+ "./replay": {
+ "require": "./runtime/replay.js",
+ "import": "./runtime/replay.js"
+ },
+ "./babel-tracer-plugin": {
+ "require": "./runtime/babel-tracer-plugin.js",
+ "import": "./runtime/babel-tracer-plugin.js"
}
},
"scripts": {
@@ -92,5 +105,11 @@
"dependencies": {
"better-sqlite3": "^12.0.0",
"@msgpack/msgpack": "^3.0.0"
+ },
+ "optionalDependencies": {
+ "@babel/core": "^7.24.0",
+ "@babel/register": "^7.24.0",
+ "@babel/preset-env": "^7.24.0",
+ "@babel/preset-typescript": "^7.24.0"
}
}
diff --git a/packages/codeflash/runtime/babel-tracer-plugin.js b/packages/codeflash/runtime/babel-tracer-plugin.js
new file mode 100644
index 000000000..558e0664f
--- /dev/null
+++ b/packages/codeflash/runtime/babel-tracer-plugin.js
@@ -0,0 +1,434 @@
+/**
+ * Codeflash Babel Tracer Plugin
+ *
+ * A Babel plugin that instruments JavaScript/TypeScript functions for tracing.
+ * This plugin wraps functions with tracing calls to capture:
+ * - Function arguments
+ * - Return values
+ * - Execution time
+ *
+ * The plugin transforms:
+ * function foo(a, b) { return a + b; }
+ *
+ * Into:
+ * const __codeflash_tracer__ = require('codeflash/tracer');
+ * function foo(a, b) {
+ * return __codeflash_tracer__.wrap(function foo(a, b) { return a + b; }, 'foo', '/path/file.js', 1)
+ * .apply(this, arguments);
+ * }
+ *
+ * Supported function types:
+ * - FunctionDeclaration: function foo() {}
+ * - FunctionExpression: const foo = function() {}
+ * - ArrowFunctionExpression: const foo = () => {}
+ * - ClassMethod: class Foo { bar() {} }
+ * - ObjectMethod: const obj = { foo() {} }
+ *
+ * Configuration (via plugin options or environment variables):
+ * - functions: Array of function names to trace (traces all if not set)
+ * - files: Array of file patterns to trace (traces all if not set)
+ * - exclude: Array of patterns to exclude from tracing
+ *
+ * Usage with @babel/register:
+ * require('@babel/register')({
+ * plugins: [['codeflash/babel-tracer-plugin', { functions: ['myFunc'] }]],
+ * });
+ *
+ * Environment Variables:
+ * CODEFLASH_FUNCTIONS - JSON array of functions to trace
+ * CODEFLASH_TRACE_FILES - JSON array of file patterns to trace
+ * CODEFLASH_TRACE_EXCLUDE - JSON array of patterns to exclude
+ */
+
+'use strict';
+
+const path = require('path');
+
+// Parse environment variables for configuration
+function getEnvConfig() {
+ const config = {
+ functions: null,
+ files: null,
+ exclude: null,
+ };
+
+ try {
+ if (process.env.CODEFLASH_FUNCTIONS) {
+ config.functions = JSON.parse(process.env.CODEFLASH_FUNCTIONS);
+ }
+ } catch (e) {
+ console.error('[codeflash-babel] Failed to parse CODEFLASH_FUNCTIONS:', e.message);
+ }
+
+ try {
+ if (process.env.CODEFLASH_TRACE_FILES) {
+ config.files = JSON.parse(process.env.CODEFLASH_TRACE_FILES);
+ }
+ } catch (e) {
+ console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_FILES:', e.message);
+ }
+
+ try {
+ if (process.env.CODEFLASH_TRACE_EXCLUDE) {
+ config.exclude = JSON.parse(process.env.CODEFLASH_TRACE_EXCLUDE);
+ }
+ } catch (e) {
+ console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_EXCLUDE:', e.message);
+ }
+
+ return config;
+}
+
+/**
+ * Check if a function should be traced based on configuration.
+ *
+ * @param {string} funcName - Function name
+ * @param {string} fileName - File path
+ * @param {string|null} className - Class name (for methods)
+ * @param {Object} config - Plugin configuration
+ * @returns {boolean} - True if function should be traced
+ */
+function shouldTraceFunction(funcName, fileName, className, config) {
+ // Check exclude patterns first
+ if (config.exclude && config.exclude.length > 0) {
+ for (const pattern of config.exclude) {
+ if (typeof pattern === 'string') {
+ if (funcName === pattern || fileName.includes(pattern)) {
+ return false;
+ }
+ } else if (pattern instanceof RegExp) {
+ if (pattern.test(funcName) || pattern.test(fileName)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Check file patterns
+ if (config.files && config.files.length > 0) {
+ const matchesFile = config.files.some(pattern => {
+ if (typeof pattern === 'string') {
+ return fileName.includes(pattern);
+ }
+ if (pattern instanceof RegExp) {
+ return pattern.test(fileName);
+ }
+ return false;
+ });
+ if (!matchesFile) return false;
+ }
+
+ // Check function names
+ if (config.functions && config.functions.length > 0) {
+ const matchesName = config.functions.some(f => {
+ if (typeof f === 'string') {
+ return f === funcName || f === `${className}.${funcName}`;
+ }
+ // Support object format: { function: 'name', file: 'path', class: 'className' }
+ if (typeof f === 'object' && f !== null) {
+ if (f.function && f.function !== funcName) return false;
+ if (f.file && !fileName.includes(f.file)) return false;
+ if (f.class && f.class !== className) return false;
+ return true;
+ }
+ return false;
+ });
+ if (!matchesName) return false;
+ }
+
+ return true;
+}
+
+/**
+ * Check if a path should be excluded from tracing (node_modules, etc.)
+ *
+ * @param {string} fileName - File path
+ * @returns {boolean} - True if file should be excluded
+ */
+function isExcludedPath(fileName) {
+ // Always exclude node_modules
+ if (fileName.includes('node_modules')) return true;
+
+ // Exclude common test runner internals
+ if (fileName.includes('jest-runner') || fileName.includes('jest-jasmine')) return true;
+ if (fileName.includes('@vitest')) return true;
+
+ // Exclude this plugin itself
+ if (fileName.includes('codeflash/runtime')) return true;
+ if (fileName.includes('babel-tracer-plugin')) return true;
+
+ return false;
+}
+
+/**
+ * Create the Babel plugin.
+ *
+ * @param {Object} babel - Babel object with types (t)
+ * @returns {Object} - Babel plugin configuration
+ */
+module.exports = function codeflashTracerPlugin(babel) {
+ const { types: t } = babel;
+
+ // Merge environment config with plugin options
+ const envConfig = getEnvConfig();
+
+ return {
+ name: 'codeflash-tracer',
+
+ visitor: {
+ Program: {
+ enter(programPath, state) {
+ // Merge options from plugin config and environment
+ state.codeflashConfig = {
+ ...envConfig,
+ ...(state.opts || {}),
+ };
+
+ // Track whether we've added the tracer import
+ state.tracerImportAdded = false;
+
+ // Get file info
+ state.fileName = state.filename || state.file.opts.filename || 'unknown';
+
+ // Check if entire file should be excluded
+ if (isExcludedPath(state.fileName)) {
+ state.skipFile = true;
+ return;
+ }
+
+ state.skipFile = false;
+ },
+
+ exit(programPath, state) {
+ // Add tracer import if we instrumented any functions
+ if (state.tracerImportAdded) {
+ const tracerRequire = t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier('__codeflash_tracer__'),
+ t.callExpression(
+ t.identifier('require'),
+ [t.stringLiteral('codeflash/tracer')]
+ )
+ ),
+ ]);
+
+ // Add at the beginning of the program
+ programPath.unshiftContainer('body', tracerRequire);
+ }
+ },
+ },
+
+ // Handle: function foo() {}
+ FunctionDeclaration(path, state) {
+ if (state.skipFile) return;
+ if (!path.node.id) return; // Skip anonymous functions
+
+ const funcName = path.node.id.name;
+ const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
+
+ if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
+ return;
+ }
+
+ // Transform the function body to wrap with tracing
+ wrapFunctionBody(t, path, funcName, state.fileName, lineNumber, null);
+ state.tracerImportAdded = true;
+ },
+
+ // Handle: const foo = function() {} or const foo = () => {}
+ VariableDeclarator(path, state) {
+ if (state.skipFile) return;
+ if (!t.isIdentifier(path.node.id)) return;
+ if (!path.node.init) return;
+
+ const init = path.node.init;
+ if (!t.isFunctionExpression(init) && !t.isArrowFunctionExpression(init)) {
+ return;
+ }
+
+ const funcName = path.node.id.name;
+ const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
+
+ if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
+ return;
+ }
+
+ // Wrap the function expression with tracer.wrap()
+ path.node.init = createWrapperCall(t, init, funcName, state.fileName, lineNumber, null);
+ state.tracerImportAdded = true;
+ },
+
+ // Handle: class Foo { bar() {} }
+ ClassMethod(path, state) {
+ if (state.skipFile) return;
+ if (path.node.kind === 'constructor') return; // Skip constructors for now
+
+ const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value));
+ if (!funcName) return;
+
+ // Get class name from parent
+ const classPath = path.findParent(p => t.isClassDeclaration(p) || t.isClassExpression(p));
+ const className = classPath && classPath.node.id ? classPath.node.id.name : null;
+
+ const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
+
+ if (!shouldTraceFunction(funcName, state.fileName, className, state.codeflashConfig)) {
+ return;
+ }
+
+ // Wrap the method body
+ wrapMethodBody(t, path, funcName, state.fileName, lineNumber, className);
+ state.tracerImportAdded = true;
+ },
+
+ // Handle: const obj = { foo() {} }
+ ObjectMethod(path, state) {
+ if (state.skipFile) return;
+
+ const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value));
+ if (!funcName) return;
+
+ const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
+
+ if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
+ return;
+ }
+
+ // Wrap the method body
+ wrapMethodBody(t, path, funcName, state.fileName, lineNumber, null);
+ state.tracerImportAdded = true;
+ },
+ },
+ };
+};
+
+/**
+ * Create a __codeflash_tracer__.wrap() call expression.
+ *
+ * @param {Object} t - Babel types
+ * @param {Object} funcNode - The function AST node
+ * @param {string} funcName - Function name
+ * @param {string} fileName - File path
+ * @param {number} lineNumber - Line number
+ * @param {string|null} className - Class name
+ * @returns {Object} - Call expression AST node
+ */
+function createWrapperCall(t, funcNode, funcName, fileName, lineNumber, className) {
+ const args = [
+ funcNode,
+ t.stringLiteral(funcName),
+ t.stringLiteral(fileName),
+ t.numericLiteral(lineNumber),
+ ];
+
+ if (className) {
+ args.push(t.stringLiteral(className));
+ } else {
+ args.push(t.nullLiteral());
+ }
+
+ return t.callExpression(
+ t.memberExpression(
+ t.identifier('__codeflash_tracer__'),
+ t.identifier('wrap')
+ ),
+ args
+ );
+}
+
+/**
+ * Wrap a function declaration's body with tracing.
+ * Transforms:
+ * function foo(a, b) { return a + b; }
+ * Into:
+ * function foo(a, b) {
+ * const __original__ = function(a, b) { return a + b; };
+ * return __codeflash_tracer__.wrap(__original__, 'foo', 'file.js', 1, null).apply(this, arguments);
+ * }
+ *
+ * @param {Object} t - Babel types
+ * @param {Object} path - Babel path
+ * @param {string} funcName - Function name
+ * @param {string} fileName - File path
+ * @param {number} lineNumber - Line number
+ * @param {string|null} className - Class name
+ */
+function wrapFunctionBody(t, path, funcName, fileName, lineNumber, className) {
+ const node = path.node;
+ const isAsync = node.async;
+ const isGenerator = node.generator;
+
+ // Create a copy of the original function as an expression
+ const originalFunc = t.functionExpression(
+ null, // anonymous
+ node.params,
+ node.body,
+ isGenerator,
+ isAsync
+ );
+
+ // Create the wrapper call
+ const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className);
+
+ // Create: return __codeflash_tracer__.wrap(...).apply(this, arguments)
+ const applyCall = t.callExpression(
+ t.memberExpression(wrapperCall, t.identifier('apply')),
+ [t.thisExpression(), t.identifier('arguments')]
+ );
+
+ const returnStatement = t.returnStatement(applyCall);
+
+ // Replace the function body
+ node.body = t.blockStatement([returnStatement]);
+}
+
+/**
+ * Wrap a method's body with tracing.
+ * Similar to wrapFunctionBody but preserves method semantics.
+ *
+ * @param {Object} t - Babel types
+ * @param {Object} path - Babel path
+ * @param {string} funcName - Function name
+ * @param {string} fileName - File path
+ * @param {number} lineNumber - Line number
+ * @param {string|null} className - Class name
+ */
+function wrapMethodBody(t, path, funcName, fileName, lineNumber, className) {
+ const node = path.node;
+ const isAsync = node.async;
+ const isGenerator = node.generator;
+
+ // Create a copy of the original function as an expression
+ const originalFunc = t.functionExpression(
+ null, // anonymous
+ node.params,
+ node.body,
+ isGenerator,
+ isAsync
+ );
+
+ // Create the wrapper call
+ const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className);
+
+ // Create: return __codeflash_tracer__.wrap(...).apply(this, arguments)
+ const applyCall = t.callExpression(
+ t.memberExpression(wrapperCall, t.identifier('apply')),
+ [t.thisExpression(), t.identifier('arguments')]
+ );
+
+ let returnStatement;
+ if (isAsync) {
+ // For async methods, we need to await the result
+ returnStatement = t.returnStatement(t.awaitExpression(applyCall));
+ } else {
+ returnStatement = t.returnStatement(applyCall);
+ }
+
+ // Replace the function body
+ node.body = t.blockStatement([returnStatement]);
+}
+
+// Export helper functions for testing
+module.exports.shouldTraceFunction = shouldTraceFunction;
+module.exports.isExcludedPath = isExcludedPath;
+module.exports.getEnvConfig = getEnvConfig;
diff --git a/packages/codeflash/runtime/index.js b/packages/codeflash/runtime/index.js
index 982912c24..864b03066 100644
--- a/packages/codeflash/runtime/index.js
+++ b/packages/codeflash/runtime/index.js
@@ -8,6 +8,8 @@
* - capturePerf: Capture performance metrics (timing only)
* - serialize/deserialize: Value serialization for storage
* - comparator: Deep equality comparison
+ * - tracer: Function tracing for replay test generation
+ * - replay: Replay test utilities
*
* Usage (CommonJS):
* const { capture, capturePerf } = require('codeflash');
@@ -30,6 +32,22 @@ const comparator = require('./comparator');
// Result comparison (used by CLI)
const compareResults = require('./compare-results');
+// Function tracing (for replay test generation)
+let tracer = null;
+try {
+ tracer = require('./tracer');
+} catch (e) {
+ // Tracer may not be available if better-sqlite3 is not installed
+}
+
+// Replay test utilities
+let replay = null;
+try {
+ replay = require('./replay');
+} catch (e) {
+ // Replay may not be available
+}
+
// Re-export all public APIs
module.exports = {
// === Main Instrumentation API ===
@@ -88,4 +106,24 @@ module.exports = {
// === Feature Detection ===
hasV8: serializer.hasV8,
hasMsgpack: serializer.hasMsgpack,
+
+ // === Function Tracing (for replay test generation) ===
+ tracer: tracer ? {
+ init: tracer.init,
+ wrap: tracer.wrap,
+ createWrapper: tracer.createWrapper,
+ disable: tracer.disable,
+ enable: tracer.enable,
+ getStats: tracer.getStats,
+ } : null,
+
+ // === Replay Test Utilities ===
+ replay: replay ? {
+ getNextArg: replay.getNextArg,
+ getTracesWithMetadata: replay.getTracesWithMetadata,
+ getTracedFunctions: replay.getTracedFunctions,
+ getTraceMetadata: replay.getTraceMetadata,
+ generateReplayTest: replay.generateReplayTest,
+ createReplayTestFromTrace: replay.createReplayTestFromTrace,
+ } : null,
};
diff --git a/packages/codeflash/runtime/replay.js b/packages/codeflash/runtime/replay.js
new file mode 100644
index 000000000..733fed41e
--- /dev/null
+++ b/packages/codeflash/runtime/replay.js
@@ -0,0 +1,454 @@
+/**
+ * Codeflash Replay Test Utilities
+ *
+ * This module provides utilities for generating and running replay tests
+ * from traced function calls. Replay tests allow verifying that optimized
+ * code produces the same results as the original code.
+ *
+ * Usage:
+ * const { getNextArg, createReplayTest } = require('codeflash/replay');
+ *
+ * // In a test file:
+ * describe('Replay tests', () => {
+ * test.each(getNextArg(traceFile, 'myFunction', '/path/file.js', 25))
+ * ('myFunction replay %#', (args) => {
+ * myFunction(...args);
+ * });
+ * });
+ *
+ * The module supports both Jest and Vitest test frameworks.
+ */
+
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+
+// Load the codeflash serializer for argument deserialization
+const serializer = require('./serializer');
+
+// ============================================================================
+// DATABASE ACCESS
+// ============================================================================
+
+/**
+ * Open a SQLite database connection.
+ *
+ * @param {string} dbPath - Path to the SQLite database
+ * @returns {Object|null} - Database connection or null if failed
+ */
+function openDatabase(dbPath) {
+ try {
+ const Database = require('better-sqlite3');
+ return new Database(dbPath, { readonly: true });
+ } catch (e) {
+ console.error('[codeflash-replay] Failed to open database:', e.message);
+ return null;
+ }
+}
+
+/**
+ * Get traced function calls from the database.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @param {string} functionName - Name of the function
+ * @param {string} fileName - Path to the source file
+ * @param {string|null} className - Class name (for methods)
+ * @param {number} limit - Maximum number of traces to retrieve
+ * @returns {Array} - Array of traced arguments
+ */
+function getNextArg(traceFile, functionName, fileName, limit = 25, className = null) {
+ const db = openDatabase(traceFile);
+ if (!db) {
+ return [];
+ }
+
+ try {
+ let stmt;
+ let rows;
+
+ if (className) {
+ stmt = db.prepare(`
+ SELECT args FROM function_calls
+ WHERE function = ? AND filename = ? AND classname = ? AND type = 'call'
+ ORDER BY time_ns ASC
+ LIMIT ?
+ `);
+ rows = stmt.all(functionName, fileName, className, limit);
+ } else {
+ stmt = db.prepare(`
+ SELECT args FROM function_calls
+ WHERE function = ? AND filename = ? AND type = 'call'
+ ORDER BY time_ns ASC
+ LIMIT ?
+ `);
+ rows = stmt.all(functionName, fileName, limit);
+ }
+
+ db.close();
+
+ // Deserialize arguments
+ return rows.map((row, index) => {
+ try {
+ const args = serializer.deserialize(row.args);
+ return args;
+ } catch (e) {
+ console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message);
+ return [];
+ }
+ });
+ } catch (e) {
+ console.error('[codeflash-replay] Database query failed:', e.message);
+ db.close();
+ return [];
+ }
+}
+
+/**
+ * Get traced function calls with full metadata.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @param {string} functionName - Name of the function
+ * @param {string} fileName - Path to the source file
+ * @param {string|null} className - Class name (for methods)
+ * @param {number} limit - Maximum number of traces to retrieve
+ * @returns {Array} - Array of trace objects with args and metadata
+ */
+function getTracesWithMetadata(traceFile, functionName, fileName, limit = 25, className = null) {
+ const db = openDatabase(traceFile);
+ if (!db) {
+ return [];
+ }
+
+ try {
+ let stmt;
+ let rows;
+
+ if (className) {
+ stmt = db.prepare(`
+ SELECT type, function, classname, filename, line_number, time_ns, args
+ FROM function_calls
+ WHERE function = ? AND filename = ? AND classname = ? AND type = 'call'
+ ORDER BY time_ns ASC
+ LIMIT ?
+ `);
+ rows = stmt.all(functionName, fileName, className, limit);
+ } else {
+ stmt = db.prepare(`
+ SELECT type, function, classname, filename, line_number, time_ns, args
+ FROM function_calls
+ WHERE function = ? AND filename = ? AND type = 'call'
+ ORDER BY time_ns ASC
+ LIMIT ?
+ `);
+ rows = stmt.all(functionName, fileName, limit);
+ }
+
+ db.close();
+
+ // Deserialize arguments and return with metadata
+ return rows.map((row, index) => {
+ let args;
+ try {
+ args = serializer.deserialize(row.args);
+ } catch (e) {
+ console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message);
+ args = [];
+ }
+
+ return {
+ args,
+ function: row.function,
+ className: row.classname,
+ fileName: row.filename,
+ lineNumber: row.line_number,
+ timeNs: row.time_ns,
+ };
+ });
+ } catch (e) {
+ console.error('[codeflash-replay] Database query failed:', e.message);
+ db.close();
+ return [];
+ }
+}
+
+/**
+ * Get all traced functions from the database.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @returns {Array} - Array of { function, fileName, className, count } objects
+ */
+function getTracedFunctions(traceFile) {
+ const db = openDatabase(traceFile);
+ if (!db) {
+ return [];
+ }
+
+ try {
+ const stmt = db.prepare(`
+ SELECT function, filename, classname, COUNT(*) as count
+ FROM function_calls
+ WHERE type = 'call'
+ GROUP BY function, filename, classname
+ ORDER BY count DESC
+ `);
+ const rows = stmt.all();
+ db.close();
+
+ return rows.map(row => ({
+ function: row.function,
+ fileName: row.filename,
+ className: row.classname,
+ count: row.count,
+ }));
+ } catch (e) {
+ console.error('[codeflash-replay] Failed to get traced functions:', e.message);
+ db.close();
+ return [];
+ }
+}
+
+/**
+ * Get metadata from the trace database.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @returns {Object} - Metadata key-value pairs
+ */
+function getTraceMetadata(traceFile) {
+ const db = openDatabase(traceFile);
+ if (!db) {
+ return {};
+ }
+
+ try {
+ const stmt = db.prepare('SELECT key, value FROM metadata');
+ const rows = stmt.all();
+ db.close();
+
+ const metadata = {};
+ for (const row of rows) {
+ metadata[row.key] = row.value;
+ }
+ return metadata;
+ } catch (e) {
+ console.error('[codeflash-replay] Failed to get metadata:', e.message);
+ db.close();
+ return {};
+ }
+}
+
+// ============================================================================
+// TEST GENERATION
+// ============================================================================
+
+/**
+ * Generate a Jest/Vitest replay test file.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @param {Array} functions - Array of { function, fileName, className, modulePath } to test
+ * @param {Object} options - Generation options
+ * @returns {string} - Generated test file content
+ */
+function generateReplayTest(traceFile, functions, options = {}) {
+ const {
+ framework = 'jest', // 'jest' or 'vitest'
+ maxRunCount = 100,
+ outputPath = null,
+ } = options;
+
+ const isVitest = framework === 'vitest';
+
+ // Build imports section
+ const imports = [];
+
+ if (isVitest) {
+ imports.push("import { describe, test } from 'vitest';");
+ }
+
+ imports.push("const { getNextArg } = require('codeflash/replay');");
+ imports.push('');
+
+ // Build function imports
+ for (const func of functions) {
+ const alias = getFunctionAlias(func.modulePath, func.function, func.className);
+
+ if (func.className) {
+ // Import class for method testing
+ imports.push(`const { ${func.className}: ${alias}_class } = require('${func.modulePath}');`);
+ } else {
+ // Import function directly
+ imports.push(`const { ${func.function}: ${alias} } = require('${func.modulePath}');`);
+ }
+ }
+
+ imports.push('');
+
+ // Metadata
+ const metadata = [
+ `const traceFilePath = '${traceFile}';`,
+ `const functions = ${JSON.stringify(functions.map(f => f.function))};`,
+ '',
+ ];
+
+ // Build test cases
+ const testCases = [];
+
+ for (const func of functions) {
+ const alias = getFunctionAlias(func.modulePath, func.function, func.className);
+ const testName = func.className
+ ? `${func.className}.${func.function}`
+ : func.function;
+
+ if (func.className) {
+ // Method test
+ testCases.push(`
+describe('Replay: ${testName}', () => {
+ const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount}, '${func.className}');
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {
+ // For instance methods, first arg is 'this' context
+ const [thisArg, ...methodArgs] = args;
+ const instance = thisArg || new ${alias}_class();
+ instance.${func.function}(...methodArgs);
+ });
+});
+`);
+ } else {
+ // Function test
+ testCases.push(`
+describe('Replay: ${testName}', () => {
+ const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount});
+
+ test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {
+ ${alias}(...args);
+ });
+});
+`);
+ }
+ }
+
+ // Combine all parts
+ const content = [
+ '// Auto-generated replay test by Codeflash',
+ '// Do not edit this file directly',
+ '',
+ ...imports,
+ ...metadata,
+ ...testCases,
+ ].join('\n');
+
+ // Write to file if outputPath provided
+ if (outputPath) {
+ const dir = path.dirname(outputPath);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ fs.writeFileSync(outputPath, content);
+ console.log(`[codeflash-replay] Generated test file: ${outputPath}`);
+ }
+
+ return content;
+}
+
+/**
+ * Create a function alias for imports to avoid naming conflicts.
+ *
+ * @param {string} modulePath - Module path
+ * @param {string} functionName - Function name
+ * @param {string|null} className - Class name
+ * @returns {string} - Alias name
+ */
+function getFunctionAlias(modulePath, functionName, className = null) {
+ // Normalize module path to valid identifier
+ const moduleAlias = modulePath
+ .replace(/[^a-zA-Z0-9]/g, '_')
+ .replace(/^_+|_+$/g, '');
+
+ if (className) {
+ return `${moduleAlias}_${className}_${functionName}`;
+ }
+ return `${moduleAlias}_${functionName}`;
+}
+
+/**
+ * Create replay tests from a trace file.
+ * This is the main entry point for Python integration.
+ *
+ * @param {string} traceFile - Path to the trace SQLite database
+ * @param {string} outputPath - Path to write the test file
+ * @param {Object} options - Generation options
+ * @returns {Object} - { success, outputPath, functions }
+ */
+function createReplayTestFromTrace(traceFile, outputPath, options = {}) {
+ const {
+ framework = 'jest',
+ maxRunCount = 100,
+ projectRoot = process.cwd(),
+ } = options;
+
+ // Get all traced functions
+ const tracedFunctions = getTracedFunctions(traceFile);
+
+ if (tracedFunctions.length === 0) {
+ console.warn('[codeflash-replay] No traced functions found in database');
+ return { success: false, outputPath: null, functions: [] };
+ }
+
+ // Convert to the format expected by generateReplayTest
+ const functions = tracedFunctions.map(tf => {
+ // Calculate module path from file name
+ let modulePath = tf.fileName;
+
+ // Make relative to project root
+ if (path.isAbsolute(modulePath)) {
+ modulePath = path.relative(projectRoot, modulePath);
+ }
+
+ // Convert to module path (remove .js extension, use forward slashes)
+ modulePath = './' + modulePath
+ .replace(/\\/g, '/')
+ .replace(/\.js$/, '')
+ .replace(/\.ts$/, '');
+
+ return {
+ function: tf.function,
+ fileName: tf.fileName,
+ className: tf.className,
+ modulePath,
+ };
+ });
+
+ // Generate the test file
+ const testContent = generateReplayTest(traceFile, functions, {
+ framework,
+ maxRunCount,
+ outputPath,
+ });
+
+ return {
+ success: true,
+ outputPath,
+ functions: functions.map(f => f.function),
+ content: testContent,
+ };
+}
+
+// ============================================================================
+// EXPORTS
+// ============================================================================
+
+module.exports = {
+ // Core API
+ getNextArg,
+ getTracesWithMetadata,
+ getTracedFunctions,
+ getTraceMetadata,
+
+ // Test generation
+ generateReplayTest,
+ createReplayTestFromTrace,
+ getFunctionAlias,
+
+ // Database utilities
+ openDatabase,
+};
diff --git a/packages/codeflash/runtime/trace-runner.js b/packages/codeflash/runtime/trace-runner.js
new file mode 100644
index 000000000..f8e34148f
--- /dev/null
+++ b/packages/codeflash/runtime/trace-runner.js
@@ -0,0 +1,381 @@
+#!/usr/bin/env node
+/**
+ * Codeflash Trace Runner
+ *
+ * Entry point script that runs JavaScript/TypeScript code with function tracing enabled.
+ * This script:
+ * 1. Registers Babel with the tracer plugin for AST transformation
+ * 2. Sets up environment variables for tracing configuration
+ * 3. Runs the user's script, tests, or module
+ *
+ * Usage:
+ * # Run a script with tracing
+ * node trace-runner.js script.js
+ *
+ * # Run tests with tracing (Jest)
+ * node trace-runner.js --jest -- --testPathPattern=mytest
+ *
+ * # Run tests with tracing (Vitest)
+ * node trace-runner.js --vitest -- --run
+ *
+ * # Run with specific functions to trace
+ * node trace-runner.js --functions='["myFunc","otherFunc"]' script.js
+ *
+ * Environment Variables (also settable via command line):
+ * CODEFLASH_TRACE_DB - Path to SQLite database for storing traces
+ * CODEFLASH_PROJECT_ROOT - Project root for relative path calculation
+ * CODEFLASH_FUNCTIONS - JSON array of functions to trace
+ * CODEFLASH_MAX_FUNCTION_COUNT - Maximum traces per function (default: 256)
+ * CODEFLASH_TRACER_TIMEOUT - Timeout in seconds for tracing
+ *
+ * For ESM (ECMAScript modules), use the loader flag:
+ * node --loader ./esm-loader.mjs trace-runner.js script.mjs
+ */
+
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+
+// ============================================================================
+// ARGUMENT PARSING
+// ============================================================================
+
+function parseArgs(args) {
+ const config = {
+ traceDb: process.env.CODEFLASH_TRACE_DB || path.join(process.cwd(), 'codeflash.trace.sqlite'),
+ projectRoot: process.env.CODEFLASH_PROJECT_ROOT || process.cwd(),
+ functions: process.env.CODEFLASH_FUNCTIONS || null,
+ maxFunctionCount: process.env.CODEFLASH_MAX_FUNCTION_COUNT || '256',
+ tracerTimeout: process.env.CODEFLASH_TRACER_TIMEOUT || null,
+ traceFiles: process.env.CODEFLASH_TRACE_FILES || null,
+ traceExclude: process.env.CODEFLASH_TRACE_EXCLUDE || null,
+ jest: false,
+ vitest: false,
+ module: false,
+ script: null,
+ scriptArgs: [],
+ };
+
+ let i = 0;
+ while (i < args.length) {
+ const arg = args[i];
+
+ if (arg === '--trace-db') {
+ config.traceDb = args[++i];
+ } else if (arg.startsWith('--trace-db=')) {
+ config.traceDb = arg.split('=')[1];
+ } else if (arg === '--project-root') {
+ config.projectRoot = args[++i];
+ } else if (arg.startsWith('--project-root=')) {
+ config.projectRoot = arg.split('=')[1];
+ } else if (arg === '--functions') {
+ config.functions = args[++i];
+ } else if (arg.startsWith('--functions=')) {
+ config.functions = arg.split('=')[1];
+ } else if (arg === '--max-function-count') {
+ config.maxFunctionCount = args[++i];
+ } else if (arg.startsWith('--max-function-count=')) {
+ config.maxFunctionCount = arg.split('=')[1];
+ } else if (arg === '--timeout') {
+ config.tracerTimeout = args[++i];
+ } else if (arg.startsWith('--timeout=')) {
+ config.tracerTimeout = arg.split('=')[1];
+ } else if (arg === '--trace-files') {
+ config.traceFiles = args[++i];
+ } else if (arg.startsWith('--trace-files=')) {
+ config.traceFiles = arg.split('=')[1];
+ } else if (arg === '--trace-exclude') {
+ config.traceExclude = args[++i];
+ } else if (arg.startsWith('--trace-exclude=')) {
+ config.traceExclude = arg.split('=')[1];
+ } else if (arg === '--jest') {
+ config.jest = true;
+ } else if (arg === '--vitest') {
+ config.vitest = true;
+ } else if (arg === '-m' || arg === '--module') {
+ config.module = true;
+ } else if (arg === '--') {
+ // Everything after -- is passed to the script/test runner
+ config.scriptArgs = args.slice(i + 1);
+ break;
+ } else if (arg === '--help' || arg === '-h') {
+ printHelp();
+ process.exit(0);
+ } else if (!arg.startsWith('-')) {
+ // First non-flag argument is the script
+ config.script = arg;
+ config.scriptArgs = args.slice(i + 1);
+ break;
+ }
+
+ i++;
+ }
+
+ return config;
+}
+
+function printHelp() {
+ console.log(`
+Codeflash Trace Runner - JavaScript Function Tracing
+
+Usage:
+ trace-runner [options]