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/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 08679b61..b43c5097 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1765,3 +1765,23 @@ BREACH: MSC-P-19 | date: 2026-10-03 | rule: §12 | guard_at_time: no | detected_ **Автотест 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: холодный старт по README нашёл баг `--yes` +**Симптом:** `install.py --yes --no-zed` печатал итоговую рамку «Next steps», но на диске не появлялось ничего: `venv python exists: False`, install root пуст. Установка «успешно» завершалась за 0 секунд. +**Корень:** `for attempt in range(0 if auto_yes else max_retries + 1)` — при `--yes` `range(0)` не делает ни одной итерации, а ветка `for/else` помечает каждый шаг как failed, ни разу его не вызвав. Логика живёт с v3.0: все неинтерактивные установки (`--yes`) были пустыми. +**Fix:** `range(1 if auto_yes else max_retries + 1)` + регрессионный тест `test_auto_yes_runs_each_step_once` (до фикса красный: фейковый шаг не вызывался). +**Холодный старт (чистые временные `MSCODEBASE_INSTALL_ROOT`/`DATA_DIR`, `--no-zed`, без `--with-models`):** `install rc=0`, **12 ok / 0 skipped / 0 failed за 96 s**; venv Python 3.14.3; `src/main.py` и маркер на месте; **модельных каталогов нет** (`model dirs present: False`, install root 1.4 GB). +**Два клиента не-Zed/не-OpenCode, ровно по `docs/en/CLIENTS.md`:** `.mcp.json` (Claude Code §3.4) и `.cursor/mcp.json` (Cursor §3.5) записаны из `--print-config`, запущены как есть → `MSCodebase Intelligence Server`, `tools=17`, `investigate` в списке (4.8 s и 4.2 s); `--doctor` на этом же каталоге: `install_root 1.4 GB / 28755 файлов`, `index 0 chunk(s)`. +**Побочный вывод:** на ветке G5 (до G6) установщик ещё качает модели — README/CLIENTS G5-ветки обещали text-first и были приведены к фактическому поведению (`6010443`); text-first проверен на G6/G7. +**Урок:** `--yes` значит «не спрашивать», а не «не делать»; нулевой `range()` плюс `for/else` выглядит безобидно на ревью и полностью ломает установку. Ни один тест не запускал установщик целиком — поймал только прогон по README в пустом каталоге. 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/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/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/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_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/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/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_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 9587c17d..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` | 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/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/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_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/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/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..8d05e251 --- /dev/null +++ b/scripts/check_docs_vs_installer.py @@ -0,0 +1,128 @@ +"""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") + +# G7 DoD "no documentation section without a code reference": every canonical page +# must cite at least one real code artefact. Section-level references are the +# "Code references" tables inside those pages; this is the page-level floor. +CANONICAL_GLOBS = ( + "README.md", + "docs/*/ARCHITECTURE.md", + "docs/*/SEARCH_PIPELINE.md", + "docs/*/CONFIGURATION.md", + "docs/*/INSTALL.md", + "docs/*/CLIENTS.md", + "docs/*/DATA_MAP.md", + "docs/*/FAQ.md", +) +CODE_REF_RE = re.compile( + r"(src/[a-z0-9_/]+\.py|scripts/[a-z0-9_/]+\.py|install\.py|\.githooks/[a-z-]+|" + r"\.opencode/plugin/[a-z0-9_.-]+|adapters/[a-z0-9_/]+\.py)" +) + + +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}") + + # Page-level floor: a canonical page that cites no code cannot be verified. + canonical = sorted( + {p for pattern in CANONICAL_GLOBS for p in root.glob(pattern) if p.is_file()} + ) + for path in canonical: + text = path.read_text(encoding="utf-8", errors="replace") + if not CODE_REF_RE.search(text): + problems.append(f"{path.relative_to(root)}: no code reference (file:symbol) anywhere") + + 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/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..becc87c4 --- /dev/null +++ b/tests/test_docs_vs_installer.py @@ -0,0 +1,94 @@ +"""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"] + + +def test_canonical_page_without_code_reference_is_reported(tmp_path): + """G7 DoD: a page that cites no code cannot be verified against anything.""" + (tmp_path / "install.py").write_text('KNOWN = "--print-config"\n', encoding="utf-8") + (tmp_path / "docs" / "en").mkdir(parents=True) + (tmp_path / "docs" / "en" / "ARCHITECTURE.md").write_text( + "# Architecture\n\nIt has layers and a toggle.\n", encoding="utf-8" + ) + (tmp_path / "docs" / "en" / "SEARCH_PIPELINE.md").write_text( + "# Pipeline\n\nSee `src/core/search/engine.py`.\n", encoding="utf-8" + ) + (tmp_path / "docs" / "en" / "CLIENTS.md").write_text( + "\n".join(f"--client {c}" for c in guard.CLIENTS) + "\n`src/main.py`\n", encoding="utf-8" + ) + + problems, _checked = guard.check(tmp_path) + assert any("ARCHITECTURE.md: no code reference" in p for p in problems), problems + assert not any("SEARCH_PIPELINE.md: no code reference" in p for p in problems), problems + + +def test_every_canonical_page_in_the_repo_cites_code(): + pages = sorted( + {p for pattern in guard.CANONICAL_GLOBS for p in REPO.glob(pattern) if p.is_file()} + ) + assert pages, "no canonical pages found" + offenders = [ + str(p.relative_to(REPO)) + for p in pages + if not guard.CODE_REF_RE.search(p.read_text(encoding="utf-8", errors="replace")) + ] + assert offenders == [], offenders