diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index 4ad3bb27..31c31399 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1,5 +1,7 @@ ## Key Historical Decisions +- **PyPI-packaging + user-data isolation (2026-08-28):** wheel теперь содержит `tools.stale_detector`, `adapters`, `locales` (норм. данные в site-packages); бинарники/модели в pip-режиме — `get_data_root()` (`%LOCALAPPDATA%\mscodebase`), гейт-маркер `__mscodebase_ext__.marker` отличает расширение от установленного пакета; CLI `--project-path/--project-dir` → env `MSCODEBASE_PROJECT_PATH` (приоритет НАД CWD, trust_self_index); `PROJECT_PATH` остался после CWD (multi-window сохранён). Live-Smoke из чистого venv: tools-ok, `en (78 ключей)`, корень резолвится. Файлы: `pyproject.toml`, `project_resolution.py`, `main.py`, `llama_install.py`. INC-C4CD. + - **Server freeze during full reindex (2026-08-25):** root cause — `begin_write()` держит `_write_lock` (RLock) весь reindex (~7.5 мин embedding), а `IndexStatusReporter.get_status()` синхронно на event-loop-потоке ждал тот же lock (intel_get_runtime_status/require_ready_project/ProjectContext) → заморозка ВСЕХ MCP-вызовов. Фикс: reindex fast-fail в get_status (кэш + status="reindexing") + asyncio.to_thread в 3 loop-точках + guard в _get_stale_warning. - **Embedder:** multilingual-e5-small-int8 + batch=32 (100 ch/s sustained) — 2026-07-17 - **Concurrency:** AsyncInferQueue → лок (тихая гонка подмены векторов) — 2026-07-18 diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 99b0455c..7948c5f5 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -5,6 +5,14 @@ --- +## 2026-08-28 — PyPI-упаковка: `tools`/`locales`/`adapters` вне wheel, модели/бинарники в site-packages, нет `--project-path` (CLOSED) + +**Что:** При упаковке в PyPI-пакет (wheel) сервер не стартовал из установленного пакета по трём причинам: (1) `ModuleNotFoundError: No module named 'tools'` — `tools.stale_detector` (single source of truth для stale-проверки, инцидент 2026-08-14) не был в `include`; (2) `locales/` и `adapters/` не пакетизировались — i18n-предупреждения при старте и `--install` не работал из пакета; (3) в pip-режиме `_get_ext_dir()` мог писать бинарники/модели в site-packages; не было CLI-выбора целевого проекта. +**Fix:** `pyproject.toml` `include += ["tools.stale_detector*", "adapters*", "locales*"]`, `package-data "locales" = ["*.json"]`; добавить `locales/__init__.py`; `_get_ext_dir()` гейтится маркером `__mscodebase_ext__.marker` → без маркера (pip/uvx) бинарники/модели уходят в `get_data_root()` (`%LOCALAPPDATA%\mscodebase`); CLI `--project-path/--project-dir` → env `MSCODEBASE_PROJECT_PATH` (приоритет над CWD, trust_self_index) в `resolve_project_root()`; `PROJECT_PATH` остался после CWD (обратная совместимость/multi-window). +**Guard:** пустые `tools/__init__.py`, `tools/stale_detector/__init__.py`, `locales/__init__.py`; Live-Smoke из чистого venv с `--project-path` (tools-ok, locales `en (78 ключей)`, корень проекта резолвится); полный прогон `1562 passed / 0 failed`; +4 теста на `MSCODEBASE_PROJECT_PATH` (multiwindow). Удалён side-effect-мусор `write_text(r"C:\temp\mscb_MAIN.txt")` в `write_tools.py` (из исходников и wheel). +**Статус:** ✅ CLOSED (live-верифицирован из wheel, чистая установка) | **Deadline:** — | **Владелец:** misha. +**Note:** `tools/__init__.py` не попадает в wheel (namespace package PEP-420) — import `doc_tools.py:14-15` работает; безвредно. + ## 2026-08-28 — Full reindex зависает в фазе «Finalizing» (PropertyGraph.optimize/create_index) (OPEN / WATCHING) **Что:** При live-верификации фиксов A/B (полный реиндекс job 1ff77294) embedding-фаза прошла БЕЗ заморозки сервера (подтверждает фикс A — QueueHandler), chunks записались корректно (1-based; см. AGENT_DIARY post-mortem off-by-one: save_symbol_index=341). После embed job завис в фазе «Finalizing» (отладка через intel_get_job_status + лог + netstat/py-spy: оба процесса 0% CPU — заблокированы, не считают) на `PropertyGraph.optimize()`/`create_index()` (`src/core/intelligence/layer.py:741` и `:1786`). Флаг `set_reindexing(True)` не снимается → блокирует concurrent search. Это PRE-EXISTING баг индексатора/графа, НЕ вызван фиксами A/B (фиксы касаются только логирования и display-строки; они live-верифицированы на этапе embed). diff --git a/locales/__init__.py b/locales/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index b04648c7..a4bdccdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,11 +143,12 @@ mscodebase-mcp = "src.main:main" [tool.setuptools.packages.find] where = ["."] -include = ["src*", "scripts*"] +include = ["src*", "scripts*", "tools.stale_detector*", "adapters*", "locales*"] [tool.setuptools.package-data] "*" = ["*.json", "*.md", "*.bat", "*.cfg", "*.scm"] "src" = ["*.json", "*.txt", "*.cfg", "*.scm"] +"locales" = ["*.json"] [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/src/core/project_resolution.py b/src/core/project_resolution.py index 4f5b9012..c33e632b 100644 --- a/src/core/project_resolution.py +++ b/src/core/project_resolution.py @@ -205,6 +205,49 @@ def _reject_self_index_target(p: Path, *, source: str) -> bool: return False +def _try_resolve_path_env(raw: str, source: str, *, trust_self_index: bool = False) -> Optional[Path]: + """Резолвит путь из env-raw (литерал $ZED_WORKTREE_ROOT или прямой путь). + + Общая логика для PROJECT_PATH и MSCODEBASE_PROJECT_PATH. Возвращает Path + или None (невалид / self-indexing / пусто). + + trust_self_index=True — доверяем явному override даже если это ext_root + (аналог ACTIVE_WORKSPACE). Используется ТОЛЬКО для MSCODEBASE_PROJECT_PATH + (клиент осознанно указал проект через --project-path); PROJECT_PATH + сохраняет блокировку self-indexing для обратной совместимости. + """ + if not raw: + return None + # Случай 1: Zed literal "$ZED_WORKTREE_ROOT" без подстановки. + if raw.startswith("$"): + zed_root = os.environ.get("ZED_WORKTREE_ROOT") + if zed_root: + p = Path(zed_root).resolve() + if p.exists() and not _reject_self_index_target(p, source="ZED_WORKTREE_ROOT"): + return p + return None + # Случай 2: прямой путь. + try: + resolved = Path(raw).resolve() + except (OSError, ValueError): + return None + if not resolved.exists() or not resolved.is_dir(): + return None + # Self-indexing guard: для автоматических fallback'ов блокируем ext_root; + # для явного CLI override (trust_self_index=True) — доверяем. + if ( + not trust_self_index + and _reject_self_index_target(resolved, source=source) + and os.environ.get("MSCODEBASE_ALLOW_SELF_INDEX", "").strip().lower() not in ("1", "true", "yes") + ): + logger.warning( + f"{source} указывает на self-indexing target ({resolved}). " + f"Игнорирую — установите MSCODEBASE_ALLOW_SELF_INDEX=1 или используйте ACTIVE_WORKSPACE." + ) + return None + return resolved + + def _resolve_env_project_root() -> Optional[Path]: """Резолвит PROJECT_PATH из окружения лениво + один раз кэширует результат. @@ -216,38 +259,9 @@ def _resolve_env_project_root() -> Optional[Path]: if _env_project_root_cache is not None: return _env_project_root_cache raw = os.environ.get("PROJECT_PATH", "").strip() - if not raw: - return None - # Случай 1: Zed literal "$ZED_WORKTREE_ROOT" без подстановки. - if raw.startswith("$"): - zed_root = os.environ.get("ZED_WORKTREE_ROOT") - if zed_root: - p = Path(zed_root).resolve() - if p.exists() and not _reject_self_index_target( - p, source="ZED_WORKTREE_ROOT" - ): - _env_project_root_cache = p - return _env_project_root_cache - return None - # Случай 2: прямой путь. - try: - resolved = Path(raw).resolve() - except (OSError, ValueError): - return None - if not resolved.exists() or not resolved.is_dir(): - return None - # Self-indexing guard (см. INC-53EC / REFC-02): если PROJECT_PATH - # Если пользователь ЯВНО задал PROJECT_PATH — доверяем ему, - # не блокируем self-indexing guard. Он знает, что делает. - # Только автоматический fallback (CWD/ext_root) блокируем. - if _reject_self_index_target(resolved, source="PROJECT_PATH") and os.environ.get("MSCODEBASE_ALLOW_SELF_INDEX", "").strip() not in ("1", "true", "yes"): - logger.warning( - f"PROJECT_PATH указывает на self-indexing target ({resolved}). " - f"Игнорирую — установите PROJECT_PATH=$ZED_WORKTREE_ROOT или MSCODEBASE_ALLOW_SELF_INDEX=1." - ) - return None - _env_project_root_cache = resolved - return _env_project_root_cache + env_root = _try_resolve_path_env(raw, "PROJECT_PATH") + _env_project_root_cache = env_root + return env_root def reset_project_root_cache() -> None: @@ -262,23 +276,39 @@ def resolve_project_root(provided: str = "") -> Path: Приоритет (каждый вызов резолвит заново — см. INC-53EC / REFC-02): 0. Явно переданный provided - 1. CWD (корень окна, для которого запущен MCP-процесс) — per-window + 1. MSCODEBASE_PROJECT_PATH env (явный override CLI/владельца, НАД CWD). + Приоритет над CWD — только для этого явного override; обычный Zed-режим + CWD-first сохраняет per-window изоляцию (INC-MULTI-WINDOW). + 2. CWD (корень окна, для которого запущен MCP-процесс) — per-window изоляция (INC-MULTI-WINDOW): Zed запускает отдельный MCP на окно и ставит CWD = корень окна. SQLite active_workspace_id глобальный на весь Zed — два окна резолвят один проект → PID-lock конфликт. Self-indexing guard: CWD == ext_root отклоняется (dev/test-режим добирается через SQLite active_workspace с доверием ACTIVE_WORKSPACE). - 2. PROJECT_PATH из окружения (lazy, с self-indexing guard) — явный + 3. PROJECT_PATH из окружения (lazy, с self-indexing guard) — явный override пользователя; Zed-литерал "$ZED_WORKTREE_ROOT" → None. - 3. SQLite multi_workspace_state.active_workspace_id (fallback для + 4. SQLite multi_workspace_state.active_workspace_id (fallback для single-window / когда CWD отклонён self-indexing guard'ом) - 4. Zed SQLite DB (workspaces table — fallback, если нет active) - 5. ZED_WORKTREE_ROOT env - 6. ext_root как fallback + 5. Zed SQLite DB (workspaces table — fallback, если нет active) + 6. ZED_WORKTREE_ROOT env + 7. ext_root как fallback """ if provided and provided.strip(): return Path(provided).resolve() + # ─── 0.5. MSCODEBASE_PROJECT_PATH: явный override НАД CWD ─── + # Отличается от PROJECT_PATH (см. ниже, позиция 3): имеет приоритет над + # CWD, т.к. это намеренный override (CLI --project-path устанавливает + # именно его). В обычном Zed-режиме env не задан → CWD-first сохраняется. + prio_raw = os.environ.get("MSCODEBASE_PROJECT_PATH", "").strip() + if prio_raw: + prio_root = _try_resolve_path_env( + prio_raw, "MSCODEBASE_PROJECT_PATH", trust_self_index=True + ) + if prio_root is not None: + logger.debug(f"resolve_project_root: MSCODEBASE_PROJECT_PATH={prio_root}") + return prio_root + # ─── 1. CWD-FIRST: per-window изоляция (INC-MULTI-WINDOW) ─── # Раньше CWD был предпоследним в цепочке, а SQLite active_workspace_id # (один на весь Zed) — первым: оба окна резолвили один проект → diff --git a/src/main.py b/src/main.py index dd2e4fea..0a2eeda8 100644 --- a/src/main.py +++ b/src/main.py @@ -181,6 +181,23 @@ def _gc_worker() -> None: pass return original_stdout + + +def _cli_project_path() -> str: + """Извлекает --project-path/--project-dir из sys.argv (значение или после =). + + Возвращает пустую строку, если аргумент не передан. Значение — путь + к целевому проекту (корень, для которого работают MCP-инструменты). + """ + for flag in ("--project-path", "--project-dir"): + for i, arg in enumerate(sys.argv): + if arg == flag and i + 1 < len(sys.argv): + return sys.argv[i + 1].strip() + if arg.startswith(flag + "="): + return arg[len(flag) + 1:].strip() + return "" + + def main(): """Главная функция запуска MCP-сервера.""" original_stdout = setup_logging() @@ -188,6 +205,20 @@ def main(): logger.info("MSCodebase Intelligence MCP Server запускается...") logger.info(f"PROJECT_ROOT: {PROJECT_ROOT}") + # ─── CLI --project-path: явный выбор целевого проекта ─── + # Устанавливаем MSCODEBASE_PROJECT_PATH (приоритет над CWD, см. + # resolve_project_root). PROJECT_ROOT (корень установки/расширения) НЕ + # трогаем — он не является корнем целевого проекта. Env задаётся ДО + # любого вызова resolve_project_root (в т.ч. при --install резолв корня). + _cli_proj = _cli_project_path() + if _cli_proj and not os.environ.get("MSCODEBASE_PROJECT_PATH"): + try: + _resolved_cli = str(Path(_cli_proj).resolve()) + except (OSError, ValueError): + _resolved_cli = _cli_proj + os.environ["MSCODEBASE_PROJECT_PATH"] = _resolved_cli + logger.info(f"CLI --project-path: {_resolved_cli}") + try: # Обработка аргументов командной строки if "--help" in sys.argv or "-h" in sys.argv: @@ -197,6 +228,10 @@ def main(): " python -m src.main # Запуск MCP сервера", file=sys.stderr, ) + print( + " python -m src.main --project-path