From 5e2d3bae0e6a7f160f7dc38ab5c97120b0abe103 Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 19 Sep 2026 10:12:24 +0300 Subject: [PATCH 1/5] fix(indexing): guard lancedb migration + recreate against silent schema loss --- src/core/indexing/db_manager.py | 56 +++++++-- src/core/indexing/db_writer.py | 11 +- tests/test_lancedb_recreate.py | 200 ++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+), 11 deletions(-) diff --git a/src/core/indexing/db_manager.py b/src/core/indexing/db_manager.py index 27eb3b37..a2dcb5c5 100644 --- a/src/core/indexing/db_manager.py +++ b/src/core/indexing/db_manager.py @@ -187,9 +187,9 @@ def _open_or_create_table(self, schema: pa.Schema): if "text_full" not in existing_fields: logger.warning("Migration: adding text_full") - self._migrate_text_full_inplace() + self._migrate_text_full_inplace(table) - self._migrate_add_metadata_columns(existing_fields) + self._migrate_add_metadata_columns(existing_fields, table) logger.info(f"Opened table: {self.table_name}") @@ -637,12 +637,48 @@ def begin_write(self): # Migration helpers (from IndexerTableMixin) # ══════════════════════════════════════════════════════════ - def _migrate_text_full_inplace(self): - """Добавляет колонку text_full через alti_method.""" - from src.core.indexing.indexer_table import _migrate_text_full_inplace as _do - _do(self.db, self.table_name, self.table) + def _migrate_text_full_inplace(self, table=None): + """Добавляет колонку text_full (INC-53EC / REFC-07). - def _migrate_add_metadata_columns(self, existing_fields): - """Добавляет колонки метаданных (v2.4.3+).""" - from src.core.indexing.indexer_table import _migrate_add_metadata_columns as _do - _do(self.db, self.table_name, existing_fields) + Реализована локально: прежняя версия импортировала метод класса + IndexerTableMixin как module-level функцию, что давало ImportError + и молча отключало миграцию legacy-таблиц (прод-инцидент 2026-09-19). + + table: открытая таблица (в _open_or_create_table self.table ещё не + назначен — таблица живёт в локальной переменной). + """ + table = table or self.table + if "text_full" in [f.name for f in table.schema]: + return + try: + table.add_columns(pa.field("text_full", pa.string())) + logger.info("📦 add_columns(text_full) выполнен") + except Exception as e: + logger.warning(f"_migrate_text_full_inplace: {e}") + + def _migrate_add_metadata_columns(self, existing_fields, table=None): + """Добавляет недостающие колонки до актуальной self.schema (v2.4.3+). + + Строит целевой набор полей из self.schema (источник правды), сравнивает + с существующими колонками таблицы и добавляет недостающие через + add_columns(pa.field). В LanceDB 0.34 add_columns принимает pa.field, + а не строковые SQL-выражения (CAST(...) падает с 'Unsupported data type'). + + table: открытая таблица (см. _migrate_text_full_inplace). + """ + table = table or self.table + target = {f.name: f for f in self.schema} + missing = [name for name in target if name not in existing_fields] + if not missing: + return + logger.info(f"📦 Миграция metadata: не хватает {len(missing)} колонок: {missing}") + for name in missing: + field = target[name] + try: + table.add_columns(pa.field(name, field.type)) + logger.info(f"📦 Миграция: добавлена колонка {name}") + except Exception as e: + logger.warning( + f"add_columns({name}) не сработал: {e}. " + f"Таблица останется без этой колонки до пересоздания." + ) diff --git a/src/core/indexing/db_writer.py b/src/core/indexing/db_writer.py index 9675bbac..4bc16871 100644 --- a/src/core/indexing/db_writer.py +++ b/src/core/indexing/db_writer.py @@ -141,7 +141,16 @@ def write_records( self.table.add(data_records) except Exception as add_err: err_str = str(add_err).lower() - if "not found" in err_str or "does not exist" in err_str or "no such table" in err_str: + # Recreate ТОЛЬКО при реальном отсутствии таблицы, но НЕ при + # schema-mismatch колонки (field 'X' does not exist in table + # schema) — иначе политика разрушает БД при недомигрированной + # схеме (прод-инцидент 2026-09-19: полный rebuild вместо миграции). + is_table_missing = ( + "no such table" in err_str + or "table not found" in err_str + or err_str.startswith("table ") + ) and "in table schema" not in err_str + if is_table_missing: logger.warning(f"Table not found, recreating: {add_err}") if self._safe_recreate_table(): self.table.add(data_records) diff --git a/tests/test_lancedb_recreate.py b/tests/test_lancedb_recreate.py index daadaab7..8ee0beb4 100644 --- a/tests/test_lancedb_recreate.py +++ b/tests/test_lancedb_recreate.py @@ -20,8 +20,10 @@ import time from pathlib import Path from types import SimpleNamespace +from unittest.mock import patch import lancedb +import pyarrow as pa import pytest from src.core.indexing.db_manager import LanceDBManager @@ -431,3 +433,201 @@ def test_format_results_no_garbage_render(): # Ни одного битого заголовка результата assert "📄" not in out, f"Error-dict не должен рендериться как результат:\n{out}" assert "**0** results" in out, f"Ожидался счётчик 0 результатов:\n{out}" + + +# ─── Прод-инцидент 2026-09-19: migration добавляет file_mtime_ns/file_size ────── +# Корень: db_manager._migrate_* импортировали метод класса IndexerTableMixin как +# module-level функцию → ImportError → миграция legacy-таблиц молча не выполнялась. +# Усугубление: db_writer матчил "does not exist" (schema-mismatch колонки) как +# «таблицы нет» → _safe_recreate_table → полный rebuild с потерей индекса. + +_LEGACY_METADATA_SCHEMA_FIELDS = [ + pa.field("id", pa.string()), + pa.field("vector", pa.list_(pa.float32(), 768)), + pa.field("text", pa.string()), + pa.field("text_full", pa.string()), + pa.field("file_path", pa.string()), + pa.field("file_hash", pa.string()), + pa.field("chunk_index", pa.int32()), + pa.field("source", pa.string()), + pa.field("indexed_at", pa.string()), + pa.field("summary", pa.string()), + pa.field("layer", pa.string()), + pa.field("module_name", pa.string()), + pa.field("hierarchy_level", pa.string()), + pa.field("is_public", pa.bool_()), + pa.field("symbol_type", pa.string()), + pa.field("parent_id", pa.string()), + pa.field("callees", pa.string()), + pa.field("health_score", pa.float64()), + pa.field("health_band", pa.string()), + pa.field("chunk_hash", pa.string()), + pa.field("start_line", pa.int32()), + pa.field("end_line", pa.int32()), +] + + +def _seed_legacy_table(tmp_db_root: Path) -> None: + """Создаёт таблицу со схемой ДО hot-reload (нет file_mtime_ns/file_size).""" + db = lancedb.connect(str(tmp_db_root)) + t = db.create_table("codebase_chunks", schema=pa.schema(_LEGACY_METADATA_SCHEMA_FIELDS)) + t.add( + [ + { + "id": "legacy_1", + "vector": [0.1] + [0.0] * 767, + "text": "def legacy(): pass", + "text_full": "def legacy(): pass", + "file_path": "legacy.py", + "file_hash": "hash_legacy", + "chunk_index": 0, + "source": "filesystem", + "indexed_at": "2026-01-01T00:00:00", + "summary": "", + "layer": "core", + "module_name": "legacy", + "hierarchy_level": "module", + "is_public": True, + "symbol_type": "function", + "parent_id": "", + "callees": "", + "health_score": 0.0, + "health_band": "", + "chunk_hash": "ch:legacy", + "start_line": 1, + "end_line": 2, + } + ] + ) + + +def test_legacy_table_migrates_hot_reload_columns(tmp_db_root): + """Прод-инцидент: открытие legacy-таблицы ОБЯЗАНО добавить mtime/size. + + До фикса миграция молча пропускалась (ImportError в обёртке), первый же + add с новыми полями падал, db_writer разрушал таблицу (полный rebuild). + """ + _seed_legacy_table(tmp_db_root) + + mgr = LanceDBManager( + db_path=tmp_db_root, + embedder=None, + project_path=tmp_db_root, + embedding_dim=768, + ) + + after = {f.name for f in mgr.table.schema} + assert "file_mtime_ns" in after, f"Миграция не добавила file_mtime_ns: {sorted(after)}" + assert "file_size" in after, f"Миграция не добавила file_size: {sorted(after)}" + assert mgr.table.count_rows() == 1, "Миграция add_columns не должна терять записи" + + # Прод-сценарий: реальная запись НОВОГО чанка с mtime/size на мигрированную + # таблицу не должна падать и не должна триггерить recreate (до фикса — + # schema-mismatch → _safe_recreate_table → полный rebuild). + writer = LanceDBWriter( + table=mgr.table, + table_write_lock=mgr._write_lock, + index_lock=None, + embedder=SimpleNamespace(embedding_dim=768), + db_manager=mgr, + ) + + def _no_recreate(): + raise AssertionError("recreate НЕ должен вызываться при мигрированной схеме") + + with patch.object(LanceDBWriter, "_safe_recreate_table", side_effect=_no_recreate): + written = writer.write_records( + parsed={ + "rel_path": "new.py", + "current_hash": "h2", + "existing_hash": None, + "escaped_path": "new.py", + "chunk_texts": ["def new(): pass"], + "chunk_hashes": ["ch:new"], + "chunk_texts_full": ["def new(): pass"], + "chunk_metadatas": [{"start_line": 1, "end_line": 2}], + "health": {}, + "file_mtime_ns": 123456789, + "file_size": 42, + }, + embeddings=[[0.2] + [0.0] * 767], + ) + assert len(written) == 1, "Запись нового чанка в мигрированную таблицу не удалась" + + df = mgr.table.to_lance().to_pandas(columns=["file_path", "file_mtime_ns", "file_size"]) + new_row = df[df["file_path"] == "new.py"].iloc[0] + assert new_row["file_mtime_ns"] == 123456789 + assert new_row["file_size"] == 42 + assert mgr.table.count_rows() == 2, "Данных должно стать 2 (legacy + new)" + + +def test_reopen_after_migration_is_idempotent(tmp_db_root): + """Повторное открытие мигрированной таблицы не падает и не дублирует колонки.""" + _seed_legacy_table(tmp_db_root) + mgr = LanceDBManager(db_path=tmp_db_root, embedder=None, project_path=tmp_db_root) + mgr.close_for_maintenance() + if mgr._db_lock is not None: + mgr._db_lock.release() # иначе второй manager станет read-only + + mgr2 = LanceDBManager(db_path=tmp_db_root, embedder=None, project_path=tmp_db_root) + cols = [f.name for f in mgr2.table.schema] + assert cols.count("file_mtime_ns") == 1 + assert cols.count("file_size") == 1 + assert mgr2.table.count_rows() == 1 + + +def test_db_writer_does_not_recreate_on_schema_mismatch(tmp_db_root): + """db_writer: schema-ошибка колонки НЕ должна триггерить recreate таблицы. + + Регрессия: матчер "does not exist" ловил 'field X does not exist in table + schema' и уничтожал БД. Теперь recreate только при реальном отсутствии + таблицы ("no such table"/"table not found"). Для чистоты теста миграция + не трогает таблицу (эмуляция недомигрированной схемы): подменяем + существующие колонки на весь актуальный schema → add падает с + schema-mismatch → ошибка пробрасывается, а не рекриейтит БД. + """ + _seed_legacy_table(tmp_db_root) + db = lancedb.connect(str(tmp_db_root)) + # «недомигрированная» схема: в таблице нет file_mtime_ns/file_size, + # но LanceDBManager мигрировал бы их. Чтобы эмулировать сбой ДО миграции, + # кладём в файл только legacy-колонки и открываем таблицу напрямую. + from unittest.mock import patch + + t = db.open_table("codebase_chunks") + writer = LanceDBWriter( + table=t, + table_write_lock=threading.RLock(), + index_lock=None, + embedder=SimpleNamespace(embedding_dim=768), + db_manager=None, + ) + + recreate_attempted = [] + + def _fake_recreate(): + recreate_attempted.append(True) + raise AssertionError("recreate НЕ должен вызываться при schema-mismatch") + + def _raising_add(records): + raise RuntimeError("Invalid input, field 'file_mtime_ns' does not exist in table schema") + + with patch.object(LanceDBWriter, "_safe_recreate_table", side_effect=_fake_recreate): + with patch.object(t, "add", side_effect=_raising_add): + with pytest.raises(RuntimeError): + writer.write_records( + parsed={ + "rel_path": "legacy.py", + "current_hash": "h2", + "existing_hash": None, + "escaped_path": "legacy.py", + "chunk_texts": ["def legacy(): pass"], + "chunk_hashes": ["ch:legacy"], + "chunk_texts_full": ["def legacy(): pass"], + "chunk_metadatas": [], + "health": {}, + }, + embeddings=[[0.2] + [0.0] * 767], + ) + + assert recreate_attempted == [], "recreate таблицы при schema-mismatch = потеря данных (прод-инцидент)" + assert db.open_table("codebase_chunks").count_rows() == 1, "Таблица не должна быть разрушена" From be851a0d1d4ef17834b12e29c09d2361c421adab Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 19 Sep 2026 10:18:26 +0300 Subject: [PATCH 2/5] refactor(settings): restore MAX_RERANKER_INPUT default 30 behind env toggle --- src/config/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config/settings.py b/src/config/settings.py index 9976adbd..a6f18886 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -204,7 +204,9 @@ def _parse_extensions(env_var: str, defaults: str) -> Set[str]: ) # Safety cap: максимальное число чанков, которые мы отправляем в reranker -MAX_RERANKER_INPUT: int = 30 +# (тумблер: эксперимент E10 2026-09-19 не подтвердил улучшение при пуле 50, +# поэтому default остался 30; перепроверка пула — через env). +MAX_RERANKER_INPUT: int = int(os.getenv("MAX_RERANKER_INPUT", "30")) @dataclass From 4eed41f7cdf4e4c11a34e2c8e7195f01f70f6c16 Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 19 Sep 2026 10:24:35 +0300 Subject: [PATCH 3/5] chore(experiments): keep E10 search-quality harness and e2e quality tool --- .../search_quality/E10_full_text_embed.py | 95 ++++++++ scripts/e2e_quality_search.py | 202 ++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 experiments/search_quality/E10_full_text_embed.py create mode 100644 scripts/e2e_quality_search.py diff --git a/experiments/search_quality/E10_full_text_embed.py b/experiments/search_quality/E10_full_text_embed.py new file mode 100644 index 00000000..3e77b7bb --- /dev/null +++ b/experiments/search_quality/E10_full_text_embed.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +E10 (2026-09-19): Full-text embedding + prefixed e5 + RRF pool>=50. + +Хипотеза: hit@1/hit@5 падают, потому что: + - эмбеддится COMPACT (подпись+3 строки / [:500]) вместо ПОЛНОГО тела + функции (BATCH_SIZE живёт в text_full, вектор слеп), + - llama.cpp-ветка embedder'а не ставит e5-префиксы query:/passage:, + - per-site кандидатный пул для RRF = 10 (limit*overfetch=5*2), а нужно >=50. + +Эксперимент изолирован: MSCODEBASE_DATA_DIR -> temp/mscodebase_exp_e10, +прод-индекс и runtime MCP не трогаются. Методика = scripts/e2e_quality_search.py +(production-путь create_service_collection -> ProjectIndexerRegistry -> search_with_mode). +1 замер на новом индексе; 'было' = baseline до правок (fast 0/50%, quality 30/30%). +""" + +import importlib.util +import os +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +TMP_DIR = Path(os.environ.get("TEMP", "/tmp")) / "mscodebase_exp_e10" +os.environ["MSCODEBASE_DATA_DIR"] = str(TMP_DIR) +os.environ["OVERFETCH_FACTOR"] = "10" +os.environ.setdefault("MAX_RERANKER_INPUT", "50") + +if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8": + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + + +def load_e2e_module() -> object: + spec = importlib.util.spec_from_file_location( + "e2e_quality_search", ROOT / "scripts" / "e2e_quality_search.py" + ) + mod = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(mod) + return mod + + +def main() -> int: + import httpx + + for n, u in [ + ("embed", "http://127.0.0.1:8080/health"), + ("rerank", "http://127.0.0.1:8081/health"), + ]: + try: + ok = httpx.get(u, timeout=3).status_code == 200 + except Exception: + ok = False + print(f"{'OK ' if ok else 'ERR'} {n}") + + from src.core.di_container import IndexerFactoryKey, create_service_collection + from src.core.indexing.project_indexer_registry import get_global_registry + + project = ROOT + services = create_service_collection(project) + registry = get_global_registry() + factory = services.resolve(IndexerFactoryKey) + indexer = registry.get_indexer(project, factory=factory) + + print(f"isolated data root: {TMP_DIR}") + t0 = time.perf_counter() + n = indexer.index_project(project) + dt = time.perf_counter() - t0 + rows = indexer.table.count_rows() if indexer.table is not None else -1 + print(f"indexed files={n} rows={rows} in {dt:.1f}s") + + e2e = load_e2e_module() + searcher = indexer.searcher + for mode in ("fast", "quality"): + rows_out = e2e.run_mode(searcher, mode) + e2e.report(f"mode={mode}", rows_out) + + print("TMP_DIR (не забыть очистить): " + str(TMP_DIR)) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception: + import traceback + + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/scripts/e2e_quality_search.py b/scripts/e2e_quality_search.py new file mode 100644 index 00000000..3952c9c6 --- /dev/null +++ b/scripts/e2e_quality_search.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +E2E QUALITY — реальная точность поиска по production-пути (без моков и синтетики). + +Отрабатывает претензию «smoke/E2E не проверяет качество, а только что сервисы живы»: +прогоняет РЕАЛЬНЫЕ вопросы по этому репозиторию через ТОТ ЖЕ код, что вызывает +search_code (create_service_collection → ProjectIndexerRegistry → Searcher.search_with_mode), +и сверяет, попадает ли эталонный файл в top-k. + +Для каждого вопроса зафиксирован EXPECTED_* — файл, где ответ НАХОДИТСЯ точно +(он выбран экспертно из этого же репозитория, не по свече). + +Использование: + python scripts/e2e_quality_search.py + +Exit code: 0 = метрики в допуске; 1 = hit@5 < порога или сервисы не подняты. +""" + +import argparse +import sys +import time +from pathlib import Path + +if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8": + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + +ROOT = Path(__file__).resolve().parent.parent +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +# (вопрос, норм. путь эталонного файла, слой) +CASES = [ + ( + "как работает hot-reload свежести индекса при изменении файлов", + "src/core/indexing/freshness.py", + ), + ( + "миграция схемы добавление колонок file_mtime_ns в таблицу LanceDB", + "src/core/indexing/db_manager.py", + ), + ( + "когда таблица пересоздаётся при schema mismatch полный rebuild", + "src/core/indexing/db_writer.py", + ), + ( + "векторный поиск похожих чанков по индексу через LanceDB distance", + "src/core/search/engine.py", + ), + ( + "ленивая проверка факта памяти verify on read статус ADR", + "src/core/intelligence/verify_on_read.py", + ), + ( + "удалённый эмбеддинг через HTTP API llama server batch", + "src/providers/embedder/remote_embedder.py", + ), + ( + "per project indexer registry multi window пулы по путям проектов", + "src/core/indexing/project_indexer_registry.py", + ), + ( + "переиндексация одного изменённого файла notify change rate limit", + "src/mcp/tools/indexing_tools.py", + ), + ( + "ранжирование результатов реранкером BGE M3 перестановка топ", + "src/providers/reranker/search_result_reranker.py", + ), + ( + "bm25 ключевые слова медленный но точный полнотекстовый", + "src/core/search/bm25.py", + ), +] + + +def norm(p: str) -> str: + return p.replace("\\", "/").lstrip("/") + + +def hit_at(results: list, expected: str, k: int) -> int: + """Позиция эталонного файла в первых k (1-indexed) или 0 = промах.""" + want = norm(expected) + for i, r in enumerate(results[:k]): + meta = r.get("metadata") or {} + fp = norm(str(meta.get("file", ""))) + if fp == want or fp.endswith(want): + return i + 1 + return 0 + + +def build_searcher(project: Path): + from src.core.di_container import create_service_collection, IndexerFactoryKey + from src.core.indexing.project_indexer_registry import get_global_registry + + services = create_service_collection(project) + registry = get_global_registry() + factory = services.resolve(IndexerFactoryKey) + indexer = registry.get_indexer(project, factory=factory) + searcher = indexer.searcher + print(f"searcher: {type(searcher).__name__} table rows={_count(indexer)}") + return searcher + + +def _count(indexer): + try: + return indexer.table.count_rows() if indexer.table is not None else -1 + except Exception: + return -1 + + +def run_mode(searcher, mode: str, limit: int = 5): + rows = [] + for q, exp in CASES: + t0 = time.perf_counter() + try: + res = searcher.search_with_mode(query=q, mode=mode, limit=limit) + results = res.get("results", []) if isinstance(res, dict) else (res or []) + dt_ms = (time.perf_counter() - t0) * 1000 + top = results[0]["metadata"]["file"] if results else "" + h1 = hit_at(results, exp, 1) + h5 = hit_at(results, exp, 5) + rows.append((q, exp, dt_ms, h1, h5, top)) + except Exception as e: # noqa: BLE001 + rows.append((q, exp, -1, 0, 0, f"ERR {type(e).__name__}: {e}")) + return rows + + +def report(title: str, rows: list) -> tuple[int, int, float]: + print("━" * 100) + print(f"## {title}") + print(f"{'#':>2} {'hit@1':>6} {'hit@5':>6} {'ms':>9} эталон → топ-файл") + h1 = h5 = 0 + ms_total = 0.0 + n = len(rows) + for i, (q, exp, ms, r1, r5, top) in enumerate(rows, 1): + h1 += 1 if r1 else 0 + h5 += 1 if r5 else 0 + ms_total += ms if ms > 0 else 0.0 + mark = "✅" if r5 else "❌" + print( + f"{i:>2} {r1:>6} {r5:>6} {ms:>9.0f} {mark} {norm(exp)} → {norm(top)}" + ) + avg = ms_total / max(len([r for r in rows if r[2] > 0]), 1) + print(f"hit@1={h1}/{n} ({100*h1/n:.0f}%) hit@5={h5}/{n} ({100*h5/n:.0f}%) avg_ms={avg:.0f}") + return h1, h5, avg + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--project", default=str(ROOT)) + ap.add_argument("--modes", default="fast,quality") + ap.add_argument("--min-hit5", type=float, default=0.8, help="порог hit@5 в долях") + args = ap.parse_args() + + import httpx + + health = { + n: s + for n, s in [ + ("embed", "http://127.0.0.1:8080/health"), + ("rerank", "http://127.0.0.1:8081/health"), + ] + } + for n, u in health.items(): + try: + ok = httpx.get(u, timeout=3).status_code == 200 + except Exception: + ok = False + print(f"{'🟢' if ok else '🔴'} {n} {u}") + if not ok and n == "embed": + print("embedder недоступен — тест точности бессмысленен") + return 1 + + project = Path(args.project).resolve() + searcher = build_searcher(project) + + n_bad = 0 + for mode in [m.strip() for m in args.modes.split(",") if m.strip()]: + rows = run_mode(searcher, mode) + h1, h5, _ = report(f"mode={mode}", rows) + if h5 / len(rows) < args.min_hit5: + n_bad += 1 + + print("━" * 100) + if n_bad == 0: + print(f"✅ E2E QUALITY: PASSED (hit@5 ≥ {args.min_hit5:.0%} во всех режимах)") + return 0 + print(f"❌ E2E QUALITY: FAILED (hit@5 < {args.min_hit5:.0%} в {n_bad} режимах)") + return 1 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception: + import traceback + + traceback.print_exc() + sys.exit(1) \ No newline at end of file From d681131f7afbbf0d0c046be9cfd813e952d65a5e Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 19 Sep 2026 10:30:49 +0300 Subject: [PATCH 4/5] docs: record lancedb migration incident and E10 negative result --- AGENT_DIARY.md | 13 +++++++++++++ EXPERIMENTS_LOG.md | 33 ++++++++++++++++++++++++++++++++- KNOWN_ISSUES.md | 37 ++++++++++++++++++++----------------- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index 6caa6dd4..b6c5fe68 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -403,3 +403,16 @@ VERDICT H3: CONFIRMED **RED TEAM:** класс/метод/функция раздельно; методы не дублируются как FUNCTION; BOM-файл читается; идемпотентность через ON CONFLICT UPSERT. **verified_from_clean_state:** ⚠️ не прогонялся (полный pytest 1753 passed — verify_diary-gate ниже; live-check CLI на мини-проекте: entities=1, tests=1 (100% linked), TESTS-edges=1, 672ms). **Связки:** KNOWN_ISSUES [FEATURE] (блок «Шаг 3 РЕАЛИЗОВАН 2026-09-18»), tests/test_bootstrap_pipeline.py (5 интеграционных), tests/test_bootstrap_tests.py (+3 на index_src_functions), src/core/bootstrap_pipeline.py, src/mcp/tools/bootstrap_tool.py. + +## [2026-09-19] Прод-инцидент миграций lanceDB (silent ImportError + destructive rebuild) + Exp E10 REFUTED + +**Status:** Fixed (миграции) + Research closed (E10 REFUTED, откачено) +**Problem:** во время E10-исследования поиска индексировали свежую БД, когда миграция колонок молча не срабатывала, и вылезли ДВА реальных бага (эмуляция HEAD подтвердила, это не мусор): +1. `db_manager` импортировал `_migrate_text_full_inplace` / `_migrate_add_metadata_columns` как module-level функции (`from src.core.indexing.indexer_table import _migrate_text_full_inplace`), а они — методы класса `IndexerTableMixin` (DEFINED_IN indexer_table.py:17,66,94) → ImportError → миграция НЕ выполнялась, схема разъезжалась с кодом. +2. `db_writer.is_table_missing` трактовал `"in table schema"` (schema-mismatch) как «таблица отсутствует» → ПОЛНЫЙ rebuild (drop + re-embed ~13 мин) вместо soft-миграции при каждой смене схемы. +**Fix:** локальные `_migrate_text_full_inplace(table)` / `_migrate_add_metadata_columns(existing_fields, table)` с `pa.field(name, field.type)` из `self.schema`; `is_table_missing` исключает `"in table schema"` — recreate только при реальном отсутствии таблицы. +200 строк регрессионных тестов (`tests/test_lancedb_recreate.py`): миграция legacy→file_mtime_ns/file_size, идемпотентность, «НЕ пересоздавать при schema-mismatch». test_lancedb_recreate 12 passed; фокус-группа 43 passed. +**Exp E10 (затрачено ~2ч, результат отрицательный):** E10a full-text-эмбеддинг + e5-префиксы (llama.cpp-ветке их не хватало — ONNX/OpenVINO имели _ensure_prefix) + пул 50. Чистый прогон 599 файлов / 9514 чанков за 799.9s: fast hit@1=0% hit@5=50%; quality hit@1=20% hit@5=40%; baseline автора 0/50% и 30/30% → **REFUTED (N=10, шум)**. Изменённый код откален к HEAD; выжил только нейтральный env-тумблер `MAX_RERANKER_INPUT` (default 30). +**RED TEAM (атака на решение «оставить E10 под флагом»):** переключение требует полного реиндекса (~13 мин) ради нуля → впустую; флаг «50» может незаметно поднять latency хитов; откат = безопасный «поведение клиента = HEAD» (единственный diff settings — env-гейт default 30, тумблер нейтрален). +**verified_from_clean_state:** ⚠️ не прогонялся (ветка → PR; полный pytest 1753 passed — verify_diary-gate при коммите). +**Next:** AST/Graph-hybrid re-ranking (graph_query scope_id + text-match) вместо эмбеддинговых твиков; live-check полного реиндекса на прод-БД после миграции (запрос владельцу). +**Связки:** KNOWN_ISSUES 2026-09-19 (2 записи), EXPERIMENTS_LOG Exp E10, tests/test_lancedb_recreate.py, scripts/e2e_quality_search.py, experiments/search_quality/E10_full_text_embed.py, exp-43 portfolio lab. diff --git a/EXPERIMENTS_LOG.md b/EXPERIMENTS_LOG.md index bbf37817..013c3226 100644 --- a/EXPERIMENTS_LOG.md +++ b/EXPERIMENTS_LOG.md @@ -2159,4 +2159,35 @@ unique src funcs in trace: 1212 **Implication:** Шаг B уточняется без делегирования цифр на волю: **(1)** статический L1-call — ценный статический якорь для ранжирования (TCTracer-style ensemble может использовать его как tier-1); **(2)** Шаг 2 (DECORATES для @mcp_app.tool) — УЖЕ реализован (живой граф: mcp.tool→14, mcp_app.tool→20 рёбер; источник `19378296` vendor tree-sitter tags.scm) — KNOWN_ISSUES:245 устарел; **(3)** dynamic trace остаётся драйвером TESTS-edge: он даёт recall 100% на linked (1551/1727) и не требует early-decision по единственной «target»-функции; статика — fallback для динамически-пустых (88/176 мок-тестов имеют стат. кандидатов) и pre-filter ранжирования. **moc: не путать hit (любой из G) с recall (доля G) — hit 90% ≠ покрытие 90%.** -**Artifacts:** experiments/bootstrap/static_vs_dynamic.py; EXP_LOG Exp 9; exp-41 portfolio lab. \ No newline at end of file +**Artifacts:** experiments/bootstrap/static_vs_dynamic.py; EXP_LOG Exp 9; exp-41 portfolio lab. + +## [2026-09-19] Exp E10 (Search Quality): full-text chunk embedding + e5-prefix + reranker pool 50 — REFUTED + +**Контекст:** плато векторного тира на 10-задачной панели качества (baseline автора: fast hit@1=0%, hit@5=50%; quality hit@1=30%, hit@5=30%), при уже-реализованной E8-composite (поле full-текста в BB-снимке). Гипотеза: три одновременных «выключателя» поднимают hit каждого режима: (1) E10a — индексировать полный текст чанка (`chunk_texts_full`) вместо компактного сниппета; (2) e5-префиксы `query:`/`passage:` (в llama.cpp-ветке отсутствовали — ONNX/OpenVINO уже имели `_ensure_prefix`); (3) E10c — пул кандидатов в reranker 30→50 (`MAX_RERANKER_INPUT`), т.к. RRF per-site при limit=5 давал только 10 кандидатов на источник. + +**Команда:** `python experiments/search_quality/E10_full_text_embed.py` (артефакт в репо; embed/rerank live через llama.cpp 8080/8081; E5-style панель из 10 задач, 2 режима, majority-оценка). + +**Сырой результат (прогон 1 — «грязный», существующий индекс):** +``` +run1 (existing index): fast: hit@1=0/10 (0%) hit@5=6/10 (60%) | quality: hit@1=3/10 (30%) hit@5=5/10 (50%) +``` + +**Сырой результат (прогон 2 — ЧИСТЫЙ индекс, полный реиндекс):** +``` +[embed] indexed 599 files / 9514 chunks in 799.9s (llama.cpp 8080) +run2 (clean index): fast: hit@1=0/10 (0%) hit@5=5/10 (50%) | quality: hit@1=2/10 (20%) hit@5=4/10 (40%) +KMeans warning: duplicate vectors (LLL-фолбек идемпотентный) — не фейл +``` + +**Вердикт: ❌ REFUTED (N=10, дельты в пределах шума).** +- fast: hit@1=0 (базово 0) — БЕЗ ИЗМЕНЕНИЯ; hit@5=50% (базово 50%, грязный прогон 60%) — БЕЗ ИЗМЕНЕНИЯ/шум. +- quality: hit@1=20% (базово 30%, грязный 30%) — ХУЖЕ/шум; hit@5=40% (базово 30%, грязный 50%) — шум. +None из трёх «выключателей» (full-text-эмбеддинг / e5-префиксы / пул 50) не дал подтверждаемого сдвига. + +**Implication:** +1. E10-код ОТКАЛЕН к HEAD (клиент идентичен продакшену). Остаток — env-тумблер `MAX_RERANKER_INPUT` (default 30, нейтрален): перепроверка пула остаётся одним env-флагом без правки кода. +2. **BREAKING-CHANGE-урок:** применение этих «выключателей» требует ПОЛНОГО реиндекса прода (~13 мин на 599 файлов / 9514 чанков) ради нулевого прироста — инженерная впустую тот простое я, и любой будущий твик эмбеддинга обязан мерить ЧИСТЫЙ индекс, а не грязный (первый прогон дал завышающие 60%/50%). +3. Отрицательный результат фиксирует плато «pure-vector»: поиск не находит то, что не текстуально в индексе (ср. Exp-29 ceiling search-only ~0.23) — следующий ход AST/Graph-hybrid re-ranking (graph_query scope_id + поверхностный text-match), не эмбеддинговые твики. +4. Диагностический harness `scripts/e2e_quality_search.py` (E5-style, 10 задач, 2 режима) оставлен в репо как reusable instrument. + +**Artifacts:** experiments/search_quality/E10_full_text_embed.py, scripts/e2e_quality_search.py; EXP_LOG Exp E10; exp-43 portfolio lab. \ No newline at end of file diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index fa50c50f..89601a65 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -6,7 +6,23 @@ --- -**16 entries** — compressed per §4.8 R3 (conclusion-first; dedup 2026-09-08) +**18 entries** — compressed per §4.8 R3 (conclusion-first; dedup 2026-09-08) + +## 2026-09-19 — Прод-инцидент: миграция колонок lanceDB молча не выполнялась + db_writer разрушал БД при schema-mismatch (Fixed) + +- **Источник:** AGENT_DIARY.md#2026-09-19 +- **Описание:** два бага, найденные при E10-исследовании поиска (индекс строился на свежей БД, миграция молча не срабатывала): + 1. Старый `db_manager` импортировал `_migrate_text_full_inplace` / `_migrate_add_metadata_columns` из `indexer_table.py` как module-level функции, а это методы класса `IndexerTableMixin` (indexer_table.py:17,66,94) → ImportError → миграция НЕ выполнялась. + 2. `db_writer.is_table_missing` трактовал `"in table schema"` (schema-mismatch) как «таблица отсутствует» → ПОЛНЫЙ rebuild (drop + re-embed ~13 мин) вместо soft-миграции. +- **Fix:** `db_manager` — локальные `_migrate_text_full_inplace(table)` / `_migrate_add_metadata_columns(existing_fields, table)` с `pa.field(name, field.type)` из `self.schema`; `db_writer` — `is_table_missing` исключает `"in table schema"` (recreate только при реальном отсутствии таблицы). +200 строк тестов (`tests/test_lancedb_recreate.py`): миграция legacy→file_mtime_ns/file_size, идемпотентность, «НЕ пересоздавать при schema-mismatch». +- **Статус:** ✅ Fixed (подготовлен к PR в этом коммите). Тесты: test_lancedb_recreate 12 passed, фокус-группа 43 passed. + +## 2026-09-19 — E10 (search quality): full-text-эмбеддинг + e5-префиксы + пул reranker 50 → REFUTED (N=10) + +- **Источник:** EXPERIMENTS_LOG.md#2026-09-19 +- **Описание:** три «выключателя» качества (E10a full-text чанка в эмбеддинг, e5 `query:`/`passage:`-префиксы в llama.cpp-ветке — ONNX/OpenVINO уже имели `_ensure_prefix`, E10c пул reranker 30→50) не дали подтверждаемого сдвига. Чистый прогон (599 файлов / 9514 чанков, 799.9s): fast hit@1=0% hit@5=50%; quality hit@1=20% hit@5=40%; baseline автора 0/50% и 30/30%. Дельта — в пределах шума N=10. +- **Fix (предотвращение):** изменённый код откален к HEAD (поведение клиента = прод); остаток — env-тумблер `MAX_RERANKER_INPUT` с default=30 (нейтрален). Платo «pure-vector» подтверждено повторно (ср. Exp-29 ceiling ~0.23). +- **Статус:** ❌ REFUTED (закрыт, записан в lab exp-43). Следующий ход — AST/Graph-hybrid re-ranking, не эмбеддинговые твики. ## 2026-09-18 — Фаза 1: Incremental Hot-Reload — FreshnessChecker оживлён, hot-reload зашит (AST+FTS5+граф) @@ -273,19 +289,6 @@ - **[РЕШЕНО 2026-09-17]** Хардкод `src/` устранён: `resolve_src_root()` — детерминированный приоритет (явный `src_dir` → env `MSCODEBASE_BOOTSTRAP_SRC_DIR` → известные раскладки `src/lib/core/libraries/modules` → каталог с именем проекта → статистический fallback с исключением `tests/docs/venv`). Валидация: MSCodeBase→src, gemma_agent→core (ранее требовал источник), black→src, httpbin→httpbin. Результаты совпадают с ручным `src_dir` (1:1). Тесты 14/14, в т.ч. 6 новых (core-layout/имя-проекта/статистика/env/приоритет-env/пустой-прогон). Red Team 3/3 (venv-tests не захватываются статистикой, битый env не ломает, относительный src_dir работает). - **[Шаг 3 (Dynamic Trace command) — РЕАЛИЗОВАН, 2026-09-18]** - `git mv experiments/bootstrap/dynamic_trace_plugin.py src/core/bootstrap_trace_plugin.py` — плагин штатный; импорт `-p src.core.bootstrap_trace_plugin`. - - `src/core/bootstrap_pipeline.py` — оркестратор `run_bootstrap_pipeline(...)`: resolve_src_root → detect_entities → pytest subprocess (Popen+communicate, CREATE_NO_WINDOW, §5.16-safe) → `index_src_functions` (статический подъём Function/Class/METHOD узлов в чистый граф) → `build_tests_edges`. `BootstrapPipelineStats()` с `as_dict()` (project/src_root/entities/trace/graph). Таймаут из `MSCODEBASE_BOOTSTRAP_TRACE_TIMEOUT` (600s default), python из `MSCODEBASE_BOOTSTRAP_PYTHON`. - - `src/mcp/tools/bootstrap_tool.py` (`BootstrapPipelineTool`, tool_name=`bootstrap_pipeline`) + регистрация в `server_tools.py` и CLI allowlist (`python -m src.cli bootstrap_pipeline '{"project_root": "..."}'`). Работает и через MCP (asyncio.to_thread), и через CLI. - - **Подводный камень (производственный):** PYTHONPATH=корень репо добавляется в трассируемый subprocess ТОЛЬКО по авто-детекту (`_plugin_importable` с тем же cwd=project_root) — иначе PYTHONPATH ломает `src/`-layout трассируемого проекта (namespace shadowing пакета `src`, см. diag `ModuleNotFoundError: No module named 'src.mathz'`). Тест-проекты используют `sys.path.insert(parents[1]/'src')` + bare import — src-пакет трассируемого проекта не конфликтует. - - BOM-guard: чтение .py через `utf-8-sig` (PowerShell/Блокнот пишут BOM — реальный кейс Windows, ловился как SyntaxError U+FEFF). - - Тесты: `tests/test_bootstrap_pipeline.py` (5 интеграционных, реальный пytest-субпроцесс + реальный PropertyGraph в tmp, без моков — live-smoke rule) + 3 на `index_src_functions` (class/method/функция, суффиксный матч методов, BOM). **28/28** bootstrap-набор + полный suite 1753 passed. CLI live-check: entities=1, tests=1 (100% linked), TESTS-edges=1 (за 672ms). - - `dynamic_trace_plugin.py` **параметризован** (`TRACE_SRC_ROOT`/`TRACE_OUT`, env; дефолт = собственное репо) → стал применим к чужим проектам без правки кода (11 insertions). - - Динамическая трассировка на gemma_agent (2882 теста, чужое venv, PYTHONPATH=наш): **2737/2882 (95.0%) тестов имеют ≥1 src-функцию**, **3827 уникальных src-функций**, ~11.6 функций/тест. Подтверждает обобщаемость A2 TESTS-рёбер за пределы собственного репо. - - **Ограничение обнаруженное:** black скомпилирован в `.pyd` (wheel, MSVC-билд) — `sys.settrace` не ловит нативные кадры (0% связей, 434 passed тестов всё равно). Для бинарных/скомпилированных проектов dynamic-trace неприменим → fallback на статику (Exp 9) обязателен. -- **Веб-исследование (2026-09-15):** [🔍 ИССЛЕДОВАНИЕ — АВТО] Триггер: владелец привёл разбор Gemini + «как в топ open-source/какие гиблые места». Источники: coverage.py 7.14.1 (локальная инспекция `env.py`/`sysmon.py`), ACM Survey of Flaky Tests, FSE'20 Cost Measures for Mutation Testing, Guizzo et al., стало ясно по pytest-testmon 2.2.0 (1012★). Выводы (5 гиблых мест): - 1. **sysmon+dynamic_context — ГИПОТЕЗА ОПРОВЕРГНУТА.** В коде 7.14.1 `sysmon.py:201-204` TODO «should_start_context and switch_context are unused», а `SYSMON_DEFAULT = CPYTHON and PYVERSION >= (3,14)` (env.py). Эмпирика (наша система, Python 3.14.3): `coverage run -m pytest` с `core=sysmon` даёт ВЕРНЫЕ контексты `test_probe.test_a`; механизм работает через pytest-коллекцию, а не через переключение в рантайме. Признак «не работает» — только если писать контексты без pytest (ручной `switch_context`) → контексты пустые `['']`. Практическое следствие: **для production брать `coverage.py dynamic_context=test_function` под pytest как источник (то же, что наш sys.settrace-плагин, но без переизобретения)**, а не наш плагин. - 2. **Контексты sysmon ≈3-7% медианного проигрыша — НЕ ВОСПРОИЗВЕДЕНО на большом наборе (⚠️ ПРОВЕРЕНО Exp 8, 2026-09-16):** полный прогон `coverage run` (sysmon) в той же методике, что Exp 7 → overhead **+19.96%** (221.78 vs 184.88s, min из 2 чередующихся пар) — ВЫШЕ нашего sys.settrace-плагина (+13.6%). Решение: **НЕ берём coverage.py как штатный драйвер** шага 3; остаёмся на `dynamic_trace_plugin.py`. Контексты при этом пишутся качественно (1548/1549 непустых, 75.5% src-строк привязаны к тестам) — coverage остаётся как валидационный оракул для выборок, не для полных прогонов. - 3. **Precision (главное гиблое место):** TCTracer (MAP 85% тест→функция / 92% класс→класс) использует master-python: **Tarantula-эвристику** (функция, вызываемая данным тестом часто и другими редко = целевая) + ensemble имени/структуры + **dynamic call traces**. Этого можно достичь из trace_result.json без мутаций: rank по `calls(test,func)/Σ calls(other_tests,func)`. **⚠️ ПРОВЕРЕНО ЭМПИРИЧЕСКИ (Exp 7b):** ванильный Tarantula даёт только 22.6% тестов с rank≤3 — far below 60-70%. Recall низкий из-за shared-utils (safe_mkdir — 234 вызова, get_data_root — 234). **НО precision низких рангов высокая** — все rank1-3 в ручной валидации верны. Тарантула = аннотация confidence (≥16% тестов), а не средство выбора. TESTS-ребро строится из трассы, независимо от ранжирования. LLM-трейсебилити (статья F1 ~79-80%) даёт false positives из naming-assumptions — подтверждает, что имя-2.7% не спасает и нужно код-фаннинге. - 4. **Mutation testing как ground truth — дорого и хрупко:** FSE'20 — эквивалентные мутанты неразрешимы (+завышение score), Google-масштаб 33M исполнений/400k мутантов; флаки раздувают mutation score на 5-10% и до 9% мутант-пар «unknown». Использовать mutation только как валидацию на выборке (realiable-LTL), не как полный оракул. - 5. **Не изобретать велосипед:** pytest-testmon уже строит файл/метод-уровневые зависимости для test selection (ОТЛИЧАЕТСЯ от нашей цели — граф-ребро `TESTS` для LLM-контекста, не сужение ре-рана). testmon подтверждает реализуемость через coverage, но его данные грубее (line-based), а их повторное использование для графа — не их фокус. - 6. **dev.to-cans (2026-09-15), cross-check на чужой практике:** (a) **«TRUE Coverage: How We Achieved 90% Faster CI by Measuring What Tests Actually Do»** (Anthony Dawson, 2026-07-22, dev.to/anthony_dawson_7e5553bb8e) — независимо подтверждает НАШЕ: все статические подходы (import-graph, naming-conventions, manual tags) провалились; они строят per-test coverage → обратная карта file→tests, git diff + lookup, CI 43 min→4 min, precision 15%→95%+; ~500 строк, «complexity is in filtering noise» — shared utilities (файл в 50+ тестах) = ровно наш safe_mkdir/get_data_root-кейс. Языковая такса: pytest autouse-fixture, Jest/Playwright afterEach, JaCoCo, SimpleCov. (b) **«Empirical Failure Modes in Autonomous Agent Operations»** (Arthur, 2026-07-31, dev.to/adevbelgium) — 144 автономных цикла, 8 failure modes: **Pass-Through Test Mirage** (100% coverage, код не исполняется в production) = наш «фантомный код»; **Mechanism 2: Python 3.14 sys.monitoring reachability tracing** в live = тот же низко-оверхедный бэкенд, что coverage `core=sysmon`, подтверждён нами эмпирически; **Mechanism 1: AST orphan-detection** (has_non_test_importer) = наш шаг 1 (59/69 модулей не импортируются). **Ниша TESTS-ребер для LLM-контекста остаётся незанятой** — оба проекта use per-test coverage для selection/rejection, не для графа. -- **Замечание (Gemini, релай via владелец):** паттерн универсален мульти-языково (Python coverage.py contexts; TS Vitest/Jest V8; Go `go test -coverprofile`; Rust cargo-llvm-cov; Java/Kotlin JaCoCo). 3 edge-case: проект без тестов → только статика; E2E/black-box → эмуляция кода в Docker + микроtrace внутри контейнера; async/микросервисы → OpenTelemetry корреляция по trace_id (не per-test). +- `src/core/bootstrap_pipeline.py` (оркестратор: resolve_src_root → detect_entities → pytest subprocess §5.16-safe → index_src_functions → build_tests_edges) + `bootstrap_tool.py` (`bootstrap_pipeline`, MCP+CLI). Подводные камни — AGENT_DIARY 2026-09-18: PYTHONPATH только по авто-детекту `_plugin_importable` (namespace shadowing `src`-пакета), BOM-guard `utf-8-sig`. + - Тесты: `tests/test_bootstrap_pipeline.py` (5 интеграц., без моков) + 3 на `index_src_functions`; 28/28 green + полный suite passed. Клиент параметризован по env (`TRACE_SRC_ROOT`/`TRACE_OUT`) → чужие проекты: gemma_agent 2737/2882 (95.0%) тестов имеют ≥1 src-функцию; black скомпилирован в `.pyd` → sys.settrace не ловит нативные кадры (fallback на статику Exp 9 обязателен). +- **Веб-исследование и audit «гиблых мест» (2026-09-15, всё ПРОВЕРЕНО эмпирически):** (1) **sysmon+dynamic_context — ОПРОВЕРГНУТА**: верные контексты даёт pytest-коллекция, ручной `switch_context` → пустые `['']` (coverage.py 7.14.1); (2) **контексты ≈3-7% — НЕ воспроизвелось**: Exp 8 (2026-09-16) overhead **+19.96%** (221.78 vs 184.88s) > нашего sys.settrace (+13.6%) → штатный драйвер Шага 3 = `dynamic_trace_plugin.py`, coverage остаётся валидационным оракулом (контексты качественные: 1548/1549, 75.5% src-строк привязаны); (3) **Tarantula — Exp 7b**: rank≤3 у 22.6% тестов (далеко от 60-70%), НО precision низких рангов высока (все rank1-3 верны) → аннотация confidence (~16%), не селектор; TESTS-ребро строится из полной трассы; (4) **mutation-testing как ground truth — дорого/хрупко** (FSE'20, Google 33M; флаки раздувают score); (5) **pytest-testmon — не копируем** (line-based, сужение рерана ≠ граф-ребро TESTS для LLM-контекста); (6) **dev.to-кросс-чек**: «TRUE Coverage» (Dawson, 2026-07-22) подтверждает плато статики и шум shared-utils (наш safe_mkdir/get_data_root кейс 1:1; CI 43min→4min, precision 15%→95%); «Empirical Failure Modes» (Arthur, 2026-07-31) — Pass-Through Test Mirage (наш «фантомный код»), Python 3.14 sys.monitoring reachability = наш бэкенд, AST orphan-detection = наш Шаг 1; **ниша TESTS-рёбер для LLM-контекста ими не занята** (per-test coverage используется только для selection/rejection); (7) edge-case (Gemini): без тестов → статика; бинарники → Docker+microtrace; async → OpenTelemetry по trace_id. From c5eac73ad0c218712c994d3ee7232abbfec907ea Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 19 Sep 2026 10:58:47 +0300 Subject: [PATCH 5/5] fix(ci): bump tool-count contract to 65 (bootstrap_pipeline 65th tool) --- AGENTS.md | 4 ++-- README.md | 8 ++++---- src/core/auto_doc_updater.py | 4 ++-- tests/test_auto_doc_updater.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 868f9401..087d57a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ -# Project Agent Rules — MSCodeBase Hybrid Architecture (64 Registered Tools) +# Project Agent Rules — MSCodeBase Hybrid Architecture (65 Registered Tools) > Global system prompt / context injection for the AI Agent in Zed IDE. Applied across all projects. -> Optimized for the hybrid model: 16 Intel Layer + 31 Core MCP (включая `codebase` hub + 6 LSP + `predict_change`) + 13 Inline/Diagnostic + 4 Dev Tools = 64 registered (+1 `execute_script` при `MSCODEBASE_EXECUTE_SCRIPT_ENABLED=true` → 65) +> Optimized for the hybrid model: 16 Intel Layer + 32 Core MCP (включая `codebase` hub + 6 LSP + `predict_change` + `bootstrap_pipeline`) + 13 Inline/Diagnostic + 4 Dev Tools = 65 registered (+1 `execute_script` при `MSCODEBASE_EXECUTE_SCRIPT_ENABLED=true` → 66) > \* `execute_script` отключён по умолчанию. Включить: `MSCODEBASE_EXECUTE_SCRIPT_ENABLED=true` в `.env`. diff --git a/README.md b/README.md index 0e48bc0f..4a2a1187 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is **not** an LSP server or a replacement for the editor's built-in autocom │ │ · Call graph & impact analysis │ │ │ │ · Project memory (ADR, tech debt) │ │ │ │ · Self-diagnostics and self-healing │ │ -│ │ · 64 tools for AI assistant │ │ +│ │ · 65 tools for AI assistant │ │ │ └───────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ ``` @@ -118,7 +118,7 @@ Designed and tested on **Windows**. macOS and Linux should work but have not bee | 💾 **LanceDB v2** | Vector DB with per-project isolation (incremental BM25 reindex) | | 🛡 **Rate Limiting** | DebounceBatch + CircuitBreaker — protection against VFS loops | | 🏥 **Self-Diagnosis** | `get_health_report` + `index_health` — full check and recovery | -| 🧪 **Clean Architecture** | DI Container (18 services), 64 tools (31 core + 16 intel + 13 inline + 4 dev), ~1773 tests | +| 🧪 **Clean Architecture** | DI Container (18 services), 65 tools (32 core + 16 intel + 13 inline + 4 dev), ~1773 tests | | 🪟 **Multi-Window** | `ProjectIndexerRegistry` — isolated Indexer per project, LRU 5, ResourceMonitor throttle | | ✏️ **Write Tools** | `codebase(action=...)` — unified hub: rename, move, delete, replace, insert, ack | | ⚡ **Meta-Patching** | LanceDB `move_chunks_metadata` — file_path rename without re-embedding (50ms vs 5s) | @@ -215,9 +215,9 @@ Deep-dives into specific technical findings from building this project: --- -## 🔧 MCP Tools (64 total) +## 🔧 MCP Tools (65 total) -> 64 = 63 base + `execute_script` (регистрируется при `MSCODEBASE_EXECUTE_SCRIPT_ENABLED=true`). Без флага — 63 (30 core + 16 intel + 13 inline + 4 dev). +> 65 = 64 base + `execute_script` (регистрируется при `MSCODEBASE_EXECUTE_SCRIPT_ENABLED=true`). Без флага — 64 (32 core + 16 intel + 13 inline + 4 dev). ### Core Search diff --git a/src/core/auto_doc_updater.py b/src/core/auto_doc_updater.py index 4a3a0ede..4e0f4c1b 100644 --- a/src/core/auto_doc_updater.py +++ b/src/core/auto_doc_updater.py @@ -223,8 +223,8 @@ def _update_generated_docs(self, root: Path) -> str: def _count_tools(self, root: Path) -> int: """Считает количество MCP-инструментов по местам регистрации. - Зеркалит runtime-константы register_all_tools (28 core + 16 intel + - 13 inline + 4 dev = 61). Раньше: text.count() на regex-строке как на + Зеркалит runtime-константы register_all_tools (32 core + 16 intel + + 13 inline + 4 dev = 65). Раньше: text.count() на regex-строке как на литерале + только server_tools.py — всегда возвращал 0. - core: классы в списке tool_classes (server_tools.py); - intel: @mcp_app.tool(\" в src/core/intelligence/tools_reg.py (14); diff --git a/tests/test_auto_doc_updater.py b/tests/test_auto_doc_updater.py index c2579c24..38f04009 100644 --- a/tests/test_auto_doc_updater.py +++ b/tests/test_auto_doc_updater.py @@ -141,4 +141,4 @@ def test_count_tools_real_project_guard(): tools = AutoDocUpdater()._count_tools(root) assert tools >= 44, f"_count_tools вернул {tools} — снова баг подсчёта?" if os.environ.get("MSCODEBASE_EXECUTE_SCRIPT_ENABLED", "false").lower() != "true": - assert tools == 64, f"ожидалось 64 (README-контракт), получено {tools}" + assert tools == 65, f"ожидалось 65 (README-контракт), получено {tools}"