Metadata-Version: 2.5
Name: agentcodemap
Version: 1.0.0
Summary: Tree-sitter code navigation and search harness for LLM agents: diff slicing, symbol lookup with impact chain, symbol grep, compact outline.
Requires-Python: >=3.11
Requires-Dist: tree-sitter-language-pack>=0.6
Requires-Dist: tree-sitter>=0.23
Description-Content-Type: text/markdown

# agentcodemap

Tree-sitter harness для навигации и поиска по коду, рассчитанный на LLM-агентов.
Возвращает компактные, машиночитаемые срезы кода вместо целых файлов.
Установка: `uv tool install agentcodemap`. Команда после установки — `codenav`.

Проверенное фактическое поведение и ограничения: [аудит CLI](docs/current-cli-audit.md).

## Команды

Общий приём: команды, которые сканируют каталог-корень (`symbol`, `impact`,
`trace`, `path`, `info`, `grep`, `astgrep`, `doctor`), принимают несколько корней одним флагом
`--root DIR...`. Индексируются только перечисленные каталоги — соседние
директории того же уровня с кодом не попадают в поиск. Например,
`--root project tests` ищет ровно в `project/` и `tests/`, игнорируя прочие
директории текущего уровня. По умолчанию корень — текущий каталог.

### `codenav outline PATH... [--top-level] [--deps] [--lines] [--filter REGEX...] [--max-chars N] [--pages SPEC]`

PATH — файл или каталог (каталог обходится рекурсивно, outline каждого модуля).
Компактная карта символов файла:

```
$ codenav outline project/mymodule.py
project.mymodule:
A MY_MODULE_ATTR

F my_func

C MyClass
 A my_attr
 M my_method
```

Модули без символов не выводятся. Дандеры не печатаются: магические методы
(`__init__`, `__repr__`, `__call__` и т.п.) и атрибуты-метаданные (`__all__`,
`__version__`) — служебная механика, а не карта кода (магические методы при
этом остаются в связях `impact`/`trace`).

Буквы: `C` класс, `M` метод, `F` функция, `A` атрибут/константа/тип.
`--lines` добавляет `L<start>-<end>` к каждой записи.

`--top-level` сокращает вывод до верхнеуровневых символов модуля: вложенные
члены (атрибуты, методы классов, внутренние классы/функции) не печатаются.
Например, тот же файл с этим флагом:

```
$ codenav outline project/mymodule.py --top-level
project.mymodule:
A MY_MODULE_ATTR

F my_func

C MyClass
```

Флаг сочетается с `--lines` и действует на каждый модуль при обходе каталога.

Флаг `--deps` печатает под каждым символом его зависимости — символы, на которые
ссылается тело этого символа, в виде `-> имя [виды]` (виды — короткие метки
типов связи: `call`, `inh`, `par`, `ref`, `ret`, `str`, см. «Типы связей»). Ссылки
разрешаются по индексу репозитория, поэтому связи видны и между файлами;
зависимость приписывается ближайшему символу, который её содержит: класс не
повторяет зависимости своих методов. Для PATH-файла индексируется содержащий
его каталог. По умолчанию под каждым символом печатается одна строка; флаг
`--deps` добавляет под ним зависимости:

```
$ codenav outline project/mymodule.py --deps
project.mymodule:
A MY_MODULE_ATTR

F my_func
 -> MY_MODULE_ATTR [ref]

C MyClass
 A my_attr
 M my_method
  -> MyClass.my_attr [ref]
```

Зависимости сочетаются с `--top-level` (тогда показаны зависимости только
верхнеуровневых символов) и `--lines`. Символы без зависимостей печатаются
как обычно, лишних строк нет.

Порядок и пагинация рассчитаны на большие проекты: модули сортируются от корня
вглубь (сначала файлы в корне PATH, затем на один уровень глубже и т.д.) и
нарезаются на страницы по `--max-chars` символов (по умолчанию 10000; строки
никогда не разрезаются, разрыв страницы проходит между модулями). По умолчанию
команда печатает первую страницу и сообщает, сколько страниц всего и как
запросить остаток:

```
(page 1 of 4; 3 more: --pages 2-4)
```

`--pages SPEC` печатает выбранные страницы за один вызов: отдельный номер,
диапазон или список (`--pages 3`, `--pages 2-4`, `--pages 1,3`; флаг можно
повторять — страницы объединяются и печатаются по возрастанию). Так после
первого запроса агент сразу берёт весь вывод:

```
$ codenav outline src --pages 2-4
```

Каждая страница заканчивается строкой `(page N of M; …)`: последняя страница —
`(page M of M)`, остальные — с подсказкой, сколько частей и каким SPEC осталось
запросить. Если один модуль больше размера страницы, он печатается на своей
странице до границы строки, а его путь честно помечается в конце (`not fully
shown: … (module(s) larger than page size …)`); чтобы прочитать такой модуль
целиком, поднимите `--max-chars`.

`--filter REGEX` оставляет только модули, путь которых совпадает с регуляркой
(проверка по пути файла, как в grep). Флаг можно повторять и перечислять
несколько значений за раз: `--filter schemas models` — модуль остаётся, если
совпал хотя бы один паттерн (OR). Например: `--filter 'schemas|services'`.
Пагинация применяется уже к отфильтрованному набору.

Если после фильтрации ничего не нашлось или каталог пуст, команда завершается
успешно с сообщением `(no modules found)` / `(no modules match: …)`, а не
пустым выводом и не ошибкой.

### `codenav diff [PATH] [--lines SPEC] [--lang LANG] [--repo DIR]`

Нарезка кода по диффу: изменённые строки разворачиваются до содержащих их символов,
соседние символы склеиваются (зазор ≤ 5 строк).

Источник изменений выбирается явно; флаги взаимоисключающие — два источника
в одном вызове отклоняются:

```
$ codenav diff --working-tree --repo ../code-master  # tracked-изменения относительно HEAD (staged + unstaged), исходники — с диска
$ codenav diff --staged --repo ../code-master        # изменения индекса относительно HEAD, исходники — из индекса
$ codenav diff --base main --repo ../code-master     # main...HEAD (от merge base), исходники — из ревизии HEAD
$ git diff HEAD | codenav diff --stdin --repo .      # unified diff из stdin, даже на терминале
$ codenav diff src                                   # без флагов (прежнее поведение): терминал → git diff HEAD, pipe → stdin
$ codenav diff src/codenav/cli.py                    # PATH ограничивает diff одним файлом/каталогом
$ codenav diff src --lines '10,15-20'                # изменённые строки задаются явно (нужен PATH)
```

При явно выбранном режиме результат одинаков с терминала и без терминала.
`--repo DIR` — каталог, в котором выполняются git-команды, поэтому результат
не зависит от текущего каталога; пути из git-диффа разрешаются от корня
репозитория, а для `--stdin`/`--lines` — от `--repo` (по умолчанию от текущего
каталога). untracked-файлы git не диффит — в нарезку они не попадут. Без PATH
нарезаются все изменённые файлы (блоки разделяются `---`, не-код файлы
пропускаются), с PATH — только указанный файл. Полностью удалённые и новые
модули не нарезаются — короткая пометка `MODULE DELETED` / `NEW MODULE`; при
отсутствии изменений — `(no changes)`.

### `codenav symbol NAME... [--root DIR...]`

Исходник каждого символа по имени (простому или квалифицированному, например
`MyClass.my_method`). Можно перечислить несколько имён в одном вызове — индекс
строится один раз. Поиск идёт только по перечисленным `--root` (по умолчанию
текущий каталог).

При нескольких совпадениях текущая реализация печатает первое в порядке обхода;
для точного выбора используйте квалифицированное имя, когда оно однозначно.
Если хотя бы одно имя не найдено, команда не печатает ничего и завершается
с ошибкой, перечисляя отсутствующие имена.

Поиск ссылок именной; строки-литералы тоже сканируются (DI-регистрации вида
`"pkg.mod:Symbol"` — тип `str`; кавычечные forward-аннотации — типы `par`/`ret`
по позиции), docstring исключены.

### Типы связей

Отношения в `impact`, `trace`, `path`, `info` и `outline` помечаются
типом ссылки, которая их создала — по тому, как имя употреблено в исходнике.
Печатается короткая метка (полные имена съедали бы контекст), полное имя
принимает `--kind`:

| Метка | Тип | Место ссылки |
|---|---|---|
| `call` | `call` | имя вызывается: `helper()`, `obj.method()` |
| `inh` | `inheritance` | имя в списке базовых классов: `class A(B)` |
| `par` | `param` | имя в аннотации типа вне позиции возврата: параметр `f(x: Service)`, поле `dep: Service`, локальная переменная `x: Service`, в том числе кавычечная forward-ссылка `body: "Service"` |
| `ref` | `reference` | прочие упоминания имени (чтение, значение) |
| `ret` | `return` | имя в позиции возврата: аннотация `-> Service` или выражение `return x` / `return build()` — сторона производителя данных |
| `str` | `string` | слово из DI-строки (`"pkg.mod:Symbol"` в `LazyService(...)`); текстовый кандидат, синтаксисом не подтверждён |

`ret` и `par` разделяют стороны потока данных по объекту: у
`trace Data --kind ret` остаются только функции, которые его возвращают, у
`--kind par` — только принимающие его на вход.

Если имя встречается в разных местах, связь получает сразу несколько типов
(`[inh,par]`), они выводятся в алфавитном порядке.

Флаг `--kind` (`impact`, `trace`, `path`, `info`) оставляет только связи
выбранных типов; несколько типов указываются сразу (`--kind call param`)
или повтором флага (`--kind call --kind str`). Принимается и короткая метка
(`--kind par`). В `impact` фильтр отсекает связи и оставляет в метках только
выбранные типы; в `trace`/`path`/`info` он действует и на обход, поэтому
цепочка не проходит через ребро отфильтрованного типа.

```
$ codenav trace Base --root project --kind call
Base:
make -[call]-> Base
```

### `codenav impact NAME... [--root DIR...] [--detailed] [--kind KIND...]`

Цепочка влияния каждого символа (несколько имён за один обход индекса):

В обычном выводе пути не печатаются: выводятся отсортированные и уникальные ID
объектов, к каждому — типы связей в квадратных скобках. `--detailed` добавляет
путь, строки, тип сущности и места ссылок в виде `метка@строка`.

* **depends-on** — пользовательские символы, на которые ссылается цель
  (включая всё её поддерево: методы и атрибуты класса);
* **dependents** — символы, чьи тела ссылаются на цель.

Номера строк в `--detailed` принадлежат ссылающейся стороне: для `depends-on`
это файл цели (он указан в заголовке), для `dependents` — сам перечисленный
символ.

Связи учитывают кавычечные forward-аннотации (`job_store: "JobStore"` — тип
`par`) и DI-строки в атрибутах класса (`LazyService("pkg.mod:Symbol")` — `str`).
Docstring и произвольные строки в телах функций связей не дают.

```
$ codenav impact CodeReviewService --root project
impact chain for CodeReviewService:
- depends-on:
Constants [ref]
...
- dependents:
Services [str]

$ codenav impact my_func --root src --detailed
impact chain for my_func (src/mod.py:20-22):
- depends-on:
src/mod.py:10-12::mid function  [call@21]
- dependents:
src/cmd.py:30-33::cmd_outline function  [call@31]
```

### `codenav trace NAME... [--root DIR...] [--direction DIRECTION] [--depth N] [--max-paths K] [--kind KIND...]`

Цепочки влияния через каждый символ (несколько имён за один обход индекса);
ребро `A -[call]-> B` означает «A ссылается на B», а метка ребра — тип ссылки
(см. «Типы связей»). Рёбра разрешаются тем же qualified/type-aware анализом,
что и в `impact`.

`--direction` выбирает, какие стороны обходить (по умолчанию `both`):

* `both` — цепочки через символ в обе стороны: кто на него ссылается и что он
  тянет за собой; бюджет `--depth` общий на обе стороны;
* `down` — только вглубь того, что символ тянет за собой; цель первой в
  цепочке, поэтому весь бюджет `--depth` уходит в одну сторону и глубина не
  съедается «шумом» от потребителей слева;
* `up` — только потребители: цепочки печатаются в порядке стрелки
  (`referrer -[call]-> NAME`).

`--depth` ограничивает глубину каждой отдельной цепочки —
сколько символов цепочки будет выведено; глубина считается в символах:
`--depth 3` печатает цепочки не длиннее трёх символов, например
`side -[call]-> my_func` (по умолчанию 3). `--depth` не ограничивает количество
найденных цепочек — для этого есть отдельный страховочный потолок
`--max-paths`. Цепочка, целиком содержащаяся в более длинной, убирается как
дубликат. Одноимённые функции в разных файлах остаются разными узлами графа.
Для списка прямых зависимостей — `impact`.

```
$ codenav trace my_func --root src
my_func:
side -[call]-> my_func -[call]-> mid -[call]-> base
top -[call]-> my_func -[call]-> mid -[call]-> base

$ codenav trace _collect_code_files --root src/codenav --direction down
_collect_code_files:
_collect_code_files -[ref]-> SKIP_DIRS
_collect_code_files -[call]-> detect_language
```

Если цепочек нет, команда печатает `no influence data (0 paths)` (при
`--direction down` — `no dependency chains (0 paths)`; усечение печатается как
`not shown: N paths (max_paths=…)`).

### `codenav path SOURCE TARGET [--root DIR...] [--depth N] [--max-paths K] [--kind KIND...]`

Ответ на вопрос «как SOURCE связан с TARGET»: все самые короткие цепочки от
SOURCE до TARGET вдоль зависимостей (ребро `A -[call]-> B` означает «A
ссылается на B», тип ребра — тип ссылки). Показываются только цепочки
минимальной длины — без посторонних ветвей, которыми перегружен `trace`.
Направление одно — от SOURCE к TARGET; обратный вопрос задаётся тем же
способом: `codenav path TARGET SOURCE`. `--depth` считает символы цепочки,
оба конца включены, и не длиннее `--depth` (по умолчанию 10): маршрут вне
глубины — честно пустой ответ. `--kind` фильтрует ребра, как в `trace`.

```
$ codenav path _print_graph impact_entity --root src/codenav
_print_graph -> impact_entity:
_print_graph -[par]-> RepoIndex -[call,ret]-> impact_entity
```

Если пути нет (или он длиннее `--depth`), команда печатает
`SOURCE -> TARGET: no chains (0 paths)` и завершается с кодом 0; усечение по
`--max-paths` — как в `trace`: `not shown: N paths (max_paths=…)`. Имя,
которое нигде не найдено, — ошибка, как у `symbol`/`trace`.

### `codenav info NAME... [--root DIR...] [--depth N] [--max-paths K] [--kind KIND...]`

Один обход индекса вместо трёх: аккумулированный ответ из `symbol` (полный
исходник), `trace` (цепочки влияния) и `impact` (depends-on/dependents) для
каждого имени. Секции идут в порядке symbol → trace → impact и повторяют
формат соответствующих команд. Для части цепочек действуют собственные
дефолты: `--depth 20` и `--max-paths 50` (флаги можно переопределить;
`--depth` — та же семантика, что у `trace`). `--kind` фильтрует обе
части: и цепочки, и список прямых зависимостей.

Каждая секция честно сообщает о невыведенной информации: отсутствующие
зависимости помечаются `(none found)`, отсутствие путей — `no influence data
(0 paths)`, усечение цепочек — `not shown: N paths (max_paths=…)`. Если имя нигде
не найдено, команда, как и `symbol`/`impact`/`trace`, завершается с ошибкой,
перечисляющей отсутствующие имена.

```
$ codenav info my_func --root src
### my_func  (src/mod.py:20-22, function)
20  def my_func():
21      return mid()

my_func:
side -[call]-> my_func -[call]-> mid -[call]-> base
top -[call]-> my_func -[call]-> mid -[call]-> base

impact chain for my_func:
- depends-on:
mid [call]
- dependents:
cmd_outline [call]
```

### `codenav grep PATTERN... [--root DIR...] [--lang LANG] [--max-chars N] [--pages SPEC]`

Короткая версия поиска по регулярке. Можно передать несколько выражений: символ,
в теле которого совпало хотя бы одно из них, печатается один раз. Область поиска
задаётся `--root` (по умолчанию текущий каталог).

Печатаются только совпавшие строки, зато заголовок блока несёт границы символа —
`путь:начало-конец::имя вид` (тот же формат локации, что в `impact --detailed`),
так что видно, в каком символе найдено и где он начинается и кончается. Обычный
grep границ символа не знает.

```
$ codenav grep 'entity.kind' --root src
src/codenav/cli.py:438-474::_print_grep function
463	                f"{entity.qualified_name} {entity.kind}"
---
src/codenav/cli.py:351-408::_print_impact function
381	                    f"{entity.qualified_name} {entity.kind}"
```

### `codenav astgrep PATTERN... [--root DIR...] [--lang LANG] [--max-chars N] [--pages SPEC]`

Расширенная версия того же поиска: вместо отдельных строк печатается полный
исходник каждого найденного символа по его границам. Заголовок здесь — просто
путь: границы видны по номерам строк самого исходника.

```
$ codenav astgrep 'def _grep_blocks' --root src
src/codenav/cli.py
411	def _grep_blocks(files: list[str], patterns: Sequence[str], lang: str | None):
412	    """Regex hits grouped by smallest enclosing symbol, in file order.
...
433	        for key in order:
434	            entity, matched_lines = blocks[key]
435	            yield file, entity, matched_lines, parsed.content_lines
```

Обе команды группируют хиты по наименьшей объемлющей сущности (метод, а не весь
класс) и разделяют блоки `---`; строки вне символов (импорты, константы модуля)
в обеих версиях печатаются как есть — у них нет символа, чьи границы можно
назвать.

Пагинация та же, что у `outline`: вывод режется на страницы по `--max-chars`
(по умолчанию 10000 символов; границы блоков не режутся), печатается первая
страница и подсказка `(page 1 of N; …: --pages 2-N)`. `--pages SPEC` выбирает
страницы (`2`, `2-4`, `1,3`; флаг повторяется). Блок крупнее страницы получает
свою страницу целиком — совпавшие строки не обрезаются.

Если ничего не нашлось, команда завершается успешно с сообщением
`(no matches for: …)` (или `(no code files found)`, когда в корнях нет
исходников), а не пустым выводом: агент отличает отработавший поиск без
совпадений от сбоя.

### `codenav doctor [--root DIR...] [--verbose]`

Диагностика индексации: что обход корней прочитал и почему остальное осталось
за бортом. Отвечает на то, что «ничего не найдено» не различает: символа нет в
исходниках — или файл вообще не индексировался (незнакомое расширение, лимит
размера, файл не читается как UTF-8, разбор не удался, каталог вырезан обходом,
корень не существует).

```
$ codenav doctor --root project tests
roots:
  project -> /work/project: 37 files indexed
  tests -> /work/tests: 12 files indexed
  vendor: does not exist
files: 49 indexed of 53 code files; 15 non-code files skipped
languages: python 40 files/780 symbols, javascript 9 files/14 symbols
skipped: too large 2, unreadable 1, parse failed 1, ignored dirs 4
extraction: 794 symbols; 3 <unknown> names in 1 files; syntax errors in 2 files; 5 files parsed without symbols
(paths behind these counts: --verbose)
```

Обход здесь ровно тот же, что у остальных команд (`RepoIndex.scan_tree`),
поэтому отчёт описывает именно тот набор файлов, который они ищут, а не
похожий: те же вырезанные каталоги (`.git`, `node_modules`, `.venv`, … и любые
скрытые), тот же лимит размера файла (512 KiB), та же проверка языка по
расширению, тот же разбор.

* `roots` — каждый `--root`, как он задан: куда ведёт (realpath) и сколько файлов
  дал. Корень, которого нет (`does not exist`), файл вместо каталога (`not a
  directory`) и корень, уже покрытый другим (`redundant (covered by another
  root)`), названы явно; первые два к тому же завершают команду кодом 1 — опечатка
  в корне не должна выглядеть как успешно проиндексированный пустой проект.
* `files` — сколько файлов попало в индекс из всех файлов с поддержанным
  расширением; файлы без поддержанного расширения считаются отдельно.
* `languages` — файлы и извлечённые символы по языкам.
* `skipped` — почему файл с поддержанным расширением не попал в индекс:
  `too large`, `unreadable`, `parse failed`, а также вырезанные каталоги
  (`ignored dirs` — один такой каталог скрывает целое поддерево).
* `extraction` — измеренные признаки неполноты извлечения: имена `<unknown>`
  (объявление найдено, имя — нет), файлы, где tree-sitter оставил
  ERROR/MISSING-узлы, и модули, разобранные без единого символа. Успешный разбор
  здесь не подаётся как доказательство полноты — команда печатает только то, что
  измерила.

Сбой одного файла (не читается, не разбирается) не скрывает остальные: файл
попадает в счётчик с причиной, а обход идёт дальше.

Обычный режим — сводка; `--verbose` допечатывает пути за каждым счётчиком
(блок `details:`).

Команда описывает текущие возможности экстракторов, а не улучшает их: ноль
символов у языка — сообщение о том, что искать в нём пока нечего.

## Языки

Python, JavaScript/TypeScript/TSX, Go, Rust, Java, Scala, Ruby, PHP, C#, C/C++.
Язык определяется по расширению; переопределяется флагом `--lang`.

Грамматики всех перечисленных языков загружаются, но полнота извлечения символов
пока различается. Например, smoke-тест выявил `<unknown>` для части объявлений
Go/C/C++ и пропущенный метод PHP; подробности есть в аудите CLI. Найти такие дыры
на своём проекте помогает `codenav doctor` (см. выше): он считает `<unknown>`,
модули с ERROR-узлами tree-sitter и модули, разобранные без единого символа.

## Скилл и сабагент

- `.agents/skills/codenav-research/SKILL.md` — скилл для агента: какую команду
  брать под какой вопрос, какие флаги режут вывод, какой бюджет держать на
  один результат.
- `.omp/agents/codenav-researcher.md` — read-only сабагент (omp): ищет через
  `codenav` и возвращает отчёт с `symbol` — `path:line` — тип связи.
- Замер A/B (codenav против `read`/`grep`/`glob` на одних и тех же задачах):
  `benchmarks/codebase-research/ab_tokens.py`.

Установка в пользовательский конфиг — симлинками, чтобы копии не расходились:

```bash
ln -sfn "$PWD/.agents/skills/codenav-research" ~/.agents/skills/codenav-research
ln -sfn "$PWD/.omp/agents/codenav-researcher.md" ~/.omp/agent/agents/codenav-researcher.md
```

Бюджет в скилле выведен из замеров на репозитории ~220 модулей:
`grep` ≈ 200 символов, `impact --detailed --kind call` ≈ 2.4k,
`symbol Class.method` ≈ 4.7k, `symbol Class` ≈ 24k, `info` ≈ 34k. `astgrep`
отдаёт полный исходник каждого совпавшего символа, поэтому его счёт — как у
`symbol`, умноженный на число попавших символов; для шага обнаружения он дорог.

## Разработка

```bash
uv sync --group dev
uv run pytest
```
