Ожидание: await self._flush() вызывается ВНУТРИ with self._lock → deadlock при 100 файлах
Команда: AST-анализ + ручное чтение rate_limiter.py L154-232
Сырой результат:
=== DebounceBatch.add() at line 154 ===
L154: async def add(self, file_path: str) -> bool:
L156: with self._lock: # L156 — lock acquired
L157: is_new = file_path not in self._files
L158: self._files.add(file_path)
L159: self._last_added_at = time.monotonic()
L160: batch_full = len(self._files) >= self._config.max_batch_size
L161: timer_missing = self._timer is None or self._timer.done()
L162: # ← with block ENDS here, lock RELEASED
L163: if batch_full: # OUTSIDE lock
L164: logger.info("Batch full, flushing immediately")
L165: await self._flush() # OUTSIDE lock ✓
L166: return is_new
=== DebounceBatch._flush() at line 216 ===
L216: async def _flush(self):
L218: with self._lock: # acquires lock (no contention)
L219: if not self._files:
L220: return
L221: files = self._files.copy()
L222: self._files.clear()
Вердикт: ОПРОВЕРГНУТА ❌ — await self._flush() вызывается ПОСЛЕ释放ения lock (L162 end of with block). Код уже исправлен. Дедлок НЕ возможен.
Ожидание: _get_process_ram использует wmic, который удалён → RAM=0
Команда: проверка наличия wmic.exe + чтение layer.py L260-270
Сырой результат:
wmic.exe exists: False
wmic: FileNotFoundError (not available on this system)
OS: Windows-11-10.0.26220-SP0
psutil available: False (sandbox), but in project: YES (used in _get_process_cpu)
Код из layer.py L260-270:
def _get_process_ram(pid: int) -> int:
try:
out = subprocess.check_output(
["wmic", "process", "where", f"processid={pid}",
"get", "WorkingSetSize", "/format:value"],
timeout=3
)Вердикт: ПОДТВЕРЖДЕНА ✅ — wmic.exe отсутствует на Win11 10.0.26220 (25H2). _get_process_ram всегда падает в except → возвращает 0. _get_process_cpu (L275) уже использует psutil.
Ожидание: asyncio.Event в _ready_events привязан к loop, set_state может вызываться из другого loop
Команда: чтение project_indexer_registry.py L102, L236-258, L270-313
Сырой результат:
L102: self._ready_events: Dict[Path, asyncio.Event] = {}
L251: ev = self._ready_events.get(p)
L253: ev.set() # asyncio.Event.set() — может быть в wrong loop
L300: if p not in self._ready_events:
L301: self._ready_events[p] = asyncio.Event() # создаётся в loop wait_until_ready
L313: await asyncio.wait_for(ev.wait(), timeout=timeout)
Вердикт: ПОДТВЕРЖДЕНА ✅ (с оговоркой) — asyncio.Event привязан к loop, в котором создан. set_state — синхронный метод, вызывается из любого потока. Если поток индексации НЕ в running loop → ev.set() поднимет RuntimeError: no running event loop. Риск реален при threaded indexer. Однако, в текущей архитектуре индексатор часто работает в asyncio.to_thread, что создаёт отдельный thread без loop — тогда ev.set() упадёт.
Ожидание: except (ImportError, Exception) ≡ except Exception маскирует баги
Команда: grep layer.py
Сырой результат:
277: except (ImportError, Exception): # L277 — _get_process_cpu
Plus ~20 except Exception в layer.py, ~5 в engine.py
Вердикт: ПОДТВЕРЖДЕНА ✅ — (ImportError, Exception) эквивалентен except Exception. Каждый широкий catch маскирует любые программные ошибки.
Ожидание: definitions попадают в outgoing_callees; references_count игнорирует callee Команда: чтение layer.py L196-247 Сырой результат:
L203: result["call_graph"]["outgoing_callees"].append(...) # definitions → callees
L229: result["call_graph"]["outgoing_callees"] = [...] # BUT callees OVERWRITES (=, not append)
L239: result["references_count"] = len(result["call_graph"]["incoming_callers"]) # ignores callees
L242: if result["references_count"] == 0 and result["definitions_count"] > 0:
L244: "potential_dead_code": True
Вердикт: ЧАСТИЧНО ПОДТВЕРЖДЕНА outgoing_callees (L203), НО если build_call_graph возвращает callees — они ПЕРЕЗАПИСЫВАЮТ список (=, не append, L229). Значит definitions попадают в callees только когда символ НЕ вызывает других функций. references_count действительно считает только callers, не callees — это подтверждено.
Ожидание: clear() вместо LRU eviction при 1000 записях
Команда: чтение engine.py L393-396
Сырой результат:
L394: if len(self._embedding_cache) >= self._embedding_cache_max:
L395: self._embedding_cache.clear()
L396: self._embedding_cache[query_hash] = query_vector
То же самое в reranker cache (L1068):
L1068: self._reranker_cache.clear()
Вердикт: ПОДТВЕРЖДЕНА ✅ — clear() сбрасывает ВЕСЬ кэш, а не одну запись. LRU (OrderedDict + popitem) был бы корректнее.
Ожидание: hybrid_search создаёт ThreadPoolExecutor + asyncio.run() на каждый вызов
Команда: чтение engine.py L280-300
Сырой результат:
L284: if loop and loop.is_running():
L285: import concurrent.futures
L286: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
L287: future = pool.submit(asyncio.run, self.hybrid_search_async(...))
L288: return future.result(timeout=30)
Аналогичный паттерн L1021-1028 для reranker. Вердикт: ПОДТВЕРЖДЕНА ✅ — каждый sync-вызов создаёт новый executor + loop. Расточительно, ломает async-примитивы в подкомпонентах.
Ожидание: hash(variant) недетерминирован между процессами (PYTHONHASHSEED)
Команда: чтение engine.py L378, L1049
Сырой результат:
L378: query_hash = hash(variant)
L1049: cache_key = f"{hash(query)}:{hash(chunk_keys)}:{top_n}"
Вердикт: ПОДТВЕРЖДЕНА ✅ — hash() для строк в Python не детерминирован (PYTHONHASHSEED). Кэш-промахи после рестарта. Для in-memory кэша одного процесса — не критично (hash стабилен в рамках одного process). Но для reranker cache (L1049) — потенциальная проблема при pickle/serialization.
Ожидание: 37/42/48/59 — четыре разных значения Команда: grep по README.md и ARCHITECTURE.md Сырой результат:
README.md L47: "42 tools for AI assistant"
README.md L70: "38 MCP tools"
README.md L121: "48 tools (19 core + 13 intel + 12 inline + 4 dev)"
README.md L208: "MCP Tools (48 total)"
ARCHITECTURE.md L18: "18 core + 13 intel + 7 inline + 3 dev + 1 optional = 42"
ARCHITECTURE.md L94: "18 core + 13 intel + 7 inline + 3 dev + 1 optional = 42"
ARCHITECTURE.md L101: "19 core + 12 inline + 4 dev"
ARCHITECTURE.md L279: "48 registered (19 core + 13 intel + 12 inline + 4 dev)"
Реальные классы MCPTool: 37 classes, plus 13 intel_* + 13 inline @mcp.tool + 4 dev tools Вердикт: ПОДТВЕРЖДЕНА ✅ — минимум 4 разных счётчика (37/38/42/48) в одном README. Архитектурный дрейф.
Ожидание: Пути не отражают refactor src/ into domains; doc_sync_engine.py не существует Команда: ls + grep ARCHITECTURE.md Сырой результат:
test -f src/core/doc_sync_engine.py → EXISTS (src/core/doc_sync_engine.py exists!)
ARCHITECTURE.md L157: doc_sync_engine.py → src/core/doc_sync_engine.py
Дубликаты: branch_aware_index.py — только в search/, не в core/ (flat deleted)
Вердикт: ЧАСТИЧНО ОПРОВЕРГНУТА
Ожидание: branch_aware_index.py, cross_project_deps.py, cypher_engine.py в core/ и search/ Команда: ls + grep imports Сырой результат:
src/core/branch_aware_index.py → NOT FOUND
src/core/cross_project_deps.py → NOT FOUND
src/core/cypher_engine.py → NOT FOUND
src/core/search/branch_aware_index.py → EXISTS (204 lines)
Все импорты ведут в src.core.search.*
Вердикт: ОПРОВЕРГНУТА ❌ — плоские дубликаты УДАЛЕНЫ. Все импорты корректны на search/.
Ожидание: MODE_HYBRID поддерживается в коде, но DI создаёт MODE_PURE Команда: grep Сырой результат:
graph_adapter.py L55: MODE_HYBRID = "hybrid"
graph_adapter.py L64-66: self._definitions, _references, _file_to_symbols
graph_adapter.py L91: if self._mode == self.MODE_HYBRID:
di_container.py L189: SymbolIndexAdapter(pg, mode=SymbolIndexAdapter.MODE_PURE)
Вердикт: ПОДТВЕРЖДЕНА ✅ — MODE_HYBRID код существует, но не используется DI. Мёртвый код.
Ожидание: _resolve_active_indexer лезет в registry._meta_lock и registry._indexers
Команда: чтение layer.py L155-160
Сырой результат:
L155: registry = self._services.resolve(ProjectIndexerRegistry)
L157: with registry._meta_lock: # приватное поле
L158: for p, idx in registry._indexers.items(): # приватное поле
Вердикт: ПОДТВЕРЖДЕНА ✅ — прямой доступ к _meta_lock и _indexers.
Ожидание: _handle_crash не terminate'ит процесс
Команда: чтение lsp_client.py L100-116, L307-320
Сырой результат:
# stop() method (L100-116):
self._process.terminate() # ← CORRECT: terminate in stop()
self._process.kill()
# _handle_crash() method (L307-320):
self._process = None # ← NO terminate, NO kill, just nullifies referenceВердикт: ПОДТВЕРЖДЕНА ✅ — _handle_crash обнуляет _process без terminate()/kill(). Процесс pyright может остаться zombie. stop() делает terminate, но _handle_crash — нет.
Ожидание: _get_total_ram хардкодит порты, а intel_get_runtime_status читает env
Команда: чтение layer.py L306-310, L430-432
Сырой результат:
L306: def _get_total_ram() -> int:
L308: for port in ['8080', '8081']: # HARDCODED
...
L430: "llama_qwen_pid": _find_pid("llama-server.exe", "8080"), # HARDCODED
L431: "llama_qwen_ram": _get_ram_by_port("8080"),
L432: "llama_rerank_ram": _get_ram_by_port("8081"),
Вердикт: ПОДТВЕРЖДЕНА ✅ — порты хардкодятся в 3 местах.
Ожидание: name в _find_pid(name, port) не используется
Команда: чтение layer.py L281-297
Сырой результат:
L281: def _find_pid(name: str, port: str) -> int:
L284: port_int = int(port)
L285: out = subprocess.check_output(["netstat", "-ano"], ...)
# name НЕ используется в теле функции
L300: pid = ProjectIntelligenceLayer._find_pid('', port) # вызывается с ''
L430: _find_pid("llama-server.exe", "8080") # name передаётся но игнорируется
Вердикт: ПОДТВЕРЖДЕНА ✅ — параметр name передаётся (L430) но никогда не читается.
Ожидание: __import__("re") вместо top-level import
Команда: grep
Сырой результат:
graph_adapter.py L69: self._id_pattern = __import__("re").compile(r"[a-zA-Z_][a-zA-Z0-9_]*")
Вердикт: ПОДТВЕРЖДЕНА ✅ — неидиоматично, мешает статанализу.
Ожидание: P2/P3 fixes (LSP terminate, public API, ports→config, ARCHITECTURE.md, dead param, top-level import, tool count) не ломают существующие тесты
Команда: python -m pytest tests/ -q --tb=short -m "not slow and not benchmark"
Сырой результат:
519 passed, 36 pre-existing failures, 9 skipped, 91 deselected in 8.25s
Вердикт: ПОДТВЕРЖДЕНА ✅ — 0 regressions. Все P2/P3 патчи не повлияли на pass/fail.
Ожидание: Записи в KNOWN_ISSUES.md корректно отражают отложенные проблемы
Команда: cat >> KNOWN_ISSUES.md + ручная проверка
Сырой результат: P2-12 (MODE_HYBRID dead code), P2-16 (532 broad except) добавлены.
Вердикт: ДОКУМЕНТИРОВАНО ✅