Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,34 @@ BREACH: MSC-P-19 | date: 2026-10-03 | rule: §12 | guard_at_time: no | detected_
**4. Гипотеза «clean-state до фикса валидировал пустую установку» — опровергнута.** `scripts/verify_clean_state.sh` вообще не вызывает `install.py`: он сам создаёт venv и делает `pip install -e .`; `grep "install.py --yes"` по `.github/` и `scripts/` пуст. Значит баг `--yes` с v3.0 не влиял ни на один прогон CI — ровно поэтому его никто и не поймал; поймал только ручной прогон по README.
**Урок:** «CI зелёный» и «инструкция работает» — разные утверждения. Пока установщик не запускали как пользователь (по README, в пустом каталоге), его неинтерактивная ветка была мёртвой, а все гейты оставались зелёными.

## [2026-11-14] Установщик падал на шаге 7: `httpx`, `wmic` и «тихий» `--with-models`

**Симптом:** `python install.py` (интерпретатор `C:\Python314\python.exe`, без зависимостей проекта) — `⚠ Step 7/12: llama.cpp engine .. No module named 'httpx'`, дальше `[r]etry / [s]kip / [c]ancel`; итог прогона `11 ok, 1 skipped, 1351 s`.

**Корни (3, один класс — «шаг установщика зависит от venv, которого ещё нет»):**
1. `step_llama` (`install.py:669`) импортирует `src.providers.reranker.llama_runner`, а тот делал top-level `import httpx`. Шагу нужны только `download_llama_binary`/`is_installed` (stdlib, urllib); httpx нужен сетевым вызовам рантайма. По документации `python install.py` — штатный путь (`docs/en/INSTALL.md:35`), без предварительного `pip install -e .`.
2. `llama_install.py:233` на импорте выполняет `_detect_platform()` → `_detect_cpu()` → `wmic cpu`/`wmic memorychip`; на Windows 11 24H2 `wmic` удалён → `FileNotFoundError` печатался трейсбеком (`logger.warning("exception", exc_info=True)`), RAM/CPU молча деградировали до `8`/`unknown`.
3. (нашло независимое ревью) `download_gguf_model` делал **безусловный** `from huggingface_hub import hf_hub_download` вне `try`, хотя `step_gguf` (шаг 7) идёт **до** `step_models` (шаг 8) — `--with-models` падал тем же `ModuleNotFoundError` на шаге 8. Комментарий «…или urllib» обещал fallback, которого не было.

**Fix:** httpx — ленивый `_httpx()` (6 мест); RAM — `GlobalMemoryStatusEx` (ctypes), CPU — `ProcessorNameString` из реестра, оба без процессов и с прежним дефолтом 8; Vulkan-проба вынесена в `_detect_vulkan(has_binary)` и **не запускается, если Vulkan-сборки нет** (раньше `vulkaninfo` спавнился на любой машине с `vulkan-1.dll`, а сбой печатался в консоль установки); GGUF — `except ImportError` + рабочий urllib-fallback (sha256, `.part` → atomic replace, `MSCODEBASE_HF_BASE_URL` для тестов); два `logger.warning("exception", exc_info=True)` → осмысленный `debug`.

**Проверка:** `tests/test_install_import_isolation.py` + `tests/test_install_offline_paths.py` — 18 тестов; на старом коде `5 failed + 2 errors` (импорт) и `5 failed + 2 errors` (офлайн-пути), после фикса 18/18. Guard расширен с `src.*` на **любой локальный пакет**, который тянет `install.py` (стало 4 модуля, включая `adapters.*`). Мутации: top-level `import httpx`, отсутствие сверки sha256, проба без сборки, снятый `except ImportError`, снятый early-return — каждая краснеет. Полный suite: `2534 passed, 10 skipped`, rc=0; `ruff check src/ tests/` чистый. На реальном `C:\Python314\python.exe`: до — `ModuleNotFoundError`, после — `import ok; installed=False version=b11476`, rc=0. `_CPU_INFO`: было `unknown`/`8`, стало `AMD Ryzen 5 5600H`/`15`.

**Остаток (не в этом PR):** `get_system_summary()` (экспорт без вызывающих) всё ещё зовёт `wmic os get caption` — ошибка ловится `OSError`, трейсбека нет, только неточность отчёта; `.decode()` там без `errors=`.

**Урок:** «модуль, который импортирует установщик» — часть контракта установки, а не рантайма: top-level импорт третьей стороны переносит venv-зависимость туда, где venv ещё не существует. Ловится только прогоном голым интерпретатором (`-S`), а не обычным pytest; и проверять надо не один шаг, а все шаги, которые идут до установки пакетов.

## [2026-11-14] Шаг 7 «прошёл», хотя движка нет: неверный пин SHA256 и зелёный отчёт

**Симптом:** после фикса импорта тот же прогон показал `ERROR … SHA256 не совпадает! Ожидался d5d7…, получен a23e…`, `🚫 Отказ от установки: SHA256 не прошёл проверку` — и при этом `✓ Step 7/12: llama.cpp engine` и итог `12 ok, 0 skipped, 0 failed`.

**Корни:** (1) Пин `LLAMA_BIN_SHA256["win-cpu-x64"]` неверен с первого коммита (`d84adee`). Архив при этом правильный: GitHub API (`repos/ggml-org/llama.cpp/releases/tags/b11476`) отдаёт для ассета `digest=sha256:a23e…` — ровно то, что скачалось; вторая запись (`win-vulkan-x64`) тоже неверна. (2) `step_llama` на провале добавлял жёлтую строку и возвращался нормально, поэтому `_step` считал шаг ok, а `main()` вообще не возвращал код ошибки — exit code был 0 при любом провале.

**Fix:** хэши приведены к GitHub `digest` (с комментарием, как перевывести); `_verify_archive_sha256` — пин совпал → ok; пин устарел, но GitHub подтверждает фактический хэш → ok + warning; пина нет → GitHub digest; подтвердить нечем → fail closed, архив удаляется. `step_llama` и `step_gguf` бросают `RuntimeError` вместо зелёной строки; `main() -> int`, `sys.exit(main())`, `return 1 if has_err else 0`. Уборка `.part`/архива — через `_discard` (на Windows занятый файл больше не превращает отказ в `PermissionError`; нашло ревью).

**Проверка:** живой прогон шага 7 в изолированный каталог (реальная сеть): `download_llama_binary -> True`, `llama-server.exe exists: True`, 23 файла, архив удалён. Реальный ассет 19 441 535 байт, `sha256 = a23e…` = пин, `_verify_archive_sha256 -> True`. Тесты: 28 в трёх файлах (`test_install_import_isolation`, `test_install_offline_paths`, `test_install_integrity_status`); мутации «устаревший пин не сверяется», «нет пина и нет сети — пропускаем», «уборка не ловит OSError», «провал шага снова даёт exit 0», «провал загрузки снова зелёный» — каждая краснеет. Полный suite: `2543 passed, 11 skipped`, rc=0.

**Урок:** «fail closed» без честного статуса — половина защиты: установщик отказался ставить непроверенный бинарь и в том же выводе объявил успех. Проверять нужно и то, что шаг напечатал, и то, что процесс вернул.
## [2026-11-14] docs = code стало механическим: гейт на ИЗМЕНЕНИЕ, а не на файл
**Проблема:** правило «каждая правка docs ссылается на факт кода» держалось на ревью. Два существующих guard'а смотрели файл целиком (флаги — `check_docs_vs_installer.py`, переменные — `check_docs_vs_config.py`) и не видели, что именно изменилось в PR.
**Сделано:** `scripts/check_docs_refs.py` (режимы `--base` / `--staged` / `--diff-file`). Изменённый `.md` обязан добавить **проверяемую** ссылку: существующий путь внутри репо, `path::symbol` (символ **объявлен**, а не упомянут), `path:NNN` (строка в пределах файла), `install.py --flag` (флаг известен), имя переменной, которую код **читает** из окружения. Косметика проходит только если файл уже ссылался на код и добавленные строки не вносят новых утверждений (перенесённая строка с уже существующей цифрой — не новое утверждение). Исключения: `docs/archive|blog|research/**`, `experiments/**`, реестры по имени файла (`docs/*/CHANGELOG.md` тоже). Лимиты: >200 файлов или >20 000 строк → `exit 3`.
Expand Down
19 changes: 15 additions & 4 deletions install.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,13 @@ def step_llama(lines, lang):
except Exception as e:
lines.append((C.YEL, f"⚠ Не удалось скопировать в расширение: {e}"))
else:
lines.append((C.YEL, "⚠ Не удалось скачать llama.cpp (будет ONNX)"))
# Раньше здесь был жёлтый «⚠ Не удалось скачать» и шаг считался ok —
# установка рапортовала `0 failed`, оставляя пользователя без движка
# (2026-11-14: неверный пин SHA256 отбивал архив, а итог был зелёным).
raise RuntimeError(
"llama.cpp не установлен: скачивание или проверка SHA256 не прошла "
"(подробности в логе выше)"
)


@_step(7)
Expand Down Expand Up @@ -766,6 +772,9 @@ def step_gguf(lines, lang):

if all_ok:
lines.append((C.GRN, "✅ GGUF модели готовы — llama.cpp embedder + reranker"))
else:
# Та же честность, что и в step_llama: не «ok» с жёлтой строкой.
raise RuntimeError("часть GGUF моделей не скачана (см. лог выше)")


@_step(8)
Expand Down Expand Up @@ -1432,7 +1441,7 @@ def _utf8_stdout() -> None:
logger.debug("utf-8 stdout wrap failed: %s", e)


def main() -> None:
def main() -> int:
argv = sys.argv[1:]
_utf8_stdout()
mode = _mode_from_argv(argv)
Expand Down Expand Up @@ -1463,7 +1472,7 @@ def main() -> None:
step_copy([], lang)
_record_install_meta()
print("✅ Sync done. Restart the MCP host (OpenCode session or Zed window) to apply.")
return
return 0

_box(
"",
Expand Down Expand Up @@ -1557,11 +1566,13 @@ def main() -> None:
print(f"\n {C.D}Extension: {ZED_EXT_DIR}{C.R}")
print(f" {C.D}Python: {PYTHON_EXE}{C.R}")
print()
# Ненулевой код при провале шага: раньше exit code всегда был 0 (2026-11-14).
return 1 if has_err else 0


if __name__ == "__main__":
try:
main()
sys.exit(main())
except KeyboardInterrupt:
print(f"\n {C.RED}Aborted by user.{C.R}")
sys.exit(1)
Loading
Loading