ایجنت بعد از هر write_file یا patch دو کانال جدا برمی‌گرداند: lint برای خطای نحوی و lsp_diagnostics برای خطای نوع، نام تعریف‌نشده و import ناموجود. با یک پروژه‌ی کوچک نشان می‌دهم هر کانال دقیقا چه چیزی می‌گیرد، چرا بدون ریپوی git هیچ‌کدام روشن نمی‌شود و چرا خطای از پیش موجود دوباره گزارش نمی‌شود.

ایجنت خطای نوع را از کجا می‌بیند

هرمس چند سرور زبان واقعی را به‌صورت زیرپردازش پس‌زمینه بالا می‌آورد و خروجی آن‌ها را به بررسی بعد از نوشتن وصل می‌کند. برای پایتون pyright-langserver و برای TypeScript و JavaScript همان typescript-language-server به کار می‌رود. راهنمای رسمی حدود بیست سرور را نام می‌برد که همه از یک مسیر واحد تغذیه می‌شوند.

هر ویرایش سه قدم دارد. پیش از نوشتن، خطاهای فعلی فایل به‌عنوان مبنا عکس‌برداری می‌شود. فایل نوشته می‌شود. زبان‌سرور دوباره پرسیده می‌شود و آنچه در مبنا نبوده گزارش می‌شود. همین ترتیب توضیح می‌دهد چرا یک خطای کهنه پایین‌تر از ناحیه‌ی ویرایش، دوباره به شما نشان داده نمی‌شود.

روی این ماشین، پیکربندی پیش‌فرض به این شکل است و هیچ‌کدام از این کلیدها در config.yaml نوشته نشده‌اند. اول باید مطمئن شوید سرور زبان نصب است، بعد کلیدها را ببینید:

$ hermes lsp install pyright
pyright already installed

# مسیر واقعی باینری را نشان می‌دهد؛ همین را سرور زبان اجرا می‌کند
$ hermes lsp which pyright
/root/.hermes/lsp/bin/pyright-langserver

# مقادیر پیش‌فرض از کد منبع، نه از حافظه‌ی ابزار
enabled: true              # کلید اصلی؛ خاموش کردنش کل زیرسیستن را حذف می‌کند
wait_mode: document        # یا full برای تحلیل کامل پروژه
wait_timeout: 5.0          # سقف ثانیه برای هر انتظار
warmup_timeout: 0          # فرصت اضافه برای اولین اجرای سرور
broken_retry_seconds: 0    # صفر یعنی تا راه‌اندازی دوباره کنار گذاشته می‌شود
idle_timeout: 600          # سرور بی‌استفاده بعد از ده دقیقه بسته می‌شود
install_strategy: auto     # نصب خودکار سرورهایی که نسخه‌ی npm دارند

راه‌اندازی: اول ریپو، بعد سرور زبان

LSP روی تشخیص ریپوی گیت سوار است. اگر پوشه‌ی کاری و فایلی که ویرایش می‌شود هیچ‌کدام داخل یک ریپو نباشند، زیرسیستن خاموش می‌ماند و فقط بررسی نحوی انجام می‌شود. پس اول ریپو بسازید، بعد وضعیت را ببینید.

$ git init -q demo && cd demo
# وضعیت را پیش از اولین ویرایش ببینید تا بدانید کدام زبان‌ها امروز تشخیص می‌گیرند
$ hermes lsp status
LSP Service
===========
  enabled:         True
  wait_mode:       document
  wait_timeout:    5.0s
  install_strategy:auto
  active clients:  none

Backend warnings
================
  ! bash-language-server is installed but shellcheck is missing
    — diagnostics will be empty (apt: shellcheck, brew: shellcheck, scoop: shellcheck).

# بیست و هشت سرور ثبت شده‌اند؛ این پنج تا همین حالا روی این ماشین بالا می‌آیند
$ hermes lsp list | head -6
pyright                  [installed  ] .py,.pyi
typescript               [installed  ] .ts,.tsx,.js,.jsx,.mjs,.cjs,.mts,.cts
vue-language-server      [missing    ] .vue
svelte-language-server   [missing    ] .svelte
astro-language-server    [missing    ] .astro
gopls                    [missing    ] .go

خروجی status دو چیز را با هم می‌گوید: زیرسیستن روشن است، و هشدار مربوط به bash-language-server یعنی همان زبان پوسته تشخیص نمی‌گیرد چون shellcheck نصب نیست. شمارش همه‌ی سرورهای ثبت‌شده در همین اجرا ۲۸ مورد بود که ۳ تای آن نصب‌شده، ۲۰ تای دیگر فاقد باینری و ۵ تای دیگر دستی هستند.

دو کانال، دو سیگنال مستقل

حالا یک فایل پایتون بنویسید که هم خطای نوع دارد و هم نام تعریف‌نشده. هر دو خطا در یک پاسخ برمی‌گردند، ولی در دو فیلد جدا:

# یک تابع درست، و دو مصرف‌کننده‌ی خراب
def add(a: int, b: int) -> int:
    return a + b


total: str = add(2, 3)
missing = undefined_name_here(1)

نتیجه‌ی write_file روی همین فایل چنین بود. مسیر در خروجی برای کوتاه شدن عرض صفحه خلاصه شده است:

{
  "bytes_written": 106,
  "lint": {"status": "ok", "output": ""},
  "lsp_diagnostics": "LSP diagnostics introduced by this edit:
<diagnostics file=\".../demo/broken.py\">
ERROR [5:14] Type \"int\" is not assignable to declared type \"str\"
         [reportAssignmentType] (Pyright)
ERROR [6:11] \"undefined_name_here\" is not defined
         [reportUndefinedVariable] (Pyright)
</diagnostics>"
}

مهم‌ترین چیزی که این خروجی می‌گوید این است که lint روی وضعیت ok مانده است. فایل از نظر نحوی سالم است و هیچ پارسری آن را نمی‌گیرد. خطاهای نوع در فیلد دوم می‌آیند، چون pyright معنای برنامه را می‌فهمد و پارسر پایتون فقط ساختار را.

همین تفکیک در TypeScript هم برقرار است، با یک تفاوت که ارزش دانستن دارد. وقتی یک زبان‌سرور فعال باشد، لینتر پوسته‌ای کنار گذاشته می‌شود تا دو بار همان فایل را ندوباره بررسی کند:

// یک تابع که رشته برمی‌گردارد، اما نتیجه‌اش را عدد می‌شماریم
function label(u: User): string {
  return u.name;
}

const count: number = label({ id: 1, name: "ada" });
// نتیجه‌ی واقعی write_file:
//   "lint": {"status": "skipped",
//            "message": "LSP server handles .ts — shell linter skipped"}
//   ERROR [10:7] Type 'string' is not assignable to type 'number'. [2322] (typescript)

فقط خطای جدید گزارش می‌شود

مهم‌ترین رفتار این زیرسیستن، تفاضل خطا با مبنای پیش از ویرایش است. یک فایل بسازید که یک خطای از پیش موجود در انتهای آن دارد:

import pathlib

VALUE = 41


def bump(n: int) -> int:
    return n + 1


def label() -> str:
    return "v1"


result = bump(VALUE)
print(label(), result)
# خطای از پیش موجود، پایین‌تر از ناحیه‌ای که در گام بعد تغییر می‌کند
orphan = name_that_does_not_exist(2)
# نام بازگشتی را عوض می‌کنیم؛ خطای پایین فایل دست‌نخورده می‌ماند
$ patch demo/shift.py   # فقط همین یک واژه در تابع label تغییر می‌کند
--- a/demo/shift.py
+++ b/demo/shift.py
@@ -8,7 +8,7 @@
 def label() -> str:
-    return "v1"
+    return "v2"
# پاسخ ابزار: هیچ فیلد lsp_diagnostics برنگشت

خطای name_that_does_not_exist هنوز در فایل هست و در خط هفدهم قرار دارد، اما در پاسخ نیامد. چون در مبنای پیش از ویرایش هم بود و فقط شماره‌خطش جابه‌جا شده بود. همین رفتار است که جلوی نویز را می‌گیرد: بدون آن، هر ویرایش کوچک فهرست بلندی از خطاهای قدیمی را دوباره نشان می‌داد و ایجنت هر بار در دام آن‌ها می‌افتاد.

سه جایی که خطا را نمی‌بینید

نخست، بیرون از ریپوی گیت هیچ فیلد lsp_diagnostics برنمی‌گردد. همان فایل خراب را در پوشه‌ای بدون .git نوشتیم و پاسخ فقط سه فیلد داشت: bytes_written، lint و مسیر فایل. این آزمون منفی نشان می‌دهد قفل گیت یک توضیح تزئینی در مستندات نیست.

دوم، خطای نحوی مسیر را می‌بندد. با یک پرانتز باز و بسته‌نشده، لایه‌ی نحوی خطا گرفت و فیلد دوم اصلا ساخته نشد. ترتیب لایه‌ها عمدی است: تا وقتی فایل قابل پارس نیست، تحلیل معنایی نتیجه‌ی قابل اعتماد نمی‌دهد.

کلیدپیش‌فرضکاری که می‌کند
enabledtrueکلید اصلی؛ با خاموش کردن، هیچ سروری بالا نمی‌آید
wait_timeout5.0سقف ثانیه برای هر انتظار؛ بیشتر برای tsserver و rust-analyzer
broken_retry_seconds0صفر یعنی جفت سرور و پوشه تا hermes lsp restart کنار گذاشته می‌شود
idle_timeout600سرور بی‌استفاده بعد از ده دقیقه بسته و در ویرایش بعدی دوباره بالا می‌آید
install_strategyautoنصب خودکار سرورهایی که نسخه‌ی npm دارند

سوم، نسخه‌ها با هم فرق دارند. مستندات زنده‌ی سایت بخشی با عنوان اعتماد به فضای کاری دارد که می‌گوید هر فضای کاری تا وقتی در فهرست trusted_workspaces نیاید، بی‌اعتماد است و بیشتر سرورها از جمله gopls و rust-analyzer اصلا بالا نمی‌آیند. در نصبی که روی این ماشین است، یعنی نسخه‌ی v0.21.5+2271.gec24378 با کامیت ec243785، هیچ اثری از این کلید در کد نیست و manager.py آن را نمی‌خواند. یعنی روی این نسخه فقط گیت تعیین می‌کند و قفل اعتماد هنوز نیامده است. اگر نسخه‌ی شما جدیدتر است، آن بخش از راهنما برای شما اثر دارد.

برای دیدن همین رفتار روی نسخه‌ی خودتان، hermes lsp status بهترین نقطه‌ی شروع است. این روش را کنار همان چیزی که در پست hermes verify توضیح داده شد بگذارید: آن یکی می‌گوید پروژه بالا می‌آید یا نه، و این یکی می‌گوید خطایی که ایجنت کرده درست بوده یا نه.

منابع

  1. راهنمای رسمی LSP هرمس: تشخیص معنایی و دو کانال خروجی
  2. متن کامل همان راهنما در مخزن هرمس
  3. کد مدیریت سرورهای زبان و خواندن کلیدهای پیکربندی
  4. مستندات pyright، سرور زبان پایتون
  5. مخزن pyright روی گیت‌هاب
  6. مرجع خط فرمان هرمس
  7. مرجع پیکربندی هرمس
  8. مخزن اصلی هرمس