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

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

Типово плагін graph відповідає на symbol, callers, impact, outline і explore зі свого індексу tree-sitter-tags у SQLite (backend = "tags"). Налаштування backend = "lsp" спрямовує ті самі п'ять MCP-інструментів — ті самі назви, ті самі виклики — натомість через мовний сервер, з яким rtok спілкується через 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.*.