From 55739b77d168e04709ee919b8e3edddedc71f778 Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Wed, 11 Nov 2026 23:34:51 +0300 Subject: [PATCH 1/2] feat(install): doctor/clean inventory and text-first default (G6) --- AGENT_DIARY.md | 8 + README.md | 17 +- docs/en/CLIENTS.md | 2 +- docs/en/DATA_MAP.md | 111 +++++++++++++ docs/en/INSTALL.md | 134 ++++++++------- docs/ru/CLIENTS.md | 2 +- docs/ru/DATA_MAP.md | 111 +++++++++++++ docs/ru/INSTALL.md | 143 +++++++++------- docs/ru/README.md | 34 +++- docs/zh/CLIENTS.md | 2 +- docs/zh/DATA_MAP.md | 106 ++++++++++++ docs/zh/INSTALL.md | 175 +++++++++++--------- docs/zh/README.md | 30 +++- install.py | 333 +++++++++++++++++++++++++++++++++++++- tests/test_install_cli.py | 205 ++++++++++++++++++++++- 15 files changed, 1199 insertions(+), 214 deletions(-) create mode 100644 docs/en/DATA_MAP.md create mode 100644 docs/ru/DATA_MAP.md create mode 100644 docs/zh/DATA_MAP.md diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index 05f139a8..08679b61 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1757,3 +1757,11 @@ BREACH: MSC-P-19 | date: 2026-10-03 | rule: §12 | guard_at_time: no | detected_ **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 только при ошибке» — это дедлок, который воспроизводится только там, где логов больше буфера. + +## [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`. diff --git a/README.md b/README.md index d88d74b3..29eb4418 100644 --- a/README.md +++ b/README.md @@ -232,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. @@ -344,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/docs/en/CLIENTS.md b/docs/en/CLIENTS.md index 98fb7eb8..2e90888f 100644 --- a/docs/en/CLIENTS.md +++ b/docs/en/CLIENTS.md @@ -36,7 +36,7 @@ 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): 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/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/ru/CLIENTS.md b/docs/ru/CLIENTS.md index 2eb4fdd7..188dc529 100644 --- a/docs/ru/CLIENTS.md +++ b/docs/ru/CLIENTS.md @@ -38,7 +38,7 @@ Linux : ~/.local/share/zed/extensions/mscodebase-intelligence Перенести в любое место: `MSCODEBASE_INSTALL_ROOT=`. Кэши, логи и артефакты проектов лежат отдельно (`MSCODEBASE_DATA_DIR`, по умолчанию -`%LOCALAPPDATA%\mscodebase`). +`%LOCALAPPDATA%\mscodebase`). Полный инвентарь — [DATA_MAP.md](DATA_MAP.md). Альтернатива без установщика (любой менеджер, дающий console scripts): 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/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/README.md b/docs/ru/README.md index c257ab84..9587c17d 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -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/zh/CLIENTS.md b/docs/zh/CLIENTS.md index 8365d187..9b7470a6 100644 --- a/docs/zh/CLIENTS.md +++ b/docs/zh/CLIENTS.md @@ -37,7 +37,7 @@ Linux : ~/.local/share/zed/extensions/mscodebase-intelligence 用 `MSCODEBASE_INSTALL_ROOT=` 可放到任意位置。缓存、日志与项目产物在别处 (`MSCODEBASE_DATA_DIR`,默认 `%LOCALAPPDATA%\mscodebase`)。完整清单见 - +[DATA_MAP.md](DATA_MAP.md)。 不用安装器的替代方式(任何提供 console scripts 的包管理器): 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/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/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/install.py b/install.py index c5a85c17..46ba870c 100644 --- a/install.py +++ b/install.py @@ -12,6 +12,9 @@ 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: @@ -46,7 +49,10 @@ машине без Zed и без adapters/ — адаптер подключается лениво внутри шагов. 2. INSTALL_ROOT отвязан от Zed: MSCODEBASE_INSTALL_ROOT переопределяет каталог установки (историческое имя папки расширения остаётся только default). - 3. Добавлены --print-config/--help: готовый stdio-сниппет для любого MCP-клиента. + 3. Добавлены --print-config/--doctor/--clean/--help: сниппет для любого MCP- + клиента, инвентарь «что стоит и сколько весит», полное удаление по слоям. + 4. Модели стали opt-in (--with-models): по умолчанию text-first, ни одного + сетевого запроса за модели во время установки. """ import io @@ -711,7 +717,12 @@ def step_llama(lines, lang): def step_gguf(lines, lang): """Скачивает 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 @@ -764,7 +775,12 @@ def step_models(lines, lang): Проверяет по порядку: 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), @@ -958,8 +974,63 @@ def zed_enabled(argv: Optional[list[str]] = None) -> bool: 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]]: @@ -1007,10 +1078,258 @@ def _print_config(client: str, project: Path, argv: list[str]) -> int: 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: @@ -1022,7 +1341,13 @@ def _print_help() -> int: " --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" @@ -1034,7 +1359,7 @@ def _print_help() -> int: def _mode_from_argv(argv: list[str]) -> Optional[str]: - for mode in ("--print-config", "--help"): + for mode in ("--print-config", "--doctor", "--clean", "--help"): if mode in argv: return mode if "-h" in argv: @@ -1119,6 +1444,10 @@ def main() -> None: 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() diff --git a/tests/test_install_cli.py b/tests/test_install_cli.py index 77a0b485..f56b3ee8 100644 --- a/tests/test_install_cli.py +++ b/tests/test_install_cli.py @@ -1,4 +1,4 @@ -"""Red-first tests for the installer's host-neutral CLI (G5). +"""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 @@ -8,8 +8,10 @@ from __future__ import annotations +import importlib.util import json import os +import sqlite3 import subprocess import sys import textwrap @@ -190,3 +192,204 @@ def test_generic_snippet_actually_starts_the_server(tmp_path): 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 From f814ba91e3e280756bec2576c6622081d2a8d363 Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Thu, 12 Nov 2026 00:14:37 +0300 Subject: [PATCH 2/2] fix(scripts): read the OpenCode entry from mcp.servers --- scripts/verify_client_snippets.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/verify_client_snippets.py b/scripts/verify_client_snippets.py index 71b2dab1..6757e602 100644 --- a/scripts/verify_client_snippets.py +++ b/scripts/verify_client_snippets.py @@ -55,10 +55,13 @@ 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"] + payload = snapshot(client, command=command, args=args, env=env) if client == "opencode": - return payload["command"][0], payload["command"][1:], payload["environment"] - return payload["command"], payload["args"], payload["env"] + # V2 nests the entry under mcp.servers. (see adapters/clients/snippets.py). + entry = payload["mcp"]["servers"]["mscodebase"] + return entry["command"][0], entry["command"][1:], entry["environment"] + entry = payload[_TOP_LEVEL[client]]["mscodebase"] + return entry["command"], entry["args"], entry["env"] def _read_message(stream, timeout: float):