LSP-бэкенд для graph

From docs/ru/lsp.md at cc732c3 View on GitHub

По умолчанию плагин 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.tomlrust-analyzer
compile_commands.jsonclangd
tsconfig.jsontypescript-language-server --stdio
pubspec.yamldart 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.*.