Metadata-Version: 2.5
Name: ravenclaw
Version: 0.3.0
Summary: Deterministic code navigation and refactoring tools for coding agents (find-references, rename, more) via tree-sitter
Project-URL: Repository, https://github.com/idin/ravenclaw
Author-email: Idin <idin@idin.ca>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pandas>=2.0.0
Requires-Dist: tree-sitter-javascript>=0.25.0
Requires-Dist: tree-sitter-python>=0.25.0
Requires-Dist: tree-sitter-typescript>=0.23.2
Requires-Dist: tree-sitter<0.26.0,>=0.25.0
Description-Content-Type: text/markdown

# ravenclaw

Deterministic code navigation and refactoring for coding agents: find a
symbol's definition and every real use of it, rename it, move it, or rename
the file that holds it, and have every import follow. It resolves bindings
(which name refers to which definition), not text.

This README is written for an agent that has never seen the repository.
Each section stands on its own.

- [When to use it](#when-to-use-it)
- [Install](#install)
- [Install the skills](#install-the-skills)
- [The operations, one call each](#the-operations-one-call-each)
- [Settings](#settings)
- [Which files a scan reads](#which-files-a-scan-reads)
- [Catalog and hygiene checks](#catalog-and-hygiene-checks)
- [Languages, and TypeScript today](#languages-and-typescript-today)
- [Known limitations](#known-limitations)
- [Errors you can see](#errors-you-can-see)
- [For contributors](#for-contributors)

## When to use it

Use ravenclaw instead of `grep`, `sed` or a hand edit when the job is "every
place this particular thing is used". Text search cannot tell two unrelated
functions that share a name apart, matches the name inside comments and
strings, and misses uses written differently (`from .area import compute_area
as area_of`, a relative import, a re-export). ravenclaw parses the code with
[tree-sitter](https://tree-sitter.github.io/tree-sitter/), builds the scopes,
and follows the binding, so it touches exactly the occurrences that are the
same thing, and nothing else. It follows an aliased import to the definition
(a rename edits the import and the alias keeps working), but `find_references`
lists the import itself, not the calls made through the alias
(see [`find_references`](#find_references)).

| You want to | Use |
| --- | --- |
| Go from a use to where it is defined | `find_definition` |
| Know who uses a symbol, or size a change before making it | `find_references` |
| Rename a function, class, method, variable or constant everywhere | `rename_symbol` |
| Rename or move a source file and fix every import of it | `rename_file` |
| Move a function or class to another file and fix every import | `move_symbol` |
| Check nothing is left dangling after one of the above | `find_unresolved_references` |

Do not use it for text that is not code (a README, a JSON file: use `git
mv`), for a one-off edit inside one function, or for a language it does not
resolve yet (see [Languages](#languages-and-typescript-today)).

Every operation takes a position: a `file`, a 1-indexed `line` and a
0-indexed `column` that points at the identifier itself, at any of its
occurrences (its definition or any use: the same set comes back either way).

## Install

Needs Python 3.10 or newer. Create a clean environment first if your default
Python is older:

```bash
uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python ravenclaw
# or, without uv:
python3.12 -m venv .venv && .venv/bin/pip install ravenclaw
```

then `pip install ravenclaw` as below, inside it. On Python 3.9 the failure
does not name the Python version, and one form is worse than a failure:
`pip install ravenclaw` there silently installs 0.1.0, the older unrelated
project (it pulls scikit-learn and has none of the operations below), and
installing the 0.2.0 wheel fails with `ERROR: No matching distribution found
for tree-sitter-javascript>=0.25.0`. After installing, check
`python -c "import importlib.metadata as m; print(m.version('ravenclaw'))"`
prints 0.2.0 or newer.

```bash
pip install ravenclaw
```

or, as a command-line tool in its own environment:

```bash
uv tool install ravenclaw
uvx --from ravenclaw ravenclaw --help   # one-off run, nothing installed
```

The PyPI project `ravenclaw` is this one from version 0.2.0 (published
2026-10-17 from [github.com/idin/ravenclaw](https://github.com/idin/ravenclaw)).
An older, unrelated project released earlier under the same name (0.1.0, and a
yanked 2020 release) is still listed there; the newest version is the one you
get. Check which you have with
`python -c "import importlib.metadata as m; print(m.version('ravenclaw'))"`.

The operations are a Python library; `ravenclaw run` also exposes the three
read-only ones as a command line (below). The package depends on `pandas`,
`tree-sitter` (pinned below 0.26.0, see [Known limitations](#known-limitations)),
`tree-sitter-python`, `tree-sitter-javascript` and `tree-sitter-typescript`.

### Upgrade and uninstall

```bash
# Upgrade the library, then refresh the installed skills to match it.
pip install -U ravenclaw
ravenclaw install-skills --overwrite
# Remove the library.
pip uninstall ravenclaw
```

The skills `install-skills` copied stay where they are; delete the
`ravenclaw-*` folders in the skills folder (see
[Install the skills](#install-the-skills)) to remove them.

### Installing without PyPI

ravenclaw makes no network calls when it runs; only installing needs a package
index. On a machine that has one, collect the files, then install from the
folder on the target machine. Installing a ravenclaw wheel by itself is not
enough: it still downloads its dependencies from an index (offline, `uv pip
install ravenclaw-0.2.0-py3-none-any.whl` stops with "Packages were unavailable
because the network was disabled") unless they are in the same folder and you
pass `--no-index --find-links` (the wheels must match the target's Python
version and platform; if it differs, add `--only-binary=:all: --python-version
<3.x> --platform <tag>` to `pip download`):

```bash
pip download ravenclaw -d wheels/
pip install --no-index --find-links wheels/ ravenclaw
```

With an internal mirror, `pip install --index-url <mirror> ravenclaw`, or set
`PIP_INDEX_URL` (uv: `UV_DEFAULT_INDEX`) once. To build from a source tree,
`pip wheel --no-deps git+https://github.com/idin/ravenclaw -w wheels/` (the
build needs `hatchling`, which `pip wheel` fetches itself).

## Install the skills

The library alone does nothing for an agent that does not know to call it:
Claude Code still reaches for grep and hand edits. ravenclaw ships Claude
Code skills that tell it to call ravenclaw instead. Copy them into your
skills folder:

```bash
ravenclaw install-skills
```

- **Where**: `$CLAUDE_CONFIG_DIR/skills` when that variable is set to a
  non-empty value, otherwise `~/.claude/skills`. `--destination <folder>`
  installs into another skills folder. The folder is created when missing.
- **What**: one folder per bundled skill, each holding a `SKILL.md`. The
  command prints `installed <skill> into <folder>` per skill.
- **Safe by default**: if any skill folder already exists, the command
  installs nothing, prints which folder is in the way, and exits 1. `--overwrite`
  replaces the existing copies wholesale (edits you made to them are lost);
  folders that are not ravenclaw's skills are never touched.
- **Exit codes**: 0 installed; 1 refused (a skill folder exists and
  `--overwrite` was not given, or the destination exists and is not a folder);
  2 a usage error (an unknown option).

Inside Claude Code, the same skills can instead be installed as a plugin:

```
/plugin marketplace add idin/ravenclaw
/plugin install ravenclaw@ravenclaw
```

Claude Code does not update a plugin from this marketplace on its own: run
`claude plugin update ravenclaw@ravenclaw` in your shell, or turn on
auto-update for the `ravenclaw` marketplace in the **Marketplaces** tab of
`/plugin`. Either way the skills call the `ravenclaw` package, so it must be
installed in the environment that runs them.

### The seven bundled skills

| Skill | Use it when |
| --- | --- |
| `ravenclaw-find-definition` | You have a use of a name and want the place it is defined ("go to definition", "which X is this"). |
| `ravenclaw-find-references` | You want every use of a symbol, to answer "who uses this" or to size a change before making it. |
| `ravenclaw-find-unresolved-references` | You finished a rename, move or deletion and want to check no import or name was left dangling. |
| `ravenclaw-rename-symbol` | You rename a function, class, method, variable or constant, however small the rename looks. |
| `ravenclaw-rename-file` | You rename or move a source file (a module) that other files import. |
| `ravenclaw-move-symbol` | You move a function or class to another file. |
| `ravenclaw-settings` | The user wants to configure ravenclaw: which folders it searches, which interpreter it checks imports against, preview before writing, or how much to report. |

Each skill's Requirements section says `pip install ravenclaw` and that it
needs Python 3.10 or newer. A skill is the instruction an agent reads; the code
it calls is in the operations below. The skills use the library's functions, so
a skill for an operation the `ravenclaw run` command does not offer (rename,
move) calls the Python function.

A skill named `ravenclaw-install` is not installed: it is a placeholder, marked
"NOT READY", for a future skill that installs ravenclaw into an environment of
its own (uv, conda, pipx or a virtual environment). Its prototype, the script
that picks the manager, lives with its plan in
`playbook/plans/environment_install/prototype/ravenclaw_install/`, and a
finished skill will replace it in a later release.

## The operations, one call each

Every example below was run against a throwaway project with the installed
wheel (`python -I` from another directory). The project:

```
shapes/
  pyproject.toml
  src/shapes/__init__.py        (empty)
  src/shapes/area.py
  src/shapes/report.py
  tests/test_area.py
```

```python
# src/shapes/area.py
def compute_area(width, height):
    return width * height


def describe(width, height):
    return f"area is {compute_area(width, height)}"
```

```python
# src/shapes/report.py
from .area import compute_area, describe


def report():
    total = compute_area(2, 3)
    return describe(2, 3), total
```

```python
# tests/test_area.py
from shapes.area import compute_area


def test_area():
    assert compute_area(2, 3) == 6
```

Rules that hold for every operation below:

- **Relative paths are read from the working directory**, in the Python API as
  well as in `ravenclaw run`: `file="src/shop/pricing.py"` from another
  directory raises `FileNotFoundError`. Run from the project root, or pass
  absolute paths.
- **Positions**: `line` is 1-based and `column` is 0-based, pointing at the
  first character of the identifier. For a top-level `def compute_total`, the
  line comes from `grep -n "def compute_total" src/shop/pricing.py` and the
  column is the index of the name in that line, 4 after `def `:
  `python3 -c "print(open('src/shop/pricing.py').read().splitlines()[4 - 1].index('compute_total'))"`.
  A wrong column (on the `def` keyword, on whitespace, past the name) raises
  `UnresolvedBindingError: None at <file>:4:0 - no enclosing scope binds this
  name` (`:4:0` is the line and column you gave).
- **An empty scan is not an all-clear unless the scope covers the code.** With
  no `scope`, an operation uses the project of the current working directory
  (see [Settings](#settings)); `find_unresolved_references` returns an empty
  result both for a clean project and for a scope that scanned nothing (a
  mistyped folder, a working directory with no Python code). GitHub issue
  [#357](https://github.com/idin/ravenclaw/issues/357) tracks making that loud;
  until then this README documents the behaviour.

In the outputs, `<project>` stands for the project's absolute path: positions
carry absolute paths.

### The command line: `ravenclaw run <operation>`

`ravenclaw run` takes the operation's keyword arguments as one JSON object on
standard input and prints one JSON document on standard output. Only the
read-only operations are offered: `find-definition`, `find-references` and
`find-unresolved-references`. The editing operations (rename, move) are Python
functions only (`ravenclaw run rename-symbol` is refused as an unknown
operation).

```bash
# find_definition from a use (report.py line 5, column 12 is the call to compute_area)
echo '{"file": "src/shapes/report.py", "line": 5, "column": 12}' | ravenclaw run find-definition
```

```json
{"result": "definition", "value": {"type": "Reference", "position": {"type": "Position", "file": "<project>/src/shapes/area.py", "line": 1, "column": 4}, "name": "compute_area", "is_definition": true, "kind": "function", "module_segments": null, "relative_level": null, "imported_name_position": null, "is_from_import": false, "is_aliased_module_import": false, "is_order_dependent": false, "is_type_only": false}}
```

`find-references` returns `{"result": "references", "value": [...]}` with one
object of that same shape per occurrence; `find-unresolved-references` returns
`{"result": "unresolved_references", "value": [...]}`. A relative `file` is
read from the working directory. Failures print one error document on
**standard error** and exit 1 (see [Errors](#errors-you-can-see)).

### `find_definition`

```python
from ravenclaw.operations import find_definition

definition = find_definition(file="src/shapes/report.py", line=5, column=12)
# definition.name == "compute_area", definition.kind == BindingKind.FUNCTION
# definition.position -> <project>/src/shapes/area.py line 1, column 4
# definition.is_definition is True
```

Returns one `Reference`. Raises `UnresolvedBindingError` when the position has
no resolvable binding (whitespace, a keyword, the wrong column, or a file the
scope leaves out).

### `find_references`

```python
from ravenclaw.operations import find_references

for reference in find_references(file="src/shapes/area.py", line=1, column=4):
    print(reference.position.file, reference.position.line, reference.position.column,
          reference.is_definition, reference.kind)
```

Real output (paths shortened):

```
src/shapes/area.py   1 4  True   BindingKind.FUNCTION   the definition
src/shapes/area.py   6 22 False  BindingKind.FUNCTION   the call inside describe
src/shapes/report.py 5 12 False  BindingKind.FUNCTION   the call in report
src/shapes/report.py 1 18 True   BindingKind.MODULE     the name in `from .area import ...`
tests/test_area.py   5 11 False  BindingKind.FUNCTION   the call in the test
tests/test_area.py   1 24 True   BindingKind.MODULE     the name in `from shapes.area import ...`
```

An import that names the symbol is listed too (kind `module`, with its
`module_segments`), so the list is exactly what a rename would edit. A call
made through an alias is not listed: with `from .pricing import compute_total as
total_of` in a file, `find_references` on `compute_total` lists that import
(kind `module`) but not the `total_of(...)` calls (checked against the built
wheel). A use through a module alias (`pr.tax(5)` after `from . import pricing
as pr`) is listed. Leaving the calls out is right for a rename, where the alias stays and keeps
working, but it undercounts "who uses this": search the alias names too.

### `find_unresolved_references`

```python
from ravenclaw.operations import find_unresolved_references

unresolved = find_unresolved_references(scope=("src", "tests"))
# () on the project above. With `from .area import missing_thing` and a use of
# `undefined_name` added, it holds a Reference for each (name, file, line, column).
```

A scope-wide scan. It never raises for an unresolved name: listing them is its
purpose. It is also how you confirm a rename, a move or a deletion left
nothing dangling. An empty result means "nothing found", and is only an
all-clear if the scope covers the code: a `scope` naming a folder that does not
exist returns an empty result too. With no `scope` it scans the project of the
**current working directory** (its `src` and `tests`, or the folder itself when
it is in no project): from a folder with no Python code, or with a mistyped
`scope=("srcc",)`, it returns `()` exactly as a clean project does, so an empty
result from outside the project root means nothing was scanned. Run it from the
project root or pass absolute paths. (#357 tracks making this loud.)

### `rename_symbol`

```python
from ravenclaw.operations import apply_edit_set, rename_symbol

# Preview: nothing is written, the EditSet comes back.
edit_set = rename_symbol(file="src/shapes/area.py", line=1, column=4,
                         new_name="calculate_area", auto_apply=False)
# EditSet with 6 edits, no file renames, no created files; edit_set.edits[0] is
# TextEdit(position=Position(file='<project>/src/shapes/area.py', line=1, column=4),
#          old_text='compute_area', new_text='calculate_area')
print(apply_edit_set(edit_set=edit_set))
# {'num_files_changed': 3, 'num_edits_applied': 6, 'num_files_renamed': 0, 'num_files_created': 0}

# Direct: with auto_apply left at its default (true) the same call writes and reports.
rename_symbol(file="src/shapes/area.py", line=1, column=4, new_name="compute_area")
# {'num_files_changed': 3, 'num_edits_applied': 6, 'num_files_renamed': 0, 'num_files_created': 0}
```

It renames the definition, every use, and every import that names the symbol,
from any of those positions. A module that lists the name as a string in its
`__all__` has that string renamed too. A report whose `num_edits_applied` is 1
for a symbol with obvious callers is a red flag: the scope probably leaves
the callers out. As with every operation below, an empty or too-small result
can mean the working directory or `scope` is not the project (see the rules
above).

Strings, comments and docstrings that mention the old name are not changed
(`"compute_total"` in a string and `# compute_total sums` in a comment stayed as
they were in a checked run). After a rename, re-read the docstrings and
comments, and `grep` for the old name as text.

`verbosity="files"` adds `edits_by_file` (an edit count per absolute path);
`verbosity="edits"` adds `edits` (every `{file, line, column, old_text,
new_text}`).

### `rename_file`

```python
from ravenclaw.operations import rename_file

rename_file(file="src/shapes/area.py", new_path="src/shapes/geometry.py")
# {'num_files_changed': 2, 'num_edits_applied': 2, 'num_files_renamed': 1, 'num_files_created': 0}
```

Moves the file on disk and rewrites every import of it (absolute, relative,
dotted, and from-imports of its package) and every relative import inside it.
A new path in a folder that does not exist creates the folder and its
`__init__.py`.

### `move_symbol`

```python
from ravenclaw.operations import move_symbol

move_symbol(file="src/shapes/geometry.py", line=1, column=4,
            destination_file="src/shapes/maths.py")
# {'num_files_changed': 4, 'num_edits_applied': 5, 'num_files_renamed': 0, 'num_files_created': 1}
```

Takes a module-level function or class (decorators included) out of its file,
puts it in the destination (created when missing) with the imports it needs,
rewrites every consumer's import, and keeps an import in the old file when code
left there still uses the name. Afterwards `report.py` read
`from .geometry import describe` and `from .maths import compute_area`, and the
test read `from shapes.maths import compute_area`.

An attribute use through a module alias is rewritten to an absolute import,
even in a file that otherwise uses relative imports: with `from . import
pricing as pr` and `pr.tax(5)`, moving `tax` to `src/shop/taxes.py` added
`import shop.taxes` to that file and changed the call to `shop.taxes.tax(5)`
(checked against the built wheel). Expect that in the diff. Like the other
editing operations, it resolves relative paths from the working directory.

### `apply_edit_set`

Shown above: writes an `EditSet` returned by a preview. The three editing
operations call it themselves unless `auto_apply` is false.

### The `ravenclaw.settings` functions

See [Settings](#settings) for the examples.

## Settings

An agent standing in a project should not have to restate where the code is on
every call, so each operation reads the arguments you leave out from settings.
An argument passed explicitly always wins. Otherwise the value comes from the
project's `.ravenclaw/settings.json` (found by walking up from the folder of
the file the operation acts on, or from the working directory for an operation
on no one file), then the user's `~/.ravenclaw/settings.json`, then a
fallback.

| Setting | Applies to | Meaning | Fallback |
| --- | --- | --- | --- |
| `scope` | every operation | The paths to search and edit. A relative path in a settings file is read from the folder that holds its `.ravenclaw` folder (the project root, or the home folder for the user's file). | The project the target file belongs to (root found by `pyproject.toml` or `.git`): its `src` folder and, when it exists, `tests`; the whole root for a flat layout or a target outside `src` and `tests`. |
| `python_executable` | every operation | The interpreter whose environment decides which imports are standard-library or third-party. | The running interpreter. |
| `auto_apply` | `rename_symbol`, `rename_file`, `move_symbol` | Whether to write the edits in the same call. | `true`: mechanical edits are the point. Set `false` to get the `EditSet` back for review. |
| `verbosity` | the same three and `apply_edit_set` | What to report: `totals`, `files` or `edits`. | `totals`, the cheapest. |

A target file, or `move_symbol`'s destination, or `rename_file`'s new path,
outside the given or stored scope raises `ScopeError` and writes nothing.

Write and read them with the functions of `ravenclaw.settings`. By convention
`scope` and `python_executable` describe the codebase (location `project`);
`auto_apply` and `verbosity` are one person's preferences (location `user`).

```python
from ravenclaw.settings import (
    read_all_settings, read_shared_setting, remove_shared_setting,
    write_function_setting, write_shared_setting,
)

path = write_shared_setting(name="scope", value=("src", "tests"),
                            location="project", start_directory="/path/to/shapes")
# '/path/to/shapes/.ravenclaw/settings.json', containing {"scope": ["src", "tests"]}

write_function_setting(name="auto_apply", value=False, function_name="rename_symbol",
                       location="user", start_directory="/path/to/shapes")
write_function_setting(name="verbosity", value="files", function_name="rename_symbol",
                       location="user", start_directory="/path/to/shapes")
# ~/.ravenclaw/settings.json now holds {"rename_symbol": {"auto_apply": false, "verbosity": "files"}}

read_all_settings(start_directory="/path/to/shapes")
# {'shared': {'scope': ('/path/to/shapes/src', '/path/to/shapes/tests'),
#             'python_executable': '/path/to/venv/bin/python'},
#  'functions': {'apply_edit_set': {'verbosity': 'totals', 'auto_apply': True},
#                'move_symbol':    {'verbosity': 'totals', 'auto_apply': True},
#                'rename_file':    {'verbosity': 'totals', 'auto_apply': True},
#                'rename_symbol':  {'verbosity': 'files',  'auto_apply': False}}}

read_shared_setting(name="scope", start_directory="/path/to/shapes")   # the stored scope: a tuple of absolute paths
remove_shared_setting(name="scope", location="project", start_directory="/path/to/shapes")
# back to the fallback; returns the path of the settings file
```

`read_all_settings` shows the effective value of each setting, fallbacks
applied, not whether it is stored. `read_function_setting` reads one function's
setting. A write checks the shape of a value, not that it works: a `scope`
naming a folder that does not exist is stored, and then the position-based
operations raise `ScopeError`. A write for an operation that has no settings
(`find_references`, say) raises `KeyError`; an invalid value (`verbosity="loud"`)
raises `ValueError` naming what is allowed. Always pass `start_directory`: it
decides which project file a `project` write goes to. Do not edit the settings
files by hand.

## Which files a scan reads

A scope-wide scan should see the project's own source, not everything that
happens to sit under its folder, or it would resolve against (and rename inside)
installed copies of the same code. So a folder in `scope` is walked skipping
`.git`, `__pycache__` and `node_modules`, any virtual environment (a folder
holding `pyvenv.cfg`, whatever it is called), any conda environment (holding
`conda-meta`), any folder of installed packages (holding a `*.dist-info`
folder), and every path git ignores. A file named in `scope` directly is read
even if git ignores it. Inside a git work tree the ignore rules come from git
itself, so git must be runnable there: if it is not, the scan raises rather
than silently reading ignored files.

## Catalog and hygiene checks

Separate from navigation and rename, for exploring a codebase and judging its
conventions. Both are Python-only library calls (no `ravenclaw run` command).

`Catalog` parses a codebase once and exposes `pandas.DataFrame` summaries:
`.classes`, `.functions` (columns such as `name`, `file`, `start_line`,
`end_line`, `kind`, `num_parameters`, `parameter_names`, `return_type`,
`has_docstring`, `is_recursive`), `.variables`, `.files` (per-file counts),
`.symbol_dependencies` (`source_name`, `source_file`, `source_line`,
`target_name`, `target_file`, `target_line`, `target_kind`) and
`.file_dependencies`. Missing values are `None`, never `NaN`. `.symbol_graph`
and `.file_graph` each have `__graph__()`, returning a plain
`{"nodes": {...}, "edges": {...}}` dict for any graph-drawing tool (rendering
is out of scope).

```python
import sys
from ravenclaw.python.catalog import Catalog

catalog = Catalog(scope=("/path/to/shapes/src",), python_executable=sys.executable)
catalog.functions[["name", "start_line", "end_line", "num_parameters"]]
#            name  start_line  end_line  num_parameters
# 0  compute_area           1         2               2
# 1      describe           5         6               2
# 2        report           4         6               0
len(catalog.symbol_dependencies)             # 3
list(catalog.file_graph.__graph__()["nodes"])  # ['shapes', 'shapes.area', 'shapes.report']
```

`find_docstring_issues(scope=...)` scans for docstrings that are missing,
whose `Args:` section does not match the real parameters, or that are not
Google style; `check_docstring` checks one definition. The checks are
deliberately strict, with no built-in exceptions.

```python
from ravenclaw.python.hygiene import find_docstring_issues

for issue in find_docstring_issues(scope=("/path/to/shapes/src",)):
    print(issue.name, issue.kind, issue.position.line)
# DocstringIssue(position=Position(file='<project>/src/shapes/area.py', line=1, column=0),
#                name='compute_area', kind=<DocstringIssueKind.MISSING: 'missing'>,
#                missing_parameter_names=(), stale_parameter_names=())
```

## Languages, and TypeScript today

**Python files only.** What happens with anything else:

| You do | What happens |
| --- | --- |
| Give a `.ts .tsx .mts .cts .js .jsx .mjs .cjs` file to a position-based operation (`find_definition`, `find_references`, `rename_symbol`, `rename_file`, `move_symbol`) | `UnsupportedLanguageError`: `TypeScript and JavaScript files are not supported yet: <file>. ravenclaw resolves Python only.` Nothing is written. |
| Run a scope-wide scan (`find_unresolved_references`) over a folder holding such files | The files are skipped, silently. |
| `ravenclaw run ...` on such a file | An error document on standard error, `{"error": "UnsupportedLanguageError", "message": ..., "version": ...}`, and exit 1. |

So a TypeScript project is not refactored by ravenclaw yet: use the editor's
or `tsc`'s rename there, and do not fall back to grep-and-replace for a
Python name that also appears in a TypeScript file (the scan will not look at
it).

**TypeScript and JavaScript are in progress on `main`**, not usable. The
package `ravenclaw.typescript` already holds the grammar and parsing, the scope
tree (bindings and scopes), module records (imports and exports) and module
resolution (specifier to file, from `tsconfig.json`, `jsconfig.json` and
`package.json`). None of it is wired into the operations, so the behaviour in
the table above holds. The first release that includes TypeScript support is
planned for the next minor release. One resolver will serve all eight file kinds. The plan and
its milestones are in
[`playbook/plans/typescript_javascript/`](https://github.com/idin/ravenclaw/tree/main/playbook/plans/typescript_javascript/overview.md);
the order of future languages is
[issue #43](https://github.com/idin/ravenclaw/issues/43).

A language's parsing and scope resolution lives in its own subpackage behind
the shared `Resolver` protocol, since each language needs its own grammar and
scoping rules.

## Known limitations

Honest and current as of this README; each item is an open issue or a stated
design limit.

**Python resolution**

- PEP 695/696 type parameters (`T` in `def f[T]`, `class A[T]`) are never
  bindings: find_definition, find_references and rename_symbol raise
  `UnresolvedBindingError` on them
  ([#345](https://github.com/idin/ravenclaw/issues/345)).
- A PEP 695 `type X = ...` statement never binds the alias name `X`
  ([#353](https://github.com/idin/ravenclaw/issues/353)).
- Type parameter defaults (PEP 696) are not parsed by the pinned
  `tree-sitter-python` 0.25.0, so a comprehension in one binds the outer name
  ([#346](https://github.com/idin/ravenclaw/issues/346)).
- A lambda or comprehension in a decorator, type alias value, type parameter
  bound or class attribute annotation binds the outer name
  ([#344](https://github.com/idin/ravenclaw/issues/344)).
- Scope spans are tracked by line, so on one line a use after a comprehension
  or lambda that binds the same name resolves to the inner variable
  ([#155](https://github.com/idin/ravenclaw/issues/155)).
- `find_definition` on a method name defined in a class returns no resolved
  definition position in some cases
  ([#355](https://github.com/idin/ravenclaw/issues/355)).
- Undecided whether the module-path segments of an import statement belong in
  `find_references` of a module
  ([#172](https://github.com/idin/ravenclaw/issues/172)).
- Documented, deliberately deferred, in the module docstring of
  `ravenclaw.python.scope_tree`: a binding written after a star import under
  an `if` or `try`, or changed by `del` or `global`, is judged by where it is
  written; dynamic `getattr`/`setattr` attribute access; decorators that
  rewrite the decorated function's effective scope; PEP 420 namespace
  packages (a directory without `__init__.py` is not a package).

**Edits**

- `move_symbol` does not refuse a destination that already binds the
  dependency's alias to a different definition; it can silently rebind the
  name ([#178](https://github.com/idin/ravenclaw/issues/178)).
- `__all__` edits: a removal silently leaves the name listed when the
  statement is a bare tuple the grammar reports as an error
  ([#200](https://github.com/idin/ravenclaw/issues/200)); cosmetic leftovers
  remain after some edits ([#199](https://github.com/idin/ravenclaw/issues/199)).
  An `__all__` built by concatenation, comprehension or with an escape is not
  edited: search the old name as a string and change each export list by hand.
- Import root of a package with no `__init__.py` files is a guess about
  `sys.path` ([#112](https://github.com/idin/ravenclaw/issues/112)).

**TypeScript** (the in-progress work, not reachable through any operation yet)

- Two block scopes on one line are not told apart
  ([#289](https://github.com/idin/ravenclaw/issues/289)); a redeclared name
  reports the first declaration ([#247](https://github.com/idin/ravenclaw/issues/247));
  for-in/for-of heads, function names and parameters are skipped until their
  milestones ([#250](https://github.com/idin/ravenclaw/issues/250)); escaped
  string names are kept raw in some imports and exports
  ([#332](https://github.com/idin/ravenclaw/issues/332)); a raw CR or CRLF in a
  template literal name keeps the CR ([#354](https://github.com/idin/ravenclaw/issues/354));
  the parse time limit is not yet a setting
  ([#349](https://github.com/idin/ravenclaw/issues/349)); `.jsx`, `.mjs` and
  `.cjs` declaration mapping is unconfirmed
  ([#234](https://github.com/idin/ravenclaw/issues/234)).

**Environment**

- `tree-sitter` must stay pinned below 0.26.0: that release has a confirmed
  upstream use-after-free bug affecting node positions past line 256 of a
  file (`playbook/bugs/resolved/2026-08-21_tree_sitter_026_start_point_use_after_free.md`).
- There is no skill yet that installs ravenclaw itself; the prototype is not
  shipped (see [the skills](#the-seven-bundled-skills)).

The full list is `gh issue list --repo idin/ravenclaw --state open`.

## Errors you can see

Every error the library raises is a `RavenclawError` (importable from
`ravenclaw.core.errors`), except the plain `ValueError`, `KeyError`,
`TypeError`, `FileNotFoundError` and `FileExistsError` that argument checks
raise. No operation writes anything when it raises.

| Error | Meaning and what to do |
| --- | --- |
| `UnresolvedBindingError` | The identifier at that position has no binding in the scope: wrong position, a keyword, or the defining file is outside `scope`. Fix the position or widen the scope. |
| `ScopeError` | The `scope` (given or stored) is invalid, or the target file, destination or new path lies outside it. Pass a `scope` that includes it, or fix the stored one. |
| `UnsupportedLanguageError` (also a `ValueError`) | The file is TypeScript or JavaScript (or another language with no resolver). See [Languages](#languages-and-typescript-today). |
| `ParseTimeoutError` | A file's parse did not finish within the time limit, so the file is refused. |
| `StarImportOrderError` | A rename is refused because a use inside a function body may run before the `from x import *` that would carry the name. Move the star import above the function, or rename the earlier binding too. |
| `UnmovableDefinitionError` | `move_symbol` was asked to move something that is not a module-level function or class. |
| `DestinationDefinesNameError` | The destination already defines a module-level function or class of that name. |
| `DestinationBindsDependencyNameError` | The destination binds a name the moved definition's import would bind, to something else; writing the import would silently rebind it. |
| `DestinationIsSourceError` (also a `ValueError`) | The destination is the file the definition is moving out of. |
| `QualifiedUseNotRewritableError` | A dotted use of a moved module or symbol (`pkg.mod.name`) cannot follow the move; the message names the use and the advice. |
| `CircularImportError` | The operation would leave two files importing each other where that cannot work. |
| `UnsupportedDunderAllError` | A file's `__all__` is built in a form that moving a name cannot keep true; the message names the statement. |

Through `ravenclaw run`, any failure (these, a bad argument, malformed JSON, a
missing file, an unknown operation) prints one JSON document to standard error,
`{"error": "<class name>", "message": "...", "version": "<ravenclaw version>"}`,
and exits 1. Standard input that is empty or not a JSON object is a
`ValueError`.

## For contributors

Needs `uv`. The suite needs no `node`, `npm` or network once the dependencies
are installed (the TypeScript and JavaScript tests ship their fixtures):

```bash
uv sync
uv run pytest
```

Tests mirror the source under `tests/`, and everything they write goes under
`tests/_output/` (git-ignored). Plans, designs and the bug logs are in
[`playbook/`](https://github.com/idin/ravenclaw/tree/main/playbook):
`plans/` for what is being built and in what order, `bugs/` for the logs of
bugs found and fixed.

## Scope of the project

Not limited to the operations above: the goal is the full category of
code-development-lifecycle operations that have a deterministic correct
answer, so they do not need to rely on an agent's judgement or guesswork.
