LSP-бэкенд для graph
По умолчанию плагин graph отвечает на symbol, callers, impact, outline и
explore из своего индекса tree-sitter-tags в SQLite (backend = "tags"). Настройка
backend = "lsp" направляет те же пять MCP-инструментов — те же имена, те же вызывающие —
через языковой сервер, с которым общение идёт по stdio. Индекс tags при этом
для такого вызова не используется. Каждый ответ LSP записывает одну строку Measurement с
plugin = "graph" и kind = "lsp.symbol" | "lsp.callers" | "lsp.impact" | "lsp.outline" | "lsp.explore", поэтому rtok stats учитывает его как любую другую экономию.
Каждый из пяти инструментов принимает необязательный project (id или каталог). Без него вызов
отвечает по проекту рабочего каталога и связанным с ним проектам; языковой сервер отвечает по
одному корню, поэтому при backend = "lsp" отвечает только первый проект этой области, и ответ
об этом сообщает (auto спрашивает каждый проект, см. ниже).
Сервер выбирается по корню рабочей области — rtok никогда не линкует и не вызывает через оболочку
ничего другого (D6); он запускает один из этих серверов из PATH:
| Маркер корня | Команда сервера |
|---|---|
Cargo.toml | rust-analyzer |
compile_commands.json | clangd |
tsconfig.json | typescript-language-server --stdio |
pubspec.yaml | dart language-server |
Пошаговое описание ниже охватывает Rust и Dart.
Rust: rust-analyzer
Установите один раз через rustup, затем проверьте, что бинарник отвечает:
rustup component add rust-analyzer
rust-analyzer --version
rustup кладёт rust-analyzer в PATH; кроме того, rtok находит его через
rustup which rust-analyzer, если rustup установлен.
Dart: Dart SDK
Установите SDK (https://dart.dev/get-dart), затем проверьте, что он отвечает:
dart --version
Сервер — это собственная подкоманда SDK language-server: rtok запускает
dart language-server, поэтому никакого дополнительного шага установки, кроме самого SDK, нет.
Как направить на него проект
Файл-маркер определяет корень рабочей области: Cargo.toml для крейта Rust,
pubspec.yaml для пакета Dart. Задайте бэкенд либо для проекта в
<git root>/.rtok.toml:
[plugins.graph]
backend = "lsp"
либо для отдельного вызова через окружение:
RTOK_PLUGINS_GRAPH_BACKEND=lsp rtok config get plugins.graph.backend
что печатает lsp (по умолчанию tags). С --sources строка выглядит как
plugins.graph.backend = lsp (env):
rtok config show --sources | grep plugins.graph.backend
Проверка работы
Вызовите MCP outline для файла-примера, затем callers для одного из его символов.
tests/graph_lsp_gate.rs — исполняемая версия этой проверки: он получает
outline файла lib/main.dart с двумя символами через dart language-server, проверяет,
что ссылка OnlyTyped находится через rust-analyzer (начиная с T52.5 она находится и через
tags; контрольный промах tags теперь — ссылка macro_callee в теле макроса),
и проверяет наличие одной строки измерения lsp*:
cargo test --test graph_lsp_gate
Все шесть тестов проходят, когда оба сервера есть в PATH; в противном случае тесты Dart и LSP
пропускаются.
Бэкенд auto
backend = "auto" (T329.9) включается по желанию; по умолчанию остаётся tags. Он выбирает режим для
каждого проекта и языка, а не один раз на процесс. Проект, у языка которого есть сервер (таблица выше),
спрашивается первым, и ответ получает заголовок (lsp). Язык без сервера даёт ответ tags с заголовком
(tags); сервер, которого нет, который не готов, отвечает дольше lsp_timeout_ms (по умолчанию 40000)
или умер посреди сессии, даёт ответ tags с заголовком (tags; lsp: <причина>), так что сбой — это
уведомление, а не ошибка. Каждый ответ tags записывает строку измерения tags.symbol | tags.callers | tags.impact | tags.outline | tags.explore рядом со строками lsp.*, поэтому rtok stats показывает, как
часто отвечал каждый режим.
В области из нескольких проектов, где сервер установлен для части из них, каждый проект отвечает сам, а
его часть помечена [имя] и снабжена заголовком с режимом: проект на Rust может прийти от rust-analyzer,
а проект на Go — из tags в одном ответе. Вызывающие из связанного проекта — это ответ самого проекта;
обход связей по tags работает только тогда, когда ни для одного проекта области сервер не установлен, и
тогда режим каждого проекта — одна строка [имя] (tags) в начале.
[plugins.graph.backend_by_language] закрепляет режим для одного языка, который определяется по файлу-
маркеру проекта (rust, c, typescript, dart, go, python, javascript, java, ruby, php,
elixir, swift): go = "tags" оставляет Go на индексе, пока остальные работают в auto.
Текстовый поиск
backend = "text" (T329.10), а также auto для проекта без языкового сервера и без файлов, которые
разбирает грамматика (например, проект на Java или Ruby), отвечают текстовым поиском. Он работает в
собственном процессе rtok с правилами обхода и игнорирования инструмента search (.gitignore,
include и exclude графа, plugins.read.search_max_bytes): rg, grep и ssh не запускаются, корни
ssh:// не поддерживаются. Каждый ответ получает заголовок (text), а ответ со списком совпадений
заканчивается строкой о том, что в нём могут быть комментарии, строки и одноимённые символы.
symbolпечатает строку каждого определения,path:line kind, найденную поfn,def,function,func,class,struct,enum,trait,interface,typeи ещё нескольким ключевым словам перед целым словом. Тело не печатается: текстовый поиск не знает, где определение заканчивается.callersпечатает файлы, упоминающие имя вне строки определения,path xN (Lline).impact— один уровень тех же файлов; цепочкиtoтребуют рёбер вызовов и отвечают «not available in text mode».outlineперечисляет ключевые слова определений одного файла;exploreсобирает определения и одноуровневый impact без путей вызовов.deadотвечает «not available in text mode» вместо догадки.
Текстовый ответ записывает строку измерения text.symbol | text.callers | text.impact | text.outline | text.explore (затраченное время, а не экономия). Проект, корень которого нельзя прочитать, исключается
из ответа строкой no graph backend available: <причина>.
Проверяется один раз
В режимах lsp и auto (T329.11) rtok проверяет каждый проект один раз: файл-маркер, бинарник сервера
в PATH и первый запрос, который запускает сервер. Результат — запись проекта, и следующие запросы
используют её, не проверяя заново. Отсутствующий сервер не ищут при каждом вызове, а сервер, который
сломался (умер, закрыл канал или не ответил вовремя), не перезапускают при каждом вызове: он один раз
исключается из ответов этого проекта с указанием причины, и проект отвечает из tags. Ответ сервера с
ошибкой на один запрос поломкой не считается. Одновременные первые запросы делят одну проверку.
Запись хранится в памяти и зеркалируется в хранилище, поэтому rtok graph projects --json (и веб-страница)
показывают её в поле backend: backend (lsp или tags), language, server (есть ли сервер для
языка), reason, config (значение backend, с которым сделана запись), checked_at и next_probe_at
(unix-секунды; не раньше этого момента проверка здоровья повторит неудавшуюся запись). Смена backend или
backend_by_language перепроверяет только проекты, у которых значение изменилось. Новый процесс пользуется
записью другого процесса до next_probe_at, затем проверяет сам, поэтому перезапуск rtok после установки
сервера подхватывает его. Запросы никогда не перепроверяют: это делает проверка здоровья ниже.
Проверка здоровья и оповещения
Каждый процесс, в котором работает graph (rtok mcp, rtok web), раз в [plugins.graph] health_check_interval_s (60 с; 0 выключает) легко проверяет проекты своей области. Только этот код
смотрит заново. В каждом раунде проверяется, что корень проекта существует, что ссылки манифестов проекта
указывают на существующие каталоги, и запись о возможностях. Сервер, установленный после создания записи,
подхватывается в этом же раунде. Сломавшийся сервер пробуется снова через 60 с, затем через 120 с, с
удвоением до 15 минут; повтор — та же проверка, что и первая (бинарник есть в PATH), поэтому испытанием
служит следующий запрос, и сервер, не прошедший его, снова исключается, а ожидание продолжает расти.
Повторяется только запись, которую держит сам процесс, так что страница rtok web не перебивает сессию
rtok mcp.
Проблема поднимает оповещение, когда её увидели две проверки подряд, а оповещение снимается после двух
хороших проверок, так что краткое отмонтирование не мигает ([plugins.graph] alerts = false выключает
оповещения):
| Оповещение | Когда поднимается |
|---|---|
missing | корень удалён или перемещён (его родитель существует) |
unreachable | корня или его родителя нет (отмонтированный диск), ошибка, или stat не отвечает 2 с |
backend down | языковой сервер работал, затем сломался, и проект отвечает из tags |
link broken | ссылка манифеста (путь Cargo, npm file:, go replace, путь Python) указывает на несуществующий каталог |
Оповещения одного вида, поднятые вместе, читаются одной строкой («b, c missing since 14:02 (2 projects)»), а проект, связанный только через другие, называет цепочку («a to b to c: c missing»). Сервер, который просто не установлен, оповещением не является. Проект, удалённый из реестра, не оповещает. Когда отсутствующий или недоступный проект возвращается и оповещение снимается, он индексируется заново.
Оповещения зеркалируются в хранилище, поэтому их видно в rtok doctor (раздел graph alerts и
graph_alerts в --json), в rtok graph projects --json (alerts у проекта) и строкой notice: в
начале ответа graph-инструмента, чья область содержит проект («notice: b missing since 14:02 (directory
does not exist); results exclude b»).
Оценка здоровья
У каждого проекта есть оценка от 0 до 100, построенная из того, что уже знают проверка здоровья и запись о возможностях (ради неё ничего не запускается). Три компоненты, каждая от 0 до 1, весят 40, 30 и 30:
| Компонента | Вес | Равна 1, когда | Ниже, когда |
|---|---|---|---|
| freshness (свежесть) | 40 | нет ожидающих файлов | есть ожидающие файлы: 0 при пятой части файлов или когда индексу больше 24 ч и после него были изменения |
| backend (бэкенд) | 30 | настроенный бэкенд работает (tags и text работают всегда) | при auto и lsp сервер не отвечает: 0,6 на запасном tree-sitter, 0,3 на текстовом, 0 если не отвечает ничто |
| links (связи) | 30 | каждая связь ведёт к существующему, доступному, проиндексированному проекту (без связей тоже 1) | цель связи отсутствует, недоступна или не проиндексирована, либо ссылка в манифесте сломана |
Язык без языкового сервера не штрафуется. К каждой причине, снизившей компоненту, прилагается исправление
(«rust-analyzer not on PATH, using tree-sitter»: «install the server; it is picked up within one
health-check interval, or restart»). Отсутствующий проект получает 0 («missing»); у проекта на первой
индексации оценки пока нет (level: "indexing").
В rtok graph projects есть столбцы health (оценка) и scope, а --json содержит health (score,
level: good от 80, warn 50-79, bad, indexing или missing, components, reasons с их fix) и
scope_health - наименьшую оценку в области проекта: решает самое слабое звено, а не среднее. Область
с оценкой ниже 80 добавляет в начало ответа graph-инструмента одну строку notice: graph health ...
(проект, уже названный строкой оповещения, пропускается; alerts = false отключает обе строки), а
rtok doctor перечисляет все проекты ниже 80 в разделе graph health (graph_health в --json).
Оценки пересчитываются не чаще раза в секунду на проект.
Без сервера
backend = "lsp" не падает, когда сервер не может ответить. Если бинарника нет,
выше запрашиваемого файла не найден файл-маркер, сервер не готов или умер, либо
он отвечает «ничего» на имя, которое знает индекс tags, инструмент отдаёт ответ
tags с заголовком (tags; lsp: <причина>) — например,
(tags; lsp: rust-analyzer not on PATH) или
(tags; lsp: no Cargo.toml / compile_commands.json / tsconfig.json / pubspec.yaml in <root>) —
так видно, какой бэкенд ответил и почему. Каждый такой откат записывает строку
измерения lsp_fallback (её before_bytes — потерянное время). Чтобы вернуться к
встроенному индексу, снова задайте backend = "tags" (значение по умолчанию); ответы tags
с выключенным флагом побайтно идентичны, согласно контрактному тесту graph_lsp_gate.
По умолчанию остаётся tags: смена значения по умолчанию требует нового гейта
с записанными строками задержки lsp.*.