diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..8f100531 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,39 @@ +# Pull request + +## What changed + + + +## Documentation = code (mandatory when any .md changes) + +Every changed documentation line must point at the code that makes it true. A +change that only "fixes a number" without a source reference is rejected before +commit. + +| Changed doc line (file) | Code fact it describes (file:symbol) | How it was verified | +|---|---|---| +| `docs/en/...` | `install.py::_print_config` | ran `python install.py --print-config --client generic` | + +- [ ] No sentence in this PR describes behaviour the code does not have. +- [ ] `python scripts/check_docs_vs_installer.py` is green (flags and client + names in docs exist in `install.py` / `adapters/clients/snippets.py`). +- [ ] `install / uninstall / clean / doctor` are described and match the behaviour. +- [ ] The "What works today" table keeps its **Entry point** column. + +## Gates + +- [ ] `python -m pytest tests/ -q` +- [ ] `ruff check src/ tests/` (or `scripts/ruff_gate.py`) +- [ ] New behaviour has a test that fails when the behaviour is removed + (paste the red run below). +- [ ] Numbers in this PR are measured, with unit and denominator. + +## Evidence + +```text + +``` + +## Risks / not verified + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22288126..696a4fd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,14 @@ jobs: - name: Lint (ruff) run: ruff check src/ tests/ + - name: Client snippets cold start (G5) + # Каждый сниппет из `install.py --print-config` запускается на чистом + # раннере: initialize + tools/list. Покрывает все 5 клиентов, включая + # не-Zed/не-OpenCode (generic, claude-code, cursor). Таймаут 300s: на + # холодной машине сервер поднимает провайдеров дольше 60s (ONNX-ожидание + # в логах), и 120s оказалось мало на windows-latest (2026-11-12). + run: python scripts/verify_client_snippets.py --install-root "${{ github.workspace }}" --timeout 300 + - name: Layer-boundary gate (Фаза 1 Universal Engine) # Три оси ТЗ: ADAPTER → TRANSPORT → SOURCE → CORE. Гейт не даёт # mcp/tools импортировать adapters/src.sources и core — adapters.* diff --git a/AGENTS.md b/AGENTS.md index fa4f30f1..c4fdf598 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -339,7 +339,17 @@ impact-guard). - **Канон:** `README.md` (EN) + `docs/ru/README.md` + `docs/zh/README.md`; `docs/en/ARCHITECTURE.md`, `docs/en/SEARCH_PIPELINE.md`, `docs/en/INSTALL.md`, - `docs/en/CONFIGURATION.md`, `docs/FACTS.md`. + `docs/en/CLIENTS.md`, `docs/en/DATA_MAP.md`, `docs/en/CONFIGURATION.md`, + `docs/FACTS.md`. +- **Документация = код.** Каждое утверждение в `.md` выводимо из кода, и правка + документации в PR ссылается на факт кода (`файл:символ`), который она отражает. + Правка «только цифра» без ссылки отклоняется до коммита (чек-лист + `.github/pull_request_template.md`). +- **Guard дрейфа флагов/клиентов:** `scripts/check_docs_vs_installer.py` — каждый + `--flag` и `--client X` из `README.md`/`docs/*/{INSTALL,CLIENTS,DATA_MAP}.md` + обязан существовать в `install.py`/`adapters/clients/snippets.py` (`tests/ + test_docs_vs_installer.py`). Колонка **Entry point** в «What works today» + обязательна. - **Три языка обязательны** для канонических страниц: EN / RU / ZH. Перевод — полный документ, не фрагмент; заголовки и числа совпадают с EN. - **Числа** (инструменты, чанки, качество, тайминги) живут только в `docs/FACTS.md` diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index 04cfc0ab..10e110df 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1737,3 +1737,47 @@ BREACH: MSC-P-19 | date: 2026-10-03 | rule: §12 | guard_at_time: no | detected_ **Дефект (воспроизведён):** `lance error: query dim(768) doesn't match the column vector dim(384)`. Модель `embeddinggemma-2` отдаёт 768 (проверено `/v1/embeddings`), схема таблицы — 384 (осталась от e5-small). `.env`: `EMBEDDING_MODEL=embeddinggemma-2` при `MSCODEBASE_DENSE_OFF=true`. **Следствие:** переключать `DENSE_OFF=false` сейчас нельзя — сначала размерность из сервера + переиндексация, потом повторный `run_curve.py` в dense. Записано в KNOWN_ISSUES с точным сообщением. **Урок:** «включить dense и станет лучше» — непроверенная гипотеза; первый же замер дал паритет и конфигурационный дефект. + +## [2026-11-12] G5: установка отвязана от Zed — сниппет на клиента вместо ручного конфига +**Симптом:** `install.py` v3.1 импортировал `adapters.zed.zed_config` на верхнем уровне и всегда писал конфиг Zed. На машине без Zed установка всё равно создавала `%APPDATA%/Zed`, а для Claude Code / Cursor / любого `mcpServers`-хоста команду и env приходилось собирать вручную по README (и она расходилась с кодом). +**Корень:** установщик и хост-адаптер были одним целым: каталог установки = каталог расширения Zed, шаг 11 = патч `settings.json`, generic-клиентам нечего было скопировать. +**Fix (v3.2):** +1. `MSCODEBASE_INSTALL_ROOT` + `resolve_install_root()`; историческое имя папки остаётся только default, содержимое — обычный venv. +2. Zed-адаптер импортируется лениво внутри `step_zed`/`step_zedcfg`, отказ адаптера не роняет установку; `--no-zed` — полностью нейтральный режим. +3. `--print-config --client {generic,opencode,zed,claude-code,cursor}` → один JSON-объект; формы конвертов в `adapters/clients/snippets.py` (zed-ключи сверены с `_make_server_entry`). +4. `scripts/verify_client_snippets.py`: рендерит сниппет каждого клиента, запускает его во временном каталоге и делает `initialize` + `tools/list`. Замер 2026-11-12: 5/5 `PASS ... tools=17`. +**Проверка:** `tests/test_install_cli.py` (16 тестов, в т.ч. реальный хендшейк из напечатанного сниппета); мутационная проба — переименование `PYTHONPATH` в env дало 5 красных. +**Найденный по ходу баг:** неинтерактивные режимы наследовали cp1251-stdout и падали на первом не-ASCII символе (`UnicodeEncodeError` на пути с кириллицей) — добавлен `_utf8_stdout()` для всех режимов, маркеры в отчёте сделаны ASCII. +**Урок:** «поддержка любого клиента» проверяется запуском сниппета, а не валидностью JSON: часть конвертов отвечала только после того, как запрос пошёл после `initialized`. + +## [2026-11-12] G6: doctor/clean и text-first по умолчанию — «что стоит и сколько весит» +**Симптом:** установщик предлагал ~657 МБ моделей при штатном `MSCODEBASE_DENSE_OFF=true`; команды «сколько это занимает и как убрать» не было; uninstaller существовал, но не был описан; лимиты кэшей жили в коде (`artifact_gc`, `log_manager`), а не в документации. +**Fix:** `install.py --doctor [--json]` (слои: install root / data root / проектные артефакты, размеры через `os.walk`, число чанков из `chunks.db`, пути без создания каталогов) и `install.py --clean [--dry-run] [--yes] [--json]` с guard'ами (отказ на исходный чекаут, корень ФС, домашний каталог) и ненулевым exit при остатке (заблокированные файлы работающим хостом). Модели стали opt-in: `models_enabled()` + `--with-models`; `--skip-models/--no-models` остались совместимыми. +**Замеры (реальная машина, 2026-11-12):** install root **2.7 GB / 21 372 файла**, data root **302.7 MB / 2 861 файл**, итого **3.0 GB / 24 233 файла**. `--sync-only` в пустой каталог: **871 файл / 18.7 MB за 3.1 с**. +**Автотест DoD:** `tests/test_install_cli.py::test_real_sync_install_then_doctor_then_clean_leaves_zero` — реальный `--sync-only` → `--doctor --json` (install_root.files > 100) → `--clean --yes` → `remaining_bytes == 0`, каталог удалён. Синтетический кейс отдельно проверяет dry-run (ничего не удалено), отказ без `--yes` (exit 2) и `doctor` после clean (`total_bytes == 0`). +**Найденный по ходу баг:** двойная обёртка stdout в UTF-8 (`_utf8_stdout()` в main + старый TUI-блок) закрывала буфер: `ValueError: I/O operation on closed file` на `--sync-only`. Убран дубль, оставлена одна обёртка на все режимы. +**Урок:** «очистка по слоям» без dry-run и guard'ов — это удаление чужого кэша по недопониманию; ноль остатка надо доказывать на файловой системе (`remaining_bytes == 0`), а не на числе вызванных `rmtree`. + +## [2026-11-12] G7: документация = код — guard вместо обещаний +**Симптом:** `docs/{en,ru,zh}/INSTALL.md` семь месяцев документировали `python install.py --uninstall` — флага никогда не существовало; INSTALL_MODELS/SYSTEM_REQUIREMENTS/CONTRIBUTING описывали «спросит перед загрузкой моделей», хотя дефолт качал их без вопроса; README перечислял 3 слоя данных из 4. +**Fix:** правило в `AGENTS.md §8` (каждая изменённая строка docs → `файл:символ`) + чек-лист `.github/pull_request_template.md` + механический guard `scripts/check_docs_vs_installer.py`: каждый `install.py --flag` и `--client X` в `README.md`/`docs/*/{INSTALL,CLIENTS,DATA_MAP}.md`/`docs/en/FAQ.md` обязан существовать в `install.py`/`adapters/clients/snippets.py`; пустой корпус = exit 2 («не доказано»), а не «прошло». +**Проверка:** `158 flag reference(s) checked, 0 problem(s)`; тот же guard на дореформенном `docs/en/INSTALL.md` из `main` даёт `✗ unknown installer flag --uninstall` (воспроизведение исходного дефекта). Тесты: `tests/test_docs_vs_installer.py`, 5/5. +**Исправлено по коду:** INSTALL §7 (`--clean --yes`/uninstaller вместо `--uninstall`), INSTALL §2 и INSTALL_MODELS×3 (`--with-models` вместо prompt/`--skip-models`), SYSTEM_REQUIREMENTS×3, CONTRIBUTING×3, FAQ×3, README/README×2 (4 слоя + DATA_MAP), `adapters/clients/*.json` (`PROJECT_PATH`, `PYTHONUTF8`, ``), `adapters/clients/README.md` (`cwd` → `PROJECT_PATH`), баннер в `AI_INSTALLATION_PROMPT.md`. +**Урок:** «документация соответствует коду» проверяется только исполнением: guard ловит исчезнувшие флаги, но не смысл. Смысловые утверждения (модели по умолчанию, Zed-обязательность) нашлись ручной сверкой с `step_models`/`step_zedcfg`. + +## [2026-11-12] G5 addendum: полный suite поймал контракт адаптера +**Симптом:** полный прогон (`2488 passed, 1 failed`) упал на `tests/test_cli.py::test_stdio_ref_valid_entrypoint`: `['-u','-m','src.main'] != ['-m','src.main']`. +**Корень:** я привёл `adapters/clients/*.json` к форме сгенерированного сниппета и добавил `-u`. Но эти JSON — публичный контракт адаптеров, и тест фиксирует именно его; генератор сниппетов (`install.py --print-config`) — отдельная сущность. +**Fix:** в JSON возвращён `args: ["-m","src.main"]`, добавленные `PROJECT_PATH`/`PYTHONUTF8`/`` оставлены. Тест не менялся (правило: не править проверку под код). +**Урок:** «shape» клиентского конфига — такая же публичная поверхность, как имя инструмента; `-u` в документации и в генераторе не даёт права менять статический адаптер. + +## [2026-11-12] G5: CI нашёл две вещи, которые локальный прогон не видел +**1. Платформенная проверка в тесте.** `test_print_config_is_valid_json_per_client` требовал, чтобы команда оканчивалась на `python.exe|python3|mscodebase-mcp*`. На ubuntu `sys.executable` = `.../venv/bin/python` → 5 красных в `test` и в `clean-state` (там тот же suite из чистого клона). Fix: проверять `Path(command[0]).name.startswith(("python","mscodebase-mcp"))` — платформенно-нейтрально; реализация не менялась. +**2. Холодная машина медленнее, чем ноутбук.** `scripts/verify_client_snippets.py` с таймаутом 120 s упал на `windows-latest` (шаг шёл ровно 121 s): первый запуск сервера на чистом раннере дольше 60 s (в логах — ожидание ONNX-сервера). Fix: таймаут по умолчанию 300 s, в CI передаётся `--timeout 300`; тот же запас в тесте-хендшейке. +**Урок:** «зелёный локально» не переносится на CI автоматически: POSIX-имя интерпретатора и холодный старт — две вещи, которые видны только на чужой машине. Оба места были бы пойманы раньше, если бы холодный старт гонялся в CI с первого коммита (теперь гоняется — шаг `Client snippets cold start (G5)`). + +## [2026-11-12] G5: третья находка CI — забитый stderr-пайп, и зелёный прогон +**Симптом:** шаг `Client snippets cold start (G5)` на `windows-latest` висел 20+ минут. Причина в самом скрипте: `proc.stderr.readline()` в ветке FAIL блокировал чтение, если сервер ничего не писал в stderr, а `stderr=PIPE` никто не вычитывал — при старте сервер пишет лог больше буфера пайпа (64 KB) и встаёт на записи в stderr, так и не ответив на `initialize`. +**Fix:** `_stderr_drain()` — фоновый поток с ring-buffer на 20 строк (пайп всегда вычитывается, хвост попадает в сообщение FAIL), плюс `LOG_LEVEL=WARNING` для пробы (`src/main.py:126` читает эту переменную) — сервер не заливает пайп на холодной машине. `_read_message` остался ограниченным по времени. +**Проверка:** локально 5/5 PASS, 13 s; CI run `43092017919` (head `20f287b`) — **7/7 jobs success**: `test` (ubuntu + windows), `clean-state`, `manifest-parity`, `secret-scan`, `docker-build`, `transport-equiv-e2e`. +**Урок:** дочерний процесс с `stdout/stderr=PIPE` обязан иметь читателя на каждом пайпе; «я прочитаю stderr только при ошибке» — это дедлок, который воспроизводится только там, где логов больше буфера. diff --git a/AI_INSTALLATION_PROMPT.md b/AI_INSTALLATION_PROMPT.md index 7e30edce..0387fe8f 100644 --- a/AI_INSTALLATION_PROMPT.md +++ b/AI_INSTALLATION_PROMPT.md @@ -1,5 +1,13 @@ # 🤖 One-Prompt Install Manifest for Zed Agent +> **Статус: Zed-специфичный сценарий (2026-08-12).** Актуальный, нейтральный к +> хосту порядок описан в `docs/en/INSTALL.md` (v3.2). Отличия, которые этот текст +> не отражает: установка больше не привязана к Zed (`MSCODEBASE_INSTALL_ROOT`, +> `--no-zed`, `install.py --print-config --client …`), модели стали opt-in +> (`--with-models`, `install.py::models_enabled`), а вместо ручного +> `printf 's\nn\n' | python install.py` используются `--yes` и `--clean/--doctor`. +> Проверить, что здесь устарело: `python scripts/check_docs_vs_installer.py`. + > Версия: 3.5.0 | Дата: 2026-08-12 > > Скопируй этот текст в чат Агента Zed (`Ctrl+Shift+P` → `Agent Panel: Toggle`) diff --git a/README.md b/README.md index cfce8494..29eb4418 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,36 @@ number and the command that regenerates it are listed in ## Quick start -Requirements: Windows is the validated platform; Python 3.14+; an MCP host -(**OpenCode is the primary one**, the Zed extension still works). +Requirements: Windows is the validated platform; Python 3.14+; any MCP host +(OpenCode, Zed, Claude Code, Cursor, or a generic `mcpServers` client). ```bash cd -python install.py # full install: venv, pip, optional models -python install.py --sync # code-only sync into the installed copy +python install.py # venv + deps + code: text-first, no models +python install.py --with-models # optional: + ONNX/GGUF models (~657 MB) +python install.py --sync # code-only sync after a source change ``` -Restart the host after `install.py` finishes (the OpenCode session, or the Zed -window). The installed copy lives in `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` -and both hosts launch that venv. Verify from the agent panel: +The install is host-neutral: it writes a plain Python environment (default +`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` — the name is +historical). Point it anywhere with `MSCODEBASE_INSTALL_ROOT`. Then print the +config for your host instead of writing it by hand: + +```bash +python install.py --print-config --client generic # mcpServers-style hosts +python install.py --print-config --client opencode +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +python install.py --print-config --client zed +``` + +Each prints one JSON object containing the interpreter, `-u -m src.main`, +`PYTHONPATH`, `PROJECT_PATH` and `PYTHONUTF8=1`; paste it into the host's MCP +config. Per-client copy-paste sections and the launch check +(`python scripts/verify_client_snippets.py`) are in +[docs/en/CLIENTS.md](docs/en/CLIENTS.md). Restart the host after configuring it. + +Verify from the agent panel: ```text tool_help() # profile, visible and hidden tools @@ -214,14 +232,22 @@ MCP host: OpenCode (primary) or Zed └── Optional embedding providers, reranker, LSP rename ``` -Canonical data locations (outside the repository): +Canonical data locations (four layers; full inventory with sizes and policies in +[docs/en/DATA_MAP.md](docs/en/DATA_MAP.md)): -- `%LOCALAPPDATA%/mscodebase/projects//` — per-project graph, memory, +- install root (`MSCODEBASE_INSTALL_ROOT`, default the historical Zed extension + directory) — the code copy, `venv/`, optional ONNX/GGUF models, uninstaller. +- `%LOCALAPPDATA%/mscodebase/projects//` — per-project graph, memory, job state, commit memory, telemetry. -- `%LOCALAPPDATA%/mscodebase/logs/` — server logs. +- `%LOCALAPPDATA%/mscodebase/logs/` — server logs (2 MB × 3 rotation, 7-day retention). +- `%LOCALAPPDATA%/mscodebase/models/`, `repos/` — shared model cache and LRU(5)+24 h + repo clones. - `/.mscodebase/chunks.db` — the text chunk store used by the default search path. +`python install.py --doctor` prints every layer with its measured size; +`python install.py --clean --dry-run` lists what `--clean` would delete. + LanceDB is part of the optional vector path and is not required by the default text-first configuration. @@ -326,7 +352,8 @@ stale/empty index, and gold-label mismatch. | [docs/FACTS.md](docs/FACTS.md) | Every public number and the command that regenerates it | | [docs/en/ARCHITECTURE.md](docs/en/ARCHITECTURE.md) | Layers, DI, module boundaries, tool profile | | [docs/en/SEARCH_PIPELINE.md](docs/en/SEARCH_PIPELINE.md) | Retrieval, ranking, docs routing, investigate fan-out | -| [docs/en/INSTALL.md](docs/en/INSTALL.md) | Installation, the sync/restart loop, troubleshooting | +| [docs/en/INSTALL.md](docs/en/INSTALL.md) | Installation, the sync/restart loop, doctor/clean, troubleshooting | +| [docs/en/DATA_MAP.md](docs/en/DATA_MAP.md) | Every path the installer/server creates, with sizes and retention policies | | [docs/en/CONFIGURATION.md](docs/en/CONFIGURATION.md) | Every environment variable and its default | | [docs/en/CLIENTS.md](docs/en/CLIENTS.md) | OpenCode, Zed, Claude Code, VS Code, CLI, Docker | | [docs/en/HOOKS_AND_GATES.md](docs/en/HOOKS_AND_GATES.md) | The OpenCode plugin, the 12 pre-commit gates, CI jobs | diff --git a/adapters/clients/README.md b/adapters/clients/README.md index 98366964..96cf97fb 100644 --- a/adapters/clients/README.md +++ b/adapters/clients/README.md @@ -1,33 +1,53 @@ -# MSCodeBase — конфиги клиентов (Фаза 5, план §4) +# MSCodeBase — client configs (Phase 5, plan §4) -Тонкие адаптеры-конфиги для подключения движка к внешним MCP-клиентам. -Каждый конфиг имеет stdio-вариант (локальный) и http-вариант (remote, Фаза 3). +Thin config shapes for connecting the engine to external MCP clients. Each file +has a stdio variant (local) and an HTTP variant (remote, Phase 3). -## stdin/stdout (локально) +**These are shapes, not the recommended way to get a config.** The generated +snippet knows the real paths of your installation: -- **Claude Code / Desktop** — `claude.code.mcp.json` (секция `mcpServers`). -- **VS Code** — `vscode.mcp.json` (`.vscode/mcp.json`, секция `servers`). -- **Cursor** — использует тот же Claude-формат (`mcpServers`), файл `.cursor/mcp.json` - или `.mcp.json` в корне проекта. +```bash +python install.py --print-config --client generic # mcpServers +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +python install.py --print-config --client opencode +python install.py --print-config --client zed +``` -Заполнить плейсхолдеры: -- `/Scripts/python.exe` — venv-py движка (POSIX: `/bin/python3`); -- `PYTHONPATH=` — путь к дереву исходников (`src/`); -- `cwd=` — проект, который индексируется. +The generator lives in `adapters/clients/snippets.py`; the launch check is +`scripts/verify_client_snippets.py`. Full per-client instructions: +`docs/en/CLIENTS.md`. -`command = -m src.main` запускает server через `run_server` (stdio). +## stdin/stdout (local) -## HTTP (remote, Фаза 3) +- **Claude Code / Desktop** — `claude.code.mcp.json` (key `mcpServers`). +- **VS Code** — `vscode.mcp.json` (`.vscode/mcp.json`, key `servers`). +- **Cursor** — same Claude shape (`mcpServers`), file `.cursor/mcp.json` or + `.mcp.json` in the project root. -http-блок `mscodebase-remote`: -- `url = http://:8089/mcp` — Streamable HTTP вход (`src/remote_main.py`); -- `headers.Authorization = Bearer ` — обязателен для сети. +Placeholders to fill: -Сборка/запуск remote-сервера: см. `deploy/docker/README.md` (образ + compose) и -`src/remote_main.py`. +- `/Scripts/python.exe` — the engine's venv interpreter + (POSIX: `/bin/python3`); +- `PYTHONPATH=` — the tree that contains `src/` (the installed copy + or the checkout); +- `PROJECT_PATH=` — the project that gets indexed + (`cwd` is not required: the server reads `PROJECT_PATH`). -## CLI (без MCP, для CI/скриптов) +`command = -m src.main` starts the server through `run_server` (stdio). -`python -m src.cli ''` — прямой вызов tool-класса через DI, -см. `src/cli.py`. Curated allowlist: `get_task_status`, `stale_detector`, +## HTTP (remote, Phase 3) + +HTTP block `mscodebase-remote`: + +- `url = http://:8089/mcp` — Streamable HTTP entry (`src/remote_main.py`); +- `headers.Authorization = Bearer ` — required on the network. + +Building/running the remote server: see `deploy/docker/README.md` (image + compose) +and `src/remote_main.py`. + +## CLI (no MCP, for CI/scripts) + +`python -m src.cli ''` — direct tool-class call through DI, see +`src/cli.py`. Curated allowlist: `get_task_status`, `stale_detector`, `get_context`, `graph_query`, `find_similar_bugs`. diff --git a/adapters/clients/claude.code.mcp.json b/adapters/clients/claude.code.mcp.json index 398e71db..b922ea8b 100644 --- a/adapters/clients/claude.code.mcp.json +++ b/adapters/clients/claude.code.mcp.json @@ -3,10 +3,13 @@ "mcpServers": { "mscodebase": { "type": "stdio", - "command": "/Scripts/python.exe", + "command": "/venv/Scripts/python.exe", "args": ["-m", "src.main"], - "env": { "PYTHONPATH": "" }, - "cwd": "" + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } }, "mscodebase-remote": { "type": "http", diff --git a/adapters/clients/snippets.py b/adapters/clients/snippets.py new file mode 100644 index 00000000..5e308c63 --- /dev/null +++ b/adapters/clients/snippets.py @@ -0,0 +1,94 @@ +"""Ready-to-paste MCP client snippets (G5). + +The server is host-neutral: one stdio command plus a few environment variables. +Hosts differ only in the JSON envelope they read, so the envelope is generated +here and `install.py --print-config --client ` prints the result. + +Envelope shapes are not invented: each one mirrors what the corresponding +adapter/host consumes today. + + generic, claude-code, cursor -> {"mcpServers": {...}} + opencode -> {"mcp": {name: {"type": "local", ...}}} + zed -> {"context_servers": {name: {command, args, + env, enabled}}} — same keys that + adapters/zed/zed_config._make_server_entry + writes into settings.json. + +Usage: + from adapters.clients.snippets import render_snippet + print(render_snippet("generic", command=exe, args=["-u", "-m", "src.main"], + env={"PYTHONPATH": root, "PROJECT_PATH": proj})) +""" + +from __future__ import annotations + +import json +from typing import Mapping, Sequence + +__all__ = ["CLIENTS", "SERVER_NAME", "render_snippet", "snapshot"] + +SERVER_NAME = "mscodebase" + +#: Clients `--print-config` accepts. Order is the order documented in CLIENTS.md. +CLIENTS = ("generic", "opencode", "zed", "claude-code", "cursor") + + +def snapshot( + client: str, + *, + command: str, + args: Sequence[str], + env: Mapping[str, str], +) -> dict: + """Return the snippet as a dict (the JSON text is just its serialization).""" + if client not in CLIENTS: + raise ValueError(f"unknown client {client!r}; known: {', '.join(CLIENTS)}") + + env_dict = dict(env) + server = { + "command": command, + "args": list(args), + "env": env_dict, + } + + if client in ("generic", "claude-code", "cursor"): + return {"mcpServers": {SERVER_NAME: server}} + if client == "opencode": + # OpenCode V2: names live under mcp.servers, never directly under mcp. + # Source: https://opencode.ai/docs/mcp-servers ("V2 does not place server + # names directly under `mcp`"). The flat `mcp.` layout of V1 is + # still accepted for migrated configs, but new configs use `servers`. + return { + "mcp": { + "servers": { + SERVER_NAME: { + "type": "local", + "command": [command, *args], + "environment": env_dict, + } + } + } + } + # Zed: keys match adapters/zed/zed_config._make_server_entry. + return { + "context_servers": { + SERVER_NAME: { + "command": command, + "args": list(args), + "env": env_dict, + "enabled": True, + } + } + } + + +def render_snippet( + client: str, + *, + command: str, + args: Sequence[str], + env: Mapping[str, str], +) -> str: + """Pretty-printed JSON snippet, newline-terminated.""" + payload = snapshot(client, command=command, args=args, env=env) + return json.dumps(payload, indent=2, ensure_ascii=False) + "\n" diff --git a/adapters/clients/vscode.mcp.json b/adapters/clients/vscode.mcp.json index ac7f1c62..8c90f923 100644 --- a/adapters/clients/vscode.mcp.json +++ b/adapters/clients/vscode.mcp.json @@ -3,10 +3,13 @@ "servers": { "mscodebase": { "type": "stdio", - "command": "/Scripts/python.exe", + "command": "/venv/Scripts/python.exe", "args": ["-m", "src.main"], - "env": { "PYTHONPATH": "" }, - "cwd": "" + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } }, "mscodebase-remote": { "type": "http", diff --git a/docs/en/ARCHITECTURE.md b/docs/en/ARCHITECTURE.md index 7f08a0ce..baf8a94e 100644 --- a/docs/en/ARCHITECTURE.md +++ b/docs/en/ARCHITECTURE.md @@ -151,3 +151,19 @@ replace/insert) with preview and impact guards. - **Vector store:** LanceDB, only when dense retrieval is enabled. None of them is required for the default text-first path. + +## 9. Code references (audited 2026-11-12) + +Every claim above resolves to a symbol, not to prose. Audit table: + +| Claim in this page | Code | +|---|---| +| entry point builds the server, DI first | `src/main.py::main`, `src/mcp/server_factory.py::create_mcp_server` (line 156), `src/core/di_container.py` | +| artifact layout (`/projects//…`) | `src/core/artifact_paths.py::get_data_root`, `::project_hash`, `::get_graph_db_path`, `::get_logs_dir` | +| text chunks in `/.mscodebase/chunks.db` | `src/config/settings.py::SearchConfig.sqlite_chunks` (`MSCODEBASE_SQLITE_CHUNKS`) | +| `experiments/**` excluded from indexing | `src/core/indexing/file_guard.py` | +| retrieval owner | `src/core/search/engine.py::Searcher` | +| parsing / graph | `src/core/indexing/parser.py`, `src/core/graph.py` | +| profile `core` = 17 of 69 registered | `src/mcp/server_tools.py::CORE_PROFILE`; measured with `tools/list`: 17 (`core`) / 69 (`full`), 2026-11-12 | +| optional LSP layer | `src/mcp/tools/` `lsp_*` tools, failure cache 30 s (`src/config/settings.py::LSPConfig`) | + diff --git a/docs/en/CLIENTS.md b/docs/en/CLIENTS.md index e3adf70e..2e90888f 100644 --- a/docs/en/CLIENTS.md +++ b/docs/en/CLIENTS.md @@ -1,96 +1,222 @@ # Clients and deployment -> Rewritten 2026-11-08. Describes how the server is launched by each host and what -> `install.py` actually installs. Verified against `install.py`, `extension.toml`, -> `adapters/clients/*`, `.opencode/plugin/`, and the live OpenCode config on the -> development machine. +> Rewritten 2026-11-12. Describes how the server is launched by each host and what +> `install.py` actually installs. Verified against `install.py`, `adapters/clients/snippets.py`, +> `src/main.py`, `extension.toml`, `.opencode/plugin/`, `tests/test_install_cli.py` and +> `scripts/verify_client_snippets.py` (the last one starts the server from every snippet). ## 1. What the server is -A Python MCP server (`src/main.py`) speaking stdio, plus an optional Streamable-HTTP -entry point (`src/remote_main.py`) for the docker/remote deployment. It is not an -editor plugin by itself — a host launches it and talks MCP. - -## 2. OpenCode (primary) - -OpenCode starts the server as a local MCP server. On the development machine the -entry looks like this (paths shortened): - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": [ - "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence\\venv\\Scripts\\mscodebase-mcp.exe", - "-u", "-m", "src.main" - ], - "environment": { - "PYTHONPATH": "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" +A Python MCP server (`src/main.py`, entry point `mscodebase-mcp = src.main:main` in +`pyproject.toml`) speaking stdio, plus an optional Streamable-HTTP entry point +(`src/remote_main.py`) for docker/remote deployment. It is not an editor plugin by +itself — a host launches it and talks MCP. + +Since v3.2 the installer is **host-neutral**: it puts a plain Python environment on +disk and any MCP host can launch it. Zed is one adapter among several, not a required +step. + +## 2. Install the server once + +```bash +cd +python install.py # venv + deps + code, text-first, no models +python install.py --with-models # same + ONNX/GGUF models (~657 MB) +python install.py --no-zed # do not touch Zed settings at all +python install.py --sync-only # after a code change: copy code only +``` + +Where it lands (default, historical path — nothing Zed-specific about the contents): + +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` + +Move it anywhere with `MSCODEBASE_INSTALL_ROOT=`; caches, logs and per-project +artifacts live elsewhere (`MSCODEBASE_DATA_DIR`, default `%LOCALAPPDATA%\mscodebase`). +The full inventory is in [DATA_MAP.md](DATA_MAP.md). + +Alternative without the installer (any Python package manager that exposes console +scripts): + +```bash +pipx install . # or: uv tool install . +mscodebase-mcp # the entry point is now on PATH +``` + +## 3. Get the config for your host + +Do not hand-write the command and the environment — print them: + +```bash +python install.py --print-config --client generic # any mcpServers-style host +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` + +Each command prints one JSON object and nothing else, so it can be redirected: +`python install.py --print-config --client cursor > .cursor/mcp.json`. The snippet +contains the resolved interpreter, `-u -m src.main`, `PYTHONPATH`, `PROJECT_PATH` and +`PYTHONUTF8=1`. + +### 3.1 Generic MCP client (`mcpServers`) + +File: wherever the host reads its MCP config. Replace `` with the workspace +you want indexed (this is what `PROJECT_PATH` means). + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } } } } ``` -Two facts that matter: +### 3.2 OpenCode + +File: `opencode.json(c)` at the project root or in `~/.config/opencode/`. OpenCode V2 +places server names under **`mcp.servers`** — the flat V1 layout `mcp.` is still +accepted for migrated configs but is not what new configs use +(source: , "V2 does not place server names +directly under `mcp`"): + +```json +{ + "mcp": { + "servers": { + "mscodebase": { + "type": "local", + "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], + "environment": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } + } +} +``` -1. The **installed copy** lives in the extension directory, not in the git checkout. - `install.py` is what puts current code there (see §5). -2. The directory is named `…\Zed\extensions\…` for historical reasons. It is a plain - Python environment; OpenCode uses it the same way the Zed extension does. +Or let the CLI write it (preserves unrelated settings): -Project-local configuration: `.opencode/plugin/mscodebase_gate.ts` (commit hooks) and -`.opencode/command/*.md` (reusable agent commands). See -[HOOKS_AND_GATES.md](HOOKS_AND_GATES.md). +```bash +opencode mcp add mscodebase -- /venv/Scripts/mscodebase-mcp.exe -u -m src.main +``` -## 3. Zed (supported, not primary) +`PROJECT_PATH` may be replaced by the host's workspace variable where the host +expands it. Config changes reload without a restart (`/reload` forces it). Project +automation lives in `.opencode/plugin/mscodebase_gate.ts` and +`.opencode/command/*.md` (see [HOOKS_AND_GATES.md](HOOKS_AND_GATES.md)). + +### 3.3 Zed (supported, not primary) + +Either let the installer patch `settings.json` (default step, skip with `--no-zed`), +or paste the printed snippet under `context_servers`: + +```json +{ + "context_servers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "$ZED_WORKTREE_ROOT", + "PYTHONUTF8": "1" + }, + "enabled": true + } + } +} +``` -The Zed extension (`extension.toml`, `__mscodebase_ext__.marker`) launches the same -server from the same directory. Behaviour that is specific to Windows/Zed is -documented in [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md); treat Zed as an -alternative host, not the reference implementation. +Keys match what `adapters/zed/zed_config.py::_make_server_entry` writes. Windows/Zed +specifics: [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md). -## 4. Other MCP clients +### 3.4 Claude Code -`adapters/clients/` contains ready shapes for stdio and Streamable-HTTP: +The file `.mcp.json` in the project root uses the same `mcpServers` shape as 3.1. +The CLI form is equivalent: -| File | Client | Section | -|---|---|---| -| `claude.code.mcp.json` | Claude Code / Desktop | `mcpServers` | -| `vscode.mcp.json` | VS Code (`.vscode/mcp.json`) | `servers` | +```bash +claude mcp add-json mscodebase '{"command":"/venv/Scripts/mscodebase-mcp.exe","args":["-u","-m","src.main"],"env":{"PYTHONPATH":"","PROJECT_PATH":"","PYTHONUTF8":"1"}}' +``` -Fill in ``, ``, ``. The HTTP variant -points at `http://:8089/mcp` and requires -`Authorization: Bearer `; the remote server is built from -`deploy/docker/`. +### 3.5 Cursor + +File: `.cursor/mcp.json` (or `~/.cursor/mcp.json` for all projects), same +`mcpServers` shape as 3.1: + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } +} +``` -For scripts and CI there is no MCP at all: +## 4. Verify a snippet without the GUI ```bash -python -m src.cli '' [--project ] +python scripts/verify_client_snippets.py --install-root ``` -The CLI uses the same DI container and tool classes; its dispatcher is a curated -allowlist (`src/cli.py`), not the whole 69-tool surface. +It renders each client's snippet exactly as the installer does, launches that command +in a throwaway directory, performs an MCP `initialize` + `tools/list` handshake and +prints one line per client. Expected (measured 2026-11-12): -## 5. Installation and the update loop +``` +generic PASS: server=MSCodebase Intelligence Server tools=17 +opencode PASS: server=MSCodebase Intelligence Server tools=17 +zed PASS: server=MSCodebase Intelligence Server tools=17 +claude-code PASS: server=MSCodebase Intelligence Server tools=17 +cursor PASS: server=MSCodebase Intelligence Server tools=17 +``` -```bash -python install.py # interactive full install (venv, pip, llama.cpp, models) -python install.py --sync-only # code only: copy + drop __pycache__, no pip/models +`tools=17` is the default `core` profile (69 are registered, 17 visible); see the +profile table in the README. + +From the agent panel, in this order: + +``` +tool_help() # profile + visible/hidden tools +get_health_report() # index, providers, resources +codebase(action="index", path="status") # chunks > 0? ``` -`install.py` copies `src/`, scripts and support files into -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` (or the platform equivalent) -and keeps the extension venv. After a code change: +## 5. The update loop + +The installed copy is not the git checkout: ``` -edit → python install.py --sync-only → restart the host (OpenCode session / Zed window) +edit src/... -> python install.py --sync-only -> restart the host (session / window) ``` Without the restart the old process keeps serving the old code; without the sync the -new code is not on disk in the installed location. Both steps are required. +new code is not on disk in the installed location. Both steps are required. Serving +from a checkout instead? Point `command`/`PYTHONPATH` at the checkout and skip the +sync step. ## 6. Remote / Docker @@ -98,8 +224,30 @@ new code is not on disk in the installed location. Both steps are required. `deploy/docker/Dockerfile` + `docker-compose.yml` build the image, and CI runs a `docker-build` job. Docker is a deployment option, not a local requirement. -## 7. Which client should a bug report mention +```json +{ + "mcpServers": { + "mscodebase-remote": { + "type": "http", + "url": "http://:8089/mcp", + "headers": { "Authorization": "Bearer " } + } + } +} +``` + +## 7. Scripts and CI (no MCP at all) + +```bash +python -m src.cli '' [--project ] +echo '' | python -m src.cli - +``` + +The CLI uses the same DI container and tool classes; its dispatcher is a curated +allowlist (`src/cli.py`), not the whole 69-tool surface. + +## 8. Which client should a bug report mention -State the host (OpenCode / Zed / Claude / VS Code / CLI), the profile +State the host (OpenCode / Zed / Claude Code / Cursor / generic / CLI), the profile (`core`/`full`), and how the server was launched. A report that omits these three cannot distinguish "tool hidden by the profile" from "tool broken". diff --git a/docs/en/CONFIGURATION.md b/docs/en/CONFIGURATION.md index e2783941..5482caba 100644 --- a/docs/en/CONFIGURATION.md +++ b/docs/en/CONFIGURATION.md @@ -76,7 +76,7 @@ frozen panels must never appear as search hits. | `MSCODEBASE_LINK_TIMEOUT_SEC` | — | Graph link-pass budget | | `MSCODEBASE_REINDEX_STALL_SEC` | — | Stall watchdog for long reindexes | | `MSCODEBASE_IVF_MAX_ABANDONS` | — | Auto-disable IVF after N failed optimize/create | -| `MSCODEBASE_MAX_FILE_SIZE_MB` (security) | `10` | Files above this are skipped | +| `MAX_FILE_SIZE_MB` (security, no prefix — as read by `SecurityConfig`, `src/config/settings.py:425`) | `10` | Files above this are skipped | ## 5. Providers (all optional) @@ -128,3 +128,17 @@ python -c "from src.config.settings import get_config; c=get_config(); print(c.s Config is built once per process: restart the MCP host after editing `.env`, then `get_health_report()` should show the new values (or at least not the old provider state). + +## 9. Code references (audited 2026-11-12) + +| Group | Where it is read | +|---|---| +| server/profile/storage (`MSCODEBASE_MCP_PROFILE`, `MSCODEBASE_DATA_DIR`, …) | `src/config/settings.py`, `src/mcp/server_tools.py::CORE_PROFILE`, `src/core/artifact_paths.py::get_data_root` | +| retrieval defaults (`MSCODEBASE_DOCS_MARGIN`, `_ONDEMAND_RERANK_*`, `_CODE_BM25_*`) | `src/config/settings.py::SearchConfig` (lines 189–268) | +| `MAX_FILE_SIZE_MB` (no prefix) | `src/config/settings.py::SecurityConfig` (line 425) | +| plugin gates (`MSCODEBASE_STALE_GATE`, `_ISOLATION_GATE`, `_GATE_LOG`, `MSCODEBASE_PY`) | `.opencode/plugin/mscodebase_gate.ts` (lines 25–27) | +| pre-commit budget (`MSCB_PRECOMMIT_FAST`, `MSCB_PRECOMMIT_GATE_TIMEOUT`) | `.githooks/pre-commit` (lines 33, 153) | + +`python scripts/check_docs_vs_config.py` fails when a name in this table stops +existing in the code. + diff --git a/docs/en/CONTRIBUTING.md b/docs/en/CONTRIBUTING.md index 911d2cfe..d6579244 100644 --- a/docs/en/CONTRIBUTING.md +++ b/docs/en/CONTRIBUTING.md @@ -35,9 +35,9 @@ python -m venv venv; venv\Scripts\activate; pip install -e ".[dev]" ``` `pip install -e .` is enough for the text-first runtime. Models are optional and -never download silently: `python install.py` asks before each download and -accepts `--skip-models`; `--sync-only` copies code into the extension directory -without touching pip or models. +never download silently: `install.py` is text-first by default and only downloads +with `--with-models` (`models_enabled`); `--sync-only` copies code into the install +root without touching pip or models. ## 3. Tests and lint diff --git a/docs/en/DATA_MAP.md b/docs/en/DATA_MAP.md new file mode 100644 index 00000000..9c4a98fe --- /dev/null +++ b/docs/en/DATA_MAP.md @@ -0,0 +1,111 @@ +# Where MSCodeBase puts data + +> Written 2026-11-12 and verified against `install.py` v3.2, +> `src/core/artifact_paths.py`, `src/core/artifact_gc.py`, +> `src/core/log_manager.py`, `src/sources/git_url/__init__.py` and a live +> `install.py --doctor` run. Numbers are measured, not estimated. + +One question this page answers: *what did the installer and the server create on +my disk, who owns each path, and how do I get rid of it?* + +```bash +python install.py --doctor # what is installed and how much it weighs +python install.py --clean --dry-run # paths and sizes; deletes nothing +python install.py --clean --yes # delete every layer below +``` + +## 1. The four layers + +| Layer | Path (default) | Created by | What it is | +|---|---|---|---| +| **Install root** | Windows `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`; macOS `~/Library/Application Support/Zed/extensions/…`; Linux `~/.local/share/zed/extensions/…` | `install.py` steps `step_copy`, `step_venv`, `step_pip`, `step_llama`, `step_gguf`, `step_models`, `step_uninst` | The installed copy: code, `venv/`, optional models, uninstaller | +| **Data root** | `%LOCALAPPDATA%\mscodebase` (Win) · `$XDG_CACHE_HOME/mscodebase` or `~/.cache/mscodebase` (POSIX) | `src/core/artifact_paths.py::get_data_root` | Per-project caches, logs, shared models, repo clones | +| **Project artifacts** | `/.mscodebase/chunks.db` | server (`MSCODEBASE_SQLITE_CHUNKS`) | FTS5 text index for that project | +| **Host config** | Zed: `%APPDATA%\Zed\settings.json` (`context_servers`) | `install.py` step `step_zedcfg` → `adapters/zed/zed_config.py` | One entry telling the host how to launch the server | + +Both roots are relocatable: `MSCODEBASE_INSTALL_ROOT` and `MSCODEBASE_DATA_DIR` +(`BASE_INDEX_DIR` is still accepted as a legacy alias for the second). + +## 2. Install root — what is inside + +| Path | Owner | Purpose | Size | +|---|---|---|---| +| `src/`, `scripts/`, `adapters/`, `locales/`, `docs/` | `step_copy` (`_sync_dir`) | Executable copy of the repository (without `experiments/`) | a few MB | +| `venv/` | `step_venv`, `step_pip` | Python environment with pinned dependencies | ~1 GB | +| `venv/Scripts/mscodebase-mcp.exe` | `pip` entry point (`pyproject.toml [project.scripts]`) | The stdio command hosts launch | — | +| `.codebase_models/onnx/…` | `step_models` (only with `--with-models`) | ONNX embedder + reranker | ~657 MB | +| `llama_msvc/` | `step_llama` | llama-server binary for the GGUF path | ~200 MB | +| `models/*.gguf` | `step_gguf` (only with `--with-models`) | GGUF embedder + reranker | ~700 MB | +| `uninstall.bat` / `uninstall.sh` | `step_uninst` (`_build_uninstaller`) | Kills the server and removes this directory | < 1 KB | +| `__mscodebase_ext__.marker` | `step_copy` | Tells `src/main.py` it runs from an installed copy | < 1 KB | + +Measured on the development machine (2026-11-12, models installed): +**3.0 GB over 28 429 files**, of which `venv/` plus models make up almost all of it. + +## 3. Data root — what is inside + +| Path | Owner | Purpose | Policy | +|---|---|---|---| +| `projects//graph.db` | `artifact_paths.get_graph_db_path` | PropertyGraph: files, symbols, calls, imports | deleted with the project dir | +| `projects//lancedb_v2/` | `get_index_dir`, `get_db_path` | Vector index (only when dense is enabled) | same | +| `projects//intelligence/` | `get_intelligence_dir` | Project memory: ADRs, incidents, debt | same | +| `projects//metrics/` | `get_metrics_dir` | Indexing job history (ETA model) | same | +| `projects//commit_memory/` | `get_commit_memory_dir` | Semantic commit cache | same | +| `projects//branches/` | `get_branches_dir` | Per-branch index state | same | +| `projects//telemetry/` | `get_telemetry_dir` | Daily JSON snapshots | files older than **90 days** pruned | +| `projects//summaries_cache/` | `get_summaries_cache_dir` | LLM chunk summaries | same as project dir | +| `projects//progress.json` | `get_progress_file` | Indexing progress contract | overwritten | +| `projects//action_receipts.jsonl` | `src/core/action_receipt.py` | Receipts for write actions | verified 60 d / inconclusive 7 d (`purge`) | +| `logs/mscodebase-intelligence.log` | `log_manager.get_main_log_path` | Server log | rotates at 2 MB × 3 files, deleted after **7 days** | +| `logs/crash.json` | `get_crash_log_path` | Last resource-monitor crash | overwritten | +| `models/.codebase_models/onnx/…` | `get_onnx_models_base` | Shared copy of downloaded models | only `--clean` | +| `repos//` | `get_repos_cache_dir` | Clones of remote repositories | LRU(5) + TTL 24 h (`src/sources/git_url`) | +| `projects//` (whole dir) | — | — | inactive dirs older than **30 days** and empty dirs are pruned by `artifact_gc.prune_stale_artifacts()` | + +Measured on the development machine (2026-11-12): **302.9 MB over 2 934 files**. +`--doctor` in a real project directory that also holds `chunks.db` (78 MB) and a +local model copy (960 MB) reported **4.3 GB over 31 385 files** across all layers. + +## 4. Project artifacts + +| Path | Owner | Purpose | +|---|---|---| +| `/.mscodebase/chunks.db` | `MSCODEBASE_SQLITE_CHUNKS` storage | Text index (FTS5/BM25) used by the default search path; `--doctor` reports its chunk count | +| `/.codebase_indices/`, `.codebase/`, `.mscodebase/telemetry/` | legacy | Moved into the data root on first project open (`artifact_paths.migrate_legacy_artifacts`) | +| `/.codebase_models/` | `scripts/download_model.py` (download staging) | Temporary copy before the model is installed under the install root | + +`experiments/**` is excluded from indexing by design, so it never grows the index. + +## 5. Policies + +- **Models are opt-in.** `install.py` is text-first: without `--with-models` it + performs no model download and writes no model directory. `--skip-models` and + `--no-models` are still accepted for old scripts. +- **Logs** rotate by size (2 MB, 3 files) and are pruned after 7 days + (`log_manager._MAX_LOG_BYTES`, `_MAX_LOG_FILES`, `_LOG_RETENTION_DAYS`). +- **Telemetry** snapshots are pruned after 90 days; **inactive project dirs** + after 30 days (`artifact_gc.DEFAULT_*`); empty project dirs are removed at once. +- **Repo clones** are an LRU of 5 with a 24 h TTL, plus a 500 MB post-clone size + cap (`git_url.DEFAULT_MAX_CLONE_BYTES`). +- **Shared model cache** (`data_root/models`) has no TTL: models are removed only + by `install.py --clean`. +- **Uninstall** is `install.py --clean --yes`, which removes all four layers + except the host config entry (delete the `mscodebase` entry from + `settings.json` / `opencode.json` / `.mcp.json` yourself, or run + `install.py --no-zed` and never create it). + +## 6. Reading the doctor output + +``` ++ install_root 3.0 GB 28429 file(s) C:\Users\\AppData\Local\Zed\extensions\mscodebase-intelligence ++ data_root 302.9 MB 2934 file(s) C:\Users\\AppData\Local\mscodebase ++ project_chunks 78.0 MB 4 file(s) D:\work\repo\.mscodebase + ... + +index 7278 chunk(s) (D:\work\repo\.mscodebase\chunks.db) +total 4.3 GB over 31385 file(s) +``` + +`+` exists, `-` absent. `index missing` means the text index for the current +directory was never built (the first `codebase(action="index")` call builds it), +not that search is broken. `--json` prints the same data for scripts. diff --git a/docs/en/FAQ.md b/docs/en/FAQ.md index 8308a1ac..d0f04a7e 100644 --- a/docs/en/FAQ.md +++ b/docs/en/FAQ.md @@ -118,7 +118,8 @@ instead. Yes. Nothing in the default path needs a GPU, a cloud API, or internet: FTS5, the AST graph, and tree-sitter parsing are all local. The only network use is -downloading optional models (GGUF/ONNX via `install.py`) and the optional +downloading optional models (`install.py --with-models`, off by default: +`models_enabled()` in `install.py`) and the optional `tree-sitter-language-pack`, whose parsers are fetched on first use (`MSCODEBASE_LANGUAGE_PACK=true`). With that flag off, every grammar comes from a pinned wheel and no download happens. diff --git a/docs/en/INSTALL.md b/docs/en/INSTALL.md index 8bd0e59f..39e162b7 100644 --- a/docs/en/INSTALL.md +++ b/docs/en/INSTALL.md @@ -4,11 +4,13 @@ MSCodeBase Banner

-[🇬🇧 English](INSTALL.md) • [🇷🇺 Русский](../ru/INSTALL.md) • [🇨🇳 中文](../zh/INSTALL.md) +[English](INSTALL.md) · [Русский](../ru/INSTALL.md) · [中文](../zh/INSTALL.md) -> Rewritten 2026-11-08. The server is a local MCP server. **OpenCode is the primary -> host**; the Zed extension is still supported. Model downloads are optional — the -> default install is text-first and needs no embeddings, GPU or network. +> Rewritten 2026-11-12 against `install.py` v3.2. The server is a local MCP server +> and the installer is **host-neutral**: it creates a plain Python environment, and +> any MCP host launches it. Zed is an optional adapter. The default install is +> **text-first** — no embeddings, no GPU, no network and no model download unless you +> ask for them. --- @@ -19,45 +21,58 @@ | OS | Windows 10+ (validated platform), Linux (CI), macOS | | Python | **3.14+** (only 3.14 is tested) | | RAM | 4 GB; 8 GB+ if you enable local models | -| Disk | ~500 MB without models; up to ~2 GB with GGUF models | -| Host | OpenCode (primary) or Zed, or any MCP client | +| Disk | ~1.5 GB without models (venv dominates); +~650 MB with `--with-models` | +| Host | OpenCode, Zed, Claude Code, Cursor or any MCP client | | Optional | LM Studio / Ollama / llama.cpp for rerank and the legacy dense path | Docker and WSL are not required on the development machine. The container build is a deployment option (`deploy/docker/`). -## 2. Full install +## 2. Install ```bash cd -python install.py # interactive: venv, pip, optional llama.cpp + models +python install.py # venv + dependencies + code; text-first +python install.py --with-models # same + ONNX/GGUF models (~657 MB, network) +python install.py --no-zed # do not touch Zed settings at all +python install.py --yes # non-interactive: accept every step ``` Windows also has `install.bat`; Linux/macOS have `install.sh`. The installer: -1. checks Python and the environment; -2. creates/updates the virtual environment and installs dependencies; -3. optionally downloads llama.cpp and GGUF models; -4. copies the code into the installed extension directory; -5. writes `uninstall.bat`. +1. stops stale MCP/llama processes so the new code is really the code that runs; +2. creates/updates the virtual environment and installs the pinned dependencies; +3. copies the code into the install root (`--sync-only` skips 1–2 and does just this); +4. optionally (Zed adapter) writes the `context_servers` entry into `settings.json`; +5. writes `uninstall.bat` / `uninstall.sh` into the install root. -Without models the server still answers code questions — search falls back to -SQLite FTS5/BM25 plus the graph. +Default install root (the name is historical, nothing Zed-specific about the +contents): -## 3. Update loop (the part people get wrong) +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` -The installed copy lives outside the repository: +Relocate it with `MSCODEBASE_INSTALL_ROOT=`; caches/logs/artifacts go to +`MSCODEBASE_DATA_DIR` (default `%LOCALAPPDATA%\mscodebase`). What lands where, with +measured sizes: [DATA_MAP.md](DATA_MAP.md). -``` -%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +Without the installer (any manager that exposes console scripts, entry point +`mscodebase-mcp = src.main:main`): + +```bash +pipx install . # or: uv tool install . ``` -The directory name is historical; OpenCode launches the same venv -(`…\venv\Scripts\mscodebase-mcp.exe -u -m src.main`). Therefore: +## 3. Update loop (the part people get wrong) + +The installed copy lives outside the repository: ```bash python install.py --sync-only # copy code, drop __pycache__, no pip/models -# then restart the host: the OpenCode session (or the Zed window) +# then restart the host: the OpenCode session, the Zed window, the CLI process ``` `--sync-only` (alias `--sync`) is the fast path after every source change. Skipping @@ -66,28 +81,18 @@ installed location. Both are required. ## 4. Connect a host -**OpenCode** — declare a local MCP server pointing at the installed venv: - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], - "environment": { - "PYTHONPATH": "", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" - } - } -} -``` +Do not hand-write the command: print it, then paste it into the host config. -**Zed** — the extension reads the same directory; no extra configuration beyond the -extension itself. +```bash +python install.py --print-config --client generic # mcpServers-style hosts +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` -**Claude Code / VS Code / Cursor** — copy the shape from `adapters/clients/` and fill -in the venv path, repo path and project path. See -[CLIENTS.md](CLIENTS.md) for the full matrix. +Per-client file names, copy-paste JSON and the launch check +(`python scripts/verify_client_snippets.py`) are in [CLIENTS.md](CLIENTS.md). ## 5. Verify @@ -109,7 +114,36 @@ python scripts/rebuild_graph.py # graph only Then restart the server so the new process opens the rebuilt stores. Measured numbers for this repository are in [FACTS.md](../FACTS.md). -## 6. Optional providers +## 6. What is on disk, and how to remove it + +```bash +python install.py --doctor # versions, paths, sizes, index state (--json) +python install.py --clean --dry-run # every artifact with its size; deletes nothing +python install.py --clean # asks for confirmation, then deletes +python install.py --clean --yes # no prompt (scripts) +``` + +`--clean` removes the install root (code + venv + models + uninstaller), the data +root (`projects/`, `logs/`, `models/`, `repos/`) and the current project's +`.mscodebase`, `.codebase_indices`, `.codebase`, `.codebase_models`. It refuses to +delete a path that is the source checkout, a filesystem root or the home directory, +and it reports any residue (locked files from a running host) with a non-zero exit +code. Cache policies (log rotation, 7/30/90-day pruning, LRU(5)+24 h repo cache) are +documented in [DATA_MAP.md](DATA_MAP.md). + +## 7. Uninstall + +```bash +python install.py --clean --yes # all layers, verified: 0 bytes left +./uninstall.bat # Windows: generated by install.py step_uninst +``` + +The generated uninstaller kills the server processes and removes only the install +root. `--clean` is the complete version. Neither touches your host config: delete the +`mscodebase` entry from `settings.json` (Zed), `opencode.json` (OpenCode) or +`.mcp.json` / `.cursor/mcp.json` yourself. + +## 8. Optional providers | Provider | Why | Where | |---|---|---| @@ -121,18 +155,7 @@ for this repository are in [FACTS.md](../FACTS.md). None of them is needed for the default text-first answer. Provider settings: [CONFIGURATION.md](CONFIGURATION.md). -## 7. Uninstall - -```bash -./uninstall.bat # Windows -python install.py --uninstall -``` - -This removes the installed extension copy. Project data under -`%LOCALAPPDATA%/mscodebase/` and the project's `.mscodebase/chunks.db` are separate; -delete them manually if you want a clean slate. - -## 8. Troubleshooting +## 9. Troubleshooting | Symptom | Likely cause | |---|---| @@ -141,3 +164,4 @@ delete them manually if you want a clean slate. | New code has no effect | no `--sync-only` and/or no host restart | | Server starts, tools time out | provider or LSP hanging — see [GRACEFUL_DEGRADATION.md](GRACEFUL_DEGRADATION.md) | | Windows console windows flashing | subprocess without `CREATE_NO_WINDOW` — see [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md) | +| `--clean` left files behind | a running host holds them (locks) — stop the host and re-run | diff --git a/docs/en/INSTALL_MODELS.md b/docs/en/INSTALL_MODELS.md index f2c3df5d..17e41f1c 100644 --- a/docs/en/INSTALL_MODELS.md +++ b/docs/en/INSTALL_MODELS.md @@ -30,15 +30,17 @@ time; the download path does not compare them, so a hash here is a reference value, not a verified-pin. (The llama.cpp **binary** archive *is* hash-checked before extraction.) -## 2. Method 1 — `install.py` (interactive) +## 2. Method 1 — `install.py` (models are opt-in) ```bash -python install.py +python install.py --with-models ``` -12 steps. Network steps ask before they act: +Without `--with-models` the installer is text-first: `step_models` and `step_gguf` +return immediately (`install.py::models_enabled`), so no model is downloaded, no +model directory is created and no prompt is shown. With `--with-models`: -- ONNX models: *"Download ONNX models (e5-small-int8 ~113 MB + reranker ~544 MB)? (Y/n)"*; +- ONNX: `multilingual-e5-small-int8` (~113 MB) + `bge-reranker-v2-m3` (~544 MB); - GGUF + llama.cpp: the GGUF step downloads `multilingual-e5-small` and `bge-reranker-v2-m3` and the llama.cpp binary for the selected backend; - every step offers `[r]etry / [s]kip / [c]ancel` on failure. @@ -47,13 +49,15 @@ Useful flags: | Flag | Effect | |---|---| -| `--sync-only` / `--sync` | copy the code into the extension directory, touch nothing else (use this after editing `src/`) | -| `--skip-models` / `--no-models` | skip the model steps entirely | -| `--yes` / `-y` | answer yes instead of prompting | +| `--sync-only` / `--sync` | copy the code into the install root, touch nothing else (use this after editing `src/`) | +| `--with-models` | run the model steps (ONNX + GGUF); this is the only way to get them | +| `--skip-models` / `--no-models` | accepted for old scripts; models are already skipped by default | +| `--no-zed` | do not touch Zed settings (host-neutral install) | +| `--yes` / `-y` | accept every step instead of prompting | | `--verbose` / `-v` | full step output instead of the compact view | -Installation targets the extension directory -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`: +Files land in the install root (`MSCODEBASE_INSTALL_ROOT`, default the historical +extension directory `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`): - ONNX → `/.codebase_models/onnx//model_quantized.onnx` (its specialised reranker subfolder uses `model.onnx`); @@ -142,8 +146,8 @@ git-ignored (`.codebase_models/`, `models/`), so nothing appears in `git status` --- -**Verified against:** `install.py` (`STEPS`, `step_models`, `step_gguf`, -`--sync-only`, `--skip-models`, `--yes`, prompt strings), +**Verified against:** `install.py` (`STEPS`, `models_enabled`, `step_models`, +`step_gguf`, `--sync-only`, `--with-models`, `--skip-models`, `--yes`, `--no-zed`), `src/providers/reranker/llama_install.py` (`GGUF_MODELS` sizes/dims/SHA-256, `LLAMA_VERSION`, `LLAMA_PORT`, `DEFAULT_EMBEDDING_MODEL`, `resolve_ubatch`, `_patch_dll_imports`), `src/providers/reranker/llama_runner.py` diff --git a/docs/en/SEARCH_PIPELINE.md b/docs/en/SEARCH_PIPELINE.md index b91b929c..10cc1d30 100644 --- a/docs/en/SEARCH_PIPELINE.md +++ b/docs/en/SEARCH_PIPELINE.md @@ -181,3 +181,15 @@ frozen before the run: `experiments/xerj_2026_11/evidence_gate/REPORT.md`. `grep_code(..., scope="all")` → did retrieval miss, or did ranking reorder? 4. `investigate(...)` → which channels found the file (`[search]`, `[grep]`)? 5. Check the log tail: `%LOCALAPPDATA%/mscodebase/logs/mcp_global.log`. + +## 9. Code references (audited 2026-11-12) + +| Claim in this page | Code | +|---|---| +| long query refused above 512 chars | `src/mcp/tools/search_tools.py:288` | +| channel weights in `investigate` (search 0.6, grep 0.3, tests 0.1, memory 0.4) | `src/mcp/tools/investigate_tool.py:44`, `:1025`, `:1041` | +| output cap: 8 rows, 180 chars per row | `src/mcp/tools/investigate_tool.py:681` (`limit: int = 8`), `:1191` (`[:180]`) | +| docs routing margin, default 4.0 | `src/config/settings.py::SearchConfig.docs_margin` (line 268) | +| BM25 constants (`K1` 1.2, `B` 0.45) | `src/config/settings.py` lines 219, 222 | +| request guard, FTS5 candidate generation | `src/core/search/engine.py` | + diff --git a/docs/en/SYSTEM_REQUIREMENTS.md b/docs/en/SYSTEM_REQUIREMENTS.md index 45681aea..c6af3d6c 100644 --- a/docs/en/SYSTEM_REQUIREMENTS.md +++ b/docs/en/SYSTEM_REQUIREMENTS.md @@ -56,9 +56,9 @@ After editing `src/`, run `python install.py --sync-only` and restart the host. Rule of thumb: the text index is proportional to the indexed corpus and roughly an order of magnitude smaller than a vector index over the same corpus. The optional model directories dominate when they exist and are absent otherwise. -`install.py` downloads models only when asked (`--skip-models`, or answering "no" -to its prompt), so a text-only installation is the size of the code plus the -chunk store. +`install.py` downloads models only with `--with-models` (`models_enabled()` in +`install.py`); the default is text-first with no prompt and no download, so a +text-only installation is the size of the code plus the chunk store. `artifact_paths.check_disk_space()` warns when the data root has less than **500 MB** free (`MIN_FREE_DISK_MB`); the CI guard fails below 1 GB. diff --git a/docs/ru/ARCHITECTURE.md b/docs/ru/ARCHITECTURE.md index d003fe54..0a908d7c 100644 --- a/docs/ru/ARCHITECTURE.md +++ b/docs/ru/ARCHITECTURE.md @@ -156,3 +156,17 @@ replace/insert) с preview и impact-guard. - **Векторное хранилище:** LanceDB, только когда включён dense-retrieval. Ни один из них не нужен для дефолтного text-first пути. + +## 9. Ссылки на код (аудит 2026-11-12) + +| Утверждение на странице | Код | +|---|---| +| точку входа строит сервер, сначала DI | `src/main.py::main`, `src/mcp/server_factory.py::create_mcp_server` (строка 156), `src/core/di_container.py` | +| раскладка артефактов | `src/core/artifact_paths.py::get_data_root`, `::project_hash`, `::get_graph_db_path`, `::get_logs_dir` | +| чанки в `/.mscodebase/chunks.db` | `src/config/settings.py::SearchConfig.sqlite_chunks` | +| `experiments/**` исключён из индексации | `src/core/indexing/file_guard.py` | +| владелец поиска | `src/core/search/engine.py::Searcher` | +| парсинг и граф | `src/core/indexing/parser.py`, `src/core/graph.py` | +| профиль `core` = 17 из 69 | `src/mcp/server_tools.py::CORE_PROFILE`; замер `tools/list`: 17 / 69, 2026-11-12 | +| опциональный LSP-слой | инструменты `lsp_*`, кеш отказов 30 с (`src/config/settings.py::LSPConfig`) | + diff --git a/docs/ru/CLIENTS.md b/docs/ru/CLIENTS.md index ece54621..188dc529 100644 --- a/docs/ru/CLIENTS.md +++ b/docs/ru/CLIENTS.md @@ -1,107 +1,253 @@ # Клиенты и развёртывание -> Перевод: 2026-11-08, источник — docs/en/CLIENTS.md. Английская версия — канон. +> Перевод: 2026-11-12, источник — docs/en/CLIENTS.md. Английская версия — канон. -> Переписано 2026-11-08. Описывает, как сервер запускается каждым хостом и что -> на самом деле устанавливает `install.py`. Проверено по `install.py`, `extension.toml`, -> `adapters/clients/*`, `.opencode/plugin/` и живому конфигу OpenCode на машине -> разработки. +> Переписано 2026-11-12. Описывает, как сервер запускается каждым хостом и что +> на самом деле устанавливает `install.py`. Проверено по `install.py`, +> `adapters/clients/snippets.py`, `src/main.py`, `extension.toml`, +> `.opencode/plugin/`, `tests/test_install_cli.py` и +> `scripts/verify_client_snippets.py` (последний запускает сервер из каждого сниппета). ## 1. Что такое сервер -Python MCP-сервер (`src/main.py`), говорящий по stdio, плюс опциональная точка -входа Streamable-HTTP (`src/remote_main.py`) для docker/удалённого развёртывания. +Python MCP-сервер (`src/main.py`, entry point `mscodebase-mcp = src.main:main` в +`pyproject.toml`), говорящий по stdio, плюс опциональная точка входа +Streamable-HTTP (`src/remote_main.py`) для docker/удалённого развёртывания. Сам по себе он не плагин редактора — хост запускает его и общается по MCP. -## 2. OpenCode (основной) - -OpenCode запускает сервер как локальный MCP-сервер. На машине разработки запись -выглядит так (пути сокращены): - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": [ - "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence\\venv\\Scripts\\mscodebase-mcp.exe", - "-u", "-m", "src.main" - ], - "environment": { - "PYTHONPATH": "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" +С v3.2 установщик **нейтрален к хосту**: на диск кладётся обычное Python-окружение, +и любой MCP-клиент может его запустить. Zed — один из адаптеров, не обязательный шаг. + +## 2. Установить сервер один раз + +```bash +cd +python install.py # venv + зависимости + код, text-first, без моделей +python install.py --with-models # то же + ONNX/GGUF модели (~657 МБ) +python install.py --no-zed # не трогать настройки Zed вообще +python install.py --sync-only # после правки кода: только копирование кода +``` + +Куда ставится (по умолчанию исторический путь; к Zed относится только имя): + +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` + +Перенести в любое место: `MSCODEBASE_INSTALL_ROOT=`. Кэши, логи и артефакты +проектов лежат отдельно (`MSCODEBASE_DATA_DIR`, по умолчанию +`%LOCALAPPDATA%\mscodebase`). Полный инвентарь — [DATA_MAP.md](DATA_MAP.md). + +Альтернатива без установщика (любой менеджер, дающий console scripts): + +```bash +pipx install . # или: uv tool install . +mscodebase-mcp # entry point появляется в PATH +``` + +## 3. Получить конфиг для своего хоста + +Не пишите команду и окружение руками — напечатайте их: + +```bash +python install.py --print-config --client generic # любой mcpServers-хост +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` + +Каждая команда печатает один JSON-объект и ничего больше, поэтому вывод можно +перенаправить: `python install.py --print-config --client cursor > .cursor/mcp.json`. +В сниппете уже разрешённый интерпретатор, `-u -m src.main`, `PYTHONPATH`, +`PROJECT_PATH` и `PYTHONUTF8=1`. + +### 3.1 Универсальный MCP-клиент (`mcpServers`) + +Файл: там, где хост читает свой MCP-конфиг. `` — рабочее пространство, +которое нужно индексировать (это и есть `PROJECT_PATH`). + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } } } } ``` -Два факта, которые важны: +### 3.2 OpenCode + +Файл: `opencode.json(c)` в корне проекта или в `~/.config/opencode/`. OpenCode V2 +держит имена серверов под **`mcp.servers`** — плоская V1-форма `mcp.` ещё +принимается для миграции, но в новых конфигах не используется (источник: +, «V2 does not place server names directly +under `mcp`»): + +```json +{ + "mcp": { + "servers": { + "mscodebase": { + "type": "local", + "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], + "environment": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } + } +} +``` -1. **Установленная копия** живёт в каталоге расширения, а не в git-чекауте. - Именно `install.py` кладёт туда актуальный код (см. §5). -2. Каталог называется `…\Zed\extensions\…` по историческим причинам. Это обычное - окружение Python; OpenCode использует его так же, как расширение Zed. +Или пусть запишет CLI (сохраняет остальные настройки): -Локальная для проекта конфигурация: `.opencode/plugin/mscodebase_gate.ts` (хуки -коммитов) и `.opencode/command/*.md` (переиспользуемые команды агента). См. -[HOOKS_AND_GATES.md](HOOKS_AND_GATES.md). +```bash +opencode mcp add mscodebase -- /venv/Scripts/mscodebase-mcp.exe -u -m src.main +``` -## 3. Zed (поддерживается, но не основной) +`PROJECT_PATH` можно заменить переменной рабочего пространства, если хост её +раскрывает. Изменения конфига применяются без перезапуска (`/reload` — принудительно). +Проектная автоматика — `.opencode/plugin/mscodebase_gate.ts` и +`.opencode/command/*.md` (см. [HOOKS_AND_GATES.md](HOOKS_AND_GATES.md)). + +### 3.3 Zed (поддерживается, но не основной) + +Либо установщик патчит `settings.json` (шаг по умолчанию, отключается `--no-zed`), +либо вставьте напечатанный сниппет под `context_servers`: + +```json +{ + "context_servers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "$ZED_WORKTREE_ROOT", + "PYTHONUTF8": "1" + }, + "enabled": true + } + } +} +``` -Расширение Zed (`extension.toml`, `__mscodebase_ext__.marker`) запускает тот же -сервер из того же каталога. Поведение, специфичное для Windows/Zed, описано в -[ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md); относитесь к Zed как к -альтернативному хосту, а не как к эталонной реализации. +Ключи совпадают с тем, что пишет `adapters/zed/zed_config.py::_make_server_entry`. +Windows/Zed-особенности: [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md). -## 4. Другие MCP-клиенты +### 3.4 Claude Code -`adapters/clients/` содержит готовые формы для stdio и Streamable-HTTP: +Файл `.mcp.json` в корне проекта — та же форма `mcpServers`, что в 3.1. +Эквивалентная CLI-форма: -| Файл | Клиент | Секция | -|---|---|---| -| `claude.code.mcp.json` | Claude Code / Desktop | `mcpServers` | -| `vscode.mcp.json` | VS Code (`.vscode/mcp.json`) | `servers` | +```bash +claude mcp add-json mscodebase '{"command":"/venv/Scripts/mscodebase-mcp.exe","args":["-u","-m","src.main"],"env":{"PYTHONPATH":"","PROJECT_PATH":"","PYTHONUTF8":"1"}}' +``` -Подставьте ``, ``, ``. HTTP-вариант -указывает на `http://:8089/mcp` и требует -`Authorization: Bearer `; удалённый сервер собирается из -`deploy/docker/`. +### 3.5 Cursor + +Файл: `.cursor/mcp.json` (или `~/.cursor/mcp.json` для всех проектов), форма +`mcpServers` как в 3.1: + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } +} +``` -Для скриптов и CI MCP вообще не нужен: +## 4. Проверить сниппет без GUI ```bash -python -m src.cli '' [--project ] +python scripts/verify_client_snippets.py --install-root ``` -CLI использует тот же DI-контейнер и те же классы инструментов; его диспетчер — -отобранный allowlist (`src/cli.py`), а не вся поверхность из 69 инструментов. +Скрипт рендерит сниппет каждого клиента ровно так же, как установщик, запускает эту +команду во временном каталоге, делает MCP-хендшейк `initialize` + `tools/list` и +печатает строку на клиента. Ожидаемо (замер 2026-11-12): -## 5. Установка и цикл обновления +``` +generic PASS: server=MSCodebase Intelligence Server tools=17 +opencode PASS: server=MSCodebase Intelligence Server tools=17 +zed PASS: server=MSCodebase Intelligence Server tools=17 +claude-code PASS: server=MSCodebase Intelligence Server tools=17 +cursor PASS: server=MSCodebase Intelligence Server tools=17 +``` + +`tools=17` — профиль `core` по умолчанию (зарегистрировано 69, видно 17); таблица +профилей — в README. + +Из панели агента, по порядку: -```bash -python install.py # interactive full install (venv, pip, llama.cpp, models) -python install.py --sync-only # code only: copy + drop __pycache__, no pip/models ``` +tool_help() # профиль + видимые/скрытые инструменты +get_health_report() # индекс, провайдеры, ресурсы +codebase(action="index", path="status") # chunks > 0? +``` + +## 5. Цикл обновления -`install.py` копирует `src/`, скрипты и вспомогательные файлы в -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` (или платформенный -эквивалент) и сохраняет venv расширения. После изменения кода: +Установленная копия — не git-чекаут: ``` -edit → python install.py --sync-only → restart the host (OpenCode session / Zed window) +правка src/... -> python install.py --sync-only -> перезапуск хоста (сессия / окно) ``` -Без перезапуска старый процесс продолжает отдавать старый код; без синхронизации -нового кода нет на диске в установленном каталоге. Нужны оба шага. +Без перезапуска старый процесс продолжает отдавать старый код; без sync нового кода +нет в установленном месте. Нужны оба шага. Если сервер запускается прямо из чекаута — +укажите его в `command`/`PYTHONPATH` и sync не нужен. + +## 6. Remote / Docker + +`src/remote_main.py` отдаёт Streamable HTTP с bearer-токеном; +`deploy/docker/Dockerfile` + `docker-compose.yml` собирают образ, CI гоняет +job `docker-build`. Docker — вариант развёртывания, не локальное требование. -## 6. Удалённо / Docker +```json +{ + "mcpServers": { + "mscodebase-remote": { + "type": "http", + "url": "http://:8089/mcp", + "headers": { "Authorization": "Bearer " } + } + } +} +``` + +## 7. Скрипты и CI (без MCP) + +```bash +python -m src.cli '' [--project ] +echo '' | python -m src.cli - +``` -`src/remote_main.py` открывает Streamable HTTP с аутентификацией по bearer-токену; -`deploy/docker/Dockerfile` + `docker-compose.yml` собирают образ, а CI гоняет job -`docker-build`. Docker — вариант развёртывания, а не локальное требование. +CLI использует тот же DI-контейнер и те же классы инструментов; диспетчер — это +отобранный allowlist (`src/cli.py`), а не все 69 инструментов. -## 7. Какого клиента указывать в отчёте о баге +## 8. Что указать в баг-репорте -Укажите хост (OpenCode / Zed / Claude / VS Code / CLI), профиль -(`core`/`full`) и то, как был запущен сервер. Отчёт без этих трёх пунктов не -позволяет отличить «инструмент скрыт профилем» от «инструмент сломан». +Укажите хост (OpenCode / Zed / Claude Code / Cursor / generic / CLI), профиль +(`core`/`full`) и способ запуска сервера. Отчёт без этих трёх вещей не отличает +«инструмент скрыт профилем» от «инструмент сломан». diff --git a/docs/ru/CONFIGURATION.md b/docs/ru/CONFIGURATION.md index bfab46e8..0b25bc8a 100644 --- a/docs/ru/CONFIGURATION.md +++ b/docs/ru/CONFIGURATION.md @@ -79,7 +79,7 @@ | `MSCODEBASE_LINK_TIMEOUT_SEC` | — | Бюджет прохода связывания графа | | `MSCODEBASE_REINDEX_STALL_SEC` | — | Watchdog зависаний для долгих переиндексаций | | `MSCODEBASE_IVF_MAX_ABANDONS` | — | Автоотключение IVF после N неудачных optimize/create | -| `MSCODEBASE_MAX_FILE_SIZE_MB` (security) | `10` | Файлы больше этого пропускаются | +| `MAX_FILE_SIZE_MB` (security, без префикса — так читает `SecurityConfig`, `src/config/settings.py:425`) | `10` | Файлы больше этого пропускаются | ## 5. Провайдеры (все опциональны) @@ -131,3 +131,16 @@ python -c "from src.config.settings import get_config; c=get_config(); print(c.s Конфигурация строится один раз на процесс: перезапустите MCP-хост после правки `.env`, затем `get_health_report()` должен показать новые значения (или хотя бы не старое состояние провайдера). + +## 9. Ссылки на код (аудит 2026-11-12) + +| Группа | Где читается | +|---|---| +| server/profile/storage (`MSCODEBASE_MCP_PROFILE`, `MSCODEBASE_DATA_DIR`, …) | `src/config/settings.py`, `src/mcp/server_tools.py::CORE_PROFILE`, `src/core/artifact_paths.py::get_data_root` | +| дефолты поиска (`MSCODEBASE_DOCS_MARGIN`, `_ONDEMAND_RERANK_*`, `_CODE_BM25_*`) | `src/config/settings.py::SearchConfig` (строки 189–268) | +| `MAX_FILE_SIZE_MB` (без префикса) | `src/config/settings.py::SecurityConfig` (строка 425) | +| гейты плагина (`MSCODEBASE_STALE_GATE`, `_ISOLATION_GATE`, `_GATE_LOG`, `MSCODEBASE_PY`) | `.opencode/plugin/mscodebase_gate.ts` (строки 25–27) | +| бюджет pre-commit (`MSCB_PRECOMMIT_FAST`, `MSCB_PRECOMMIT_GATE_TIMEOUT`) | `.githooks/pre-commit` (строки 33, 153) | + +`python scripts/check_docs_vs_config.py` падает, если имя из этой таблицы +перестаёт существовать в коде. diff --git a/docs/ru/CONTRIBUTING.md b/docs/ru/CONTRIBUTING.md index 0f280ac5..2d80d42d 100644 --- a/docs/ru/CONTRIBUTING.md +++ b/docs/ru/CONTRIBUTING.md @@ -37,9 +37,9 @@ python -m venv venv; venv\Scripts\activate; pip install -e ".[dev]" ``` `pip install -e .` достаточно для text-first runtime. Модели опциональны и -никогда не скачиваются молча: `python install.py` спрашивает перед каждым -скачиванием и принимает `--skip-models`; `--sync-only` копирует код в каталог -расширения, не трогая pip и модели. +никогда не скачиваются молча: `install.py` по умолчанию text-first и качает модели +только с `--with-models` (`models_enabled`); `--sync-only` копирует код в каталог +установки, не трогая pip и модели. ## 3. Тесты и линт diff --git a/docs/ru/DATA_MAP.md b/docs/ru/DATA_MAP.md new file mode 100644 index 00000000..a5399445 --- /dev/null +++ b/docs/ru/DATA_MAP.md @@ -0,0 +1,111 @@ +# Где MSCodeBase хранит данные + +> Написано 2026-11-12 и сверено с `install.py` v3.2, `src/core/artifact_paths.py`, +> `src/core/artifact_gc.py`, `src/core/log_manager.py`, +> `src/sources/git_url/__init__.py` и живым прогоном `install.py --doctor`. +> Числа измерены, не оценены. + +Один вопрос, на который отвечает эта страница: *что установщик и сервер создали на +диске, кому принадлежит каждый путь и как это убрать?* + +```bash +python install.py --doctor # что стоит и сколько весит +python install.py --clean --dry-run # пути и размеры; ничего не удаляет +python install.py --clean --yes # удалить все слои ниже +``` + +## 1. Четыре слоя + +| Слой | Путь (по умолчанию) | Кто создаёт | Что это | +|---|---|---|---| +| **Install root** | Windows `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`; macOS `~/Library/Application Support/Zed/extensions/…`; Linux `~/.local/share/zed/extensions/…` | `install.py`: `step_copy`, `step_venv`, `step_pip`, `step_llama`, `step_gguf`, `step_models`, `step_uninst` | Установленная копия: код, `venv/`, опциональные модели, деинсталлятор | +| **Data root** | `%LOCALAPPDATA%\mscodebase` (Win) · `$XDG_CACHE_HOME/mscodebase` или `~/.cache/mscodebase` (POSIX) | `src/core/artifact_paths.py::get_data_root` | Проектные кэши, логи, общие модели, клоны репозиториев | +| **Артефакты проекта** | `/.mscodebase/chunks.db` | сервер (`MSCODEBASE_SQLITE_CHUNKS`) | FTS5 текстовый индекс проекта | +| **Конфиг хоста** | Zed: `%APPDATA%\Zed\settings.json` (`context_servers`) | `install.py` шаг `step_zedcfg` → `adapters/zed/zed_config.py` | Одна запись, по которой хост запускает сервер | + +Оба корня переносимы: `MSCODEBASE_INSTALL_ROOT` и `MSCODEBASE_DATA_DIR` +(для второго по-прежнему принимается legacy-алиас `BASE_INDEX_DIR`). + +## 2. Install root — что внутри + +| Путь | Владелец | Назначение | Размер | +|---|---|---|---| +| `src/`, `scripts/`, `adapters/`, `locales/`, `docs/` | `step_copy` (`_sync_dir`) | Исполняемая копия репозитория (без `experiments/`) | несколько МБ | +| `venv/` | `step_venv`, `step_pip` | Python-окружение с запиненными зависимостями | ~1 ГБ | +| `venv/Scripts/mscodebase-mcp.exe` | entry point из `pyproject.toml [project.scripts]` | Команда stdio, которую запускают хосты | — | +| `.codebase_models/onnx/…` | `step_models` (только с `--with-models`) | ONNX-эмбеддер + реранкер | ~657 МБ | +| `llama_msvc/` | `step_llama` | бинарник llama-server для GGUF-пути | ~200 МБ | +| `models/*.gguf` | `step_gguf` (только с `--with-models`) | GGUF-эмбеддер + реранкер | ~700 МБ | +| `uninstall.bat` / `uninstall.sh` | `step_uninst` (`_build_uninstaller`) | Убивает сервер и удаляет этот каталог | < 1 КБ | +| `__mscodebase_ext__.marker` | `step_copy` | Сообщает `src/main.py`, что код запущен из установленной копии | < 1 КБ | + +Замер на машине разработки (2026-11-12, модели установлены): +**3.0 ГБ на 28 429 файлах**, почти всё — `venv/` и модели. + +## 3. Data root — что внутри + +| Путь | Владелец | Назначение | Политика | +|---|---|---|---| +| `projects//graph.db` | `artifact_paths.get_graph_db_path` | PropertyGraph: файлы, символы, вызовы, импорты | удаляется вместе с папкой проекта | +| `projects//lancedb_v2/` | `get_index_dir`, `get_db_path` | Векторный индекс (только при включённом dense) | то же | +| `projects//intelligence/` | `get_intelligence_dir` | Память проекта: ADR, инциденты, техдолг | то же | +| `projects//metrics/` | `get_metrics_dir` | История задач индексации (ETA-модель) | то же | +| `projects//commit_memory/` | `get_commit_memory_dir` | Семантический кэш коммитов | то же | +| `projects//branches/` | `get_branches_dir` | Состояние индекса по веткам | то же | +| `projects//telemetry/` | `get_telemetry_dir` | Ежедневные JSON-снимки | файлы старше **90 дней** чистятся | +| `projects//summaries_cache/` | `get_summaries_cache_dir` | LLM-описания чанков | как папка проекта | +| `projects//progress.json` | `get_progress_file` | Контракт прогресса индексации | перезаписывается | +| `projects//action_receipts.jsonl` | `src/core/action_receipt.py` | Квитанции write-действий | verified 60 д / inconclusive 7 д (`purge`) | +| `logs/mscodebase-intelligence.log` | `log_manager.get_main_log_path` | Лог сервера | ротация 2 МБ × 3 файла, удаление через **7 дней** | +| `logs/crash.json` | `get_crash_log_path` | Последний краш resource-monitor | перезаписывается | +| `models/.codebase_models/onnx/…` | `get_onnx_models_base` | Общая копия скачанных моделей | только `--clean` | +| `repos//` | `get_repos_cache_dir` | Клоны удалённых репозиториев | LRU(5) + TTL 24 ч (`src/sources/git_url`) | +| `projects//` (целиком) | — | — | неактивные папки старше **30 дней** и пустые папки чистит `artifact_gc.prune_stale_artifacts()` | + +Замер на машине разработки (2026-11-12): **302.9 МБ на 2 934 файлах**. +`--doctor` в реальном каталоге проекта, где есть ещё `chunks.db` (78 МБ) и локальная +копия модели (960 МБ), показал **4.3 ГБ на 31 385 файлах** по всем слоям. + +## 4. Артефакты проекта + +| Путь | Владелец | Назначение | +|---|---|---| +| `/.mscodebase/chunks.db` | хранилище `MSCODEBASE_SQLITE_CHUNKS` | Текстовый индекс (FTS5/BM25) основного пути поиска; `--doctor` показывает число чанков | +| `/.codebase_indices/`, `.codebase/`, `.mscodebase/telemetry/` | legacy | Переносятся в data root при первом открытии проекта (`artifact_paths.migrate_legacy_artifacts`) | +| `/.codebase_models/` | `scripts/download_model.py` (staging) | Временная копия перед установкой модели в install root | + +`experiments/**` намеренно исключён из индексации, поэтому индекс от него не растёт. + +## 5. Политики + +- **Модели — opt-in.** `install.py` по умолчанию text-first: без `--with-models` нет + ни одного сетевого запроса за модель и ни одного модельного каталога. + `--skip-models`/`--no-models` по-прежнему принимаются для старых скриптов. +- **Логи** ротируются по размеру (2 МБ, 3 файла) и удаляются через 7 дней + (`log_manager._MAX_LOG_BYTES`, `_MAX_LOG_FILES`, `_LOG_RETENTION_DAYS`). +- **Телеметрия** — 90 дней; **неактивные папки проектов** — 30 дней + (`artifact_gc.DEFAULT_*`); пустые папки проектов удаляются сразу. +- **Клоны репозиториев** — LRU из 5 с TTL 24 ч и лимитом 500 МБ после клона + (`git_url.DEFAULT_MAX_CLONE_BYTES`). +- **Общий кэш моделей** (`data_root/models`) без TTL: удаляется только + `install.py --clean`. +- **Удаление** — `install.py --clean --yes`: снимает все четыре слоя, кроме записи + в конфиге хоста (удалите запись `mscodebase` из `settings.json` / `opencode.json` / + `.mcp.json` сами, либо ставьте с `--no-zed` и не создавайте её). + +## 6. Как читать вывод doctor + +``` ++ install_root 3.0 GB 28429 file(s) C:\Users\\AppData\Local\Zed\extensions\mscodebase-intelligence ++ data_root 302.9 MB 2934 file(s) C:\Users\\AppData\Local\mscodebase ++ project_chunks 78.0 MB 4 file(s) D:\work\repo\.mscodebase + ... + +index 7278 chunk(s) (D:\work\repo\.mscodebase\chunks.db) +total 4.3 GB over 31385 file(s) +``` + +`+` — существует, `-` — нет. `index missing` значит, что текстовый индекс для +текущего каталога ещё не построен (его создаст первый вызов +`codebase(action="index")`), а не что поиск сломан. `--json` печатает те же данные +для скриптов. diff --git a/docs/ru/FAQ.md b/docs/ru/FAQ.md index 6b56ff78..0122c7a4 100644 --- a/docs/ru/FAQ.md +++ b/docs/ru/FAQ.md @@ -123,7 +123,8 @@ project_root=".")` (полный профиль: `intel_trigger_reindex()`), и Да. В дефолтном пути ничего не требует GPU, облачного API или интернета: FTS5, AST-граф и разбор через tree-sitter — всё локальное. Сеть используется только для -скачивания опциональных моделей (GGUF/ONNX через `install.py`) и опционального +скачивания опциональных моделей (`install.py --with-models`, по умолчанию выключено: +`models_enabled()` в `install.py`) и опционального `tree-sitter-language-pack`, чьи парсеры подтягиваются при первом использовании (`MSCODEBASE_LANGUAGE_PACK=true`). С выключенным флагом каждая грамматика берётся из закреплённого wheel, и никаких скачиваний не происходит. diff --git a/docs/ru/INSTALL.md b/docs/ru/INSTALL.md index a9a63075..dfe1586a 100644 --- a/docs/ru/INSTALL.md +++ b/docs/ru/INSTALL.md @@ -1,6 +1,6 @@ # Установка MSCodebase Intelligence -> Перевод: 2026-11-08, источник — docs/en/INSTALL.md. Английская версия — канон. +> Перевод: 2026-11-12, источник — docs/en/INSTALL.md. Английская версия — канон.

MSCodeBase Banner @@ -8,9 +8,11 @@ [🇬🇧 English](../en/INSTALL.md) • [🇷🇺 Русский](INSTALL.md) • [🇨🇳 中文](../zh/INSTALL.md) -> Переписано 2026-11-08. Сервер — локальный MCP-сервер. **OpenCode — основной -> хост**; расширение Zed по-прежнему поддерживается. Загрузка моделей опциональна — -> дефолтная установка text-first и не требует эмбеддингов, GPU или сети. +> Переписано 2026-11-12 по `install.py` v3.2. Сервер — локальный MCP-сервер, а +> установщик **нейтрален к хосту**: он создаёт обычное Python-окружение, и любой +> MCP-хост его запускает. Zed — опциональный адаптер. Дефолтная установка +> **text-first**: без эмбеддингов, GPU, сети и без загрузки моделей, если их не +> попросили явно. --- @@ -21,45 +23,57 @@ | ОС | Windows 10+ (проверенная платформа), Linux (CI), macOS | | Python | **3.14+** (тестируется только 3.14) | | RAM | 4 ГБ; 8 ГБ+ при включении локальных моделей | -| Диск | ~500 МБ без моделей; до ~2 ГБ с моделями GGUF | -| Хост | OpenCode (основной) или Zed, либо любой MCP-клиент | +| Диск | ~1.5 ГБ без моделей (основное — venv); +~650 МБ с `--with-models` | +| Хост | OpenCode, Zed, Claude Code, Cursor или любой MCP-клиент | | Опционально | LM Studio / Ollama / llama.cpp для реранка и legacy dense-пути | Docker и WSL на машине разработчика не нужны. Сборка контейнера — вариант развёртывания (`deploy/docker/`). -## 2. Полная установка +## 2. Установка ```bash cd -python install.py # interactive: venv, pip, optional llama.cpp + models +python install.py # venv + зависимости + код; text-first +python install.py --with-models # то же + ONNX/GGUF модели (~657 МБ, сеть) +python install.py --no-zed # вообще не трогать настройки Zed +python install.py --yes # без вопросов: принимать каждый шаг ``` В Windows также есть `install.bat`; в Linux/macOS — `install.sh`. Установщик: -1. проверяет Python и окружение; -2. создаёт/обновляет виртуальное окружение и ставит зависимости; -3. опционально скачивает llama.cpp и модели GGUF; -4. копирует код в каталог установленного расширения; -5. пишет `uninstall.bat`. +1. останавливает старые процессы MCP/llama, чтобы работал именно новый код; +2. создаёт/обновляет виртуальное окружение и ставит запиненные зависимости; +3. копирует код в каталог установки (`--sync-only` пропускает 1–2 и делает только это); +4. опционально (адаптер Zed) пишет запись `context_servers` в `settings.json`; +5. пишет `uninstall.bat` / `uninstall.sh` в каталог установки. -Без моделей сервер всё равно отвечает на вопросы о коде — поиск откатывается на -SQLite FTS5/BM25 плюс граф. +Каталог установки по умолчанию (имя историческое, к Zed относится только оно): -## 3. Цикл обновления (часть, которую обычно путают) +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` -Установленная копия живёт вне репозитория: +Перенос: `MSCODEBASE_INSTALL_ROOT=`; кэши/логи/артефакты — в +`MSCODEBASE_DATA_DIR` (по умолчанию `%LOCALAPPDATA%\mscodebase`). Что куда пишется и +сколько весит (с замерами): [DATA_MAP.md](DATA_MAP.md). -``` -%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +Без установщика (любой менеджер, дающий console scripts, entry point +`mscodebase-mcp = src.main:main`): + +```bash +pipx install . # или: uv tool install . ``` -Имя каталога историческое; OpenCode запускает тот же venv -(`…\venv\Scripts\mscodebase-mcp.exe -u -m src.main`). Поэтому: +## 3. Цикл обновления (часть, которую обычно путают) + +Установленная копия живёт вне репозитория: ```bash -python install.py --sync-only # copy code, drop __pycache__, no pip/models -# then restart the host: the OpenCode session (or the Zed window) +python install.py --sync-only # копирует код, чистит __pycache__, без pip/моделей +# затем перезапуск хоста: сессия OpenCode, окно Zed, процесс CLI ``` `--sync-only` (алиас `--sync`) — быстрый путь после каждого изменения исходников. @@ -68,50 +82,69 @@ python install.py --sync-only # copy code, drop __pycache__, no pip/models ## 4. Подключение хоста -**OpenCode** — объявите локальный MCP-сервер, указывающий на установленный venv: - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], - "environment": { - "PYTHONPATH": "", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" - } - } -} -``` +Не пишите команду руками — напечатайте её и вставьте в конфиг хоста. -**Zed** — расширение читает тот же каталог; дополнительной настройки сверх самого -расширения не требуется. +```bash +python install.py --print-config --client generic # хосты формы mcpServers +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` -**Claude Code / VS Code / Cursor** — скопируйте форму из `adapters/clients/` и -подставьте путь к venv, путь к репозиторию и путь к проекту. Полная матрица — -в [CLIENTS.md](CLIENTS.md). +Имена файлов, готовый JSON для копипаста и проверка запуска +(`python scripts/verify_client_snippets.py`) — в [CLIENTS.md](CLIENTS.md). ## 5. Проверка Из панели агента, в этом порядке: ``` -tool_help() # profile + visible/hidden tools -get_health_report() # index, providers, resources +tool_help() # профиль + видимые/скрытые инструменты +get_health_report() # индекс, провайдеры, ресурсы codebase(action="index", path="status") # chunks > 0? ``` Если текстовый индекс отсутствует или устарел: ```bash -python scripts/reindex_text_only.py --project . # chunks + graph, no embeddings -python scripts/rebuild_graph.py # graph only +python scripts/reindex_text_only.py --project . # чанки + граф, без эмбеддингов +python scripts/rebuild_graph.py # только граф ``` Затем перезапустите сервер, чтобы новый процесс открыл пересобранные хранилища. Замеренные числа для этого репозитория — в [FACTS.md](../FACTS.md). -## 6. Опциональные провайдеры +## 6. Что на диске и как это убрать + +```bash +python install.py --doctor # версии, пути, размеры, состояние индекса (--json) +python install.py --clean --dry-run # каждый артефакт с размером; ничего не удаляет +python install.py --clean # спрашивает подтверждение, затем удаляет +python install.py --clean --yes # без вопросов (скрипты) +``` + +`--clean` удаляет каталог установки (код + venv + модели + деинсталлятор), data root +(`projects/`, `logs/`, `models/`, `repos/`) и в текущем проекте `.mscodebase`, +`.codebase_indices`, `.codebase`, `.codebase_models`. Он отказывается удалять путь, +который является исходным чекаутом, корнем файловой системы или домашним каталогом, +и сообщает об остатке (заблокированные файлы работающим хостом) с ненулевым кодом +возврата. Политики кэшей (ротация логов, чистка 7/30/90 дней, LRU(5)+24 ч для клонов) +— в [DATA_MAP.md](DATA_MAP.md). + +## 7. Удаление + +```bash +python install.py --clean --yes # все слои, проверено: 0 байт остатка +./uninstall.bat # Windows: генерируется шагом step_uninst +``` + +Сгенерированный деинсталлятор убивает процессы сервера и удаляет только каталог +установки. `--clean` — полная версия. Ни то, ни другое не трогает конфиг хоста: +удалите запись `mscodebase` из `settings.json` (Zed), `opencode.json` (OpenCode) или +`.mcp.json` / `.cursor/mcp.json` сами. + +## 8. Опциональные провайдеры | Провайдер | Зачем | Где | |---|---|---| @@ -123,18 +156,7 @@ python scripts/rebuild_graph.py # graph only Ни один из них не нужен для дефолтного text-first ответа. Настройки провайдеров: [CONFIGURATION.md](CONFIGURATION.md). -## 7. Удаление - -```bash -./uninstall.bat # Windows -python install.py --uninstall -``` - -Это удаляет установленную копию расширения. Данные проекта в -`%LOCALAPPDATA%/mscodebase/` и `.mscodebase/chunks.db` проекта — отдельно; -удалите их вручную, если нужен чистый лист. - -## 8. Диагностика проблем +## 9. Диагностика проблем | Симптом | Вероятная причина | |---|---| @@ -143,3 +165,4 @@ python install.py --uninstall | Новый код не даёт эффекта | нет `--sync-only` и/или перезапуска хоста | | Сервер стартует, инструменты уходят в таймаут | завис провайдер или LSP — см. [GRACEFUL_DEGRADATION.md](GRACEFUL_DEGRADATION.md) | | Мигают консольные окна в Windows | подпроцесс без `CREATE_NO_WINDOW` — см. [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md) | +| `--clean` оставил файлы | их держит запущенный хост (блокировки) — остановите хост и повторите | diff --git a/docs/ru/INSTALL_MODELS.md b/docs/ru/INSTALL_MODELS.md index c6d37d1c..ae7742d8 100644 --- a/docs/ru/INSTALL_MODELS.md +++ b/docs/ru/INSTALL_MODELS.md @@ -33,15 +33,17 @@ SHA-256 записаны для части записей (qwen3-embedding, bge- справочное значение, а не проверенный пин. (Архив **бинарника** llama.cpp, наоборот, проверяется по хэшу перед распаковкой.) -## 2. Способ 1 — `install.py` (интерактивный) +## 2. Способ 1 — `install.py` (модели по явному запросу) ```bash -python install.py +python install.py --with-models ``` -12 шагов. Сетевые шаги спрашивают перед действием: +Без `--with-models` установщик text-first: `step_models` и `step_gguf` сразу +возвращаются (`install.py::models_enabled`), поэтому не скачивается ни одна модель, +не создаётся модельный каталог и не задаётся ни одного вопроса. С `--with-models`: -- ONNX-модели: *«Download ONNX models (e5-small-int8 ~113 MB + reranker ~544 MB)? (Y/n)»*; +- ONNX: `multilingual-e5-small-int8` (~113 МБ) + `bge-reranker-v2-m3` (~544 МБ); - GGUF + llama.cpp: шаг GGUF скачивает `multilingual-e5-small` и `bge-reranker-v2-m3`, а также бинарник llama.cpp для выбранного бэкенда; - каждый шаг при сбое предлагает `[r]etry / [s]kip / [c]ancel`. @@ -50,13 +52,15 @@ python install.py | Флаг | Эффект | |---|---| -| `--sync-only` / `--sync` | скопировать код в каталог расширения, больше ничего не трогать (используйте после правки `src/`) | -| `--skip-models` / `--no-models` | полностью пропустить шаги с моделями | -| `--yes` / `-y` | отвечать «да» вместо вопроса | +| `--sync-only` / `--sync` | скопировать код в каталог установки, больше ничего не трогать (после правки `src/`) | +| `--with-models` | выполнить шаги с моделями (ONNX + GGUF); единственный способ их получить | +| `--skip-models` / `--no-models` | принимаются для старых скриптов; по умолчанию модели и так пропускаются | +| `--no-zed` | не трогать настройки Zed (нейтральная установка) | +| `--yes` / `-y` | принимать каждый шаг вместо вопросов | | `--verbose` / `-v` | полный вывод шагов вместо компактного вида | -Установка целенаправленно пишет в каталог расширения -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`: +Файлы попадают в каталог установки (`MSCODEBASE_INSTALL_ROOT`, по умолчанию +исторический `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`): - ONNX → `/.codebase_models/onnx//model_quantized.onnx` (его специализированная подпапка реранкера использует `model.onnx`); diff --git a/docs/ru/README.md b/docs/ru/README.md index c257ab84..5417db74 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -27,7 +27,7 @@ ## Что работает сегодня -| Область | Что делает | Инструмент | +| Область | Что делает | Точка входа (entry point) | |---|---|---| | **Один вызов на вопрос** | Вопрос → параллельно поиск + символы + grep, затем impact + вызывающие + тесты + память проекта; слияние по file:line с провенансом и `degraded[]` | `investigate` | | Поиск по коду | BM25/FTS5 по чанкам; короткое замыкание по графу для идентификаторов; опциональный rerank верхних кандидатов | `search_code` | @@ -86,17 +86,36 @@ investigate("как индексатор решает, что файл надо ## Быстрый старт -Требования: Windows — проверенная платформа; Python 3.14+; MCP-хост (**основной — OpenCode**, расширение Zed тоже работает). +Требования: Windows — проверенная платформа; Python 3.14+; любой MCP-хост +(OpenCode, Zed, Claude Code, Cursor или универсальный `mcpServers`-клиент). ```bash cd -python install.py # полная установка: venv, pip, опциональные модели -python install.py --sync # только код — в установленную копию +python install.py # venv + зависимости + код: text-first, без моделей +python install.py --with-models # опционально: + ONNX/GGUF модели (~657 МБ) +python install.py --sync # после правки кода — только копирование кода ``` -После `install.py` перезапустить хост: сессию OpenCode или окно Zed. Установленная -копия лежит в `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`, и оба хоста -запускают именно её venv. Проверка из панели агента: +Установка нейтральна к хосту: на диск кладётся обычное Python-окружение (по умолчанию +`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` — имя историческое). Другой +путь задаётся `MSCODEBASE_INSTALL_ROOT`. Затем напечатайте конфиг для своего хоста +вместо того, чтобы писать его руками: + +```bash +python install.py --print-config --client generic # хосты формы mcpServers +python install.py --print-config --client opencode +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +python install.py --print-config --client zed +``` + +Каждая команда печатает один JSON-объект: интерпретатор, `-u -m src.main`, +`PYTHONPATH`, `PROJECT_PATH`, `PYTHONUTF8=1`; вставьте его в MCP-конфиг хоста. +Секции для каждого клиента и проверка запуска +(`python scripts/verify_client_snippets.py`) — в [docs/ru/CLIENTS.md](CLIENTS.md). +После настройки перезапустите хост. + +Проверка из панели агента: ```text tool_help() # профиль, видимые и скрытые инструменты @@ -244,7 +263,8 @@ python scripts/smoke_e2e.py --project . | [docs/FACTS.md](../FACTS.md) | Все публичные числа и команды перевывода | | [docs/ru/ARCHITECTURE.md](ARCHITECTURE.md) | Слои, DI, границы модулей, профиль инструментов | | [docs/ru/SEARCH_PIPELINE.md](SEARCH_PIPELINE.md) | Retrieval, ранжирование, docs-роутинг, fan-out `investigate` | -| [docs/ru/INSTALL.md](INSTALL.md) | Установка, цикл sync+restart, диагностика | +| [docs/ru/INSTALL.md](INSTALL.md) | Установка, цикл sync+restart, doctor/clean, диагностика | +| [docs/ru/DATA_MAP.md](DATA_MAP.md) | Каждый путь, созданный установщиком/сервером: размеры и политики хранения | | [docs/ru/CONFIGURATION.md](CONFIGURATION.md) | Все переменные окружения и их значения по умолчанию | | [docs/ru/CLIENTS.md](CLIENTS.md) | OpenCode, Zed, Claude Code, VS Code, CLI, Docker | | [docs/ru/HOOKS_AND_GATES.md](HOOKS_AND_GATES.md) | Хук OpenCode, 12 гейтов pre-commit, джобы CI | diff --git a/docs/ru/SEARCH_PIPELINE.md b/docs/ru/SEARCH_PIPELINE.md index ffdbdda9..83c5e912 100644 --- a/docs/ru/SEARCH_PIPELINE.md +++ b/docs/ru/SEARCH_PIPELINE.md @@ -187,3 +187,15 @@ hints=["get_progress_file"])` использует их в символьном переставило? 4. `investigate(...)` → какие каналы нашли файл (`[search]`, `[grep]`)? 5. Проверьте хвост лога: `%LOCALAPPDATA%/mscodebase/logs/mcp_global.log`. + +## 9. Ссылки на код (аудит 2026-11-12) + +| Утверждение на странице | Код | +|---|---| +| запрос длиннее 512 символов отклоняется | `src/mcp/tools/search_tools.py:288` | +| веса каналов `investigate` (search 0.6, grep 0.3, tests 0.1, memory 0.4) | `src/mcp/tools/investigate_tool.py:44`, `:1025`, `:1041` | +| потолок вывода: 8 строк, 180 символов | `src/mcp/tools/investigate_tool.py:681`, `:1191` | +| margin документов, дефолт 4.0 | `src/config/settings.py::SearchConfig.docs_margin` (строка 268) | +| константы BM25 (`K1` 1.2, `B` 0.45) | `src/config/settings.py` строки 219, 222 | +| request guard и генерация кандидатов FTS5 | `src/core/search/engine.py` | + diff --git a/docs/ru/SYSTEM_REQUIREMENTS.md b/docs/ru/SYSTEM_REQUIREMENTS.md index 81f41b5b..3efdc6c3 100644 --- a/docs/ru/SYSTEM_REQUIREMENTS.md +++ b/docs/ru/SYSTEM_REQUIREMENTS.md @@ -61,9 +61,9 @@ OpenCode запускает MCP-сервер напрямую: Практическое правило: текстовый индекс пропорционален индексируемому корпусу и примерно на порядок меньше векторного индекса по тому же корпусу. Опциональные каталоги моделей доминируют, когда они есть, и отсутствуют в остальных случаях. -`install.py` скачивает модели только по запросу (`--skip-models` или ответ «нет» -на его вопрос), поэтому установка только с текстом занимает размер кода плюс -хранилище чанков. +`install.py` скачивает модели только с `--with-models` (`models_enabled()` в +`install.py`); дефолт — text-first без вопросов и без загрузки, поэтому установка +только с текстом занимает размер кода плюс хранилище чанков. `artifact_paths.check_disk_space()` предупреждает, когда на data root остаётся меньше **500 MB** (`MIN_FREE_DISK_MB`); гейт в CI падает ниже 1 GB. diff --git a/docs/zh/ARCHITECTURE.md b/docs/zh/ARCHITECTURE.md index a45ff4a1..8c09cb2d 100644 --- a/docs/zh/ARCHITECTURE.md +++ b/docs/zh/ARCHITECTURE.md @@ -140,3 +140,17 @@ replace/insert),带 preview 与 impact guard。 - **向量存储:** LanceDB,仅在启用 dense 检索时使用。 默认的文本优先路径不需要其中任何一项。 + +## 9. 代码引用(2026-11-12 审计) + +| 本页断言 | 代码 | +|---|---| +| 入口先建 DI 再建服务器 | `src/main.py::main`、`src/mcp/server_factory.py::create_mcp_server`(第 156 行)、`src/core/di_container.py` | +| 产物布局 | `src/core/artifact_paths.py::get_data_root`、`::project_hash`、`::get_graph_db_path`、`::get_logs_dir` | +| 分块在 `/.mscodebase/chunks.db` | `src/config/settings.py::SearchConfig.sqlite_chunks` | +| `experiments/**` 不参与索引 | `src/core/indexing/file_guard.py` | +| 检索实现 | `src/core/search/engine.py::Searcher` | +| 解析与图 | `src/core/indexing/parser.py`、`src/core/graph.py` | +| 配置档 `core` = 69 中的 17 | `src/mcp/server_tools.py::CORE_PROFILE`;`tools/list` 实测:17 / 69,2026-11-12 | +| 可选 LSP 层 | `lsp_*` 工具,30 s 失败缓存(`src/config/settings.py::LSPConfig`) | + diff --git a/docs/zh/CLIENTS.md b/docs/zh/CLIENTS.md index 97acac00..9b7470a6 100644 --- a/docs/zh/CLIENTS.md +++ b/docs/zh/CLIENTS.md @@ -1,104 +1,243 @@ # 客户端与部署 -> 翻译:2026-11-08,来源 docs/en/CLIENTS.md。英文版为准。 +> 翻译:2026-11-12,来源 docs/en/CLIENTS.md。英文版为准。 -> 2026-11-08 重写。描述各宿主如何启动服务器,以及 `install.py` 究竟安装了什么。 -> 已对照 `install.py`、`extension.toml`、`adapters/clients/*`、`.opencode/plugin/` -> 以及开发机上的实时 OpenCode 配置核实。 +> 2026-11-12 重写。描述各宿主如何启动服务器,以及 `install.py` 究竟安装了什么。 +> 已对照 `install.py`、`adapters/clients/snippets.py`、`src/main.py`、`extension.toml`、 +> `.opencode/plugin/`、`tests/test_install_cli.py` 与 `scripts/verify_client_snippets.py` +> (后者会从每个片段真正启动服务器)核实。 ## 1. 服务器是什么 -一个使用 stdio 通信的 Python MCP 服务器(`src/main.py`),外加一个可选的 +一个使用 stdio 通信的 Python MCP 服务器(`src/main.py`,入口点 +`mscodebase-mcp = src.main:main`,见 `pyproject.toml`),外加一个可选的 Streamable-HTTP 入口(`src/remote_main.py`),用于 docker/远程部署。它本身不是 编辑器插件 —— 由宿主启动它并通过 MCP 通信。 -## 2. OpenCode(主要宿主) - -OpenCode 把服务器作为本地 MCP 服务器启动。开发机上的条目如下(路径已截短): - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": [ - "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence\\venv\\Scripts\\mscodebase-mcp.exe", - "-u", "-m", "src.main" - ], - "environment": { - "PYTHONPATH": "C:\\Users\\\\AppData\\Local\\Zed\\extensions\\mscodebase-intelligence", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" +自 v3.2 起安装器**与宿主无关**:它只在磁盘上放置一个普通 Python 环境,任何 MCP +客户端都能启动它。Zed 只是其中一个适配器,不是必需步骤。 + +## 2. 安装一次服务器 + +```bash +cd +python install.py # venv + 依赖 + 代码,text-first,无模型 +python install.py --with-models # 同上 + ONNX/GGUF 模型(约 657 MB) +python install.py --no-zed # 完全不修改 Zed 设置 +python install.py --sync-only # 改代码后:仅复制代码 +``` + +默认安装位置(历史路径,仅名字与 Zed 有关): + +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` + +用 `MSCODEBASE_INSTALL_ROOT=` 可放到任意位置。缓存、日志与项目产物在别处 +(`MSCODEBASE_DATA_DIR`,默认 `%LOCALAPPDATA%\mscodebase`)。完整清单见 +[DATA_MAP.md](DATA_MAP.md)。 + +不用安装器的替代方式(任何提供 console scripts 的包管理器): + +```bash +pipx install . # 或:uv tool install . +mscodebase-mcp # 入口点已在 PATH 中 +``` + +## 3. 为你的宿主生成配置 + +不要手写命令与环境变量 —— 让安装器打印: + +```bash +python install.py --print-config --client generic # 任何 mcpServers 风格宿主 +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` + +每条命令只打印一个 JSON 对象,因此可以重定向: +`python install.py --print-config --client cursor > .cursor/mcp.json`。片段中已包含 +解析后的解释器、`-u -m src.main`、`PYTHONPATH`、`PROJECT_PATH` 和 `PYTHONUTF8=1`。 + +### 3.1 通用 MCP 客户端(`mcpServers`) + +文件:宿主读取其 MCP 配置的位置。`` 是要索引的工作区(即 `PROJECT_PATH`)。 + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } } } } ``` -两个关键事实: +### 3.2 OpenCode + +文件:项目根目录或 `~/.config/opencode/` 下的 `opencode.json(c)`。OpenCode V2 把 +服务器名称放在 **`mcp.servers`** 之下 —— 扁平的 V1 形式 `mcp.` 仍被接受 +(迁移兼容),但不是新配置的写法(来源:, +"V2 does not place server names directly under `mcp`"): + +```json +{ + "mcp": { + "servers": { + "mscodebase": { + "type": "local", + "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], + "environment": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } + } +} +``` -1. **已安装副本**位于扩展目录,而不是 git checkout。把当前代码放进去的正是 - `install.py`(见 §5)。 -2. 该目录因历史原因命名为 `…\Zed\extensions\…`。它就是一个普通 Python 环境; - OpenCode 使用它的方式与 Zed 扩展完全相同。 +也可以让 CLI 写入(会保留其它设置): -项目级配置:`.opencode/plugin/mscodebase_gate.ts`(提交钩子)和 -`.opencode/command/*.md`(可复用的 agent 命令)。见 -[HOOKS_AND_GATES.md](HOOKS_AND_GATES.md)。 +```bash +opencode mcp add mscodebase -- /venv/Scripts/mscodebase-mcp.exe -u -m src.main +``` -## 3. Zed(受支持,但非主要) +若宿主会展开工作区变量,`PROJECT_PATH` 可替换为该变量。配置修改无需重启即生效 +(`/reload` 可强制重载)。项目级自动化在 `.opencode/plugin/mscodebase_gate.ts` 与 +`.opencode/command/*.md`(见 [HOOKS_AND_GATES.md](HOOKS_AND_GATES.md))。 + +### 3.3 Zed(支持,但不是主要宿主) + +可以让安装器修改 `settings.json`(默认步骤,用 `--no-zed` 关闭),或把打印出的片段 +粘贴到 `context_servers` 下: + +```json +{ + "context_servers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "$ZED_WORKTREE_ROOT", + "PYTHONUTF8": "1" + }, + "enabled": true + } + } +} +``` -Zed 扩展(`extension.toml`、`__mscodebase_ext__.marker`)从同一目录启动同一个 -服务器。Windows/Zed 特有的行为记录在 [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md); -请把 Zed 视为替代宿主,而不是参考实现。 +键与 `adapters/zed/zed_config.py::_make_server_entry` 写入的一致。Windows/Zed 细节: +[ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md)。 -## 4. 其他 MCP 客户端 +### 3.4 Claude Code -`adapters/clients/` 中包含 stdio 与 Streamable-HTTP 的现成配置形状: +项目根目录的 `.mcp.json`,与 3.1 相同的 `mcpServers` 结构。等价的 CLI 写法: -| 文件 | 客户端 | 配置段 | -|---|---|---| -| `claude.code.mcp.json` | Claude Code / Desktop | `mcpServers` | -| `vscode.mcp.json` | VS Code(`.vscode/mcp.json`) | `servers` | +```bash +claude mcp add-json mscodebase '{"command":"/venv/Scripts/mscodebase-mcp.exe","args":["-u","-m","src.main"],"env":{"PYTHONPATH":"","PROJECT_PATH":"","PYTHONUTF8":"1"}}' +``` -填入 ``、``、``。HTTP 变体指向 -`http://:8089/mcp`,并要求 -`Authorization: Bearer `;远程服务器由 -`deploy/docker/` 构建。 +### 3.5 Cursor + +文件:`.cursor/mcp.json`(或 `~/.cursor/mcp.json` 用于所有项目),结构与 3.1 相同: + +```json +{ + "mcpServers": { + "mscodebase": { + "command": "/venv/Scripts/mscodebase-mcp.exe", + "args": ["-u", "-m", "src.main"], + "env": { + "PYTHONPATH": "", + "PROJECT_PATH": "", + "PYTHONUTF8": "1" + } + } + } +} +``` -脚本和 CI 完全不需要 MCP: +## 4. 不用 GUI 验证片段 ```bash -python -m src.cli '' [--project ] +python scripts/verify_client_snippets.py --install-root ``` -CLI 使用同一个 DI 容器和工具类;其分发器是一份精选 allowlist(`src/cli.py`), -而不是全部 69 个工具的接口。 +脚本按安装器的相同方式渲染每个客户端的片段,在临时目录中启动该命令,完成 MCP +`initialize` + `tools/list` 握手,并为每个客户端打印一行。预期结果(2026-11-12 实测): + +``` +generic PASS: server=MSCodebase Intelligence Server tools=17 +opencode PASS: server=MSCodebase Intelligence Server tools=17 +zed PASS: server=MSCodebase Intelligence Server tools=17 +claude-code PASS: server=MSCodebase Intelligence Server tools=17 +cursor PASS: server=MSCodebase Intelligence Server tools=17 +``` -## 5. 安装与更新循环 +`tools=17` 是默认 `core` 档案(注册 69,可见 17);档案表见 README。 -```bash -python install.py # interactive full install (venv, pip, llama.cpp, models) -python install.py --sync-only # code only: copy + drop __pycache__, no pip/models +在代理面板中依次执行: + +``` +tool_help() # 档案 + 可见/隐藏工具 +get_health_report() # 索引、提供方、资源 +codebase(action="index", path="status") # chunks > 0? ``` -`install.py` 把 `src/`、脚本和支持文件复制到 -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`(或平台对应位置), -并保留扩展 venv。代码改动之后: +## 5. 更新循环 + +安装副本不是 git 检出: ``` -edit → python install.py --sync-only → restart the host (OpenCode session / Zed window) +修改 src/... -> python install.py --sync-only -> 重启宿主(会话 / 窗口) ``` -不重启,旧进程会继续提供旧代码;不同步,新代码就不在已安装位置的磁盘上。 -两个步骤都必需。 +不重启则旧进程继续提供旧代码;不 sync 则安装位置没有新代码。两步都必需。如果直接 +从检出运行 —— 把检出路径写进 `command`/`PYTHONPATH`,就不需要 sync。 ## 6. 远程 / Docker -`src/remote_main.py` 暴露带 bearer token 认证的 Streamable HTTP; -`deploy/docker/Dockerfile` + `docker-compose.yml` 构建镜像,CI 运行 -`docker-build` 任务。Docker 是部署选项,不是本地必需。 +`src/remote_main.py` 提供带 bearer 令牌的 Streamable HTTP; +`deploy/docker/Dockerfile` + `docker-compose.yml` 构建镜像,CI 运行 `docker-build` +job。Docker 是部署选项,不是本地必需。 + +```json +{ + "mcpServers": { + "mscodebase-remote": { + "type": "http", + "url": "http://:8089/mcp", + "headers": { "Authorization": "Bearer " } + } + } +} +``` + +## 7. 脚本与 CI(完全不用 MCP) + +```bash +python -m src.cli '' [--project ] +echo '' | python -m src.cli - +``` + +CLI 使用同一 DI 容器与同一批工具类;分发器是精选的 allowlist(`src/cli.py`), +而不是全部 69 个工具。 -## 7. bug 报告应说明哪个客户端 +## 8. 缺陷报告应包含什么 -说明宿主(OpenCode / Zed / Claude / VS Code / CLI)、profile(`core`/`full`), -以及服务器是如何启动的。缺少这三项的报告无法区分“工具被 profile 隐藏”与 -“工具损坏”。 +说明宿主(OpenCode / Zed / Claude Code / Cursor / generic / CLI)、档案 +(`core`/`full`)以及服务器的启动方式。缺少这三项的报告无法区分 +"工具被档案隐藏" 与 "工具损坏"。 diff --git a/docs/zh/CONFIGURATION.md b/docs/zh/CONFIGURATION.md index 3b2a4f7c..1b9ad4dd 100644 --- a/docs/zh/CONFIGURATION.md +++ b/docs/zh/CONFIGURATION.md @@ -75,7 +75,7 @@ | `MSCODEBASE_LINK_TIMEOUT_SEC` | — | 图连边阶段预算 | | `MSCODEBASE_REINDEX_STALL_SEC` | — | 长时间重建的停滞看门狗 | | `MSCODEBASE_IVF_MAX_ABANDONS` | — | N 次 optimize/create 失败后自动关闭 IVF | -| `MSCODEBASE_MAX_FILE_SIZE_MB`(security) | `10` | 超过此大小的文件会被跳过 | +| `MAX_FILE_SIZE_MB`(security,无前缀 —— 由 `SecurityConfig` 读取,`src/config/settings.py:425`) | `10` | 超过此大小的文件会被跳过 | ## 5. Provider(全部可选) @@ -125,3 +125,15 @@ python -c "from src.config.settings import get_config; c=get_config(); print(c.s 配置在每个进程中只构建一次:修改 `.env` 后重启 MCP 宿主,随后 `get_health_report()` 应显示新值(至少不应再显示旧的 provider 状态)。 + +## 9. 代码引用(2026-11-12 审计) + +| 分组 | 读取位置 | +|---|---| +| server/profile/storage(`MSCODEBASE_MCP_PROFILE`、`MSCODEBASE_DATA_DIR` 等) | `src/config/settings.py`、`src/mcp/server_tools.py::CORE_PROFILE`、`src/core/artifact_paths.py::get_data_root` | +| 检索默认值(`MSCODEBASE_DOCS_MARGIN`、`_ONDEMAND_RERANK_*`、`_CODE_BM25_*`) | `src/config/settings.py::SearchConfig`(第 189–268 行) | +| `MAX_FILE_SIZE_MB`(无前缀) | `src/config/settings.py::SecurityConfig`(第 425 行) | +| 插件门禁(`MSCODEBASE_STALE_GATE`、`_ISOLATION_GATE`、`_GATE_LOG`、`MSCODEBASE_PY`) | `.opencode/plugin/mscodebase_gate.ts`(第 25–27 行) | +| pre-commit 预算(`MSCB_PRECOMMIT_FAST`、`MSCB_PRECOMMIT_GATE_TIMEOUT`) | `.githooks/pre-commit`(第 33、153 行) | + +当上表中的名称在代码里消失时,`python scripts/check_docs_vs_config.py` 会失败。 diff --git a/docs/zh/CONTRIBUTING.md b/docs/zh/CONTRIBUTING.md index aaef0d64..07670ee7 100644 --- a/docs/zh/CONTRIBUTING.md +++ b/docs/zh/CONTRIBUTING.md @@ -36,8 +36,8 @@ python -m venv venv; venv\Scripts\activate; pip install -e ".[dev]" ``` 文本优先的运行时只需要 `pip install -e .`。模型是可选的,且绝不会静默下载: -`python install.py` 在每次下载前询问,并接受 `--skip-models`;`--sync-only` 只把 -代码复制进扩展目录,不碰 pip 也不碰模型。 +`install.py` 默认 text-first,只有 `--with-models` 才会下载(`models_enabled`); +`--sync-only` 只把代码复制进安装根,不碰 pip 也不碰模型。 ## 3. 测试与 lint diff --git a/docs/zh/DATA_MAP.md b/docs/zh/DATA_MAP.md new file mode 100644 index 00000000..9dcc5712 --- /dev/null +++ b/docs/zh/DATA_MAP.md @@ -0,0 +1,106 @@ +# MSCodeBase 数据存放位置 + +> 编写于 2026-11-12,已对照 `install.py` v3.2、`src/core/artifact_paths.py`、 +> `src/core/artifact_gc.py`、`src/core/log_manager.py`、 +> `src/sources/git_url/__init__.py` 以及一次真实的 `install.py --doctor` 运行核实。 +> 数字为实测,不是估算。 + +本页回答一个问题:*安装器与服务器在磁盘上创建了什么,每个路径归谁,如何清除?* + +```bash +python install.py --doctor # 装了什么、占多少空间 +python install.py --clean --dry-run # 只列路径与大小,不删除 +python install.py --clean --yes # 删除下面所有层 +``` + +## 1. 四个层 + +| 层 | 默认路径 | 创建者 | 内容 | +|---|---|---|---| +| **安装根** | Windows `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`;macOS `~/Library/Application Support/Zed/extensions/…`;Linux `~/.local/share/zed/extensions/…` | `install.py`:`step_copy`、`step_venv`、`step_pip`、`step_llama`、`step_gguf`、`step_models`、`step_uninst` | 已安装副本:代码、`venv/`、可选模型、卸载器 | +| **数据根** | `%LOCALAPPDATA%\mscodebase`(Win)· `$XDG_CACHE_HOME/mscodebase` 或 `~/.cache/mscodebase`(POSIX) | `src/core/artifact_paths.py::get_data_root` | 项目缓存、日志、共享模型、仓库克隆 | +| **项目产物** | `/.mscodebase/chunks.db` | 服务器(`MSCODEBASE_SQLITE_CHUNKS`) | 该项目的 FTS5 文本索引 | +| **宿主配置** | Zed:`%APPDATA%\Zed\settings.json`(`context_servers`) | `install.py` 的 `step_zedcfg` → `adapters/zed/zed_config.py` | 一条让宿主启动服务器的条目 | + +两个根都可重定位:`MSCODEBASE_INSTALL_ROOT` 与 `MSCODEBASE_DATA_DIR` +(后者仍接受历史别名 `BASE_INDEX_DIR`)。 + +## 2. 安装根内容 + +| 路径 | 归属 | 用途 | 大小 | +|---|---|---|---| +| `src/`、`scripts/`、`adapters/`、`locales/`、`docs/` | `step_copy`(`_sync_dir`) | 仓库的可执行副本(不含 `experiments/`) | 数 MB | +| `venv/` | `step_venv`、`step_pip` | 含锁定依赖的 Python 环境 | ~1 GB | +| `venv/Scripts/mscodebase-mcp.exe` | `pyproject.toml [project.scripts]` 的入口点 | 宿主启动的 stdio 命令 | — | +| `.codebase_models/onnx/…` | `step_models`(仅 `--with-models`) | ONNX 嵌入 + 重排模型 | ~657 MB | +| `llama_msvc/` | `step_llama` | GGUF 路径用的 llama-server | ~200 MB | +| `models/*.gguf` | `step_gguf`(仅 `--with-models`) | GGUF 嵌入 + 重排模型 | ~700 MB | +| `uninstall.bat` / `uninstall.sh` | `step_uninst`(`_build_uninstaller`) | 结束后台进程并删除本目录 | < 1 KB | +| `__mscodebase_ext__.marker` | `step_copy` | 告诉 `src/main.py` 当前运行的是已安装副本 | < 1 KB | + +开发机实测(2026-11-12,已安装模型):**3.0 GB / 28 429 个文件**,几乎全部是 +`venv/` 与模型。 + +## 3. 数据根内容 + +| 路径 | 归属 | 用途 | 策略 | +|---|---|---|---| +| `projects//graph.db` | `artifact_paths.get_graph_db_path` | PropertyGraph:文件、符号、调用、导入 | 随项目目录一起删除 | +| `projects//lancedb_v2/` | `get_index_dir`、`get_db_path` | 向量索引(仅 dense 开启时) | 同上 | +| `projects//intelligence/` | `get_intelligence_dir` | 项目记忆:ADR、事件、技术债 | 同上 | +| `projects//metrics/` | `get_metrics_dir` | 索引任务历史(ETA 模型) | 同上 | +| `projects//commit_memory/` | `get_commit_memory_dir` | 提交语义缓存 | 同上 | +| `projects//branches/` | `get_branches_dir` | 按分支的索引状态 | 同上 | +| `projects//telemetry/` | `get_telemetry_dir` | 每日 JSON 快照 | 超过 **90 天**清理 | +| `projects//summaries_cache/` | `get_summaries_cache_dir` | 分块 LLM 摘要 | 同项目目录 | +| `projects//progress.json` | `get_progress_file` | 索引进度契约 | 覆盖写入 | +| `projects//action_receipts.jsonl` | `src/core/action_receipt.py` | 写操作回执 | verified 60 天 / inconclusive 7 天(`purge`) | +| `logs/mscodebase-intelligence.log` | `log_manager.get_main_log_path` | 服务器日志 | 2 MB × 3 文件轮转,**7 天**后删除 | +| `logs/crash.json` | `get_crash_log_path` | 最近一次资源监控崩溃 | 覆盖写入 | +| `models/.codebase_models/onnx/…` | `get_onnx_models_base` | 已下载模型的共享副本 | 仅 `--clean` | +| `repos//` | `get_repos_cache_dir` | 远程仓库克隆 | LRU(5) + 24 小时 TTL(`src/sources/git_url`) | +| `projects//`(整个目录) | — | — | 不活跃超过 **30 天**的目录与空目录由 `artifact_gc.prune_stale_artifacts()` 清理 | + +开发机实测(2026-11-12):**302.9 MB / 2 934 个文件**。 +在同时含有 `chunks.db`(78 MB)与本地模型副本(960 MB)的真实项目目录中, +`--doctor` 对所有层报告 **4.3 GB / 31 385 个文件**。 + +## 4. 项目产物 + +| 路径 | 归属 | 用途 | +|---|---|---| +| `/.mscodebase/chunks.db` | `MSCODEBASE_SQLITE_CHUNKS` 存储 | 默认搜索路径使用的文本索引(FTS5/BM25);`--doctor` 报告其分块数 | +| `/.codebase_indices/`、`.codebase/`、`.mscodebase/telemetry/` | 历史遗留 | 首次打开项目时迁入数据根(`artifact_paths.migrate_legacy_artifacts`) | +| `/.codebase_models/` | `scripts/download_model.py`(暂存) | 模型安装到安装根之前的临时副本 | + +`experiments/**` 按设计不参与索引,因此不会让索引增长。 + +## 5. 策略 + +- **模型为 opt-in。** `install.py` 默认 text-first:不带 `--with-models` 时不会发起 + 任何模型下载,也不会创建模型目录。`--skip-models`/`--no-models` 仍被接受。 +- **日志**按大小轮转(2 MB,3 个文件),7 天后删除 + (`log_manager._MAX_LOG_BYTES`、`_MAX_LOG_FILES`、`_LOG_RETENTION_DAYS`)。 +- **遥测**保留 90 天;**不活跃项目目录**保留 30 天(`artifact_gc.DEFAULT_*`); + 空项目目录立即删除。 +- **仓库克隆**为 LRU(5) + 24 小时 TTL,并有 500 MB 克隆后上限 + (`git_url.DEFAULT_MAX_CLONE_BYTES`)。 +- **共享模型缓存**(`data_root/models`)无 TTL:只能由 `install.py --clean` 删除。 +- **卸载** = `install.py --clean --yes`,清除四层中的三层半,宿主配置条目除外 + (请自行从 `settings.json` / `opencode.json` / `.mcp.json` 删除 `mscodebase` + 条目,或用 `--no-zed` 安装,根本不创建它)。 + +## 6. 如何阅读 doctor 输出 + +``` ++ install_root 3.0 GB 28429 file(s) C:\Users\\AppData\Local\Zed\extensions\mscodebase-intelligence ++ data_root 302.9 MB 2934 file(s) C:\Users\\AppData\Local\mscodebase ++ project_chunks 78.0 MB 4 file(s) D:\work\repo\.mscodebase + ... + +index 7278 chunk(s) (D:\work\repo\.mscodebase\chunks.db) +total 4.3 GB over 31385 file(s) +``` + +`+` 表示存在,`-` 表示不存在。`index missing` 表示当前目录的文本索引尚未建立 +(首次 `codebase(action="index")` 会创建),而不是搜索损坏。`--json` 输出同样数据。 diff --git a/docs/zh/FAQ.md b/docs/zh/FAQ.md index a38c2414..3aee48da 100644 --- a/docs/zh/FAQ.md +++ b/docs/zh/FAQ.md @@ -109,7 +109,8 @@ Remove-Item "$env:LOCALAPPDATA\mscodebase\projects\" -Recurse -Force ## 没有 GPU 或没有网络能运行吗? 能。默认路径中没有任何东西需要 GPU、云 API 或互联网:FTS5、AST 图与 tree-sitter -解析都在本地。唯一的网络用途是下载可选模型(通过 `install.py` 获取 GGUF/ONNX) +解析都在本地。唯一的网络用途是下载可选模型(`install.py --with-models`,默认关闭: +`install.py` 的 `models_enabled()`) 以及可选的 `tree-sitter-language-pack`,其解析器在首次使用时抓取 (`MSCODEBASE_LANGUAGE_PACK=true`)。关掉该开关后,所有语法都来自固定版本的 wheel,不会发生任何下载。 diff --git a/docs/zh/INSTALL.md b/docs/zh/INSTALL.md index 6124d1bc..48de7eb9 100644 --- a/docs/zh/INSTALL.md +++ b/docs/zh/INSTALL.md @@ -1,139 +1,162 @@ +# 安装 MSCodebase Intelligence + +> 翻译:2026-11-12,来源 docs/en/INSTALL.md。英文版为准。 +

MSCodeBase Banner

[🇬🇧 English](../en/INSTALL.md) • [🇷🇺 Русский](../ru/INSTALL.md) • [🇨🇳 中文](INSTALL.md) -# 安装 MSCodebase Intelligence - -> 翻译:2026-11-08,来源 docs/en/INSTALL.md。英文版为准。 - -> 2026-11-08 重写。本服务器是一个本地 MCP 服务器。**OpenCode 是主要宿主**; -> Zed 扩展仍然受支持。模型下载是可选的 —— 默认安装是文本优先的,不需要 -> embedding、GPU 或网络。 +> 2026-11-12 依据 `install.py` v3.2 重写。服务器是本地 MCP 服务器,安装器 +> **与宿主无关**:它只创建普通 Python 环境,任何 MCP 宿主都能启动。Zed 是可选 +> 适配器。默认安装是 **text-first**:不使用嵌入、GPU、网络,除非明确要求,否则 +> 不下载任何模型。 --- -## 1. 环境要求 +## 1. 要求 | 组件 | 要求 | |---|---| -| 操作系统 | Windows 10+(已验证平台)、Linux(CI)、macOS | -| Python | **3.14+**(仅测试过 3.14) | +| 操作系统 | Windows 10+(验证平台)、Linux(CI)、macOS | +| Python | **3.14+**(仅测试 3.14) | | 内存 | 4 GB;启用本地模型时 8 GB+ | -| 磁盘 | 不含模型约 500 MB;含 GGUF 模型最多约 2 GB | -| 宿主 | OpenCode(主要)或 Zed,或任意 MCP 客户端 | -| 可选 | LM Studio / Ollama / llama.cpp,用于 rerank 和 legacy dense 路径 | +| 磁盘 | 无模型约 1.5 GB(主要是 venv);`--with-models` 再加约 650 MB | +| 宿主 | OpenCode、Zed、Claude Code、Cursor 或任意 MCP 客户端 | +| 可选 | LM Studio / Ollama / llama.cpp(重排与 legacy dense 路径) | -开发机上不需要 Docker 和 WSL。容器构建是一种部署选项(`deploy/docker/`)。 +开发机不需要 Docker/WSL。容器构建是部署选项(`deploy/docker/`)。 -## 2. 完整安装 +## 2. 安装 ```bash cd -python install.py # interactive: venv, pip, optional llama.cpp + models +python install.py # venv + 依赖 + 代码;text-first +python install.py --with-models # 同上 + ONNX/GGUF 模型(约 657 MB,需网络) +python install.py --no-zed # 完全不修改 Zed 设置 +python install.py --yes # 非交互:接受每个步骤 ``` -Windows 还有 `install.bat`;Linux/macOS 有 `install.sh`。安装器会: +Windows 另有 `install.bat`;Linux/macOS 有 `install.sh`。安装器会: -1. 检查 Python 与环境; -2. 创建/更新虚拟环境并安装依赖; -3. 可选地下载 llama.cpp 和 GGUF 模型; -4. 把代码复制到扩展安装目录; -5. 写出 `uninstall.bat`。 +1. 结束旧的 MCP/llama 进程,确保运行的是新代码; +2. 创建/更新虚拟环境并安装锁定依赖; +3. 复制代码到安装根(`--sync-only` 跳过 1–2,只做这一步); +4. 可选(Zed 适配器)向 `settings.json` 写入 `context_servers` 条目; +5. 在安装根写入 `uninstall.bat` / `uninstall.sh`。 -没有模型时服务器仍能回答代码问题 —— 搜索会回落到 SQLite FTS5/BM25 加图。 +默认安装根(名字是历史遗留,内容与 Zed 无关): -## 3. 更新循环(最容易出错的部分) +``` +Windows : %LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence +macOS : ~/Library/Application Support/Zed/extensions/mscodebase-intelligence +Linux : ~/.local/share/zed/extensions/mscodebase-intelligence +``` -已安装的副本位于仓库之外: +用 `MSCODEBASE_INSTALL_ROOT=` 可移动;缓存/日志/产物在 +`MSCODEBASE_DATA_DIR`(默认 `%LOCALAPPDATA%\mscodebase`)。各路径与实测大小: +[DATA_MAP.md](DATA_MAP.md)。 +不用安装器(任何提供 console scripts 的管理器,入口点 +`mscodebase-mcp = src.main:main`): + +```bash +pipx install . # 或:uv tool install . ``` -%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence -``` -该目录名是历史原因留下的;OpenCode 启动的是同一个 venv -(`…\venv\Scripts\mscodebase-mcp.exe -u -m src.main`)。因此: +## 3. 更新循环(最容易出错的环节) + +安装副本不在仓库里: ```bash -python install.py --sync-only # copy code, drop __pycache__, no pip/models -# then restart the host: the OpenCode session (or the Zed window) +python install.py --sync-only # 复制代码、清 __pycache__,不装 pip/模型 +# 然后重启宿主:OpenCode 会话、Zed 窗口或 CLI 进程 ``` -`--sync-only`(别名 `--sync`)是每次源码改动后的快速路径。跳过重启会让旧进程继续 -运行;跳过同步则安装位置里仍是旧代码。两者都必须做。 +`--sync-only`(别名 `--sync`)是改代码后的快速路径。不重启则旧进程继续运行;不 +sync 则安装位置仍是旧代码。两者都必需。 ## 4. 连接宿主 -**OpenCode** —— 声明一个指向已安装 venv 的本地 MCP 服务器: - -```jsonc -"mcp": { - "mscodebase-intelligence": { - "type": "local", - "command": ["/venv/Scripts/mscodebase-mcp.exe", "-u", "-m", "src.main"], - "environment": { - "PYTHONPATH": "", - "PROJECT_PATH": "${workspaceRoot}", - "PYTHONUTF8": "1" - } - } -} -``` +不要手写命令 —— 打印出来再粘贴到宿主配置: -**Zed** —— 扩展读取同一目录;除扩展本身外无需额外配置。 +```bash +python install.py --print-config --client generic # mcpServers 风格宿主 +python install.py --print-config --client opencode +python install.py --print-config --client zed +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +``` -**Claude Code / VS Code / Cursor** —— 从 `adapters/clients/` 复制配置形状,填入 -venv 路径、仓库路径和项目路径。完整矩阵见 [CLIENTS.md](CLIENTS.md)。 +各客户端的文件名、可复制 JSON 与启动校验 +(`python scripts/verify_client_snippets.py`)见 [CLIENTS.md](CLIENTS.md)。 ## 5. 验证 -在 agent 面板中按以下顺序执行: +在助手面板中依次执行: ``` -tool_help() # profile + visible/hidden tools -get_health_report() # index, providers, resources +tool_help() # 配置档 + 可见/隐藏工具 +get_health_report() # 索引、提供方、资源 codebase(action="index", path="status") # chunks > 0? ``` -如果文本索引缺失或过期: +若文本索引缺失或过期: ```bash -python scripts/reindex_text_only.py --project . # chunks + graph, no embeddings -python scripts/rebuild_graph.py # graph only +python scripts/reindex_text_only.py --project . # 分块 + 图,不用嵌入 +python scripts/rebuild_graph.py # 仅图 ``` 然后重启服务器,让新进程打开重建后的存储。本仓库的实测数字见 [FACTS.md](../FACTS.md)。 -## 6. 可选 provider +## 6. 磁盘上有什么,以及如何清除 -| Provider | 用途 | 位置 | -|---|---|---| -| llama.cpp (GGUF) | 按需 rerank、legacy dense 路径 | `LLAMA_CPP_PORT` (8080),reranker 8081 | -| LM Studio | 通过 HTTP 提供 embedding/rerank | `LM_STUDIO_HOST:1234` | -| Ollama | 通过 HTTP 提供 embedding/rerank | `OLLAMA_HOST:11434` | -| basedpyright | 六个 `lsp_*` 工具 | PATH 或扩展 venv | +```bash +python install.py --doctor # 版本、路径、大小、索引状态(--json) +python install.py --clean --dry-run # 列出每个产物及大小;不删除 +python install.py --clean # 询问确认后删除 +python install.py --clean --yes # 不询问(脚本) +``` -默认的文本优先答案不需要其中任何一项。Provider 设置见 -[CONFIGURATION.md](CONFIGURATION.md)。 +`--clean` 删除安装根(代码 + venv + 模型 + 卸载器)、数据根 +(`projects/`、`logs/`、`models/`、`repos/`)以及当前项目的 `.mscodebase`、 +`.codebase_indices`、`.codebase`、`.codebase_models`。它拒绝删除源代码检出、文件 +系统根或用户主目录,并在有残留(被运行中的宿主锁定)时以非零码退出。缓存策略 +(日志轮转、7/30/90 天清理、克隆 LRU(5)+24 小时)见 [DATA_MAP.md](DATA_MAP.md)。 ## 7. 卸载 ```bash -./uninstall.bat # Windows -python install.py --uninstall +python install.py --clean --yes # 所有层,已验证:剩余 0 字节 +./uninstall.bat # Windows:由 step_uninst 生成 ``` -这会移除已安装的扩展副本。`%LOCALAPPDATA%/mscodebase/` 下的项目数据和项目的 -`.mscodebase/chunks.db` 是分开的;如需彻底干净,请手动删除它们。 +生成的卸载器只结束服务器进程并删除安装根;`--clean` 是完整版本。两者都不修改 +宿主配置:请自行从 `settings.json`(Zed)、`opencode.json`(OpenCode)或 +`.mcp.json` / `.cursor/mcp.json` 删除 `mscodebase` 条目。 + +## 8. 可选提供方 + +| 提供方 | 用途 | 位置 | +|---|---|---| +| llama.cpp (GGUF) | 按需重排、legacy dense 路径 | `LLAMA_CPP_PORT`(8080)、重排 8081 | +| LM Studio | HTTP 嵌入/重排 | `LM_STUDIO_HOST:1234` | +| Ollama | HTTP 嵌入/重排 | `OLLAMA_HOST:11434` | +| basedpyright | 六个 `lsp_*` 工具 | PATH 或扩展 venv | + +默认 text-first 回答不需要它们中的任何一个。提供方配置: +[CONFIGURATION.md](CONFIGURATION.md)。 -## 8. 故障排查 +## 9. 故障排查 | 症状 | 可能原因 | |---|---| -| 预期工具不出现 | `core` profile —— 运行 `tool_help()`,切换到 `full` | -| 任何查询都“未找到” | 索引为空 —— 检查 `codebase(action="index", path="status")` | -| 新代码没有生效 | 未执行 `--sync-only` 和/或未重启宿主 | -| 服务器启动后工具超时 | provider 或 LSP 卡住 —— 见 [GRACEFUL_DEGRADATION.md](GRACEFUL_DEGRADATION.md) | -| Windows 控制台窗口闪烁 | subprocess 未使用 `CREATE_NO_WINDOW` —— 见 [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md) | +| 缺少期望的工具 | `core` 档案 —— 运行 `tool_help()`,切换到 `full` | +| 任何查询都"未找到" | 索引为空 —— 检查 `codebase(action="index", path="status")` | +| 新代码没有效果 | 未执行 `--sync-only` 和/或未重启宿主 | +| 服务器启动但工具超时 | 提供方或 LSP 挂起 —— 见 [GRACEFUL_DEGRADATION.md](GRACEFUL_DEGRADATION.md) | +| Windows 弹出控制台窗口 | 子进程缺少 `CREATE_NO_WINDOW` —— 见 [ZED_WINDOWS_QUIRKS.md](ZED_WINDOWS_QUIRKS.md) | +| `--clean` 留下文件 | 被运行中的宿主锁定 —— 停止宿主后重试 | diff --git a/docs/zh/INSTALL_MODELS.md b/docs/zh/INSTALL_MODELS.md index 216d0987..c82c9448 100644 --- a/docs/zh/INSTALL_MODELS.md +++ b/docs/zh/INSTALL_MODELS.md @@ -30,15 +30,17 @@ GGUF 表(`src/providers/reranker/llama_install.py` 中的 `GGUF_MODELS`)还 下载时写入日志;下载路径并不比对它们,所以这里的哈希是参考值,不是已验证的固定值。 (llama.cpp **二进制**归档在解压前*确实*会做哈希校验。) -## 2. 方法一 —— `install.py`(交互式) +## 2. 方法一 —— `install.py`(模型需显式开启) ```bash -python install.py +python install.py --with-models ``` -12 个步骤。涉及网络的步骤在执行前会询问: +不带 `--with-models` 时安装器为 text-first:`step_models` 与 `step_gguf` 立即返回 +(`install.py::models_enabled`),不下载任何模型、不创建模型目录、也不弹出任何提示。 +带 `--with-models` 时: -- ONNX 模型:*"Download ONNX models (e5-small-int8 ~113 MB + reranker ~544 MB)? (Y/n)"*; +- ONNX:`multilingual-e5-small-int8`(约 113 MB)+ `bge-reranker-v2-m3`(约 544 MB); - GGUF + llama.cpp:GGUF 步骤下载 `multilingual-e5-small` 与 `bge-reranker-v2-m3`,以及所选后端的 llama.cpp 二进制; - 每一步在失败时都提供 `[r]etry / [s]kip / [c]ancel`。 @@ -47,12 +49,15 @@ python install.py | 标志 | 作用 | |---|---| -| `--sync-only` / `--sync` | 把代码复制进扩展目录,不碰其它任何东西(修改 `src/` 之后用这个) | -| `--skip-models` / `--no-models` | 完全跳过模型步骤 | -| `--yes` / `-y` | 直接回答 yes,而不是提示 | +| `--sync-only` / `--sync` | 把代码复制进安装根,不碰其它任何东西(修改 `src/` 之后用这个) | +| `--with-models` | 执行模型步骤(ONNX + GGUF);获取模型的唯一途径 | +| `--skip-models` / `--no-models` | 为旧脚本保留;默认已跳过 | +| `--no-zed` | 不修改 Zed 设置(宿主中立安装) | +| `--yes` / `-y` | 接受每个步骤,而不是提示 | | `--verbose` / `-v` | 输出完整步骤内容,而不是紧凑视图 | -安装目标是扩展目录 `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`: +文件写入安装根(`MSCODEBASE_INSTALL_ROOT`,默认历史路径 +`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`): - ONNX → `/.codebase_models/onnx//model_quantized.onnx` (其专用 reranker 子目录使用 `model.onnx`); diff --git a/docs/zh/README.md b/docs/zh/README.md index 2a430f73..12f7c432 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -86,16 +86,33 @@ investigate("索引器如何决定重新索引一个文件") ## 快速开始 -要求:以 Windows 为验证平台;Python 3.14+;MCP 宿主(**主要是 OpenCode**,Zed 扩展也可用)。 +要求:以 Windows 为验证平台;Python 3.14+;任意 MCP 宿主(OpenCode、Zed、 +Claude Code、Cursor 或通用 `mcpServers` 客户端)。 ```bash cd -python install.py # 完整安装:venv、pip、可选模型 -python install.py --sync # 仅同步代码到已安装副本 +python install.py # venv + 依赖 + 代码:text-first,无模型 +python install.py --with-models # 可选:+ ONNX/GGUF 模型(约 657 MB) +python install.py --sync # 改代码后:仅复制代码 ``` -`install.py` 完成后重启宿主:OpenCode 会话或 Zed 窗口。已安装副本位于 -`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`,两种宿主都启动该 venv。 +安装与宿主无关:磁盘上放置的是一个普通 Python 环境(默认 +`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`,名字是历史遗留)。 +用 `MSCODEBASE_INSTALL_ROOT` 可放到别处。然后让安装器打印配置,而不是手写: + +```bash +python install.py --print-config --client generic # mcpServers 风格宿主 +python install.py --print-config --client opencode +python install.py --print-config --client claude-code +python install.py --print-config --client cursor +python install.py --print-config --client zed +``` + +每条命令只打印一个 JSON 对象:解释器、`-u -m src.main`、`PYTHONPATH`、 +`PROJECT_PATH`、`PYTHONUTF8=1`;粘贴到宿主的 MCP 配置中。各客户端的分节与启动校验 +(`python scripts/verify_client_snippets.py`)见 [docs/zh/CLIENTS.md](CLIENTS.md)。 +配置完成后重启宿主。 + 在助手面板验证: ```text @@ -243,7 +260,8 @@ python scripts/smoke_e2e.py --project . | [docs/FACTS.md](../FACTS.md) | 所有公开数字与复现命令 | | [docs/zh/ARCHITECTURE.md](ARCHITECTURE.md) | 分层、DI、模块边界、工具配置档 | | [docs/zh/SEARCH_PIPELINE.md](SEARCH_PIPELINE.md) | 检索、排序、文档路由、`investigate` fan-out | -| [docs/zh/INSTALL.md](INSTALL.md) | 安装、sync+restart 循环、故障排查 | +| [docs/zh/INSTALL.md](INSTALL.md) | 安装、sync+restart 循环、doctor/clean、故障排查 | +| [docs/zh/DATA_MAP.md](DATA_MAP.md) | 安装器/服务器创建的每个路径:大小与保留策略 | | [docs/zh/CONFIGURATION.md](CONFIGURATION.md) | 所有环境变量及其默认值 | | [docs/zh/CLIENTS.md](CLIENTS.md) | OpenCode、Zed、Claude Code、VS Code、CLI、Docker | | [docs/zh/HOOKS_AND_GATES.md](HOOKS_AND_GATES.md) | OpenCode 钩子、12 个 pre-commit 门禁、CI 任务 | diff --git a/docs/zh/SEARCH_PIPELINE.md b/docs/zh/SEARCH_PIPELINE.md index 1498eccc..f27b747e 100644 --- a/docs/zh/SEARCH_PIPELINE.md +++ b/docs/zh/SEARCH_PIPELINE.md @@ -170,3 +170,15 @@ hint 传入后为 3/4、3/4、3/4。剩下的未命中是 gold 存在歧义( 同一查询 → 是检索漏了,还是排序重排了? 4. `investigate(...)` → 哪些通道找到了该文件(`[search]`、`[grep]`)? 5. 检查日志尾部:`%LOCALAPPDATA%/mscodebase/logs/mcp_global.log`。 + +## 9. 代码引用(2026-11-12 审计) + +| 本页断言 | 代码 | +|---|---| +| 超过 512 字符的查询被拒绝 | `src/mcp/tools/search_tools.py:288` | +| `investigate` 通道权重(search 0.6、grep 0.3、tests 0.1、memory 0.4) | `src/mcp/tools/investigate_tool.py:44`、`:1025`、`:1041` | +| 输出上限:8 行、每行 180 字符 | `src/mcp/tools/investigate_tool.py:681`、`:1191` | +| 文档通道 margin,默认 4.0 | `src/config/settings.py::SearchConfig.docs_margin`(第 268 行) | +| BM25 常量(`K1` 1.2、`B` 0.45) | `src/config/settings.py` 第 219、222 行 | +| 请求守卫与 FTS5 候选生成 | `src/core/search/engine.py` | + diff --git a/docs/zh/SYSTEM_REQUIREMENTS.md b/docs/zh/SYSTEM_REQUIREMENTS.md index dbc0fdff..4d684eb4 100644 --- a/docs/zh/SYSTEM_REQUIREMENTS.md +++ b/docs/zh/SYSTEM_REQUIREMENTS.md @@ -55,8 +55,9 @@ OpenCode 直接启动 MCP 服务器: | `models/`(可选) | **839.4 MiB** | 为扩展复制的 GGUF 模型 | 经验法则:文本索引与被索引语料成正比,并且大致比同一语料上的向量索引小一个数量级。 -可选模型目录在存在时占主导,不存在时为零。`install.py` 只在被要求时下载模型 -(`--skip-models`,或在其提示处回答 "no"),所以纯文本安装的体积 = 代码 + 块存储。 +可选模型目录在存在时占主导,不存在时为零。`install.py` 只在指定 `--with-models` +时下载模型(`install.py` 的 `models_enabled()`);默认 text-first,既不提示也不下载, +所以纯文本安装的体积 = 代码 + 块存储。 当 data root 的空闲空间低于 **500 MB** 时,`artifact_paths.check_disk_space()` 会 警告(`MIN_FREE_DISK_MB`);CI 守卫在低于 1 GB 时失败。`artifact_gc` 在启动 3 s diff --git a/install.py b/install.py index 6b3d6bad..46ba870c 100644 --- a/install.py +++ b/install.py @@ -1,8 +1,22 @@ """ -MSCodebase Intelligence — Installer & Updater v3.1 +MSCodebase Intelligence — Installer & Updater v3.2 ================================================= Robust TUI with error recovery, self-healing, and safe cross-platform I/O. +Host-neutral since v3.2: the server is installed into a plain Python +environment and any MCP host can launch it. Zed is an optional adapter, not a +required step. The default install root keeps the historical Zed extension path +so existing installs keep updating in place; set MSCODEBASE_INSTALL_ROOT to put +it anywhere else. + +Non-interactive modes (no TUI, safe for scripts and tests): + install.py --print-config [--client generic|opencode|zed|claude-code|cursor] + print a ready-to-paste stdio snippet + install.py --doctor [--json] versions, paths, sizes, index state + install.py --clean [--dry-run] [--yes] + remove every artifact this installer created + install.py --help list modes + Design principles: - Every step wrapped in try/except with retry/skip/cancel - Progress bar via single \r line (no flicker) @@ -29,6 +43,16 @@ резолвится за пределы ZED_EXT_DIR. 7. step_pip теперь предпочитает requirements-lock.txt, если он есть рядом с requirements.txt (совместимо с dependency-lock практикой, принятой в проекте). + +Changelog v3.1 -> v3.2 (G5 universal install + G6 data hygiene): + 1. Zed больше не импортируется на верхнем уровне: install.py запускается на + машине без Zed и без adapters/ — адаптер подключается лениво внутри шагов. + 2. INSTALL_ROOT отвязан от Zed: MSCODEBASE_INSTALL_ROOT переопределяет каталог + установки (историческое имя папки расширения остаётся только default). + 3. Добавлены --print-config/--doctor/--clean/--help: сниппет для любого MCP- + клиента, инвентарь «что стоит и сколько весит», полное удаление по слоям. + 4. Модели стали opt-in (--with-models): по умолчанию text-first, ни одного + сетевого запроса за модели во время установки. """ import io @@ -45,18 +69,34 @@ from pathlib import Path from typing import Optional -from adapters.zed.zed_config import get_zed_config_dir, patch_zed_settings - logger = logging.getLogger(__name__) logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s") PROJECT_ROOT = Path(__file__).resolve().parent -ZED_EXT_DIR = ( + +# ─── Roots: server installation vs host adapter ───────────────────────────── +# The installed copy is a plain Python environment. Nothing about it is +# Zed-specific; the path below is only the historical default so that existing +# installs keep updating in place. MSCODEBASE_INSTALL_ROOT moves it anywhere +# (a neutral location, a CI temp dir, a per-user tools directory). +LEGACY_INSTALL_ROOT = ( Path(os.environ.get("LOCALAPPDATA", os.path.expanduser("~"))) / "Zed" / "extensions" / "mscodebase-intelligence" ) + + +def resolve_install_root() -> Path: + """INSTALL_ROOT from MSCODEBASE_INSTALL_ROOT, else the legacy Zed path.""" + raw = os.environ.get("MSCODEBASE_INSTALL_ROOT", "").strip() + if raw: + return Path(raw).expanduser().resolve() + return LEGACY_INSTALL_ROOT + + +INSTALL_ROOT = resolve_install_root() +ZED_EXT_DIR = INSTALL_ROOT # legacy alias: the Zed extension reads this tree VENV_DIR = ZED_EXT_DIR / "venv" IS_WINDOWS = sys.platform == "win32" PYTHON_EXE = VENV_DIR / "Scripts" / "python.exe" if IS_WINDOWS else VENV_DIR / "bin" / "python3" @@ -383,6 +423,19 @@ def deco(fn): @_step(0) def step_zed(lines, lang): + """Optional host adapter: the Zed config directory only. + + The server itself does not need Zed. On a machine without the adapter the + step is a no-op, not a failure (G5: server != host adapter). + """ + if not zed_enabled(): + lines.append((C.D, "· Skipped (--no-zed)")) + return + try: + from adapters.zed.zed_config import get_zed_config_dir + except Exception as e: # adapter absent / broken: server still installs + lines.append((C.YEL, f"· Zed adapter unavailable ({e})")) + return zcd = get_zed_config_dir() zcd.mkdir(parents=True, exist_ok=True) lines.append((C.GRN, f"✓ {zcd}")) @@ -662,7 +715,15 @@ def step_llama(lines, lang): @_step(7) def step_gguf(lines, lang): - """Скачивает GGUF модели: multilingual-e5-small (embedder) + bge-reranker-v2-m3.""" + """Скачивает GGUF модели: multilingual-e5-small (embedder) + bge-reranker-v2-m3. + + G6: opt-in. Text-first — дефолт, поэтому без --with-models шаг ничего не + качает и не трогает диск. + """ + if not models_enabled(): + lines.append((C.D, "· Skipped (text-first default; add --with-models)")) + return + from src.providers.reranker.llama_runner import is_model_downloaded, download_gguf_model, GGUF_MODELS, _get_models_dir all_ok = True @@ -711,9 +772,15 @@ def step_gguf(lines, lang): def step_models(lines, lang): """Скачивает ONNX-модели: multilingual-e5-small-int8 (embedder) + bge-reranker-v2-m3. - Проверяет по порядку: ZED_EXT_DIR -> общий кэш ~/.cache/mscodebase -> PROJECT_ROOT. + Проверяет по порядку: ZED_EXT_DIR -> общий кэш data_root/models -> PROJECT_ROOT. Найдено в (2) или (3) -> копирует в (1). Не найдено нигде -> скачивает. + + G6: opt-in. Без --with-models шаг не делает сетевых запросов: дефолтная + установка text-first и весит без моделей. """ + if not models_enabled(): + lines.append((C.D, "· Skipped (text-first default; add --with-models)")) + return SHARED_DIR = Path.home() / ".cache" / "mscodebase" / "models" models = { "multilingual-e5-small-int8": ("keisuke-miyako/multilingual-e5-small-onnx-int8", "embedding", 113), @@ -839,6 +906,18 @@ def step_zedcfg(lines, lang): # Имя exe — то, что показывает Task Manager (у pythonw нет консоли для # title): запускаемся через mscodebase-mcp.exe-хардлинк → pythonw.exe, # fallback — plain pythonw при ошибке создания линка. + # + # G5: шаг опционален. Любой другой хост получает конфиг через + # `install.py --print-config --client <...>`; отказ Zed-адаптера не роняет + # установку. + if not zed_enabled(): + lines.append((C.D, "· Skipped (--no-zed)")) + return + try: + from adapters.zed.zed_config import patch_zed_settings + except Exception as e: + lines.append((C.YEL, f"· Zed adapter unavailable ({e})")) + return mcp_exe = str(_ensure_mcp_hardlink()) if IS_WINDOWS else str(MCP_PYTHON) cmd = f"{mcp_exe} -u -m src.main" if patch_zed_settings( @@ -888,6 +967,406 @@ def _build_uninstaller() -> str: ) +# ─── Non-interactive modes: config snippet / doctor / clean ──────────────── +def zed_enabled(argv: Optional[list[str]] = None) -> bool: + """Zed adapter is opt-out: --no-zed keeps the install host-neutral.""" + argv = sys.argv if argv is None else argv + return "--no-zed" not in argv + + +def models_enabled(argv: Optional[list[str]] = None) -> bool: + """Models are opt-in: text-first by default (G6). + + `--skip-models`/`--no-models` stay accepted so old commands keep working. + """ + argv = sys.argv if argv is None else argv + return "--with-models" in argv + + +def _human_bytes(n: int) -> str: + size = float(n) + for unit in ("B", "KB", "MB", "GB", "TB"): + if size < 1024 or unit == "TB": + return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" + + +def _usage(path: Path) -> tuple[int, int]: + """(bytes, files) под path; симлинки не разворачиваются, ошибки — не фатальны.""" + if not path.exists(): + return 0, 0 + try: + if path.is_file(): + return path.stat().st_size, 1 + except OSError: + return 0, 0 + total = files = 0 + for root, _dirs, names in os.walk(path, followlinks=False): + for name in names: + fp = Path(root) / name + try: + total += fp.stat().st_size + files += 1 + except OSError: + continue + return total, files + + +def _data_root_readonly() -> Path: + """data_root без создания каталога (source of truth: src/core/artifact_paths.get_data_root). + + Порядок источников повторяет artifact_paths: MSCODEBASE_DATA_DIR → + BASE_INDEX_DIR → платформенный default. Отличие только одно: doctor/clean + не должны создавать пустые каталоги, поэтому mkdir здесь не вызывается. + """ + env = os.getenv("MSCODEBASE_DATA_DIR", "").strip() + if env: + return Path(env).expanduser() + legacy = os.getenv("BASE_INDEX_DIR", "").strip() + if legacy and Path(legacy).is_absolute(): + return Path(legacy).expanduser() + if IS_WINDOWS: + return Path(os.environ.get("LOCALAPPDATA", "")) / "mscodebase" + xdg = os.environ.get("XDG_CACHE_HOME", "").strip() + base = Path(xdg) if xdg else Path.home() / ".cache" + return base / "mscodebase" + + +def _mcp_launch_command(argv: list[str]) -> tuple[str, list[str]]: + """The exact command a host must run: installed entry point, else venv python. + + `--python PATH` overrides the interpreter (used by the cold-start test and by + people who installed with pipx/uv, where `mscodebase-mcp` is on PATH). + """ + if "--python" in argv: + idx = argv.index("--python") + if idx + 1 >= len(argv): + raise ValueError("--python needs a path") + return argv[idx + 1], ["-u", "-m", "src.main"] + for exe in (MCP_EXE, PYTHON_EXE): + try: + if Path(exe).exists(): + return str(exe), ["-u", "-m", "src.main"] + except OSError: + continue + return sys.executable, ["-u", "-m", "src.main"] + + +def _print_config(client: str, project: Path, argv: list[str]) -> int: + """Print a ready-to-paste stdio snippet for one MCP host.""" + try: + from adapters.clients.snippets import CLIENTS, render_snippet + except Exception as e: # pragma: no cover - adapters are shipped with the repo + print(f"error: client adapters unavailable: {e}", file=sys.stderr) + return 1 + + if client not in CLIENTS: + print( + f"error: unknown client '{client}'. Known: {', '.join(CLIENTS)}", + file=sys.stderr, + ) + return 2 + + command, args = _mcp_launch_command(argv) + env = { + "PYTHONPATH": str(INSTALL_ROOT), + "PROJECT_PATH": str(project), + "PYTHONUTF8": "1", + } + print(render_snippet(client, command=command, args=args, env=env), end="") + return 0 + + +def _count_chunks(db_path: Path) -> Optional[int]: + """Число чанков в SQLite-сторе; None — файла нет/схема не та.""" + if not db_path.is_file(): + return None + import sqlite3 + + try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5) + try: + return int(conn.execute("SELECT COUNT(*) FROM chunks").fetchone()[0]) + finally: + conn.close() + except Exception as e: + logger.debug("chunk count failed for %s: %s", db_path, e) + return None + + +def _doctor_report(project: Path) -> dict: + """Inventory of everything this project puts on disk (G6). + + Read-only: no directory is created. Sizes are measured, never estimated. + """ + data_root = _data_root_readonly() + venv_python = PYTHON_EXE + + raw_layers = [ + ("install_root", INSTALL_ROOT), + ("data_root", data_root), + ("project_chunks", project / ".mscodebase"), + ("project_legacy_index", project / ".codebase_indices"), + ("project_legacy_codebase", project / ".codebase"), + ("project_models", project / ".codebase_models"), + ] + + # Nested paths would be counted twice (e.g. data_root inside install_root). + seen: list[Path] = [] + layers = [] + for name, path in raw_layers: + try: + resolved = path.resolve() + except OSError: + resolved = path + if any(resolved == s or s in resolved.parents for s in seen): + continue + seen.append(resolved) + size, files = _usage(path) + layers.append( + { + "name": name, + "path": str(path), + "exists": path.exists(), + "bytes": size, + "files": files, + } + ) + + chunks_db = project / ".mscodebase" / "chunks.db" + chunks = _count_chunks(chunks_db) + + zed_settings = None + try: + from adapters.zed.zed_config import get_zed_config_dir + + zed_settings = str(get_zed_config_dir() / "settings.json") + except Exception as e: + logger.debug("zed adapter unavailable for doctor: %s", e) + + venv_version = None + if venv_python.exists(): + r = _run(f'"{venv_python}" --version', timeout=10) + if r and r.returncode == 0: + venv_version = r.stdout.strip() + + return { + "python": { + "version": sys.version.split()[0], + "executable": sys.executable, + "venv_python": str(venv_python), + "venv_python_exists": venv_python.exists(), + "venv_version": venv_version, + }, + "install_root": str(INSTALL_ROOT), + "data_root": str(data_root), + "layers": layers, + "index": { + "path": str(chunks_db), + "exists": chunks_db.exists(), + "chunks": chunks, + }, + "host_config": {"zed_settings": zed_settings}, + "total_bytes": sum(layer["bytes"] for layer in layers), + "total_files": sum(layer["files"] for layer in layers), + } + + +def _doctor_cli(argv: list[str]) -> int: + project = Path.cwd() + report = _doctor_report(project) + if "--json" in argv: + print(json.dumps(report, indent=2, ensure_ascii=False)) + return 0 + + print("MSCodeBase — installed state") + print(f" python {report['python']['version']} ({report['python']['executable']})") + print(f" install root {report['install_root']}") + print(f" data root {report['data_root']}") + print() + for layer in report["layers"]: + mark = "+" if layer["exists"] else "-" + print( + f" {mark} {layer['name']:<22} {_human_bytes(layer['bytes']):>9}" + f" {layer['files']:>5} file(s) {layer['path']}" + ) + index = report["index"] + state = "missing" if not index["exists"] else ( + "unreadable" if index["chunks"] is None else f"{index['chunks']} chunk(s)" + ) + print(f"\n index {state} ({index['path']})") + if report["host_config"]["zed_settings"]: + print(f" zed settings {report['host_config']['zed_settings']}") + print( + f"\n total {_human_bytes(report['total_bytes'])}" + f" over {report['total_files']} file(s)" + ) + return 0 + + +def _clean_targets(project: Path) -> list[tuple[str, Path]]: + """Every layer an install can create, outermost first.""" + return [ + ("install_root", INSTALL_ROOT), + ("data_root", _data_root_readonly()), + ("project_chunks", project / ".mscodebase"), + ("project_legacy_index", project / ".codebase_indices"), + ("project_legacy_codebase", project / ".codebase"), + ("project_models", project / ".codebase_models"), + ] + + +def _clean_refusal(path: Path) -> str: + """Why deleting path would be unsafe ('' = safe). Guards the source checkout.""" + try: + resolved = path.resolve() + except OSError as e: + return f"unresolvable path ({e})" + if not resolved.exists(): + return "" + if resolved == PROJECT_ROOT or resolved in PROJECT_ROOT.parents: + return "would delete the source checkout" + if resolved.parent == resolved: + return "filesystem root" + if resolved == Path.home().resolve(): + return "home directory" + return "" + + +def _clean_cli(argv: list[str]) -> int: + """Remove every artifact created by the installer, layer by layer.""" + project = Path.cwd() + dry_run = "--dry-run" in argv + assume_yes = "--yes" in argv or "-y" in argv + as_json = "--json" in argv + + plan = [] + for name, path in _clean_targets(project): + size, files = _usage(path) + plan.append( + { + "name": name, + "path": str(path), + "exists": path.exists(), + "bytes": size, + "files": files, + "refusal": _clean_refusal(path), + } + ) + + existing = [item for item in plan if item["exists"]] + removed_bytes = 0 + + if not dry_run: + blocked = [item for item in existing if item["refusal"]] + if blocked: + for item in blocked: + print(f" ! {item['name']}: {item['refusal']} — {item['path']}", file=sys.stderr) + if not assume_yes: + if not sys.stdin.isatty(): + print( + "refusing to delete without confirmation: re-run with --yes" + " (or use --dry-run to see the list)", + file=sys.stderr, + ) + return 2 + print("This will delete:") + for item in existing: + print(f" - {item['name']:<22} {_human_bytes(item['bytes']):>9} {item['path']}") + answer = input("Type 'yes' to delete everything listed above: ").strip().lower() + if answer != "yes": + print("aborted: nothing deleted") + return 1 + + for item in existing: + if item["refusal"]: + continue + path = Path(item["path"]) + if path.is_dir(): + _safe_rmtree(path) + else: + _safe_unlink(path) + removed_bytes += item["bytes"] + + remaining = 0 + remaining_detail = [] + for name, path in _clean_targets(project): + size, _files = _usage(path) + remaining += size + if size: + remaining_detail.append((name, size, str(path))) + + if as_json: + print( + json.dumps( + { + "dry_run": dry_run, + "targets": plan, + "removed_bytes": removed_bytes, + "remaining_bytes": remaining, + "remaining": [ + {"name": n, "bytes": s, "path": p} for n, s, p in remaining_detail + ], + }, + indent=2, + ensure_ascii=False, + ) + ) + else: + mode = "would remove" if dry_run else "removed" + for item in existing: + suffix = f" SKIPPED: {item['refusal']}" if item["refusal"] else "" + print( + f" {mode:<12} {item['name']:<22} {_human_bytes(item['bytes']):>9}" + f" {item['path']}{suffix}" + ) + print(f"\n {mode}: {_human_bytes(removed_bytes) if not dry_run else _human_bytes(sum(i['bytes'] for i in existing))}") + print(f" remaining: {_human_bytes(remaining)}") + for name, size, path in remaining_detail: + print(f" · {name}: {_human_bytes(size)} {path}") + + if dry_run: + return 0 + # Locked files (a running MCP host) leave residue; that is a real failure. + return 0 if remaining == 0 else 1 + + +def _print_help() -> int: + print( + "usage: install.py [mode] [options]\n\n" + "Modes (no TUI):\n" + " (default) interactive install: venv, pip, optional Zed adapter\n" + " --sync-only copy the current sources into the install root\n" + " --print-config print a stdio snippet for an MCP host\n" + " --client {generic,opencode,zed,claude-code,cursor} (default: generic)\n" + " --python PATH interpret the snippet around this interpreter\n" + " --doctor versions, paths, sizes, index state (--json)\n" + " --clean remove every artifact created by the installer\n" + " --dry-run list paths and sizes, delete nothing\n" + " --yes skip the interactive confirmation\n" + " --json machine-readable report\n\n" + "Install options:\n" + " --with-models download ONNX/GGUF models (default: text-first, no models)\n" + " --no-zed do not touch Zed settings; host-neutral install\n" + " --yes accept every step without prompts\n\n" + "Environment:\n" + " MSCODEBASE_INSTALL_ROOT where the installed copy lives\n" + " MSCODEBASE_DATA_DIR where caches/logs/project artifacts live\n" + " PROJECT_PATH project the host should index\n" + ) + return 0 + + +def _mode_from_argv(argv: list[str]) -> Optional[str]: + for mode in ("--print-config", "--doctor", "--clean", "--help"): + if mode in argv: + return mode + if "-h" in argv: + return "--help" + return None + + # ─── Main ────────────────────────────────────────────────── def _record_install_meta() -> None: """DEV-ONLY: метаданные для детекции рассинхрона source<->extension. @@ -934,11 +1413,47 @@ def _record_install_meta() -> None: logger.debug("_record_install_meta failed: %s", e) +def _utf8_stdout() -> None: + """UTF-8 stdout on Windows for every mode (paths and reports contain non-ASCII). + + The TUI already did this; the non-interactive modes used to inherit cp1251 + and crashed on the first non-ASCII byte (found 2026-11-12 on a Cyrillic path). + """ + if not IS_WINDOWS: + return + try: + sys.stdout = io.TextIOWrapper( + sys.stdout.buffer, encoding="utf-8", errors="replace", line_buffering=True + ) + sys.stderr = io.TextIOWrapper( + sys.stderr.buffer, encoding="utf-8", errors="replace", line_buffering=True + ) + except Exception as e: # noqa: BLE001 - never fail because of cosmetics + logger.debug("utf-8 stdout wrap failed: %s", e) + + def main() -> None: + argv = sys.argv[1:] + _utf8_stdout() + mode = _mode_from_argv(argv) + if mode == "--help": + sys.exit(_print_help()) + if mode == "--print-config": + client = "generic" + if "--client" in argv: + idx = argv.index("--client") + client = argv[idx + 1] if idx + 1 < len(argv) else "" + sys.exit(_print_config(client, Path.cwd(), argv)) + if mode == "--doctor": + sys.exit(_doctor_cli(argv)) + if mode == "--clean": + sys.exit(_clean_cli(argv)) + _enable_ansi() lang = _detect_lang() - if IS_WINDOWS: - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + # stdout is already UTF-8: _utf8_stdout() ran at the top of main(). Re-wrapping + # here closed the previous wrapper's buffer (ValueError: I/O on closed file, + # found 2026-11-12 by the install→doctor→clean roundtrip). STEPS.sort(key=lambda x: x[0]) diff --git a/scripts/check_docs_vs_config.py b/scripts/check_docs_vs_config.py new file mode 100644 index 00000000..b4a1f54a --- /dev/null +++ b/scripts/check_docs_vs_config.py @@ -0,0 +1,123 @@ +"""Docs = code, part 2: every environment variable in CONFIGURATION.md must exist. + +`check_docs_vs_installer.py` covers installer flags and client names. This guard +covers the other mechanical class of drift: a documented setting the code never +reads. It was written after finding `MSCODEBASE_MAX_FILE_SIZE_MB` in +`docs/*/CONFIGURATION.md` while `src/config/settings.py:425` reads +`MAX_FILE_SIZE_MB` (no prefix) — a name a user would copy and get nothing from. + +Scope: names in the first column of the tables in `docs/en/CONFIGURATION.md`, +looked up in the code corpus (src/, scripts/, adapters/, .opencode/, .githooks/, +.env.example, install.py). Defaults are not compared here: they live in lambdas +and conditional expressions, and a wrong comparison is worse than none. + +Exit: 0 clean, 1 violations, 2 nothing to check (a guard that finds no variables +proves nothing). + +Usage: + python scripts/check_docs_vs_config.py [--root .] +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +VAR_RE = re.compile(r"`([A-Z][A-Z0-9_]{2,})`") + +CORPUS_GLOBS = ( + "src/**/*.py", + "scripts/**/*.py", + "adapters/**/*.py", + "install.py", + ".env.example", + ".opencode/**/*.ts", + ".githooks/*", +) + +# Tokens that look like variables but are documentation artefacts, not settings. +IGNORED = { + "README", + "JSON", + "JSONC", + "MCP", + "LSP", + "CI", + "API", + "HTTP", + "SQL", + "CPU", + "GPU", + "OS", + "URL", + "URI", + "ID", + "TTL", + "LRU", + "FTS", + "BM25", + "RCE", + "AST", + "VOR", +} + + +def documented_vars(config_doc: Path) -> list[str]: + names: list[str] = [] + for line in config_doc.read_text(encoding="utf-8", errors="replace").splitlines(): + if not line.startswith("|"): + continue + first_cell = line.strip().strip("|").split("|")[0] + for name in VAR_RE.findall(first_cell): + if name in IGNORED or name in names: + continue + names.append(name) + return names + + +def corpus(root: Path) -> str: + chunks: list[str] = [] + for pattern in CORPUS_GLOBS: + for path in root.glob(pattern): + if not path.is_file(): + continue + try: + chunks.append(path.read_text(encoding="utf-8", errors="replace")) + except OSError: + continue + return "\n".join(chunks) + + +def check(root: Path) -> tuple[list[str], int]: + doc = root / "docs" / "en" / "CONFIGURATION.md" + if not doc.is_file(): + return ["docs/en/CONFIGURATION.md not found"], 0 + names = documented_vars(doc) + if not names: + return ["no environment variables found in CONFIGURATION.md"], 0 + blob = corpus(root) + missing = [n for n in names if n not in blob] + return [f"CONFIGURATION.md documents {n!r}, the code never reads it" for n in missing], len(names) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", default=str(Path(__file__).resolve().parents[1])) + args = ap.parse_args() + + problems, checked = check(Path(args.root).resolve()) + if checked == 0 and problems: + for problem in problems: + print(f" ✗ {problem}") + print("docs=config guard: nothing proven") + return 2 + for problem in problems: + print(f" ✗ {problem}") + print(f"docs=config guard: {checked} variable(s) checked, {len(problems)} problem(s)") + return 0 if not problems else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_docs_vs_installer.py b/scripts/check_docs_vs_installer.py new file mode 100644 index 00000000..a29eb37b --- /dev/null +++ b/scripts/check_docs_vs_installer.py @@ -0,0 +1,101 @@ +"""Docs = code: every installer flag named in documentation must exist. + +G7 rule: no sentence in the docs may describe behaviour the code does not have. +Full semantic verification is a human job; this guard covers the mechanical part +that already produced a real defect — `docs/*/INSTALL.md` documented +`install.py --uninstall`, a flag the installer never had. + +What it checks: + 1. every `install.py --flag` mentioned in docs/ and README.md exists in + install.py (source literal or the usage text); + 2. every `--client ` mentioned in docs is accepted by + `adapters/clients/snippets.CLIENTS`; + 3. every documented client has a section in docs/en/CLIENTS.md. + +Exit codes: 0 clean, 1 violations, 2 nothing to check (empty corpus is a +failure, not a pass — a guard that finds no files proves nothing). + +Usage: + python scripts/check_docs_vs_installer.py [--root .] +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from adapters.clients.snippets import CLIENTS # noqa: E402 + +FLAG_RE = re.compile(r"install\.py\s+((?:--[a-z][a-z0-9-]*\s*)+)") +CLIENT_RE = re.compile(r"--client\s+([a-z][a-z0-9-]*)") + +DOC_GLOBS = ("README.md", "docs/*/INSTALL.md", "docs/*/CLIENTS.md", "docs/*/DATA_MAP.md", "docs/en/FAQ.md") + + +def install_flags(root: Path) -> set[str]: + """Flags the installer actually knows about (string literals in install.py).""" + text = (root / "install.py").read_text(encoding="utf-8", errors="replace") + return set(re.findall(r'"(--[a-z][a-z0-9-]*)"', text)) + + +def doc_files(root: Path) -> list[Path]: + files: list[Path] = [] + for pattern in DOC_GLOBS: + files.extend(sorted(root.glob(pattern))) + return [p for p in files if p.is_file()] + + +def check(root: Path) -> tuple[list[str], int]: + flags = install_flags(root) + files = doc_files(root) + if not files: + return ["no documentation found to check"], 0 + + problems: list[str] = [] + checked_flags = 0 + for path in files: + text = path.read_text(encoding="utf-8", errors="replace") + for match in FLAG_RE.finditer(text): + for flag in match.group(1).split(): + checked_flags += 1 + if flag not in flags: + problems.append(f"{path.relative_to(root)}: unknown installer flag {flag}") + for match in CLIENT_RE.finditer(text): + client = match.group(1) + if client not in CLIENTS and client != "": + problems.append( + f"{path.relative_to(root)}: unknown client {client!r}" + f" (known: {', '.join(CLIENTS)})" + ) + + en_clients = root / "docs" / "en" / "CLIENTS.md" + if en_clients.is_file(): + body = en_clients.read_text(encoding="utf-8", errors="replace") + for client in CLIENTS: + if f"--client {client}" not in body: + problems.append(f"docs/en/CLIENTS.md: no copy-paste section for {client!r}") + + return problems, checked_flags + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", default=str(Path(__file__).resolve().parents[1])) + args = ap.parse_args() + + problems, checked = check(Path(args.root).resolve()) + if checked == 0 and not problems: + print("docs=code guard: no installer flags referenced in docs — nothing proven") + return 2 + for problem in problems: + print(f" ✗ {problem}") + print(f"docs=code guard: {checked} flag reference(s) checked, {len(problems)} problem(s)") + return 0 if not problems else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/verify_client_snippets.py b/scripts/verify_client_snippets.py new file mode 100644 index 00000000..71b2dab1 --- /dev/null +++ b/scripts/verify_client_snippets.py @@ -0,0 +1,177 @@ +"""Cold-start check for the client snippets printed by `install.py --print-config`. + +For every client it renders the snippet exactly as the installer does, launches +the command from it in a throwaway directory, performs an MCP `initialize` +handshake and then `tools/list`, and reports PASS/FAIL per client. This is the +mechanical proof behind the copy-paste sections of docs/CLIENTS.md: the snippet +is not just valid JSON, it starts a server that answers. + +Usage: + python scripts/verify_client_snippets.py [--python PATH] [--timeout SEC] +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO)) + +# ENCODING SAFETY (Windows): the server's stderr tail is Russian, and a cp1252 +# console crashes the report with UnicodeEncodeError (windows-latest, 2026-11-12). +if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8": + try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + except (AttributeError, ValueError): + pass + +from adapters.clients.snippets import CLIENTS, snapshot # noqa: E402 + +_TOP_LEVEL = { + "generic": "mcpServers", + "claude-code": "mcpServers", + "cursor": "mcpServers", + "opencode": "mcp", + "zed": "context_servers", +} + +INITIALIZE = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "snippet-smoke", "version": "0"}, + }, +} +LIST_TOOLS = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} + + +def _entry(client: str, command: str, args: list[str], env: dict[str, str]) -> tuple[str, list[str], dict[str, str]]: + payload = snapshot(client, command=command, args=args, env=env)[_TOP_LEVEL[client]]["mscodebase"] + if client == "opencode": + return payload["command"][0], payload["command"][1:], payload["environment"] + return payload["command"], payload["args"], payload["env"] + + +def _read_message(stream, timeout: float): + """Next JSON object from stdout, or None on timeout/EOF.""" + import queue + import threading + + box: queue.Queue = queue.Queue(maxsize=1) + + def _worker(): + while True: + line = stream.readline() + if not line: + box.put(None) + return + line = line.strip() + if line.startswith("{"): + try: + box.put(json.loads(line)) + return + except json.JSONDecodeError: + continue + + threading.Thread(target=_worker, daemon=True).start() + try: + return box.get(timeout=timeout) + except queue.Empty: + return None + + +def _stderr_drain(stream, sink) -> None: + """Keep reading stderr so the child never blocks on a full pipe. + + Found 2026-11-12 on windows-latest: the startup log is larger than the pipe + buffer, the server blocked writing stderr and never answered `initialize`. + """ + import threading + + def _worker(): + try: + for line in iter(stream.readline, ""): + sink.append(line.rstrip()) + except (ValueError, OSError): + pass + + threading.Thread(target=_worker, daemon=True).start() + + +def _probe(client: str, command: str, args: list[str], env: dict[str, str], timeout: int) -> str: + from collections import deque + + exe, cli_args, cli_env = _entry(client, command, args, env) + full_env = os.environ.copy() + full_env.update(cli_env) + # src/main.py:126 reads LOG_LEVEL; a quiet server keeps the stderr pipe from + # filling on a cold runner (the probe only needs the protocol answer). + full_env.setdefault("LOG_LEVEL", "WARNING") + stderr_tail: deque = deque(maxlen=20) + with tempfile.TemporaryDirectory(prefix="mcp-snippet-", ignore_cleanup_errors=True) as cwd: + proc = subprocess.Popen( + [exe, *cli_args], + cwd=cwd, + env=full_env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + ) + _stderr_drain(proc.stderr, stderr_tail) + try: + proc.stdin.write(json.dumps(INITIALIZE) + "\n") + proc.stdin.flush() + init = _read_message(proc.stdout, timeout) + if not init or "result" not in init: + tail = " | ".join(stderr_tail) if stderr_tail else "" + return f"FAIL: no initialize result ({tail[-300:]})" + + proc.stdin.write( + json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n" + ) + proc.stdin.write(json.dumps(LIST_TOOLS) + "\n") + proc.stdin.flush() + listing = _read_message(proc.stdout, timeout) + tools = len(listing.get("result", {}).get("tools", [])) if listing else 0 + name = init["result"].get("serverInfo", {}).get("name", "?") + return f"PASS: server={name} tools={tools}" + finally: + try: + proc.kill() + except OSError: + pass + proc.wait(timeout=30) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--python", default="", help="interpreter/entry point to probe (default: current)") + ap.add_argument("--timeout", type=int, default=300) + ap.add_argument("--install-root", default=os.environ.get("MSCODEBASE_INSTALL_ROOT", str(REPO))) + args = ap.parse_args() + + command = args.python or sys.executable + env = {"PYTHONPATH": args.install_root, "PROJECT_PATH": tempfile.gettempdir(), "PYTHONUTF8": "1"} + + failures = 0 + for client in CLIENTS: + result = _probe(client, command, ["-u", "-m", "src.main"], env, args.timeout) + failures += 0 if result.startswith("PASS") else 1 + print(f"{client:<12} {result}") + return 0 if failures == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_docs_vs_config.py b/tests/test_docs_vs_config.py new file mode 100644 index 00000000..743a7a6a --- /dev/null +++ b/tests/test_docs_vs_config.py @@ -0,0 +1,59 @@ +"""Red-first tests for the configuration docs guard (G7). + +Written after `docs/*/CONFIGURATION.md` documented `MSCODEBASE_MAX_FILE_SIZE_MB` +while `src/config/settings.py:425` reads `MAX_FILE_SIZE_MB`: a user copying the +documented name gets no effect, and nothing noticed for months. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO / "scripts")) + +import check_docs_vs_config as guard # noqa: E402 + + +def test_real_tree_is_clean_and_checks_many_vars(): + problems, checked = guard.check(REPO) + assert checked >= 50, f"only {checked} variables parsed — the guard is blind" + assert problems == [], problems + + +def test_documented_but_unread_variable_is_reported(tmp_path): + (tmp_path / "docs" / "en").mkdir(parents=True) + (tmp_path / "docs" / "en" / "CONFIGURATION.md").write_text( + "| Variable | Default | Effect |\n" + "|---|---|---|\n" + "| `MSCODEBASE_REAL_FLAG` | `true` | real |\n" + "| `MSCODEBASE_INVENTED_FLAG` | `10` | invented |\n", + encoding="utf-8", + ) + (tmp_path / "src").mkdir() + (tmp_path / "src" / "settings.py").write_text( + 'FLAG = os.getenv("MSCODEBASE_REAL_FLAG", "true")\n', encoding="utf-8" + ) + + problems, checked = guard.check(tmp_path) + assert checked == 2 + assert any("MSCODEBASE_INVENTED_FLAG" in p for p in problems), problems + assert not any("MSCODEBASE_REAL_FLAG" in p for p in problems), problems + + +def test_max_file_size_name_matches_the_code(): + """The historical defect: the doc must not resurrect the prefixed name.""" + doc = (REPO / "docs" / "en" / "CONFIGURATION.md").read_text(encoding="utf-8") + settings = (REPO / "src" / "config" / "settings.py").read_text(encoding="utf-8") + assert 'os.getenv("MAX_FILE_SIZE_MB", "10")' in settings + assert "MSCODEBASE_MAX_FILE_SIZE_MB" not in doc + assert "MAX_FILE_SIZE_MB" in doc + + +def test_empty_corpus_is_not_a_pass(tmp_path): + (tmp_path / "docs" / "en").mkdir(parents=True) + (tmp_path / "docs" / "en" / "CONFIGURATION.md").write_text("# no tables\n", encoding="utf-8") + problems, checked = guard.check(tmp_path) + assert checked == 0 + assert problems diff --git a/tests/test_docs_vs_installer.py b/tests/test_docs_vs_installer.py new file mode 100644 index 00000000..f4af410e --- /dev/null +++ b/tests/test_docs_vs_installer.py @@ -0,0 +1,62 @@ +"""Red-first tests for the docs=code guard (G7). + +The guard exists because documentation drifted from `install.py` and nobody +noticed: `docs/*/INSTALL.md` shipped `install.py --uninstall`, a flag that never +existed. These tests pin both directions: a stale flag must be reported, and the +real tree must pass. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO / "scripts")) + +import check_docs_vs_installer as guard # noqa: E402 + + +def test_real_tree_passes_and_checks_something(): + problems, checked = guard.check(REPO) + assert checked > 0, "guard found no flag references — it would pass vacuously" + assert problems == [], problems + + +def test_installed_flag_vocabulary_is_complete(): + flags = guard.install_flags(REPO) + for expected in ("--print-config", "--doctor", "--clean", "--dry-run", "--json", + "--with-models", "--no-zed", "--sync-only", "--client", "--python"): + assert expected in flags, f"{expected} missing from install.py" + + +def test_unknown_flag_in_docs_is_reported(tmp_path): + (tmp_path / "install.py").write_text('KNOWN = "--doctor"\n', encoding="utf-8") + (tmp_path / "docs" / "en").mkdir(parents=True) + (tmp_path / "README.md").write_text("run `install.py --uninstall`\n", encoding="utf-8") + (tmp_path / "docs" / "en" / "INSTALL.md").write_text("`install.py --doctor`\n", encoding="utf-8") + (tmp_path / "docs" / "en" / "CLIENTS.md").write_text( + "\n".join(f"--client {c}" for c in guard.CLIENTS), encoding="utf-8" + ) + + problems, checked = guard.check(tmp_path) + assert checked == 2 + assert any("--uninstall" in p for p in problems), problems + + +def test_unknown_client_in_docs_is_reported(tmp_path): + (tmp_path / "install.py").write_text('KNOWN = "--print-config"\n', encoding="utf-8") + (tmp_path / "docs" / "en").mkdir(parents=True) + (tmp_path / "docs" / "en" / "CLIENTS.md").write_text( + "\n".join(f"--client {c}" for c in guard.CLIENTS) + "\n--client emacs\n", + encoding="utf-8", + ) + problems, _checked = guard.check(tmp_path) + assert any("emacs" in p for p in problems), problems + + +def test_empty_docs_corpus_is_not_a_pass(tmp_path): + (tmp_path / "install.py").write_text("", encoding="utf-8") + problems, checked = guard.check(tmp_path) + assert checked == 0 + assert problems == ["no documentation found to check"] diff --git a/tests/test_install_cli.py b/tests/test_install_cli.py new file mode 100644 index 00000000..f56b3ee8 --- /dev/null +++ b/tests/test_install_cli.py @@ -0,0 +1,395 @@ +"""Red-first tests for the installer's host-neutral CLI (G5/G6). + +Every check drives `install.py` as a subprocess with isolated roots +(`MSCODEBASE_INSTALL_ROOT`, `MSCODEBASE_DATA_DIR`) so nothing touches the real +user profile. The snippets are not just parsed: the generic snippet's command is +actually launched and must answer an MCP `initialize` request. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import sqlite3 +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parents[1] +INSTALL_PY = REPO / "install.py" + +SNIPPET_CLIENTS = ["generic", "opencode", "zed", "claude-code", "cursor"] + +TOP_LEVEL_KEY = { + "generic": "mcpServers", + "claude-code": "mcpServers", + "cursor": "mcpServers", + "opencode": "mcp", + "zed": "context_servers", +} + + +def _env(install_root: Path, data_dir: Path) -> dict[str, str]: + env = os.environ.copy() + env.update( + { + "MSCODEBASE_INSTALL_ROOT": str(install_root), + "MSCODEBASE_DATA_DIR": str(data_dir), + "PYTHONIOENCODING": "utf-8", + "PYTHONUTF8": "1", + } + ) + return env + + +def run_install( + args: list[str], + *, + install_root: Path, + data_dir: Path, + cwd: Path, + stdin_text: str | None = None, + timeout: int = 120, +) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(INSTALL_PY), *args], + cwd=str(cwd), + env=_env(install_root, data_dir), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + input=stdin_text if stdin_text is not None else "", + timeout=timeout, + ) + + +def entry_json(client: str, payload: dict) -> dict: + if client == "opencode": + # V2: mcp.servers. + return payload["mcp"]["servers"]["mscodebase"] + return payload[TOP_LEVEL_KEY[client]]["mscodebase"] + + +# ─── G5: --print-config ──────────────────────────────────────────────── + + +@pytest.mark.parametrize("client", SNIPPET_CLIENTS) +def test_print_config_is_valid_json_per_client(client, tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + project.mkdir() + + proc = run_install( + ["--print-config", "--client", client], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert proc.returncode == 0, proc.stderr + payload = json.loads(proc.stdout) + entry = entry_json(client, payload) + + if client == "opencode": + command = entry["command"] + assert entry["type"] == "local" + env = entry["environment"] + assert "servers" in payload["mcp"], "OpenCode V2 keeps servers under mcp.servers" + elif client == "zed": + command = [entry["command"], *entry["args"]] + env = entry["env"] + assert entry["enabled"] is True + else: + command = [entry["command"], *entry["args"]] + env = entry["env"] + + name = Path(command[0]).name + assert name.startswith(("python", "mscodebase-mcp")), name + assert command[1:] == ["-u", "-m", "src.main"] + assert Path(env["PYTHONPATH"]).resolve() == install_root.resolve() + assert env["PYTHONUTF8"] == "1" + assert Path(env["PROJECT_PATH"]).resolve() == project.resolve() + + +def test_print_config_rejects_unknown_client(tmp_path): + proc = run_install( + ["--print-config", "--client", "not-a-client"], + install_root=tmp_path / "install", + data_dir=tmp_path / "data", + cwd=tmp_path, + ) + assert proc.returncode == 2 + assert "not-a-client" in (proc.stdout + proc.stderr) + + +def test_print_config_default_client_is_generic(tmp_path): + proc = run_install( + ["--print-config"], + install_root=tmp_path / "install", + data_dir=tmp_path / "data", + cwd=tmp_path, + ) + assert proc.returncode == 0, proc.stderr + assert "mcpServers" in json.loads(proc.stdout) + + +def test_generic_snippet_actually_starts_the_server(tmp_path): + """The printed snippet must be copy-paste launchable (real MCP handshake).""" + project = tmp_path / "project" + project.mkdir() + + proc = run_install( + ["--print-config", "--client", "generic", "--python", sys.executable], + install_root=REPO, + data_dir=tmp_path / "data", + cwd=project, + ) + assert proc.returncode == 0, proc.stderr + entry = entry_json("generic", json.loads(proc.stdout)) + + env = os.environ.copy() + env.update(entry["env"]) + env["MSCODEBASE_DATA_DIR"] = str(tmp_path / "data") + + request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "snippet-probe", "version": "0"}, + }, + } + with subprocess.Popen( + [entry["command"], *entry["args"]], + cwd=str(project), + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + ) as proc2: + out, err = proc2.communicate(json.dumps(request) + "\n", timeout=300) + + answer = None + for line in out.splitlines(): + line = line.strip() + if line.startswith("{"): + try: + candidate = json.loads(line) + except json.JSONDecodeError: + continue + if candidate.get("id") == 1: + answer = candidate + break + assert answer is not None, textwrap.shorten(out + err, 800) + assert answer["result"]["serverInfo"]["name"], answer + + +# ─── G6: --doctor ────────────────────────────────────────────────────── + + +def make_install_artifacts(install_root: Path, data_dir: Path, project: Path) -> None: + project.mkdir(parents=True, exist_ok=True) + (install_root / "venv" / "Scripts").mkdir(parents=True) + (install_root / "venv" / "Scripts" / "python.exe").write_bytes(b"x" * 100) + (install_root / "src").mkdir() + (install_root / "src" / "main.py").write_text("print('hi')\n", encoding="utf-8") + (data_dir / "projects" / "deadbeef").mkdir(parents=True) + (data_dir / "projects" / "deadbeef" / "graph.db").write_bytes(b"g" * 200) + (data_dir / "logs").mkdir() + (data_dir / "logs" / "server.log").write_text("line\n" * 5, encoding="utf-8") + (project / ".mscodebase").mkdir() + conn = sqlite3.connect(project / ".mscodebase" / "chunks.db") + conn.execute("CREATE TABLE chunks (id TEXT, text TEXT)") + conn.executemany("INSERT INTO chunks VALUES (?, ?)", [("1", "a"), ("2", "b"), ("3", "c")]) + conn.commit() + conn.close() + + +def test_doctor_reports_layers_and_index(tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + make_install_artifacts(install_root, data_dir, project) + + proc = run_install( + ["--doctor", "--json"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert proc.returncode == 0, proc.stderr + report = json.loads(proc.stdout) + + layers = {layer["name"]: layer for layer in report["layers"]} + assert layers["install_root"]["bytes"] > 0 + assert layers["data_root"]["bytes"] > 0 + assert layers["project_chunks"]["bytes"] > 0 + assert report["index"]["chunks"] == 3 + assert report["total_bytes"] >= 500 + + +def test_doctor_human_output_has_paths(tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + make_install_artifacts(install_root, data_dir, project) + + proc = run_install( + ["--doctor"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert proc.returncode == 0, proc.stderr + assert str(install_root) in proc.stdout + assert str(data_dir) in proc.stdout + assert "chunks" in proc.stdout.lower() + + +# ─── G6: --clean ─────────────────────────────────────────────────────── + + +def test_clean_dry_run_removes_nothing(tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + make_install_artifacts(install_root, data_dir, project) + + proc = run_install( + ["--clean", "--dry-run"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert proc.returncode == 0, proc.stderr + assert str(install_root) in proc.stdout + assert str(data_dir) in proc.stdout + assert install_root.exists() and data_dir.exists() + assert (project / ".mscodebase" / "chunks.db").exists() + + +def test_clean_without_confirmation_refuses(tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + make_install_artifacts(install_root, data_dir, project) + + proc = run_install( + ["--clean"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + stdin_text="", + ) + assert proc.returncode == 2 + assert "--yes" in (proc.stdout + proc.stderr) + assert install_root.exists() and data_dir.exists() + + +def test_clean_yes_leaves_zero_residue(tmp_path): + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + make_install_artifacts(install_root, data_dir, project) + + proc = run_install( + ["--clean", "--yes", "--json"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert proc.returncode == 0, proc.stdout + proc.stderr + report = json.loads(proc.stdout) + assert report["removed_bytes"] > 0 + assert report["remaining_bytes"] == 0 + assert not install_root.exists() + assert not (project / ".mscodebase").exists() + assert not (data_dir / "projects").exists() + + after = run_install( + ["--doctor", "--json"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + ) + assert after.returncode == 0, after.stderr + assert json.loads(after.stdout)["total_bytes"] == 0 + + +# ─── G6: models are opt-in (text-first default) ──────────────────────── + + +def _install_module(): + spec = importlib.util.spec_from_file_location("mscodebase_install", INSTALL_PY) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_models_are_opt_in(): + mod = _install_module() + assert mod.models_enabled([]) is False + assert mod.models_enabled(["--sync-only"]) is False + assert mod.models_enabled(["--with-models"]) is True + + +def test_real_sync_install_then_doctor_then_clean_leaves_zero(tmp_path): + """DoD G6: a real install (--sync-only), doctor sees it, clean leaves nothing.""" + install_root = tmp_path / "install" + data_dir = tmp_path / "data" + project = tmp_path / "project" + project.mkdir() + + install = run_install( + ["--sync-only"], + install_root=install_root, + data_dir=data_dir, + cwd=project, + timeout=300, + ) + assert install.returncode == 0, install.stderr + assert (install_root / "install.py").is_file() + assert (install_root / "src" / "main.py").is_file() + + doctor = run_install(["--doctor", "--json"], install_root=install_root, data_dir=data_dir, cwd=project) + assert doctor.returncode == 0, doctor.stderr + report = json.loads(doctor.stdout) + layers = {layer["name"]: layer for layer in report["layers"]} + assert layers["install_root"]["files"] > 100, layers["install_root"] + assert report["total_bytes"] > 1_000_000 + + clean = run_install( + ["--clean", "--yes", "--json"], install_root=install_root, data_dir=data_dir, cwd=project + ) + assert clean.returncode == 0, clean.stdout + clean.stderr + assert json.loads(clean.stdout)["remaining_bytes"] == 0 + assert not install_root.exists() + + +@pytest.mark.parametrize("step_name", ["step_gguf", "step_models"]) +def test_text_first_default_skips_downloads(step_name, monkeypatch): + mod = _install_module() + step = getattr(mod, step_name) + + def _boom(*_a, **_k): # pragma: no cover - must never run + raise AssertionError("download attempted without --with-models") + + monkeypatch.setattr(mod.sys, "argv", ["install.py"]) + if step_name == "step_models": + monkeypatch.setattr(mod, "_download_onnx_models", _boom, raising=False) + + lines: list[tuple[str, str]] = [] + step(lines, "en") + joined = " ".join(text for _c, text in lines).lower() + assert "skip" in joined, lines + assert "--with-models" in joined, lines