Skip to content

Latest commit

 

History

History
730 lines (590 loc) · 82.3 KB

File metadata and controls

730 lines (590 loc) · 82.3 KB

Накопленные проблемы аудита — MSCodeBase

Создано: 2026-07-27. Статус: активный. Источник: 4 раунда протокольного аудита (graph.py, cypher-стек, indexing-стек, write_tools, search/engine, error_handler, remote_embedder, ci.yml, server_tools).


P0 — Критические (безопасность / CI)

P0-1: SQL injection через alias в Cypher (цепочка lexer→parser→sql)

  • Файлы: src/core/search/cypher_parser.py:427, src/core/search/cypher_sql.py:84
  • Статус: ✅ FIXED (parser + sql.py)
  • Детали:
    • Parser _parse_return_item берёт self.advance().value без проверки типа токена (L427)
    • cypher_sql.py L84: f" AS {item.alias}" — f-string подстановка без валидации
    • PoC: MATCH (n:Function) RETURN n.name AS "x FROM nodes-- " → SQL: SELECT n.name AS x FROM nodes-- FROM nodes AS n
    • -- обрезает остаток SQL, возвращает ВСЕ строки таблицы nodes вместо отфильтрованные
    • Контраст: _property_ref_to_sql (L321-324) валидирует имена свойств через re.fullmatch, alias — нет
  • Фикс (parser): ✅ Добавлена проверка alias_token.type not in (TokenType.IDENTIFIER, TokenType.STRING) → SyntaxError
  • Фикс (sql.py): ✅ Добавлена валидация alias через re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", item.alias) перед f-string подстановкой (L84-88)

P0-2: SQL injection через layer параметр в search/engine.py

  • Файл: src/core/search/engine.py:352,736
  • Статус: ✅ FIXED
  • Детали:
    • filter_expr = f"layer = '{layer}'" if layer else "" — f-string подстановка пользовательского ввода в LanceDB SQL
    • PoC: search_code(query="test", filter_layer="core' OR '1'='1") → layer = 'core' OR '1'='1' → возвращает чанки из ВСЕХ слоёв
    • Контраст: indexer_table.py:_escape_sql_value корректно экранирует кавычки и backslash, но здесь не использовался
  • Фикс: ✅ Вызов IndexerTableMixin._escape_sql_value(layer) перед f-string подстановкой в обоих местах (L352-356 и L740-742). Импорт IndexerTableMixin уже есть на уровне модуля (L18).

P0-3: CI clean-state скрипл использует Windows-пути на Linux

  • Файлы: .github/workflows/ci.yml:59, scripts/verify_clean_state.sh:53,55,58,62
  • Статус: ✅ FIXED (verify_clean_state.sh --no-clone)
  • Детали:
    • verify_clean_state.sh использовал venv/Scripts/pip.exe и venv/Scripts/python.exe — Windows-формат
    • CI job clean-state запускается на ubuntu-latest → venv/Scripts/pip.exe не существует → exit 127
    • Также bash scripts/verify_clean_state.sh в CI вызывает скрипт, который внутри делает git clone внешнего репозитория — ненадёжно в CI
  • Фикс: ✅ Заменены venv/Scripts/pip.exe → venv/bin/pip, venv/Scripts/python.exe → venv/bin/python
  • Примечание: ✅ закрыто — verify_clean_state.sh параметризован ($1 = repo URL, default сохранён; флаг --no-clone пропускает clone и тестирует текущий каталог), CI вызывает bash scripts/verify_clean_state.sh --no-clone "${{ github.repository }}" — тестируется тот SHA, который checkout-нул раннер. Self-clone убран, локальный ручной запуск без аргументов сохраняет прежний полный клон.

P0-4: codebase_tool.py docstring противоречит коду (sandbox)

  • Файл: src/mcp/tools/codebase_tool.py:148-164
  • Статус: ✅ FIXED
  • Детали:
    • Docstring: ⚠️ ВНИМАНИЕ: Изоляция (sandbox) ОТСУТСТВЕТ.
    • Код реально использует sandbox: execute_sandboxed с SANDBOX_MODE_STRICT по умолчанию (AST validation + module allowlist + subprocess isolation)
    • Администратор, читающий docstring, мог решить инструмент нельзя включать
  • Фикс: ✅ Docstring синхронизирован: указано что sandbox активен (execute_sandboxed + SANDBOX_MODE_STRICT), перечислены механизмы изоляции

P0-5: sandbox — несоответствие слоёв allowlist + утечка секретов в env (Qwen review F-1/F-4)

  • Файлы: src/core/sandbox/executor.py:45-52,350
  • Статус: ✅ FIXED (2026-07-31)
  • Детали:
    • F-1: ALLOWED_MODULES содержал import-механику (importlib*, pkgutil, runpy, modulefinder, zipimport) — AST-валидация пропускала import importlib, runtime _safe_import блокировал (несоответствие слоёв). importlib.import_module("os") — RCE-вектор при расхождении runtime-слоя.
    • F-4: env = os.environ.copy() — ВСЕ секреты родителя (API-ключи, токены) доступны sandbox-скрипту.
    • F-2 (сопутствующий): __build_class__ отсутствовал в BLOCKED_NAMES; F-3: "sys" в runtime-allowlist при AST-блокировке import sys.
  • Фикс: import-механика удалена из ALLOWED_MODULES; __build_class__ добавлен в BLOCKED_NAMES; "sys" убран из _USER_ALLOWED; _build_minimal_env() (PATH="", SYSTEMROOT, SYSTEMDRIVE, TEMP/TMP, PYTHONPATH) вместо os.environ.copy(). +6 тестов в tests/test_sandbox.py (40 passed).
  • Верификация: Qwen review (2026-07-31): F-1/F-2/F-3/F-4 ✅ CONFIRMED, F-5 ❌ REFUTED (mkstemp уже 0600).

P1 — Высокий приоритет (race conditions / data loss / crash)

P1-1: shortest_path в graph.py — экспоненциальное потребление памяти

  • Файл: src/core/graph.py:918-975
  • Статус: ✅ FIXED (2026-07-31 — parent-pointer BFS + пакетная реконструкция)
  • Детали: BFS хранит все пути целиком в queue (List[List[Tuple[int, Optional[int]]]]). Для графа со средней степенью 10 и max_depth=10 — до 10^10 путей в памяти. Стандартный BFS хранит parent-pointer и восстанавливает путь в конце.
  • Фикс: shortest_path переписан на parent-pointer BFS (parent: Dict[node_id, (parent_id, edge_id)]) — память O(V) вместо O(V×depth); _reconstruct_path — 2 пакетных запроса (nodes IN (...), edges IN (...)) вместо N+1.

P1-2: batch_add_edges — N+1 запросов

  • Файл: src/core/graph.py:1235-1285
  • Статус: ✅ FIXED (2026-07-31 — батч-lookup узлов одним запросом)
  • Детали: Для каждого ребра 2 SELECT + 1 INSERT. Для батча из 10000 рёбер — 30000 SQL-вызовов вместо одного INSERT ... SELECT FROM ... JOIN.
  • Фикс: предзагрузка всех qualified_name → id одним SELECT ... WHERE qualified_name IN (...), затем цикл только INSERT.

P1-3: db_manager.py — search без _write_lock vs reset_connection

  • Файл: src/core/indexing/db_manager.py:318-364, indexer.py:449-460,338-380
  • Статус: ✅ FIXED (2026-07-31 — read-секции под RLock)
  • Детали: reset_connection пересоздаёт self.table под _write_lock, но search() (L304) этот lock не захватывает. Параллельный write во время search → LanceError: table modified during scan или stale reference.
  • Фикс: _index_single_file/_parse_file_only/move_chunks_metadata — чтения self.table.search() обёрнуты в with self._table_write_lock: (RLock, реентерабельно с reset_connection).

P1-4: switch_db без _write_lock

  • Файл: src/core/indexing/db_manager.py:269-316
  • Статус: ✅ FIXED (2026-07-31)
  • Детали: Закрывает старую БД и открывает новую без захвата _write_lock. Параллельный write в этот момент → запись в закрытую БД → crash.
  • Фикс: всё тело switch_db обёрнуто в with self._write_lock: (RLock).

P1-5: move_chunks_metadata — delete+add не атомарно

  • Файл: src/core/indexing/indexer.py:540-591
  • Статус: ✅ FIXED (2026-07-31 — сериализация под _table_write_lock)
  • Детали: Read → Delete → Modify → Add. Если процесс упадёт между Delete и Add, чанки пропадут из индекса без восстановления.
  • Фикс: вся последовательность read→delete→add обёрнута в with self._table_write_lock: — исключено «чтение чужого половинчатого состояния» и stale table reference.

P1-6: CypherExecutor.execute обходит блокировку PropertyGraph

  • Файл: src/core/search/cypher_executor.py:66-68
  • Статус: ✅ FIXED (2026-07-31)
  • Детали: _get_conn() возвращает raw sqlite3.Connection без захвата self._graph._lock. Параллельный add_edge во время Cypher-запроса → sqlite3.OperationalError: database is locked.
  • Фикс: execute обёрнут в with self._graph._lock:.

P1-7: write_tools.py — file_path без FileGuard (path traversal)

  • Файл: src/mcp/tools/write_tools.py (все write-операции)
  • Статус: 🔍 IN PROGRESS
  • Детали: file_path от пользователя передаётся в Path(file_path).resolve() без проверки, что он внутри project_path. Можно записать файл за пределами проекта.

P1-8: write_tools.py — new_name без валидации идентификатора

  • Файл: src/mcp/tools/write_tools.py:99-126
  • Статус: 🔍 IN PROGRESS
  • Детали: new_name и symbol не проверяются как идентификаторы. Пользователь может передать new_name = "evil(); import os; os.system('rm -rf /')" — текстовая замена через str.replace вставит вредоносный код.

P1-9: write_tools.py — _uri_to_path без проверки project_path

  • Файл: src/mcp/tools/write_tools.py:450-455
  • Статус: 🔍 IN PROGRESS
  • Детали: LSP может вернуть URI для файла за пределами проекта — запись выполнится без проверки.

P1-10: error_handler.py — elapsed = ... - 1000 вместо * 1000

  • Файл: src/core/error_handler.py:454
  • Статус: 🔍 IN PROGRESS
  • Детали: При timeout elapsed = int((time.perf_counter() - start_time) - 1000) — вычитает 1000 секунд вместо перевода в миллисекунды. Записывает отрицательную latency в метрики, ломая min_ms, avg_ms, P50/P95.

P1-11: error_handler.py — future.cancel() не вызывается при timeout

  • Файл: src/core/error_handler.py:593-598
  • Статус: ✅ CONFIRMED FIXED (уже было в 5601de39: except TimeoutError: future.cancel(); raise)
  • Детали: При TimeoutError future остаётся в пуле, поток продолжает выполнение. 4 зависших timeout-а исчерпают _SYNC_POOL (max_workers=4), все последующие sync-вызовы будут ждать вечно.

P1-12: remote_embedder.py — silent zero-vector fallback маскирует отказ

  • Файл: src/providers/embedder/remote_embedder.py:717-718,731-732
  • Статус: 🔍 IN PROGRESS
  • Детали: При падении провайдера возвращаются нулевые векторы. Индексация проходит «успешно», но векторный поиск возвращает случайные результаты (cosine similarity с нулевым вектором = 0 для всех). Нет маркера в БД «этот чанк имеет fallback-вектор».

P1-13: db_manager.py — _write_lock = threading.Lock, не RLock

  • Файл: src/core/indexing/db_manager.py:60, indexer.py:76
  • Статус: ✅ FIXED (2026-07-31 — threading.RLock в обоих местах)
  • Детали: threading.Lock не реентерабелен. Будущий рефакторинг, добавляющий reset_connection внутрь with self._table_write_lock:, создаст deadlock.
  • Фикс: заменено на RLock (требуется: reset_connection вызывает _warmup_cache, read-секции вложены в write-lock). Проверено: acquire(blocking=False) нигде не используется.

P1-14: db_manager.py — PID lock race → crash вместо ожидания

  • Файл: src/core/indexing/db_manager.py:428-490
  • Статус: ✅ FIXED (2026-07-31 — raise при таймауте + retry-loop)
  • Детали: Между lock_path.unlink() и os.open(... O_EXCL) другой процесс может создать lock file. os.open падает с FileExistsError, ловится и превращается в RuntimeError — процесс крашится вместо ожидания. Бонус-находка: после 30с ожидания код молча возвращался БЕЗ захвата лока (писатель без блокировки).
  • Фикс: таймаут ожидания → явный RuntimeError; захват после steal — retry-loop 5 попыток с паузой 0.5с.

P1-15: zed_config.remove_zed_settings уничтожает JSONC-комментарии (КРИТ-2 Claude review)

  • Файл: src/utils/zed_config.py:389-397
  • Статус: ✅ FIXED (2026-07-31 — хирургическое удаление через _set_top_level)
  • Детали: remove_zed_settings парсил JSONC и сериализовал через json.dumps — ВСЕ комментарии пользователя в settings.json терялись. Модуль декларирует контракт «JSONC comments stay byte-for-byte» (docstring L15-16) — нарушение. Docstring функции документировал потерю как tradeoff, но фикс возможен тем же приёмом, что в patch_zed_settings.
  • Фикс: удаление через текстовую хирургию _set_top_level по обоим ключам (перезаписываются только управляемые блоки, комментарии вне их — byte-for-byte) + атомарная запись.
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED по коду (json.dumps rewrite), severity понижена с P0 до P1 (документировано в docstring).

P1-16: zed_config.patch_zed_settings неатомарная запись (КРИТ-3 Claude review)

  • Файл: src/utils/zed_config.py:343
  • Статус: ✅ FIXED (2026-07-31 — _atomic_write_text temp+os.replace)
  • Детали: settings_path.write_text(new_content) на Windows неатомарно (truncate + write). Zed читает settings.json при каждом focus — окно нулевого файла → пользователь получает пустой конфиг.
  • Фикс: общий хелпер _atomic_write_text (mkstemp в той же директории + fsync + os.replace), применён в patch_zed_settings и remove_zed_settings.
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED по коду.

P1-17: CodeParser — tree-sitter Parser не потокобезопасен при параллельном парсинге (Qwen review B-1)

  • Файл: src/core/indexing/parser.py:69,331,671 (+ index_project_runner.py:200-206)
  • Статус: ✅ FIXED (2026-07-31 — thread-local parsers + кэш)
  • Детали: CodeParser — DI-singleton, а IndexProjectRunner парсит в ThreadPoolExecutor(max_workers=4): все воркеры вызывают self.parsers[ext].parse() на ОДНОМ tree-sitter Parser (не reentrant, переиспользует cursor) + общий _cache_* (гоночный повтор дерева другого файла). Ошибка B-1 Qwen оказалась серьёзнее заявленного Medium — реальный race при каждой переиндексации.
  • Фикс: _get_parser(ext) — thread-local копии Parser'ов (Language иммутабелен, безопасно шарить); кэш дерева перенесён в threading.local. Обе точки parse() (L343, L693) переведены на _get_parser.
  • Верификация: Qwen review (2026-07-31): ✅ CONFIRMED (severity поднят Medium→High — race в пуле парсинга); tests/test_assignments.py + test_symbol_index_call_graph.py (105 passed).

P2 — Средний приоритет

P2-1: layer.py — 22 except Exception молчаливая деградация

  • Файл: src/core/intelligence/layer.py
  • Статус: ⏳ PARTIAL (2026-07-31 — legacy grandfathered через BLE001 per-file-ignores; ruff enforce для новых файлов)
  • Детали: 18 из 22 except Exception с pass или return []. Программные ошибки маскируются под «нет данных».
  • Решено: ruff select включает BLE (P2-17) — новые файлы обязаны быть без BLE001; 664 legacy-нарушения задедклайрены. Полная расчистка — отдельный рефакторинг.

P2-2: layer.py — hash(line) % 10000 недетерминированный ID

  • Файл: src/core/intelligence/layer.py:783-800
  • Статус: ✅ FIXED (2026-07-31 — blake2b вместо hash())
  • Детали: hash() рандомизирован через PYTHONHASHSEED. Один и тот же код-фрагмент при разных запусках получит разные incident_id, ломая дедупликацию.

P2-3: layer.py — netstat -ano Windows-only

  • Файл: src/core/intelligence/layer.py:340-375
  • Статус: ✅ FIXED (2026-07-31 — psutil / ss fallback)
  • Детали: netstat -ano не работает на Linux/macOS. _find_pid всегда возвращает 0 на не-Windows. RAM-метрика неполная.

P2-4: layer.py — asyncio.Lock + threading.Lock смешаны

  • Файл: src/core/intelligence/layer.py:124-130
  • Статус: ✅ FIXED (2026-07-31 — единый threading.Lock + async-адаптер)
  • Детали: intel_log_incident использует asyncio.Lock, intel_auto_collect_adrs использует threading.Lock. Оба пишут в IntelligenceStore — race condition.
  • Фикс: _write_lock = threading.Lock() один на sync+async; async-методы используют _AsyncLockAdapter (asyncio.to_thread acquire); _sync_write_lock удалён.

P2-5: engine.py — _cache без TTL, stale после reindex

  • Файл: src/core/search/engine.py:89-101,735-810
  • Статус: ✅ FIXED (2026-07-31 — TTL 30с + invalidate_cache())
  • Детали: Кэш на 500 записей без TTL. После reindex кэш не инвалидируется — поиск возвращает устаревшие результаты.
  • Фикс: записи хранят (timestamp, results); чтение проверяет time.monotonic() - ts <= 30; истёкшие удаляются; добавлен публичный invalidate_cache().

P2-6: engine.py — asyncio.run в ThreadPoolExecutor bottleneck

  • Файл: src/core/search/engine.py:303-317
  • Статус: ⏳ TECH DEBT (ACCEPTED, 2026-07-31) — закрыт как осознанный выбор владельцем-протоколом: НЕ deadlock, а starvation с future.result(timeout=30); протокол проекта (§0.1 AGENTS.md) сам запрещает 3+ параллельных MCP-вызовов, поэтому max_workers=2 не достижимо легитимно; persistent loop — риск выше пользы
  • Детали: asyncio.run создаёт новый event loop в каждом вызове. 3+ параллельных запроса → третий ждёт.
  • Решено: общий пул уже был (batch 5601de39); полный переход на persistent loop — отдельный рефакторинг (риск выше пользы для текущего использования).
  • Верификация (Claude review 2026-07-31, вторая волна): ✅ CONFIRMED по коду (L308-316, max_workers=2, вызовы только search_with_mode/search из SearchCodeTool.execute) — starvation с таймаутом 30с, не circular deadlock; воркеры пула не ждут друг друга; дубликат не создавался; закрыт с обоснованием.

P2-7: engine.py — 18 except Exception с возвратом []

  • Файл: src/core/search/engine.py (множество методов)
  • Статус: ⏳ PARTIAL (2026-07-31 — см. P2-1: BLE001 enforce для новых, legacy grandfathered)
  • Детали: Пользователь не отличит «нет результатов» от «поиск упал».

P2-8: write_tools.py — _infer_package некорректный rstrip(".py")

  • Файл: src/mcp/tools/write_tools.py:307-310
  • Статус: ✅ FIXED (2026-07-31 — p.stem вместо rstrip)
  • Детали: rstrip(".py") удаляет любую комбинацию символов ., p, y с конца. happy.py → ha. Должно быть p.stem.
  • Фикс: stem = p.stem — статус был устаревшим (🔍 IN PROGRESS), фактически закрыт предыдущей сессией.

P2-9: write_tools.py — неатомарная запись без backup (КРИТ-1 Claude review)

  • Файл: src/mcp/tools/write_tools.py — 7 точек записи (было L386-413, факт.: _action_replace L380, _action_insert L446, _apply_changes L610, _apply_workspace_edit L662, _apply_delete L720, _apply_move L749/L753/L765)
  • Статус: ✅ FIXED (2026-07-31 — общий хелпер _atomic_write)
  • Детали: Read → modify → write_text без tempfile+rename. Если процесс упадёт — файл в неконсистентном состоянии. Scope расширен по Claude review (КРИТ-1): атомарный паттерн был только в _apply_changes (mkstemp+os.replace), остальные 6 точек писали напрямую.
  • Фикс: модульный _atomic_write(path, content) (mkstemp в той же директории + fsync + os.replace + cleanup при ошибке), применён во всех 7 точках (включая inline-блок _apply_changes — унифицирован).
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED (6 неатомарных точек + 1 атомарная); ruff clean; py_compile OK.

P2-10: remote_embedder.py — mode_lock только на чтение current_mode

  • Файл: src/providers/embedder/remote_embedder.py:644-660
  • Статус: ✅ FIXED (2026-07-31 — смена mode теперь даёт явный RuntimeError+fallback, не нулевые векторы; см. P1-12 фикс batch)
  • Детали: Блокировка отпускается после чтения self.mode, но HTTP-запрос выполняется без блокировки. Смена mode посреди batch → часть чанков получит векторы от одного провайдера, часть — нулевые.

P2-11: remote_embedder.py — один httpx.Client на все провайдеры

  • Файл: src/providers/embedder/remote_embedder.py:63-69,382-520
  • Статус: ✅ FIXED (2026-07-31 — отдельный _sync_client с timeout=2s для сканера/health; embedding-client остаётся один)
  • Детали: Один Client с connection pool для llama.cpp, LM Studio, ONNX. Медленный провайдер блокирует остальных.

P2-12: error_handler.py — _TIMELINE.pop(0) O(n) под локом

  • Файл: src/core/error_handler.py:235-250
  • Статус: ✅ FIXED (2026-07-31 — collections.deque(maxlen=50) и deque(maxlen=1000) для latencies)
  • Детали: list.pop(0) сдвигает все элементы. Под _TOOL_METRICS_LOCK на каждый вызов инструмента. Bottleneck при высокой нагрузке.

P2-13: indexer_table.py — ручное _escape_sql_value хрупко

  • Файл: src/core/indexing/indexer_table.py:26-47
  • Статус: 🔍 IN PROGRESS
  • Детали: Экранирование покрывает 3 вектора, но не двойные кавычки, Unicode-обходы. Зависит от того, что DataFusion не изменит escape-правила.

P2-14: indexer.py — get_status читает кэш без _index_lock

  • Файл: src/core/indexing/index_status.py:37-90, indexer.py:141-146
  • Статус: ✅ FIXED (2026-07-31 — lock передан в IndexStatusReporter, чтения под ним)
  • Детали: total_chunks обновился, а unique_files ещё нет — статус вернёт неконсистентные числа.

P2-15: db_manager.py — _warmup_cache без _write_lock

  • Файл: src/core/indexing/db_manager.py:192-222
  • Статус: ✅ FIXED (2026-07-31 — обёрнуто в with self._write_lock:, RLock)
  • Детали: Читает self.table без _write_lock. Если параллельный поток делает reset_connection, может попасть на закрытую таблицу.

P2-16: ci.yml — Node 20 deprecated, нет Python 3.10

  • Файл: .github/workflows/ci.yml
  • Статус: ✅ FIXED (2026-07-31 — Python 3.10 в matrix, actions/checkout@v5)
  • Детали: actions/setup-python@v5 с python 3.11/3.12, но нет 3.10. Node 20 в matrix может быть deprecated.

P2-17: ruff.toml — BLE001 (broad except) не в select

  • Файл: ruff.toml
  • Статус: ✅ FIXED (2026-07-31 — BLE в select + per-file-ignores для 84 legacy-файлов, 664 нарушения)
  • Детали: 532 broad except по проекту не ловятся ruff-ом.

P2-18: ServiceCollection.resolve — race при параллельном создании фабрики (Claude review P1)

  • Файл: src/core/di_container.py:123-147
  • Статус: ✅ FIXED (2026-07-31 — threading.Lock в resolve)
  • Детали: resolve без блокировки: чтение _instances → вызов factory → запись. Два параллельных resolve создадут два экземпляра (напр., два PropertyGraph на один WAL). Сейчас латентно: фабрики через add_factory не регистрируются (все — add_singleton), create_service_collection однопоточный.
  • Фикс: threading.Lock вокруг lookup + factory-вызов; при появлении фабрик с re-entrant resolve потребуется RLock (закомментировано в коде).
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED (severity P1→P2 — латентно).

P2-19: zed_config — command.split() ломается на путях с пробелами (Claude review P2)

  • Файл: src/utils/zed_config.py:296-301
  • Статус: ✅ FIXED (2026-07-31 — space-aware executable detection)
  • Детали: split(maxsplit=1) обрезает путь C:\Users\John Doe\...\python.exe до C:\Users\John. Реальный кейс: Windows-профили с пробелом. install.py передаёт f"{PYTHON_EXE} -u -m src.main" без кавычек.
  • Фикс: ищем самый длинный существующий файл-префикс как executable; если префикс не существует (команда из PATH, напр. "python") — первый токен целиком.
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED по коду.

P2-20: llama_runner — stderr fd leak при исключении Popen (Claude review P2)

  • Файл: src/providers/reranker/llama_runner.py:991-996 (+ start L885, _spawn_reranker L1072)
  • Статус: ✅ FIXED (2026-07-31 — закрытие fh в except, 3 места)
  • Детали: stderr=(_fh := open(self._log_path(), 'ab')) — walrus внутри вызова _popen_with_job; при исключении except логировал, но НЕ закрывал fd. Паттерн в 3 местах (start, _spawn_embedder, _spawn_reranker).
  • Фикс: локальная log_fh (init None перед try — защита от NameError при отказе open()), walrus → stderr=(log_fh := ...), присваивание self._*_log_fh = log_fh после успеха, закрытие в except. stop()/stop_reranker() уже закрывали fh на успешном пути.
  • Верификация: Claude review (2026-07-31): ✅ CONFIRMED по коду.

P2-21: graph._CrossProcessMutex — hash() рандомизирован per-process (Qwen review D-1)

  • Файл: src/core/graph.py:55-60
  • Статус: ✅ FIXED (2026-07-31 — blake2b)
  • Детали: hash(str(path)) (PYTHONHASHSEED) → два MCP-процесса получали РАЗНЫЕ имена мутексов для одной БД → cross-process lock не работал на Windows multi-window.
  • Фикс: hashlib.blake2b(str(path).encode(), digest_size=4).hexdigest() (паттерн P1-8/engine cache).

P2-22: graph.delete_node/delete_edge/import_compressed — conn.total_changes cumulative (Qwen review C-5)

  • Файл: src/core/graph.py:520,831,1475
  • Статус: ✅ FIXED (2026-07-31 — cursor.rowcount)
  • Детали: total_changes — суммарно с момента открытия соединения: delete_node несуществующего узла возвращал True; node_count в логе импорта был завышен.
  • Фикс: cur = conn.execute(...) → return cur.rowcount > 0; node_count = node_cur.rowcount.

P2-23: scoring.RRF — нестабильная сортировка при равных скорах (Qwen review C-1)

  • Файл: src/core/search/scoring.py:74,127
  • Статус: ✅ FIXED (2026-07-31 — детерминированный tie-break)
  • Детали: sorted(key=lambda k: scores[k], reverse=True) — tie-break зависел от порядка вставки в dict.
  • Фикс: key=lambda k: (-scores[k], k) — вторичный ключ file:chunk_index.

P2-24: MMR до bucket/co-change — финальный sort отменял диверсификацию (Qwen review C-2)

  • Файлы: src/core/search/engine.py:480-521, src/core/search/scoring.py:314-413
  • Статус: ✅ FIXED (2026-07-31 — MMR после sort+cut, reorder-only)
  • Детали: MMR переупорядочивал + бустил скоры, но последующий sort(final_score) (после bucket weights и co-change boost) отменял MMR-порядок; искусственный boost ×1.08 искажал выдачу.
  • Фикс: MMR перенесён ПОСЛЕ sort+cut и перед reranker'ом (при отсутствии reranker'а MMR-порядок доживает до выдачи); убран блок мутации final_score (reorder-only).

P2-25: graph.get_neighbors — BFS без лимита узлов (Qwen review C-4)

  • Файл: src/core/graph.py:842-887
  • Статус: ✅ FIXED (2026-07-31 — max_nodes=1000)
  • Детали: на hub-файлах (1000+ рёбер) обход O(V+E) без ограничения.
  • Фикс: параметр max_nodes: int = 1000, обход прерывается при достижении (и на входе уровня, и в цикле рёбер).

P2-26: graph._get_conn — mmap_size 256MB (Qwen review D-6)

  • Файл: src/core/graph.py:376
  • Статус: ✅ FIXED (2026-07-31 — 64MB)
  • Детали: 256MB × 5 проектов (multi-window) = 1.25GB виртуальной памяти; на 32-bit Python — риск.
  • Фикс: PRAGMA mmap_size=67108864 (64MB, вровень с page cache).

P2-27: di_container monkey-patching embedder._breaker (Qwen review B-6)

  • Файлы: src/core/di_container.py:369-374, src/providers/embedder/remote_embedder.py:365
  • Статус: ✅ FIXED (2026-07-31 — публичный set_circuit_breaker)
  • Детали: if hasattr(embedder, "_breaker"): embedder._breaker = ... — запись в приватный атрибут извне.
  • Фикс: метод RemoteEmbedder.set_circuit_breaker(breaker) + вызов через getattr(embedder, "set_circuit_breaker", None) с fallback.

P3 — Низкий приоритет

P3-1: graph.py — export_compressed / import_compressed unlink без finally

  • Файл: src/core/graph.py:1395-1420,1453-1500
  • Статус: ✅ FIXED (2026-07-31 — try/finally + unlink(missing_ok=True))
  • Детали: Если subprocess.run(check=True) упадёт, temp_db.unlink() не выполнится → .tmp.db останется на диске.

P3-2: graph.py — close() — PRAGMA wal_checkpoint(TRUNCATE) без таймаута

  • Файл: src/core/graph.py:409-420,370
  • Статус: ✅ CONFIRMED FIXED (busy_timeout=30000 уже установлен на connection, L370)
  • Детали: На больших графах checkpoint может занять минуты и заблокировать закрытие. Нет PRAGMA busy_timeout.
  • Верификация: PRAGMA busy_timeout=30000 присутствует в _get_conn — ожидание блокировки ограничено 30с, ошибка checkpoint перехватывается try/except, conn.close() выполняется в finally.

P3-3: graph.py — _get_conn check_same_thread=False + RLock bottleneck

  • Файл: src/core/graph.py:353,359-374
  • Статус: ⏳ ACCEPTED tech debt (2026-07-31 — задокументировано: сериализация через RLock — осознанный компромисс безопасности)
  • Детали: Все операции сериализуются через один threading.RLock. WAL mode позволяет конкурентные чтения, но блокировка это отменяет.
  • Решение: переход на read-write lock (RWLock) — отдельный рефакторинг с аудитом всех 40+ методов; текущая сериализация исключает класс гонок целиком. Зафиксировано как осознанный техдолг.

P3-4: graph.py — detect_dead_code молчаливый LIMIT 200

  • Файл: src/core/graph.py:995-1027
  • Статус: ✅ FIXED (2026-07-31 — параметр limit + logger.warning при усечении)
  • Детали: Возвращает максимум 200 кандидатов; если dead code больше, пользователь не узнает об усечении.

P3-5: cypher_executor.py — SQL и params утекают в ответ MCP

  • Файл: src/core/search/cypher_executor.py:81-95
  • Статус: ✅ FIXED (2026-07-31 — sql/sql_params удалены из stats)
  • Детали: Сгенерированный SQL и параметры возвращаются в stats и попадают в MCP-ответ. Раскрывает внутреннюю структуру таблиц.

P3-6: cypher_sql.py — variable-length paths [*1..3] молча игнорируются

  • Файл: src/core/search/cypher_sql.py:252-262
  • Статус: ✅ FIXED (2026-07-31 — явный NotImplementedError вместо неверного single-hop)
  • Детали: Запрос MATCH (n)-[:CALLS*1..5]->(m) возвращает только прямых соседей, не 5 уровней. Только debug-level лог.
  • Фикс: max_hops > 1 → raise NotImplementedError с пояснением (пользователь получает ошибку, а не тихо неверные результаты).

P3-7: write_tools.py — _apply_delete по line_no без проверки содержимого

  • Файл: src/mcp/tools/write_tools.py:674-704
  • Статус: ✅ FIXED (2026-07-31 — проверка short_name in line перед удалением, иначе skip + error)
  • Детали: Удаляет строки по номеру без проверки symbol in text_lines[idx]. Если индекс устарел — удалятся неправильные строки.

P3-8: write_tools.py — _action_replace без проверки синтаксиса

  • Файл: src/mcp/tools/write_tools.py:312-390
  • Статус: ✅ FIXED (2026-07-31 — ast.parse для .py перед записью)
  • Детали: new_code вставляется как есть. Нет ast.parse для Python-файлов перед записью.

P3-9: write_tools.py — _apply_changes неатомарная запись без backup

  • Файл: src/mcp/tools/write_tools.py:569-602
  • Статус: ✅ CONFIRMED FIXED (batch 5601de39: tempfile.mkstemp + os.replace)
  • Детали: Нет backup-файла, нет tempfile + os.replace паттерна.

P3-10: error_handler.py — traceback в MCP-ответ (info leak)

  • Файл: src/core/error_handler.py:562-574,620-630
  • Статус: ✅ FIXED (2026-07-31 — detail=None; traceback остаётся в логах)
  • Детали: traceback.format_exc(limit=3) возвращается в MCP-ответ пользователю. Раскрывает пути к файлам, имена модулей.

P3-11: layer.py — ручной парсинг git objects через zlib

  • Файл: src/core/intelligence/layer.py:858-885
  • Статус: ✅ FIXED (2026-07-31 — git log fallback для packfiles)
  • Детали: Не поддерживает packfiles (.git/objects/pack/*.pack). Если git gc был сделан — ADR-коллектор молча вернёт «не найдено».
  • Фикс: _read_commit_msg — сначала loose-объект; при отсутствии (packfile) — git log --format=%s%x00%b для хэша.

P3-12: db_manager.py — asyncio.Lock в __init__ — wrong loop на Python 3.10

  • Файл: src/core/indexing/db_manager.py:56,228-250
  • Статус: ✅ FIXED (2026-07-31 — ленивое создание lock в ensure_async_table)
  • Детали: asyncio.Lock() создаётся без running loop. Если LanceDBManager инстанцируется в sync-коде startup, а ensure_async_table вызывается из async MCP-handler в другом event loop — lock может привязаться к неправильному loop.

P3-13: server_tools.py — двойная инстанциация каждого инструмента

  • Файл: src/mcp/server_tools.py:156-210
  • Статус: ✅ FIXED (2026-07-31 — экземпляры кэшируются в _name_cache)
  • Детали: Каждый класс создаётся дважды: первый раз для .name (фильтр), второй раз для регистрации. Кэш хранит только имя, не экземпляр. 38 инстанциаций вместо 19.

P3-14: server_tools.py — _action_write создаёт SymbolWriteTool на каждый вызов

  • Файл: src/mcp/server_tools.py
  • Статус: ✅ CONFIRMED FIXED (grep: SymbolWriteTool/_action_write отсутствуют в server_tools.py)
  • Детали: Локальный import + новая инстанция каждый вызов. Для MCP-сервера с сотнями write-операций — лишний overhead.

Протокол нарушения (предыдущая сессия)

# Нарушение Пункт AGENTS.md
1 MCP-FIRST не соблюдён — grep/read_file вместо MCP-инструментов §0.2
2 .agent_task_state.md не обновлялся для текущей задачи §0.1
3 EXPERIMENTS_LOG.md не заполнен §1.6
4 ## Verification Results не создана §0.1.1
5 verified_from_clean_state не выполнен §7 п.7
6 ## Definition of Done не проверен §7
7 Числа без команды замера §5.15
8 Verified vs Recalled не разграничены §1.14
9 AGENT_DIARY.md не обновлён §4
10 KNOWN_ISSUES.md не синхронизирован §4 п.6

Что сделано в текущей сессии

  • ✅ Parser _parse_return_item — добавлена валидация alias token type (P0-1 частично)
  • ✅ .agent_task_state.md обновлён
  • ✅ MCP connectivity проверена (debug_runtime_passport — RUN_ID: ae0c5dfb6fab)
  • ✅ Все P0-P3 проблемы выписаны в ISSUE.md

Что осталось

  • ✅ G-1 (2026-07-31): 5 stub-тестов закрыты — test_file_exists (FileGuard), test_searcher (Searcher sync-путь), test_chunk_cache (IndexPipeline.process_file), test_idle_reload (OnnxEmbedderClient), test_real_path (real-path резолюция). 52 теста вместо 10 stub; 658 passed; ruff clean; verify_diary 20/20.
  • ✅ G-2 (2026-07-31): E2E MCP smoke-тест — tests/e2e/test_e2e_mcp_smoke.py: реальный embedder (llama.cpp :8080) → реальная LanceDB (временная) → реальный поиск (fast, FTS5-fusion). Проверка входа→выхода: запрос move_chunks_metadata → чанк из file_move_manager.py. Требует живого MCP+embedder; в CI скипается (без MSCODEBASE_E2E=1). Команда: MSCODEBASE_E2E=1 python -m pytest tests/e2e/test_e2e_mcp_smoke.py -v.
  • ⏳ P2-1/P2-7, P3-3: осознанный техдолг — задокументировано в статусах (legacy broad excepts grandfathered через BLE001 ignores; RWLock — отдельный рефакторинг)
  • ✅ P2-6: закрыт как TECH DEBT (ACCEPTED, 2026-07-31) — starvation, не deadlock; max_workers=2 недостижим легитимно (протокол запрещает 3+ параллельных MCP); persistent loop отложен намеренно
  • ✅ P0 deadlock реиндекса (2026-07-31, регрессия ac6e5ba0e P1-3) — см. «z.ai review верификация»: bulk known_hashes в index_project_runner.py + тест test_index_runner_deadlock (валидирован)
  • Верификация через pytest после каждого фикса — выполнено: 666 passed, 0 failed
  • AGENT_DIARY.md и KNOWN_ISSUES.md — синхронизированы
  • ✅ HTTP 400 llama.cpp embedder (2026-08-01) — v2: нативный /tokenize truncation (лимит 480 < 512), фикс 48e695b8 (HF 512) опровергнут замером (макс 502, в прогоне 526). Реиндекс 22:37→22:47 ЗАВЕРШЁН: 4677 chunks, FTS5 built, HTTP 400=0, Aborted=0, E2E search_code OK. Попутно: vulkaninfo cp1251-краш (§5.16) + pylance==9.0.0 (known_hashes). Тесты 667 passed, 13 skipped; версия 3.3.11 (локально).
  • ✅ Pre-commit hook блокер (2026-08-01) — verify_diary: время в заголовке опционально (склейка записей 31.07), negative lookbehind в _extract_code_functions/_check_test_file_exists, поле clean_state_reason (§0.2); hook-шаблон: Popen+utf-8+CREATE_NO_WINDOW (§5.16) + reconfigure (9.9); SyntaxError:36 в git_hooks_installer.py починен (экранирование \"\"\" в PRE_COMMIT_HOOK); dead-ссылки generate_docs удалены (скрипт не существовал). Hook прогон: verify_diary 36 ✅/0 ❌ + stale_detector OK, RC 0.

z.ai review верификация (2026-07-31)

Проверено 16 пунктов код-ревью z.ai по §1.14: 3 ✅ CONFIRMED (починены), 1 ⏳ PARTIAL (орфанный код починен, продакшн-путь был чист), 12 ❌ REFUTED (уже исправлены ранее).

ID Утверждение File:Line Статус Вердикт / Фикс
LOGIC-4 Blocker await self._flush() внутри with self._lock → deadlock batch_full rate_limiter.py:168-171 ❌ REFUTED flush уже ВНЕ lock (INC-53EC); _flush callback тоже вне lock (L234)
WIN-1 High hash(str(db_path)) рандомизирован → mutex не работает graph.py:59 ❌ REFUTED уже blake2b (a9d92e00 P2-21)
LOGIC-1 High move ищет по file_hash, не file_path file_move_manager.py:37 ⏳ PARTIAL орфанный класс никем не вызывается (продакшн — Indexer.move_chunks_metadata, уже по file_path); сам класс ПОЧИНЕН
LOGIC-2 High delete+add нетранзакционен file_move_manager.py:34-41 ⏳ PARTIAL то же; починено: read→delete→add под lock
SEC-1 High socket/ssl/http/multiprocessing в ALLOWED_MODULES executor.py:36-46 ❌ REFUTED allowlist уже чистый (только stdlib)
SEC-2 High urllib.request в _USER_ALLOWED executor.py:126-135 ❌ REFUTED отсутствует (Blocks: os/subprocess/socket/...)
ARCH-1 High resolve() не thread-safe di_container.py:136 ❌ REFUTED уже threading.Lock (P2-18)
WIN-3/4 High UNC-пути в _path_to_uri/_uri_to_path lsp_client.py:657-669 ✅ CONFIRMED Path.as_uri() + netloc-ветка; тест test_lsp_uri_conversion
LOGIC-5 Med cache TTL 30s без инвалидации engine.py:96 ✅ CONFIRMED invalidate_cache() в runner + _index_single_file
LOGIC-7 Low _apply_co_change_boost не присвоен Searcher engine.py:1121-1125 ❌ REFUTED assign-as-method есть
LOGIC-8 Low MMR remaining не по relevance scoring.py:402 ✅ CONFIRMED remaining.sort(key=relevance, reverse=True)
WIN-2 Med mutex silent fallback graph.py:75-76,86-87 ✅ CONFIRMED warning при CreateMutexW fail/exception
WIN-8 Med f-string JSON с str(e) server_factory.py:308-319 ✅ CONFIRMED json.dumps + ensure_ascii=False
SEC-4 Low str(e) в MCP-ответе error_handler.py:569,625 ✅ CONFIRMED _sanitize_error_message (пути → , 200 симв.)
SEC-5 Low sandbox_mode env без валидации codebase_tool.py:295 ✅ CONFIRMED fallback на strict при невалидном значении
LOGIC-3 Med ручное replace("'","''") file_move_manager.py:21 ⏳ PARTIAL переведено на _escape_sql_value
WIN-7 Low md5[:8] коллизии indexer.py:36 ❌ ACCEPTED смена на [:16] = смена имени БД = reindex (breaking, §1.11); отложено
ARCH-6 Low add_singleton(None) silent no-op di_container.py:109 ⏳ DEFERRED все вызовы передают реальные инстансы; ValueError рискован

Claude review верификация (2026-07-31)

  • Проверено 8 находок код-ревью: 7 ✅ CONFIRMED, 1 ❌ REFUTED
  • ✅ КРИТ-1 (неатомарные записи write_tools) → P2-9 (scope расширен до 7 точек), закрыт
  • ✅ КРИТ-2 (remove_zed_settings комментарии) → P1-15, закрыт (severity P0→P1: документирован в docstring)
  • ✅ КРИТ-3 (patch_zed_settings неатомарна) → P1-16, закрыт
  • ✅ P1 di_container resolve race → P2-18, закрыт (severity P1→P2: латентно)
  • ✅ P1 engine.py asyncio.run → = существующий P2-6 (tech debt, дубликат не создавался)
  • ✅ P2 command.split пробелы → P2-19, закрыт
  • ✅ P2 llama_runner fd leak → P2-20, закрыт
  • ❌ P3 server.py _env_project_root_cache — REFUTED: env процесса фиксирован при спавне, reset_project_root_cache() вызывается из server_factory.py delayed bridge recheck, динамический путь идёт через LSP bridge, не через этот кэш

Claude review верификация — вторая волна (2026-07-31)

  • ✅ A: engine.py asyncio.run в _sync_executor → P2-6, закрыт как TECH DEBT (ACCEPTED) с обоснованием (см. P2-6 статус)
  • ❌ B: di_container.py closure late-binding _create_indexer_for_path — REFUTED: default-args capture уже применён (L286-290), фабрика регистрируется через add_singleton, ветка _factories латентная (L140-142 комментарий); риск late binding = 0
  • ❌ C: zed_config.py $ZED_WORKTREE_ROOT в env — REFUTED: server.py:_resolve_env_project_root (L393-405) явно обрабатывает literal raw.startswith("$"); доки Zed не описывают $VAR-интерполяцию в env MCP; live-паспорт: PROJECT_PATH=literal, резолв работает через SQLite bridge (приоритет 0)

Qwen review верификация (2026-07-31)

ID Вердикт Куда записан Доказательство
F-1 ✅ CONFIRMED P0-5 executor.py:45-47 importlib* в ALLOWED_MODULES; runtime _safe_import блокирует (несоответствие слоёв)
F-2 ✅ CONFIRMED P0-5 executor.py:86 — __build_class__ отсутствовал
F-3 ✅ CONFIRMED P0-5 executor.py:134 — "sys" в _USER_ALLOWED при AST-блокировке import sys
F-4 ✅ CONFIRMED P0-5 executor.py:350 — os.environ.copy() → секреты в sandbox
F-5 ❌ REFUTED — tempfile.mkstemp уже создаёт файл с mode 0600
D-1 ✅ CONFIRMED P2-21 graph.py:55 — hash() (PYTHONHASHSEED)
C-5 ✅ CONFIRMED P2-22 graph.py:520,831,1475 — total_changes cumulative
E-1 ❌ REFUTED — MCP жив (RUN_ID 4ad0072c3a68, PID 2064) из ext dir с относительным command — Zed резолвит относительно корня расширения
Shutdown-race ⏳ ACCEPTED — server_factory.py:584-598 — guards уже есть (running loop → create_task, иначе asyncio.run); процесс завершается, worst case — закрытие клиентов best-effort
C-2 ✅ CONFIRMED P2-24 engine.py:482-521 — sort после MMR отменял переупорядочивание
C-1 ✅ CONFIRMED P2-23 scoring.py:74,127 — tie-break по вставке
C-4 ✅ CONFIRMED P2-25 graph.py:864-885 — BFS без max_nodes
D-6 ✅ CONFIRMED P2-26 graph.py:376 — mmap 256MB
B-6 ✅ CONFIRMED P2-27 di_container.py:369-370 — hasattr + приватный атрибут
E-7 ❌ REFUTED — server_factory.py:56-61 — GetLastError() != 87 уже считает ACCESS_DENIED (5≠87) живым
D-3 ⏳ ACCEPTED (tech debt) — _start_llama_sync new_event_loop+set_event_loop выполняется до asyncio.run() и им же заменяется; утечка loop'а на старте — один раз, безвредно
B-1 ✅ CONFIRMED (severity ↑ High) P1-17 parser.py:69,331,671 — общий Parser + общий _cache_* при пуле из 4 потоков
DI resolve race ❌ REFUTED = P2-18 di_container.py:136 — threading.Lock уже добавлен (Claude review P1, закрыт P2-18)

Итого: 12 ✅ CONFIRMED (все закрыты фиксами), 4 ❌ REFUTED, 2 ⏳ ACCEPTED. Полный pytest 616 passed, 0 failed; ruff clean; bump_version 3.3.9.


Аудит Bot_snow 2026-08-07 — остаток (передача в новый чат)

Источник: `debug_runtime_passport`.md L3394-3466 (аудит MCP-инструментов из окна Bot_snow). Уже закрыто этой сессией: multi-window CWD-first (229c7156), stale_detector/_grep_fallback __file__-root (e2950a14), search_quality мониторинг (f54bea8b). Тесты 894 passed.

BS-1 (🔴 #12): search_code — ложная уверенность: мусор вместо пустого ответа ✅ FIXED (2026-08-07)

  • Фикс: файлы без единой строки кода (init.py с комментарием) не индексируются (_has_code_lines в index_parser.py + parser.py fallback); тесты BS-1.
  • Файл: src/mcp/tools/search_tools.py (путь выдачи); корень — эмбеддер-fallback → мусорные чанки
  • Детали: на несуществующий запрос возвращает 6 «результатов» (5/6 — пустые __init__.py с fallback_lines). Должен вернуть честный пустой ответ + подсказку. Проверить, не должны ли пустые чанки индексироваться/фильтроваться при выдаче.

BS-2 (🔴 #16): search_code пропускает точные хиты ✅ FIXED (2026-08-07)

  • Фикс: реальный chunk_index в FTS5-метаданных (не 0 → не схлопывается с dense в RRF), буст точного имени (запрос-идентификатор), дедуп (file, symbol); тесты BS-2.
  • Файл: src/mcp/tools/search_tools.py
  • Детали: запрос «финансовый отчёт инструктора за неделю» не нашёл get_report (financial.py), дублирует delete_schedule в выдаче.

BS-3 (🟠 #1): search_code — неверные координаты строк ✅ FIXED (2026-08-07)

  • Фикс: start_line/end_line добавлены в metadata vector_search/search_async/FTS5; рендер 0→1-based; тесты BS-3.
  • Файл: src/mcp/tools/search_tools.py
  • Детали: line 0/2 вместо реальных L27-71 (все 4 проверенных символа).

BS-4 (🟠 #5): search_code пропускает определения ✅ FIXED (2026-08-07)

  • Фикс: буст точного имени (get_db над dense-мусором) в hybrid_search_async и fast-ветке; тест test_bs4.
  • Файл: src/mcp/tools/search_tools.py
  • Детали: запрос get_db не нашёл async def get_db() (database.py:37).

BS-5 (🟠 #17): intel_code_topology — обрезанный/пустой вывод ✅ FIXED (2026-08-07)

  • Фикс: layer читает c.get("symbol") (был c.get("name") → всегда ""); фильтр пустых; format_analysis_result не режет значения посреди (str[:60] → рекурсивный рендер); тесты BS-5.
  • Файл: src/core/intelligence/ (topology-инструмент)
  • Детали: symbol='', file='' у callees, обрыв строки в JSON.

BS-6 (🟠 #10): get_health_report — false positive overall_health=critical ✅ FIXED (2026-08-07)

  • Фикс: «индексер молчит» — critical только если reindex идёт (is_reindexing+молчит); idle после завершённой индексации → метрика watchdog_state; тесты BS-6.
  • Файл: src/core/intelligence/health.py
  • Детали: «индексер молчит 278с» — нормальный idle после завершённой индексации, а не критично.

BS-7 (🟠 #14): graph_query — cypher/flow недостижимы ✅ FIXED (2026-08-07)

  • Фикс: параметры query/name добавлены в execute-схему (target — backward-compat); тесты BS-7.
  • Файл: src/mcp/tools/graph_tools.py (или регистрация схемы)
  • Детали: «query required»/«name required», таких параметров в схеме нет.

BS-8 (🟠 #8): противоречие embedder-провайдера между инструментами ✅ FIXED (2026-08-07)

  • Фикс: телеметрия читает DI-инстанс embedder (_resolve_active_embedder), а не новый RemoteEmbedder() → единая правда с health (llama_cpp); тесты BS-8.
  • Файлы: логи — «E5-base ONNX не загрузился, fallback»; телеметрия — Provider: onnx, multilingual-e5-small-int8; runtime status — llama.cpp 🟢
  • Детали: три инструмента показывают три разных провайдера. Привести к единой правде (кто реально активен).

BS-9 (🟡 #9): registry passport vs health рассинхрон счётчиков ✅ FIXED (2026-08-07)

  • Фикс: DI регистрирует глобальный singleton get_global_registry() (было два разных реестра); тест BS-9.
  • Детали: passport «Cached: 1» vs health registry_cached_projects: 0.

BS-10 (🟡 #11): auto_update_docs — ложное предупреждение про README tool count ✅ FIXED (2026-08-07)

  • Фикс: проверка счётчика только при наличии маркера «N total/tools» в README; тесты BS-10.
  • Детали: «README tool count устарел» — в README его нет.

BS-11 (🟡 #18): intel_predict_root_cause — 16 сек на «не найдено» ✅ FIXED (2026-08-07)

  • Фикс: run_full_diagnostic/hotspots в asyncio.to_thread + wait_for(3s); замер 15634→1261ms; тест BS-11.
  • Детали: дефолтный ответ (probability 0.3) занимает 16 сек.

BS-12 (🟡 #19): impact_analysis/graph_query — пустые элементы в списках путей ✅ FIXED (2026-08-07)

  • Фикс: модули — reversed-сегменты пути (не «D:»), пустые file_path фильтруются (symbol_index.py + graph_adapter.py); тесты BS-12.
  • Детали: affected_files: [-, bot.py], affected_modules: [D:].

BS-13 (🟡 #20): codebase hub — action=symbol не существует ✅ FIXED (2026-08-07)

  • Фикс: action="symbol" → делегирование в get_symbol_info; system-под-действия работают (проверено); тесты BS-13.
  • Файл: src/mcp/tools/codebase_tool.py
  • Детали: под-действия system недостижимы (есть прямые эквиваленты).

BS-14 (⏭ #3): get_symbol_info — отрицательная латентность (−994 ms) ✅ CLOSED (2026-08-07)

  • Статус P1-10: код-баг исправлен 2026-07-27 (везде * 1000); оставались старые метрики в tool_metrics.json → санитизация при загрузке + guard в record/summary; тесты BS-14.
  • Связь: уже покрыто P1-10 (error_handler.py:454 — elapsed = ... - 1000 вместо * 1000). Проверить статус P1-10 и закрыть.

Не делать

  • ❌ DEV_DIARY.md в расширении — НЕ нарушение (§0.6: заглушка-редирект на AGENT_DIARY.md).
  • ⚠️ financial.py:72-75 (сравнение дат) — код проекта Bot_snow, НЕ расширения. Отдельная задача для окна Bot_snow.

Поисковое качество / E13 — исследовательские задачи (2026-09-20)

Источник: исследование поиска/RAG (exp-5, E13, Exp 43). Порядок выполнения — см. ниже.

KI-R1 (P1) — Исправить измерение перед любыми выводами

  • Статус: ⏳ Open
  • Действия:
    1. Перезапустить exp-5 после фикса KI-101 (cache-hit пропускал dense-уровень); добавить в лог отдельную строку vector-only.
    2. Ввести золотой набор с gold_chunk_id → Recall@5 по нему (без текстовой метрики). Предложение Edward из статьи — закрывает «попал ли нужный чанк в топ-5».
    3. Увеличить наборы: 10–16 вопросов — шум (один вопрос = 6–10 пунктов).
  • Why first: без корректного измерения остальное — догадки.

KI-R2 (P1) — Довести E13 до нормального эксперимента

  • Статус: ⏳ Open
  • Действия: Два дешёвых прогона на тех же 16 запросах:
    1. с intent_hint="docs" — проверяет маршрутизацию;
    2. на индексе только из документации — проверяет перекос корпуса.
  • Why second: только после них можно писать причину в дневник, а не гипотезу.

KI-R3 (P2) — Интерфейс: «найдено N, использовано M»

  • Статус: ⏳ Open
  • Действия: Добавить в обычный ответ (не только explain=True): found=N, used=M, eligible_seen (0 из 0 против 0 из N). Отличие промаха retrieval от отказа генератора.

KI-R4 (P2) — Регрессионный тест на дедупликацию

  • Статус: ⏳ Open
  • Действия: Тест на две почти одинаковые записи в одном запросе: в топ-5 должна остаться одна, в цитатах — обе. Случай SCT / SCT Inst. Напоминание: MMR тихо отменялся финальной сортировкой (P2-24).

KI-R5 (P2) — Гибрид: не считать улучшением по умолчанию

  • Статус: ⏳ Open
  • Действия: vector поверх BM25 снижает recall на 0.098 (у автора статьи гибрид тоже проигрывал). Вместо RRF — фильтр для лексической ветки (порог по числу совпавших чанков / по редким терминам), замер на том же наборе.

KI-R6 (P2) — Навести порядок перед публичными ссылками

  • Статус: ⏳ Open
  • Действия:
    1. Повторяющаяся запись exp-29 на странице Lab (7 копий).
    2. Дубли и противоречивые записи в архиве дневника (две записи «2026-07-27» с разным статусом проверки).
    3. Проверить, что числа в README (число инструментов, тесты) совпадают с текущими.

Порядок выполнения (рекомендация)

  1. KI-R1 (перезапуск exp-5 + gold_chunk_id) → KI-R2 (два прогона E13). Дешёвые, от них зависит публичные утверждения.
  2. KI-R3 … KI-R6 параллельно/последующе.

KI-R7 (P2) — Авто-замер на проекте пользователя

  • Статус: ⏳ Open
  • Действия: заголовок раздела / имя символа / docstring → gold_chunk_id по построению → Recall@5 в get_health_report. Оговорка: запросы похожи на текст, это проверка исправности, не качества.

KI-R8 (P2) — Дедупликация по хешу содержимого

  • Статус: ⏳ Open
  • Действия: vendored/переводы/CHANGELOG — одинаковый текст в двух файлах занимал 2 из 5 слотов. Схлопывайте по хешу перед выдачей, оба пути оставляйте в цитате.

KI-R9 (P2) — Не подбирать один режим на всех

  • Статус: ⏳ Open
  • Действия: у нас vector+BM25 снизил recall на 0.098, у автора hybrid проиграл vector. Каскад «быстрый + граф» 0.433 vs 0.267 — более общий вывод, чем выбор режима.

KI-R10 (P2) — Слабый запрос → не молчать

  • Статус: ⏳ Open
  • Действия: возвращать найденное + оценки + «низкая уверенность»; fallback на grep, если векторный поиск ничего уверенного не дал (у нас exp-26).

KI-R11 (P2, первым) — «Найдено N / использовано M» + «проиндексировано ли»

  • Статус: ⏳ Open
  • Действия: промах retrieval ≠ отказ генератора. У нас exp-25: 4 вызова впустую на неиндексированных папках.

Порядок выполнения (рекомендация)

  1. KI-R11 → KI-R7 → KI-R8. Потом KI-R1→R6 по плану.

Bootstrap Pipeline — внешний обзор (2026-09-20)

Источник: внешний обзор (через community-memory). Числа и утверждения НЕ проверялись — только описание подхода. Статус: research-отзыв, без кода.

Что подтверждено

  1. Динамическая связка test→code — стандартный приём. pytest-testmon и coverage.py делают то же же (сбор зависимостей тест↔код через coverage). Вывод Exp 7 «динамика лучше статики» совпадает с наукой: статика включает лишние вспомогательные методы, динамические трассы — нет. Exp 9 (статика как fallback/pre-filter) разумна.
  2. Ниша TESTS-ребра для LLM-контекста незанята. PyPI Chisel (LLM-агенты, граф тестов + git + статика), gograph untested (Go) — аналогичные задачи, но без динамических рёбер. Наше отличие — динамический источник, но точность нужно сравнить.
  3. «Исполнено» ≠ «тестируется». В среднем 10 функций на тест, макс 118. Наука: точность растёт с признаками (имя теста, последний вызов перед assert). TCTracer (динамика+статика) дал MAP ~85 на связи test↔function. Наш Tarantula (rank≤3 только 22.6%) — та же проблема.

Что стоит упростить

  • Плагин повторяет coverage.py с контекстами. sys.monitoring пока не поддерживает динамические контексты; в одном отчёте контексты по тестам удвоили CI на Django. Наши +13.6% на этом фоне неплохи.
  • Поверхность инструментов: из 62 использовались 5 (exp-24). Codebase-Memory (14 инструментов) достигает 90% качества разведки файлов с в разы меньшим числом токенов/вызовов. Перемудрена скорее поверхность, чем трассировка.

Три шага для проверки

  1. Запустить pytest-testmon или coverage на тех же 5 проектах, сравнить связи с ours.
  2. Добавить ранг «что именно тестируется» (имя теста + последний вызов перед assert).
  3. Разметить вручную 20–30 тестов, посчитать точность.

P2 — Инвентаризация пробелов собственной системы (2026-10-04)

Метод: инвентаризация проведена по коду с измерением (каждое утверждение — file:line, числа получены командой). Запись не ссылается на источник заимствования: это список собственных пробелов, а не заимствованные идеи. Все referents внутренние. Разделение принципиально: F = исправить своё, A = дополнить своё. Что уже есть и что дублировать не надо: память с терминальными статусами и каскадной ретракцией, протокол замороженных экспериментов с фальсификаторами, clean-state проверка, UNKNOWN ≠ ALLOW, контроли с право�� падать.

F — исправить (по цене/пользе)

# Пробел Где Почему это дефект Цена
F1 В ответах инструментов нет типизированных состояний деградации: «0 результатов» неотличимо от «не смог проверить» src/mcp/tools/search_tools.py:197,607 Наше правило §19.6 существует только как детектор в гейте; продукт по-прежнему отдаёт голое значение. Тихий ноль на выходе — самый опасный вид S
F2a Нет точного runtime-guard'а на состав MCP-поверхности. Есть только floor >= 40 под importorskip и == 65 против текстового счётчика tests/test_mcp_schema_flat.py:38, tests/test_auto_doc_updater.py:157, src/core/auto_doc_updater.py:459 Инструмент, выпадающий из tool_classes, не ломает ни один тест. Единственный точный guard помечен importorskip — при отсутствии SDK он просто исчезает S
F2b _write_tool_names содержит 7 имён, которые никогда не регистрируются как инструменты src/mcp/server_tools.py:246-257 readOnlyHint считается для несуществующих имён → аннотация для призраков S
F2c 20 из 52 классов MCPTool не в tool_classes; часть дублирует inline-реализацию того же имени (GetLogsTool/get_logs, GetHealthReportTool/get_health_report, ReadLiveFileTool/read_live_file, DualArmHealthCheckTool/dual_arm_health_check, NotifyChangeTool/notify_change) src/mcp/tools/ + src/mcp/server_tools.py Два кодовых пути на одно имя: правится один — второй остаётся. Тот же класс, что расхождение 18/34/2 M
F3 Ведро слоёв core→src.sources.* объявлено «transitional», но печатает счётчик и выходит 0; срока давности нет scripts/check_layer_boundaries.py:80-84,105 «Временное» без срока — постоянное. Правило, которое не может заблокировать, скоро перестанут читать S (реестр исключений с обязательным протуханием)
F4 tools/verification/run_all.py — «докажи, что каждый guard умеет падать» — не в CI и не в pre-commit tools/verification/run_all.py Сильнейший актив репо опционален S (одна строка в .github/workflows/ci.yml)
F5 scripts/audit_protocol_guards.py сейчас красный: exit 1, 7 находок (6 rate-tools без проверки пустой популяции + 1 артефакт без sha256) и не в CI прогон 2026-10-03; scripts/audit_protocol_guards.py:66,87-98 T10/T11 не имеют тестовых утверждений (проверяется только check_191). Гейт, который красный и не запускается, хуже отсутствующего S
F6 Три подтверждённых расхождения док↔код src/mcp/server_tools.py:6,10 (31/64 против фактических 32/65) · .githooks/pre-commit:8-16 (8 против фактических 11) · tools/verification/run_all.py:71-73 (публикует 8/62.5% при фактических 7) Ровно класс из §19.11: число, которое нельзя получить сегодняшней командой S
F7 Порог покрытия 38% при 67k строк; mypy --strict не включён .github/workflows/ci.yml:93 Низкая планка не ловит регрессию качества; типизация отсутствует целиком M
F8 Нет CI-задачи, которая падает, если lane flaky/отменён. Нужен if: always() + needs: по всем lane + проверка результата каждой .github/workflows/ci.yml Flaky lane, проскочивший мимо required-checks, тихо убирает гейт S
F9 В negative-control манифесте drift_gate и stale_detector имеют expected_exit=0 — «негативный контроль», ожидающий успех scripts/negative_controls/manifest.json Либо контроль назван неверно, либо он проверяет не то, что думают. Проверено запуском: drift_gate красный ещё и в другом дереве S
F10 experiments/ (193 файла, 29 974 строки) исключены из сбора pytest pyproject.toml:172 Их собственные тесты не идут в CI; причина («*_test.py ломали сюиту») лечится переименованием, а не исключением каталога M

A — дополнить (нет вообще)

# Чего не хватает Зачем Цена
A1 Fail-closed доказательство свежести на границе выдачи. Сейчас есть отдельный гейт stale_detector и отпечаток по git HEAD в verify_on_read.py:577-601, но выдача не проверяет свежесть той строки, которую возвращает Превращает «индекс протух?» из глобального флага в доказательство для каждого ответа. Инструмент, печатающий метрику, обязан доказать, что мерил то, что отдаёт M
A2 Генерация AGENTS.md/системного промпта с build-hash + побайтовая сверка зеркал. Автогенерация есть только для счётчиков README Три копии промпта расходятся, и расхождение обнаруживается глазами. С артефактом-хешем дрейф становится падающим гейтом M
A3 Hard-negatives как обязательная колонка в frozen/*/manifest.json Механизм заморозки есть (scripts/frozen_overlap_check.py, HYPOTHESES.md с фальсификатором), но требования «≥2 отрицательных контроля на кейс» нет — контроль остаётся привычкой ревьюера S
A4 Schema-snapshot для действий-дискриминаторов. graph_tools.py:186-198 (8 действий) и codebase_tool.py:107 (action_map): новое значение в перечислении = тихий runtime else Тот же класс, что F2a, но на внутренних маршрутах вместо MCP-поверхности S
A5 Счётчик доли fallback_lines. parser.py:185-186,2488 помечает построчные окна FALLBACK_CHUNK_LINES=56 с типом fallback_lines, но сколько файлов реально ушло в этот путь не измеряется нигде 16 языков tree-sitter выглядят полным покрытием, пока часть файлов индексируется вслепую. Число «покрытие парсером 99%» сегодня недостоверно S
A6 Политика отказа при незаданном корне. Fail-closed где-то есть (find_project_root в хуке), но в других точках незаданный корень приводит к молчаливому return вместо блока Молчаливый возврат при отсутствии условия — источник fail-open путей S
A7 Один прогон mutmut со счётом либо явная пометка «не измерено» Заявлен в pyproject.toml:117, вне CI, на Windows пропускается без WSL. Сейчас это декларация, а не измерение S

Порядок (обоснован, а не «сначала самое важное»)

  1. F5 — гейт уже красный: чинить чужое, пока оно не начало вредить. 7 находок — это готовый список работы.
  2. F6 — три числа в документации, которые нельзя получить сегодняшней командой. Сегодняшняя работа уже оплатила эту цену дважды.
  3. F1 — единственная позиция, где дефект бьёт по пользователю агента, а не по нам.
  4. F2a + F4 + F8 — три строки, которые закрывают целый класс «сильное есть, но не выполняется».
  5. A3, A4, A5 — дешёвые, делают существующие механизмы обязательными вместо рекомендуемых.
  6. F2b, F2c, F3, F7, F10, A6 — после, по отдельным задачам.
  7. A1, A2, A7 — M/S, но требуют отдельного замера до начала.

Чего НЕ надо брать (у нас уже есть или хуже)

  • Сужать 65 инструментов до 8 «мега-тулов». Меньший размер поверхности не заменяет её защиты: без гварда на дискриминаторы новое значение в перечислении — тихий else. Наша проблема не размер, а отсутствие снимка схемы (F2a, A4).
  • Собственный re-вектор-индекс. Мы уже на LanceDB с IVF-guard'ом; JSON-хранилище с лексическим пре-фильтром — шаг назад.
  • Свой memory/decay без потребителя. 4 120 строк офлайн-харнесса, выключенного по умолчанию и не подключённого к продуктовому пути, — это наш будущий src/memory/, которого у нас нет и не нужно.