# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Carve is a Text Editor for Agents. It uses tree-sitter ASTs as the source of truth, treating source files as generated artifacts. The server exposes navigation and mutation tools via MCP (stdio) and HTTP (REST) transports.

**Phase 1 and Phase 2 are complete.** See `TODO.md` for Phase 3 roadmap.

## Commands

```bash
# Install (editable with dev deps)
uv pip install -e ".[dev]"

# Run all tests (149 tests, pytest-asyncio with asyncio_mode=auto)
uv run pytest

# Run a single test file
uv run pytest tests/test_tools.py

# Run a single test by name
uv run pytest -k "test_detect_python"

# Type check (strict mode enabled)
uv run mypy carve/

# Lint
uv run ruff check carve/ tests/
```

## Architecture

```
carve/
  models.py    - Pydantic models: TreeNode, NodeMetadata, TreeStore (in-memory single-file store)
  node_id.py   - Hierarchical ID generation (e.g. "module::ClassName::method_name")
  parser.py    - Tree-sitter parser with auto language detection from file extensions
  tools.py     - All tool implementations: navigation (subtree, ancestors, inspect, search),
                 mutation (insert, update, delete, validate_snippet),
                 project (project_import, project_serialize)
  server.py    - CarveServer wrapper + MCP transport (create_mcp_server) + HTTP transport (create_http_app via FastAPI)
  cli.py       - CLI entry point (`carve serve --mcp` or `carve serve --http`)
```

**Key data flow:** Source file → `TreeSitterParser.to_tree_store()` → `TreeStore` (dict of `TreeNode` keyed by node ID) → tools operate on the store → `project_serialize()` regenerates source code.

**Node IDs** are deterministic hierarchical paths built from the AST: `module::ClassName::method_name`. Each language defines its own set of named types that form hierarchy components (see `PYTHON_NAMED_TYPES`, `JAVASCRIPT_NAMED_TYPES`, etc. in `node_id.py`).

**Dual transport:** `CarveServer` wraps all tool logic. Both MCP and HTTP transports delegate to it, so tool behavior is transport-agnostic.

**Supported languages:** Python, JavaScript, HTML, CSS, JSON (via tree-sitter grammars). Unsupported extensions are stored as `text` (no AST, raw content only).

## Using Carve Efficiently

When using carve MCP tools to edit this codebase, prefer carve over file-based tools (Read, Edit, Write, Bash). Also use carve's custom tools (`run_test`, `run_typecheck`, `run_lint`, `run_install`, `run_flask`) instead of Bash for running commands.

For the full reference on Carve workflows, read the `carve://guide` MCP resource.

**Target the smallest node that covers your change.** Carve nodes go all the way down to individual statements. Replacing a single `assert_statement` or `expression_statement` is far more efficient than replacing an entire function.

```
# GOOD: update just the one statement that changed
update(node_id="...::test_foo::assert_statement[1]", snippet='assert result == "new_value"')

# WASTEFUL: replace the entire function to change one line
update(node_id="...::test_foo", snippet="def test_foo(): ...")
```

**Use navigation tools for understanding, mutation tools for editing:**
- `subtree` / `search` — structural overview without reading full files
- `find_references` — cross-file dependency tracing
- `inspect` — read a specific node's source
- `update` at the statement level — surgical edits

**Always pass sidecar metadata on every mutation.** The `insert`, `update`, `delete`, `move`, and `batch` tools accept `prompt`, `model`, and `notes` parameters that are logged to `.crv` sidecar files. You MUST pass these on every mutation call — without them the audit trail is empty.

```
# GOOD: includes sidecar metadata
update(
    node_id="module::Calculator::add",
    snippet="def add(self, a, b): return a + b",
    prompt="Simplify the add method",
    model="claude-opus-4-6",
    notes="Removed type annotations per user request"
)

# BAD: sidecar log entry will be full of nulls
update(node_id="module::Calculator::add", snippet="def add(self, a, b): return a + b")
```

**Call `project_serialize` before testing.** Carve holds mutations in-memory. Files on disk are only updated when you serialize. Always serialize before running the server, tests, or any tool that reads from disk.

**Use `insert` on a directory node to create new files.** Pass `file_name` along with `parent_id` pointing to the directory. New file creation via `insert` skips tree-sitter validation, which is important for languages with incomplete grammars (see Jinja note below).

**For complete rewrites, use file-level `update`.** When every line changes (e.g., converting a todo app to a kanban board), update the file node directly rather than trying to surgically edit dozens of nodes. Reserve surgical edits for incremental changes.

**Markdown works great for targeted edits.** The tree-sitter markdown grammar is complete — sections, headings, lists, code blocks are all distinct nodes. You can update a single `### Subsection` without touching the rest of the file.

### Language-specific tips

**Python:** Best Carve experience. Functions, classes, decorators, individual statements are all targetable. Use `subtree` with `depth=2` to see the structure, then `update` specific nodes.

**Jinja/HTML templates:** Limited by incomplete `tree-sitter-jinja` grammar (see `BUGS.md` BUG-001). Key limitations:
- `{% include %}`, `{% block %}`, `{% extends %}`, `{% set %}`, `{% macro %}` all cause syntax errors during validation
- `insert` (new file creation) works because it skips validation
- `update` fails on any file containing unsupported tags — even if your edit is on a different node
- **Workaround:** Delete the file and recreate it with `insert` (whole-file replace)
- Templates that only use `{% if %}`, `{% for %}`, and `{{ }}` can be updated normally
- `template_data` nodes represent raw HTML fragments between Jinja tags — they slice across HTML element boundaries, so they're awkward to target

**CSS-in-templates:** Styles embedded in `<style>` tags within Jinja templates land inside `template_data` nodes. If the template uses `{% include %}`, you can't update them surgically. Consider putting shared styles in a partial that only uses supported Jinja tags.

### The example Flask app

The `examples/` directory contains a Kanban board app used for demoing Carve. Useful commands:
- `run_flask start` — start the server on port 5000
- `run_flask stop` — stop it
- `run_flask status` — check if running
- The database is `examples/kanban.db` (SQLite, auto-created on first run)
- Templates are in `examples/templates/` using Jinja2 with Pico CSS + htmx + SortableJS via CDN## Code Conventions

- Python 3.11+, strict mypy, Pydantic models for all data structures
- Ruff with rules: E, F, I, N, W, UP, B, C4, SIM; line length 100
- Tests are class-based (`TestClassName`), use fixtures, and live in `tests/`
- Commit messages follow conventional commits (`feat:`, `docs:`, `fix:`)

## Testing Guidelines

Tests must be **black-box behavioral tests** that survive internal refactoring. The public API surface is:

1. **Tool functions** in `carve.tools` (subtree, ancestors, inspect, search, insert, update, delete, move, create_file, find_references, error_locate, ts_query, validate_snippet, project_import, project_serialize)
2. **CarveServer** methods in `carve.server`
3. **HTTP endpoints** via FastAPI

### Rules for New Tests

**Assert on tool return values, not model internals.**

```python
# GOOD: verify through the tool API
result = insert(store, parent_id, "end", snippet)
assert "error" not in result
inspected = inspect(store, result["node_id"])
assert "def greet" in inspected["source"]

# BAD: reaching into model internals
result = insert(store, parent_id, "end", snippet)
node = store.get_node(result["node_id"])  # internal model access
assert node.source == expected              # Pydantic field
assert node.metadata.version == 2           # internal metadata
```

**Discover node IDs dynamically, don't hardcode them.**

```python
# GOOD: discover via search
results = search(store, pattern="Calculator")
calc_id = results[0]["node_id"]

# BAD: hardcoded ID format
calc_id = "module::Calculator"
```

**Set up stores via `project_import`, not parser internals.**

```python
# GOOD: use the tool API
py_file = tmp_path / "test.py"
py_file.write_text(source)
store = project_import(str(py_file))

# BAD: coupling to parser internals
store = parser.to_tree_store(source, "python", "test.py")
```

**Verify side effects through tools or disk, not internal dicts.**

```python
# GOOD: verify deletion via search
delete(store, node_id)
results = search(store, pattern="greet")
assert not any(r["node_id"] == node_id for r in results)

# GOOD: verify disk write by reading the file
result = update(store, node_id, new_snippet)
content = Path(store.file_path).read_text()
assert "new_function" in content

# BAD: poking internal dicts
delete(store, node_id)
assert node_id not in store.file_sources  # internal dict
assert store.get_node(node_id) is None     # internal method
```

**Verify sidecar writes by reading the .crv JSON file, not private functions.**

```python
# GOOD: read the file directly
crv = tmp_path / "app.py.crv"
entries = json.loads(crv.read_text())
assert entries[-1]["operation"] == "update"

# BAD: importing private helpers
from carve.tools import _read_sidecar  # underscore = private
entries = _read_sidecar(crv)
```

**Never import underscore-prefixed functions** (`_read_sidecar`, `_reindent_snippet`, `_validate_in_context`, etc.). These are implementation details. If behavior needs testing, test it through the public tool that calls it.

### Acceptable Internal Tests

`test_parser.py` tests parser and model internals directly — this is fine and expected for unit-testing the parser layer. These tests are understood to break when parser internals change. Keep them isolated in `test_parser.py` and label the file clearly.

### Test File Organization

| File | Purpose | API Layer |
|------|---------|-----------|
| `test_parser.py` | Parser/model unit tests (internal, expected to break on refactor) | `TreeSitterParser`, `TreeNode`, `TreeStore` |
| `test_server.py` | Server + HTTP endpoint tests (gold standard for black-box) | `CarveServer`, HTTP |
| `test_tools.py` | Tool behavior tests | Tool functions |
| `test_multifile.py` | Multi-file/ProjectStore behavior | Tool functions + `project_import` |
| `test_bugs.py` | Regression tests for fixed bugs | Tool functions |
| `test_sidecar.py` | Sidecar persistence behavior | Tool functions + disk |
| `test_ts_query.py` | Tree-sitter query behavior | `ts_query` tool |
| `test_draft_promote.py` | Validation/indentation behavior | `insert`, `update` tools |

## Bug Reporting

When you encounter bugs during development or testing, write detailed bug reports to `BUGS.md`. Each report should include:
- Summary of the issue
- Exact steps to reproduce (code snippets or MCP tool call sequences)
- Observed vs expected behavior
- Likely cause if apparent
