# ty-find

> LSP adapter for AI coding agents — symbol name in, structured code intelligence out.

ty-find (`tyf`) lets AI coding agents query Python's type system by symbol name.
It wraps ty's LSP server so that `tyf show MyClass` returns the definition
location, type signature, and all references — without requiring file paths or
line numbers. A background daemon keeps responses under 100ms.

Also useful for humans who want fast, precise Python navigation from the terminal.

## Relationship to ty

tyf is an adapter over ty's LSP server; it does no Python analysis of its own.

- **tyf does not ship ty.** ty is a required peer dependency you install yourself
  (`uv add --dev ty`). tyf does not bundle or pin a copy — at runtime it binds to
  whatever ty is on PATH, falling back to `uvx ty`. The analysis you get depends on
  the installed ty version, not the tyf version.
- **ty is pre-release (`0.0.x`)** with frequent breaking changes, including to
  LSP/diagnostic behavior. A ty upgrade can change tyf output with no change to tyf.
  Keep ty current within the supported range; if results shift after a ty upgrade,
  check `ty --version` (or `uvx ty --version`) against the supported list.
- **Navigation, not diagnostics.** tyf surfaces ty's navigation/symbol knowledge —
  definitions, signatures, references, members — not ty's type-checking diagnostics.
  tyf never reports type errors, so the false-positive noise that affects `ty check`
  on dynamic frameworks (Pydantic, SQLAlchemy, etc.) does not appear in tyf output.

## Commands

### show — Definition, type signature, and usages of a symbol by name

The most useful command. Given a symbol name, it shows:
- Where it's defined (file + line)
- Its type signature
- Optionally, docstring (`--doc`) and every place it's used (`--references`)
- Or everything at once with `--all` (doc + refs + test refs)

No file path needed — it searches the whole project by name.
`inspect` still works as a hidden alias for backward compatibility.

When ty cannot resolve a type (e.g. a third-party library's stubs are not
installed), the `Signature` shows the literal source annotation the developer
wrote instead of `Unknown`; a symbol with no annotation is marked
`(unannotated)`.

```
tyf show MyClass
tyf show MyClass.get_data                # narrow to a specific class method
tyf show calculate_sum UserService       # multiple symbols at once
tyf show MyClass --references            # also list all usages
tyf show MyClass --doc                   # include docstring
tyf show MyClass --all                   # show everything: doc + refs + test refs
tyf show MyClass --file src/models.py    # narrow to one file
tyf --format json show MyClass           # JSON output for scripting
```

### find — Find where a symbol is defined by name

Given a symbol name, returns the file and line where it's defined.
Searches the whole project — no need to know which file it's in.
Use `--fuzzy` for partial/prefix matching with richer output (kind + container).

```
tyf find calculate_sum
tyf find Calculator.add                     # find a specific class method
tyf find calculate_sum multiply divide      # multiple symbols at once
tyf find handler --file src/routes.py       # narrow to one file
tyf find handle_ --fuzzy                    # fuzzy/prefix match
```

### refs — All usages of a symbol across the codebase

Find every location in the codebase that references a symbol. Supports both
name-based and position-based lookup. Essential before renaming or removing
code — tells you exactly what will break.

```
tyf refs my_func my_class                   # by name
tyf refs Calculator.add                     # refs for a specific method
tyf refs -f myfile.py -l 10 -c 5           # by position
tyf refs file.py:10:5 my_func              # mixed
... | tyf refs --stdin                      # piped input
```

### members — Public interface of a class

Shows the public interface of a class: methods with signatures, properties,
and class variables with types. Like `list` scoped to a class, with type info.
Excludes private/dunder members by default; `--all` includes everything.
Only shows directly-defined members, not inherited ones.

When ty cannot resolve a member's type (e.g. missing third-party stubs), the
literal source annotation is shown instead of `Unknown` (read from source, so
it works without the project's dependencies installed); a member with no
annotation is marked `(unannotated)`.

```
tyf members MyClass
tyf members MyClass UserService          # multiple classes at once
tyf members MyClass --all                # include __init__, __repr__, etc
tyf members MyClass --file src/models.py # narrow to one file
```

### calls — Call tree of a symbol: what it calls, or what calls it

Walks the call graph recursively and renders it as a tree. Outgoing (the
default) answers "what does this do, transitively" in one command instead of a
chain of file reads; incoming (`--in`) answers "who calls this" — impact
analysis before an edit, giving the *calling function's identity* rather than
the raw text ranges `refs` returns.

Depth defaults to 2 and is capped at 5 (larger values are clamped, not
rejected). A callee reached from two parents is expanded once and marked `↑`
thereafter; recursion is marked `(cycle)`; code outside the workspace is a
single `(external)` line and is never expanded, under any flag. `--external`
adds those locations without expanding them.

Calls through callbacks and dynamic dispatch are invisible to static analysis
and do not appear. Requires ty 0.0.41 or newer (the release that added LSP call
hierarchy); on an older ty the command exits 3 naming the installed version —
distinct from "not found" (exit 0) and a usage error (exit 2).

```
tyf calls process_order                  # what it calls, 2 levels deep
tyf calls process_order --depth 3        # deeper (max 5)
tyf calls check_inventory --in           # who calls it
tyf calls OrderPipeline.run              # a specific class method
tyf calls a b c                          # several symbols at once
tyf calls process_order --external       # locate stdlib callees too
```

### list — All functions, classes, and variables defined in a file

Shows all functions, classes, and variables defined in a file — like a table
of contents. Useful for getting an overview of a file you haven't seen before.

```
tyf list src/services/user.py
```

### daemon — Manage the background server

The daemon starts automatically on first use and shuts down after 5 minutes
of inactivity. You usually don't need these commands.

```
tyf daemon start     # start manually
tyf daemon status    # check if running, uptime, cache info
tyf daemon stop      # stop the daemon
```

## Output Formats

All commands support `--format` (placed before the subcommand):
- `human` — default, readable text with source context
- `json` — structured JSON, good for piping to jq or scripting
- `csv` — tabular, good for spreadsheets or further processing
- `paths` — just file paths, one per line (for xargs, editors, etc.)

```
tyf --format json show MyClass
tyf --format paths find calculate_sum | head -1 | xargs code
```

## Why tyf?

**vs grep/ripgrep:**
- grep matches text; tyf understands Python's type system
- grep returns hits in comments, strings, and docstrings; tyf returns only real symbol references

**vs raw LSP (in editors):**
- LSP requires file:line:col positions to answer queries
- An LLM doesn't know positions without searching first — it would need to grep, which is imprecise
- tyf accepts symbol names directly, resolves positions internally, and returns structured results

Use **grep** for: string literals, config values, TODOs, non-Python files.

## Performance Tips

Install [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) to speed up lookups for non-existent symbols. When a symbol isn't found on the first LSP attempt, tyf uses `rg` to quickly check if the symbol text exists in any `.py` file. If not, it skips the retry chain (~3 seconds) and returns immediately.

## Installation

Requires [ty](https://github.com/astral-sh/ty) (`uv add --dev ty`) — see Relationship to ty above.
Optional: [ripgrep](https://github.com/BurntSushi/ripgrep) for faster non-existent symbol lookups.

```
pip install ty-find
# or
uv add --dev ty-find
```

## Supported ty versions

Placeholder — pending the version-floor task. The concrete list of supported ty
versions is owned by a single source of truth (version-floor / CI task) that is not
in the repository yet, so no version numbers are listed here (and none are invented).
Policy: tyf is tested against specific pinned ty versions and supports a contiguous
range, from a documented floor up to the latest tested release. Until the source
lands, check your own version with `ty --version` (or `uvx ty --version`).

One capability boundary is established empirically: `tyf calls` needs LSP call
hierarchy, which ty first advertises in **0.0.41**. Every other command works
below that. Check your own version with `ty --version` (or `uvx ty --version`).

## Testing

tyf's integration tests drive a real `ty lsp` process (ty is never mocked): each test
spawns the actual tyf binary, which starts a real daemon and a real ty LSP server, and
asserts on structured output for fixtures at the repo root (`example.py`,
`test_project/`, `test_project2/`). Suites live in `tests/integration/`.

```
uv add --dev ty              # ty required for integration tests
cargo test --all-features    # unit + integration
cargo test --test test_basic # just the basic integration suite
```

CI runs `cargo test --all-features` with ty installed, plus a smoke-test workflow
(`benchmarks/smoke.sh`) against the release binary.

## Troubleshooting

- **Empty results for something that should exist** has two causes. (a) The symbol
  isn't statically visible (dynamic constructs: `getattr`, runtime-generated classes,
  metaprogramming). (b) ty couldn't resolve the environment (wrong interpreter, missing
  stubs, third-party package not installed). Diagnose with `ty check <file>`: unresolved
  imports mean an environment problem, not a tyf bug.
- **Which ty am I running, and is it supported?** Print `ty --version` (or
  `uvx ty --version`) and compare against the supported list.
- **Signatures show a source annotation or `(unannotated)`.** Expected, not a bug:
  when ty can't resolve a type (e.g. missing stubs), tyf falls back to the literal
  source annotation instead of `Unknown`; no annotation is shown as `(unannotated)`.
