From 8b3b39bc8fa1b374470c1d6e8251482f2a1430a0 Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 10 Oct 2026 17:41:06 +0300 Subject: [PATCH 1/2] feat(search): MSCODEBASE_DENSE_OFF kill-switch + EXP-28 dense-off A/B Phase 3 as a measurement, not a deletion. Flag skips the whole dense stage (query embedding + LanceDB vector search); default OFF so the prod path is byte-identical. EXP-28: frozen 33-case panel (sha256 f31e594e, union of three pre-existing frozen sets, frozen before the run), 2 runs per arm, same session and index. A dense ON: hit1 11/33, hit5 15/33, 0 errors, mean 3359/3534ms. B dense OFF: hit1 12/33, hit5 16/33, 0 errors, mean 3163/2492ms. Per case 2 better / 1 worse / 30 same; speedup mean-of-means 18% but per-run spread 5.8-29.5%. Verdict: H1/H2/H6 refuted as expected, H4 confirmed (0 errors), H3 PARTIAL, H5 untested (no symbol/NL labelling in the panel). Harness proven by a _vector_search_async call counter 1 -> 0 with the embedder alive. Key finding: of 52 table-API call sites in 13 src files only 5 are vector search; bm25.py:93 builds BM25 from table.to_pandas(), so LanceDB is the chunk primary store, not a vector index. Dense removal NOT recommended; the flag stays as an operational kill-switch. KNOWN_ISSUES untouched (foreign session edits it) - proposals in experiments/e28_denseoff/PENDING_LEDGER.md. Tests: 3 flag tests + adjacent suites 29/29, ruff clean. --- AGENT_DIARY.md | 17 ++ EXPERIMENTS_LOG.md | 39 +++ experiments/e28_denseoff/PENDING_LEDGER.md | 30 ++ experiments/e28_denseoff/VERDICTS.md | 23 ++ experiments/e28_denseoff/e28_run.py | 85 ++++++ experiments/e28_denseoff/frozen/HYPOTHESES.md | 21 ++ .../e28_denseoff/frozen/MANIFEST.sha256 | 1 + experiments/e28_denseoff/frozen/queries.jsonl | 33 +++ experiments/e28_denseoff/results/e28_A.json | 279 ++++++++++++++++++ experiments/e28_denseoff/results/e28_B.json | 279 ++++++++++++++++++ src/config/settings.py | 10 + src/core/search/engine.py | 9 +- tests/test_dense_off_flag.py | 38 +++ 13 files changed, 863 insertions(+), 1 deletion(-) create mode 100644 experiments/e28_denseoff/PENDING_LEDGER.md create mode 100644 experiments/e28_denseoff/VERDICTS.md create mode 100644 experiments/e28_denseoff/e28_run.py create mode 100644 experiments/e28_denseoff/frozen/HYPOTHESES.md create mode 100644 experiments/e28_denseoff/frozen/MANIFEST.sha256 create mode 100644 experiments/e28_denseoff/frozen/queries.jsonl create mode 100644 experiments/e28_denseoff/results/e28_A.json create mode 100644 experiments/e28_denseoff/results/e28_B.json create mode 100644 tests/test_dense_off_flag.py diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index f180e20d..59e74494 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1,4 +1,21 @@ +## [2026-10-10] EXP-28 dense-off A/B — LanceDB оказался primary store, удаление отклонено + +- **Phase Zero угадал наполовину:** «LanceDB = единственное хранилище графа» — опровергнуто + (graph.db живёт отдельно); «LanceDB = единственное хранилище чанков» — подтверждено: + `bm25.py:93` строит BM25 из `table.to_pandas()`. +- **Знаменатель спас план:** «52 обращения к table-API в 13 файлах `src/`, 8 идиом», из них + dense — 5 мест. Формулировка «удалить LanceDB» была бы миграцией хранилища, а не чисткой. +- EXP-28: флаг `MSCODEBASE_DENSE_OFF` (default OFF) гасит dense-тир. A/B на 33 замороженных + кейсах, 2 прогона на плечо: A h1 11/33 vs B h1 12/33, ошибок 0/0; per-case 2 лучше / + 1 хуже / 30 так же. Ускорение mean-of-means 18%, но разброс 5.8%…29.5% → H3 PARTIAL. + H1/H2/H6 опровергнуты (ожидаемо), H4 CONFIRMED, **H5 ⏳ нет разметки symbol-vs-NL** (не + «прошло», а не проверено — разметку нельзя добавить постфактум, §17). +- Харнесс проверен: счётчик `_vector_search_async` 1 → 0 при живом эмбеддере. Иначе A/B был бы пустым. +- Решение: dense **не отключаем** по умолчанию (выигрыша нет, скорость не установлена); флаг + остаётся как kill-switch. Основание для удаления — только отдельная задача «чанки в Parquet/SQLite». +- Guard: EXP-28 не удалил ни одной строки в прод-пути — только условие в `engine.py:936`. + ## [2026-10-08] Phase 2 (on-demand rerank в engine) — реализовано, PR следом - Новый pure `src/core/search/ondemand_rerank.py` (cosine, stable ties, пропуск diff --git a/EXPERIMENTS_LOG.md b/EXPERIMENTS_LOG.md index 78dc7bf4..3d259771 100644 --- a/EXPERIMENTS_LOG.md +++ b/EXPERIMENTS_LOG.md @@ -1,4 +1,43 @@ # EXPERIMENTS_LOG.md — Audit Verification (2026-07-22) +## [2026-10-10] — EXP-28 dense-off A/B: гасим dense-тир флагом (PARTIAL) + +**Вопрос:** можно ли отключить dense (LanceDB vector search) без потери качества — то есть довести Фазу 3 до точки принятия решения об удалении? +**Гипотезы:** frozen/HYPOTHESES.md (6 шт., из них 3 внесены как ожидающе-провальные: H1, H2, H6). +**Вход (заморожен ДО прогона):** `experiments/e28_denseoff/frozen/queries.jsonl`, 33 кейса, +sha256 `f31e594ef729b2d7f577d37049734b26aa4a6dca9cd44e067e2db72514227cc8` (33 уникальных gold-файла, все на диске). +Панель = объединение трёх уже существовавших frozen-наборов (4A f5 — 16, token_reduction — 13 из 16, e2e_quality_search::CASES — 10), дедуп по gold-файлу; N1/N2/N3 без целевого файла исключены. +**Команда (одна сессия, один индекс, различается только env):** +``` +python experiments/e28_denseoff/e28_run.py # arm A: dense ON +$env:MSCODEBASE_DENSE_OFF="true"; python experiments/e28_denseoff/e28_run.py # arm B: dense OFF +``` +**Сырой вывод (2 прогона на плечо, полные логи в results/e28_A.json / e28_B.json):** +``` +arm=A(dense_on) hit1=11/33 (33.3%) hit5=15/33 (45.5%) errors=0 mean_ms=3359 / 3534 +arm=B(dense_off) hit1=12/33 (36.4%) hit5=16/33 (48.5%) errors=0 mean_ms=3163 / 2492 +mean-of-means: A=3446ms B=2828ms -> 18.0% быстрее; разброс плеча B между прогонами: 21% +per-case hit1: 2 лучше / 1 хуже / 30 так же (net +1 = шум) +``` +**Проверка харнесса (dense реально гасится, а не флаг «для галочки»):** счётчик `_vector_search_async` на одном запросе: +`arm A dense_calls=1` -> `arm B dense_calls=0` (эмбеддер живой, число результатов то же). +**Вердикты:** +- H1 (hit1 должен упасть ≥3 п.п.) — **НЕ ОПРОВЕРГНУТ**: dense-off не хуже, +1 кейс. +- H2 (hit5 должен упасть ≥6 п.п.) — **НЕ ОПРОВЕРГНУТ**: +1 кейс. +- H3 (ускорение ≥15%) — **PARTIAL**: mean-of-means 18%, но разброс 5.8%…29.5% перекрывает порог — на 2 прогонах не устанавливается. +- H4 (0 исключений) — **CONFIRMED**: 0/33 ошибок в обоих плечах, оба прогона. +- H5 (символьные запросы не пострадают) — **⏳ НЕ ПРОВЕРЕНО**: в панели нет разметки symbol-vs-NL, подмножество не выделено. Единственная регрессия — TR-P2 (`src/core/search/engine.py`), лексический, а не символьный кейс. +- H6 (dense-off не лучше ни на одном кейсе) — **ОПРОВЕРГНУТ, как и ожидалось**: 2 кейса улучшились (E2E-05, E2E-07) — там, где dense уводил топ-1 в документ. + +**Вывод (важнее цифр):** из 52 обращений к table-API в 13 файлах `src/` на векторный поиск приходится 5 мест. +Остальные 47 — хранение чанков: `bm25.py:93` строит BM25-индекс из `table.to_pandas()`, `db_writer`/`indexer`/`indexer_table` +пишут и удаляют чанки, `file_move_manager` и `index_guard` делают `version()`/`where()`-сканы для meta-patching и ремонта. +**LanceDB здесь — не «векторный индекс», а primary store чанков.** PropertyGraph живёт отдельно (`graph.db`), это +единственная Phase-Zero гипотеза, которая НЕ подтвердилась. + +**Решение по Фазе 3:** удаление dense — **НЕ рекомендовано** (выигрыша в качестве нет — разница в шуме; скорость не установлена). +`MSCODEBASE_DENSE_OFF` остаётся как операционный kill-switch (default OFF, прод-путь байт-в-байт прежний). +Основание для удаления можно рассматривать только после отдельной задачи «чанки в Parquet/SQLite». + ## [2026-09-27] — stale_after + discriminator for memory notes (CONFIRMED) diff --git a/experiments/e28_denseoff/PENDING_LEDGER.md b/experiments/e28_denseoff/PENDING_LEDGER.md new file mode 100644 index 00000000..e2c3a57c --- /dev/null +++ b/experiments/e28_denseoff/PENDING_LEDGER.md @@ -0,0 +1,30 @@ +# PENDING_LEDGER — предложения в реестры (чужая сессия владеет KNOWN_ISSUES.md) + +Правок в `KNOWN_ISSUES.md` не делаю: файл сейчас меняет другая сессия (незакоммиченные правки), +по §21.3 реестр принадлежит одной сессии. + +## Предложение 1 — в KNOWN_ISSUES (новый пункт, не баг, а инвентаризация) + +**Статус:** предложено 2026-10-10, не подтверждено как дефект. +**Тема:** LanceDB в проекте — primary store чанков, а не «векторный индекс». +**Факт (проверено):** 52 обращения к table-API в 13 файлах `src/`; 8 идиом (`add`, `delete`, +`version`, `count_rows`, `search`, `to_pandas`, `to_arrow`, `merge_insert/query`). +На векторный поиск — 5 мест; остальные: запись/удаление чанков (`db_writer`, `indexer`, +`indexer_table`), чтение данных (`bm25.py:93` строит BM25 из `table.to_pandas()`, `index_status`, +`health`, 3× `mcp/tools/*`), метаданные (`file_move_manager` `version()`/`where()`, `index_guard` repair), +обвязка (`settings.dense`-соседние `lancedb_version`, `artifact_paths`, `file_guard`). +**Следствие:** любая формулировка «удалить LanceDB» неверна без слов «перенести чанки в другое хранилище». +**Температура:** 🟢 (факт, не проблема). **Ссылка:** `EXPERIMENTS_LOG.md` EXP-28. + +## Предложение 2 — в WISDOM / ADR + +Правило «измеряй знаменатель, а не числитель» на живом примере: первая формулировка была +«5 мест завязаны на dense → их можно отключить», реальный свип дал 52 места в 13 файлах, +из которых dense — меньшинство. Свип занял один вызов; неверная формулировка успела бы уйти +в план миграции и в PR. + +## Предложение 3 — в KNOWN_ISSUES (маленькое) + +`EXPERIMENTS_LOG.md` не имеет таблицы вердиктов по гипотезам: шесть гипотез эксперимента +разбросаны по абзацам. `experiments/e28_denseoff/VERDICTS.md` — образец таблицы; стоит +попросить формализацию при следующем большом эксперименте. \ No newline at end of file diff --git a/experiments/e28_denseoff/VERDICTS.md b/experiments/e28_denseoff/VERDICTS.md new file mode 100644 index 00000000..837aa5c3 --- /dev/null +++ b/experiments/e28_denseoff/VERDICTS.md @@ -0,0 +1,23 @@ +# EXP-28 — вердикты (дописывается ПОСЛЕ прогона; HYPOTHESES.md не редактируется) + +Дата: 2026-10-10. N=33 кейса, 2 прогона на плечо, одна сессия, один индекс. + +| # | Гипотеза | Фальсификатор | Наблюдение | Вердикт | +|---|---|---|---|---| +| H1 | hit1 без dense падает ≥3 п.п. | hit1 падает ≥3 п.п. | 11/33 → 12/33 (33.3% → 36.4%) | ❌ REFUTED (dense-off не хуже) | +| H2 | hit5 без dense падает ≥6 п.п. | hit5 падает ≥6 п.п. | 15/33 → 16/33 (45.5% → 48.5%) | ❌ REFUTED | +| H3 | dense-off ускоряет ≥15% | ускорение <5% | mean-of-means 18.0%, но 5.8% и 29.5% по прогонам | ⏳ PARTIAL — порог перекрыт разбросом | +| H4 | dense-off даёт 0 исключений | ≥1 ERROR | 0/33 в обоих плечах, оба прогона | ✅ CONFIRMED (2 прогона) | +| H5 | символьные запросы не пострадают | hit1 на символьных падает | подмножество не размечено в панели | ⏳ UNTESTED — нет разметки, кейс TR-P2 лексический | +| H6 | dense-off не лучше ни на одном кейсе | есть кейс, где dense-off лучше | E2E-05, E2E-07 лучше; TR-P2 хуже | ❌ REFUTED (ожидаемый провал) | + +## Что стоит за H5 (почему не «закрыт») +Панель объединена из трёх источников без поля «тип запроса». Чтобы честно проверить H5, нужна +разметка symbol-vs-NL в frozen-входе. Добавлять её после прогона нельзя (§17 freeze-before-look) — +значит H5 проверяется только новой панелью. Это следующий шаг, а не «мелочь». + +## Ограничения вердикта +- Один индекс, один день, один хост: перенос на другое железо не проверялся. +- Разница качества (net +1) — шум: 2 лучше / 1 хуже / 30 так же. +- Разница скорости не установлена: разброс между прогонами одного плеча 21%. +- Проверка «флаг реально гасит dense» выполнена (dense_calls 1 → 0) — иначе весь A/B был бы пустым. \ No newline at end of file diff --git a/experiments/e28_denseoff/e28_run.py b/experiments/e28_denseoff/e28_run.py new file mode 100644 index 00000000..8cb6a08b --- /dev/null +++ b/experiments/e28_denseoff/e28_run.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""E28 dense-off A/B — один и тот же код для обоих плеч. + +Различается ТОЛЬКО env MSCODEBASE_DENSE_OFF (плечо задаётся снаружи). +Вход: frozen/queries.jsonl (33 кейса, sha256 — в frozen/MANIFEST.sha256). + +USAGE (PowerShell): + python experiments/e28_denseoff/e28_run.py # arm=A dense ON + $env:MSCODEBASE_DENSE_OFF="true"; python experiments/e28_denseoff/e28_run.py # arm=B +Exit: 0 — оба плеча отработали; 2 — пустой вход (тихий ноль запрещён, §19.6). +""" +import json +import os +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.parent +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts")) + +import e2e_quality_search as H # noqa: E402 + +FROZEN = ROOT / "experiments/e28_denseoff/frozen/queries.jsonl" + + +def main() -> int: + rows = [json.loads(l) for l in FROZEN.read_text(encoding="utf-8").splitlines() if l.strip()] + if not rows: # §19.6: метрика на пустой выборке запрещена + print("EMPTY INPUT — панель пуста, метрика не считается", file=sys.stderr) + return 2 + + searcher = H.build_searcher(ROOT) + arm = "B(dense_off)" if searcher._dense_off else "A(dense_on)" + print(f"arm={arm} flag_dense_off={searcher._dense_off} cases={len(rows)}") + + import asyncio + + hits1 = hits5 = errors = 0 + t_all = time.perf_counter() + per_case = [] + for r in rows: + t0 = time.perf_counter() + try: + res = asyncio.run(searcher.hybrid_search_async(r["query"], limit=10)) + items = res.get("results", []) if isinstance(res, dict) else (res or []) + files = [str((it.get("metadata") or {}).get("file", "")).replace("\\", "/") + for it in items] + except Exception as e: # noqa: BLE001 — эксперимент должен считать и падения + files = [] + errors += 1 + print(f" ERR {r['id']}: {type(e).__name__}: {e}") + dt = (time.perf_counter() - t0) * 1000 + gold = r["gold_file"].replace("\\", "/") + h1 = bool(files[:1] == [gold]) + h5 = gold in files[:5] + hits1 += h1 + hits5 += h5 + per_case.append({"id": r["id"], "gold": gold, "ms": round(dt, 1), + "hit1": h1, "hit5": h5, "top1": files[0] if files else None}) + print(f" {r['id']:8s} h1={int(h1)} h5={int(h5)} {dt:7.0f}ms {gold} -> {per_case[-1]['top1']}") + wall = time.perf_counter() - t_all + n = len(rows) + summary = { + "arm": arm, "n": n, "hit1": hits1, "hit5": hits5, "errors": errors, + "hit1_pct": round(100 * hits1 / n, 1), "hit5_pct": round(100 * hits5 / n, 1), + "mean_ms": round(sum(c["ms"] for c in per_case) / n, 1), + "wall_s": round(wall, 1), + } + out = ROOT / "experiments/e28_denseoff/results" / f"e28_{arm.split('(')[0]}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"summary": summary, "cases": per_case}, ensure_ascii=False, indent=1), encoding="utf-8") + print(json.dumps(summary, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/experiments/e28_denseoff/frozen/HYPOTHESES.md b/experiments/e28_denseoff/frozen/HYPOTHESES.md new file mode 100644 index 00000000..b439e93f --- /dev/null +++ b/experiments/e28_denseoff/frozen/HYPOTHESES.md @@ -0,0 +1,21 @@ +# E28 — dense-off A/B (Фаза 3, проверка перед удалением dense-тира) + +Дата заморозки: 2026-10-10. Вход: `queries.jsonl` (33 кейса, sha256 f31e594e…278cc8). +Панель собрана из ТРЁХ уже существовавших frozen-наборов, ничего не сочинено: +`4A_unit_of_return/frozen/f5/queries.jsonl` (16), `token_reduction_v3_lancedb/frozen/rules.jsonl` (13 из 16; +N1/N2/N3 — негативные правила без целевого файла, исключены и это зафиксировано), +`scripts/e2e_quality_search.py::CASES` (10). Дедуп по gold-файлу: 33 уникальных файла, все существуют на диске. + +Оба плеча — одна сессия, один индекс, один день. Различается ОДНА переменная окружения. + +| # | Гипотеза | Фальсификатор | Ожидание | +|---|---|---|---| +| H1 | dense-off не снизит hit@1 более чем на 2 п.п. | hit@1 упадёт ≥3 п.п. | **ожидаю ПРОВАЛ** | +| H2 | dense-off не снизит hit@5 более чем на 5 п.п. | hit@5 упадёт ≥6 п.п. | **ожидаю ПРОВАЛ** | +| H3 | dense-off ускорит quality-поиск ≥15% (исчезают эмбеддинг запроса + IVF_FLAT) | ускорение <5% | ожидаю подтверждение | +| H4 | dense-off даст 0 исключений: путь BM25+FTS5+graph самодостаточен | хотя бы один ERROR | ожидаю подтверждение | +| H5 | символьные (идентификаторные) запросы не пострадают — их находит BM25/FTS | hit@1 на символьных упадёт | **ожидаю ПРОВАЛ** (на кейсах E2E вида «миграция схемы…» слово «LanceDB» есть и в тексте, и в имени файла — это не символьный, а лексический случай) | +| H6 | Симметрия: dense-off НЕ повысит hit@1 ни на одном кейсе | есть хотя бы один кейс, где dense-off лучше | **ожидаю ПРОВАЛ** — на lexical-совпадениях без dense срабатывает чистый BM25 | + +H1/H2/H6 внесены «слабыми» намеренно (§19.1): если бы все шесть подтвердились, отчёт +выглядел бы как 6/6 и ничего не значил бы. \ No newline at end of file diff --git a/experiments/e28_denseoff/frozen/MANIFEST.sha256 b/experiments/e28_denseoff/frozen/MANIFEST.sha256 new file mode 100644 index 00000000..6fee3094 --- /dev/null +++ b/experiments/e28_denseoff/frozen/MANIFEST.sha256 @@ -0,0 +1 @@ +f31e594ef729b2d7f577d37049734b26aa4a6dca9cd44e067e2db72514227cc8 queries.jsonl diff --git a/experiments/e28_denseoff/frozen/queries.jsonl b/experiments/e28_denseoff/frozen/queries.jsonl new file mode 100644 index 00000000..f7d49675 --- /dev/null +++ b/experiments/e28_denseoff/frozen/queries.jsonl @@ -0,0 +1,33 @@ +{"also_ids": [], "gold_file": "src/core/rate_limiter.py", "id": "F5S-01", "query": "Почему в ограничителе скорости используется threading.Lock, а не asyncio.Lock, и с каким инцидентом это связано?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/redact.py", "id": "F5S-02", "query": "Насколько надёжно модуль redact вырезает секреты и по какому принципу он ловит credential-строки?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/gitignore_parser.py", "id": "F5S-03", "query": "Почему парсер .gitignore использует формат 'gitignore', а не 'gitwildmatch'?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/embedder_lease.py", "id": "F5S-04", "query": "Как решается проблема, что idle-watchdog убивает общий embedder во время полной переиндексации?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/reindex_ledger.py", "id": "F5S-05", "query": "Какое главное ограничение наложено на модуль durable reindex ledger, чтобы он не мог сломать индексацию?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/quiet_break_gate.py", "id": "F5S-06", "query": "Как quiet-break gate ведёт себя при инфраструктурном сбое git или графа?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/providers/reranker/reranker_scoring.py", "id": "F5S-07", "query": "Что делает validate_scores с неоценёнными чанками, у которых score равен NaN или Inf?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "src/core/error_envelope.py", "id": "F5S-08", "query": "Зачем в конверте ошибок MCP-инструментов есть поле next_action?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/GRACEFUL_DEGRADATION.md", "id": "F5S-09", "query": "На сколько уровней деградации рассчитана система и что происходит, когда внешние провайдеры недоступны?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/SEARCH_PIPELINE.md", "id": "F5S-10", "query": "Почему в RRF-слиянии ранги считаются отдельно для каждого канала, а не общим enumerate?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/TRUST_BOUNDARY.md", "id": "F5S-11", "query": "Как система относится к инструкциям из проиндексированного чужого репозитория — как к командам или как к данным?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/TELEMETRY.md", "id": "F5S-12", "query": "Сохраняются ли метрики по инструментам между перезапусками MCP-сервера?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/adr/0003-verify-on-read.md", "id": "F5S-13", "query": "Какой вердикт получает ACTIVE-узел памяти, если резолвер недоступен и проверка не дала результата?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/HANDFOFF.md", "id": "F5S-14", "query": "Каким способом MCP-сервер определяет открытый в Zed проект на Windows, если переменные окружения не передаются?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/ZED_WINDOWS_QUIRKS.md", "id": "F5S-15", "query": "Что известно о переменной ZED_WORKTREE_ROOT на Windows?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": [], "gold_file": "docs/en/SYSTEM_REQUIREMENTS.md", "id": "F5S-16", "query": "Насколько переход с ONNX на llama.cpp GGUF снизил потребление памяти MCP-процессом?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl"} +{"also_ids": ["E2E-03"], "gold_file": "src/core/indexing/db_writer.py", "id": "TR-R1", "query": "begin_write _write_lock RLock reindex freeze", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": ["TR-P3"], "gold_file": "src/core/artifact_gc.py", "id": "TR-R2", "query": "ArtifactGC 30d projects 90d telemetry 7d logs retention", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/providers/reranker/llama_install.py", "id": "TR-R3", "query": "ubatch embed 512 rerank 1024 resolve_ubatch", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/providers/reranker/llama_runner.py", "id": "TR-R4", "query": "_InterProcessLock _is_pid_alive OpenProcess SYNCHRONIZE stale PID", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": ["TR-R7", "E2E-06"], "gold_file": "src/providers/embedder/remote_embedder.py", "id": "TR-R5", "query": "search_code timeout idle embedder _revive_llama_cpp hot-reload", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/core/graph.py", "id": "TR-R6", "query": "PropertyGraph add_node add_edge SQLite transaction per node edge", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/core/search/cypher_sql.py", "id": "TR-R8", "query": "collect Cypher json_group_array cypher_sql", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/core/bootstrap_tests.py", "id": "TR-R9", "query": "TESTS edges verifying tests decoys bootstrap_tests", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": ["E2E-01"], "gold_file": "src/core/indexing/freshness.py", "id": "TR-R10", "query": "FreshnessChecker to_pandas columns lancedb broken dead code", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/core/indexing/parser.py", "id": "TR-P1", "query": "CodeParser _parse_with_tree_sitter _walk_node AST chunks", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": ["E2E-04"], "gold_file": "src/core/search/engine.py", "id": "TR-P2", "query": "hybrid_search_async reciprocal_rank_fusion FTS5 BM25", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl"} +{"also_ids": [], "gold_file": "src/core/indexing/db_manager.py", "id": "E2E-02", "query": "миграция схемы добавление колонок file_mtime_ns в таблицу LanceDB", "source": "scripts/e2e_quality_search.py::CASES"} +{"also_ids": [], "gold_file": "src/core/intelligence/verify_on_read.py", "id": "E2E-05", "query": "ленивая проверка факта памяти verify on read статус ADR", "source": "scripts/e2e_quality_search.py::CASES"} +{"also_ids": [], "gold_file": "src/core/indexing/project_indexer_registry.py", "id": "E2E-07", "query": "per project indexer registry multi window пулы по путям проектов", "source": "scripts/e2e_quality_search.py::CASES"} +{"also_ids": [], "gold_file": "src/mcp/tools/indexing_tools.py", "id": "E2E-08", "query": "переиндексация одного изменённого файла notify change rate limit", "source": "scripts/e2e_quality_search.py::CASES"} +{"also_ids": [], "gold_file": "src/providers/reranker/search_result_reranker.py", "id": "E2E-09", "query": "ранжирование результатов реранкером BGE M3 перестановка топ", "source": "scripts/e2e_quality_search.py::CASES"} +{"also_ids": [], "gold_file": "src/core/search/bm25.py", "id": "E2E-10", "query": "bm25 ключевые слова медленный но точный полнотекстовый", "source": "scripts/e2e_quality_search.py::CASES"} diff --git a/experiments/e28_denseoff/results/e28_A.json b/experiments/e28_denseoff/results/e28_A.json new file mode 100644 index 00000000..3cca74ec --- /dev/null +++ b/experiments/e28_denseoff/results/e28_A.json @@ -0,0 +1,279 @@ +{ + "summary": { + "arm": "A(dense_on)", + "n": 33, + "hit1": 11, + "hit5": 15, + "errors": 0, + "hit1_pct": 33.3, + "hit5_pct": 45.5, + "mean_ms": 3534.3, + "wall_s": 116.6 + }, + "cases": [ + { + "id": "F5S-01", + "gold": "src/core/rate_limiter.py", + "ms": 9696.8, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-02", + "gold": "src/core/redact.py", + "ms": 651.2, + "hit1": true, + "hit5": true, + "top1": "src/core/redact.py" + }, + { + "id": "F5S-03", + "gold": "src/core/gitignore_parser.py", + "ms": 4091.4, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-04", + "gold": "src/core/embedder_lease.py", + "ms": 562.3, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-05", + "gold": "src/core/reindex_ledger.py", + "ms": 4996.1, + "hit1": false, + "hit5": true, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-06", + "gold": "src/core/quiet_break_gate.py", + "ms": 666.9, + "hit1": true, + "hit5": true, + "top1": "src/core/quiet_break_gate.py" + }, + { + "id": "F5S-07", + "gold": "src/providers/reranker/reranker_scoring.py", + "ms": 20255.0, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/reranker_scoring.py" + }, + { + "id": "F5S-08", + "gold": "src/core/error_envelope.py", + "ms": 639.5, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-09", + "gold": "docs/en/GRACEFUL_DEGRADATION.md", + "ms": 5848.4, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_judge/mapping.json" + }, + { + "id": "F5S-10", + "gold": "docs/en/SEARCH_PIPELINE.md", + "ms": 575.3, + "hit1": false, + "hit5": false, + "top1": "experiments/prompt_robustness/frozen/matcher_calibration.json" + }, + { + "id": "F5S-11", + "gold": "docs/TRUST_BOUNDARY.md", + "ms": 5588.4, + "hit1": false, + "hit5": false, + "top1": "EXPERIMENTS_LOG.md" + }, + { + "id": "F5S-12", + "gold": "docs/en/TELEMETRY.md", + "ms": 436.9, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_smoke_answers.json" + }, + { + "id": "F5S-13", + "gold": "docs/adr/0003-verify-on-read.md", + "ms": 6007.4, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-14", + "gold": "docs/en/HANDFOFF.md", + "ms": 483.9, + "hit1": false, + "hit5": false, + "top1": "tests/conftest.py" + }, + { + "id": "F5S-15", + "gold": "docs/en/ZED_WINDOWS_QUIRKS.md", + "ms": 4202.9, + "hit1": false, + "hit5": false, + "top1": "docs/ru/ZED_WINDOWS_QUIRKS.md" + }, + { + "id": "F5S-16", + "gold": "docs/en/SYSTEM_REQUIREMENTS.md", + "ms": 469.5, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_smoke_answers.json" + }, + { + "id": "TR-R1", + "gold": "src/core/indexing/db_writer.py", + "ms": 6318.7, + "hit1": false, + "hit5": false, + "top1": "experiments/misc_probes/exp_in_situ_parse_timeout.py" + }, + { + "id": "TR-R2", + "gold": "src/core/artifact_gc.py", + "ms": 480.5, + "hit1": false, + "hit5": true, + "top1": "src/core/log_manager.py" + }, + { + "id": "TR-R3", + "gold": "src/providers/reranker/llama_install.py", + "ms": 5173.2, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/llama_install.py" + }, + { + "id": "TR-R4", + "gold": "src/providers/reranker/llama_runner.py", + "ms": 586.6, + "hit1": false, + "hit5": true, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "TR-R5", + "gold": "src/providers/embedder/remote_embedder.py", + "ms": 4533.6, + "hit1": true, + "hit5": true, + "top1": "src/providers/embedder/remote_embedder.py" + }, + { + "id": "TR-R6", + "gold": "src/core/graph.py", + "ms": 592.4, + "hit1": true, + "hit5": true, + "top1": "src/core/graph.py" + }, + { + "id": "TR-R8", + "gold": "src/core/search/cypher_sql.py", + "ms": 5243.0, + "hit1": true, + "hit5": true, + "top1": "src/core/search/cypher_sql.py" + }, + { + "id": "TR-R9", + "gold": "src/core/bootstrap_tests.py", + "ms": 524.1, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/a2_link_rate.py" + }, + { + "id": "TR-R10", + "gold": "src/core/indexing/freshness.py", + "ms": 4514.8, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/freshness.py" + }, + { + "id": "TR-P1", + "gold": "src/core/indexing/parser.py", + "ms": 718.3, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/parser.py" + }, + { + "id": "TR-P2", + "gold": "src/core/search/engine.py", + "ms": 6186.6, + "hit1": true, + "hit5": true, + "top1": "src/core/search/engine.py" + }, + { + "id": "E2E-02", + "gold": "src/core/indexing/db_manager.py", + "ms": 518.4, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_guard.py" + }, + { + "id": "E2E-05", + "gold": "src/core/intelligence/verify_on_read.py", + "ms": 4452.4, + "hit1": false, + "hit5": false, + "top1": "docs/adr/0003-verify-on-read.md" + }, + { + "id": "E2E-07", + "gold": "src/core/indexing/project_indexer_registry.py", + "ms": 394.6, + "hit1": false, + "hit5": true, + "top1": "src/core/multi_project_searcher.py" + }, + { + "id": "E2E-08", + "gold": "src/mcp/tools/indexing_tools.py", + "ms": 6331.4, + "hit1": false, + "hit5": false, + "top1": "experiments/context_engine/results_tasks_v3.json" + }, + { + "id": "E2E-09", + "gold": "src/providers/reranker/search_result_reranker.py", + "ms": 354.7, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_smoke_answers.json" + }, + { + "id": "E2E-10", + "gold": "src/core/search/bm25.py", + "ms": 4536.8, + "hit1": true, + "hit5": true, + "top1": "src/core/search/bm25.py" + } + ] +} \ No newline at end of file diff --git a/experiments/e28_denseoff/results/e28_B.json b/experiments/e28_denseoff/results/e28_B.json new file mode 100644 index 00000000..a2db0721 --- /dev/null +++ b/experiments/e28_denseoff/results/e28_B.json @@ -0,0 +1,279 @@ +{ + "summary": { + "arm": "B(dense_off)", + "n": 33, + "hit1": 12, + "hit5": 16, + "errors": 0, + "hit1_pct": 36.4, + "hit5_pct": 48.5, + "mean_ms": 2492.4, + "wall_s": 82.2 + }, + "cases": [ + { + "id": "F5S-01", + "gold": "src/core/rate_limiter.py", + "ms": 10083.5, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-02", + "gold": "src/core/redact.py", + "ms": 292.7, + "hit1": true, + "hit5": true, + "top1": "src/core/redact.py" + }, + { + "id": "F5S-03", + "gold": "src/core/gitignore_parser.py", + "ms": 3734.9, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-04", + "gold": "src/core/embedder_lease.py", + "ms": 289.0, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-05", + "gold": "src/core/reindex_ledger.py", + "ms": 3772.7, + "hit1": false, + "hit5": true, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-06", + "gold": "src/core/quiet_break_gate.py", + "ms": 244.0, + "hit1": true, + "hit5": true, + "top1": "src/core/quiet_break_gate.py" + }, + { + "id": "F5S-07", + "gold": "src/providers/reranker/reranker_scoring.py", + "ms": 12425.1, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/reranker_scoring.py" + }, + { + "id": "F5S-08", + "gold": "src/core/error_envelope.py", + "ms": 447.0, + "hit1": false, + "hit5": false, + "top1": "src/core/bootstrap_entities.py" + }, + { + "id": "F5S-09", + "gold": "docs/en/GRACEFUL_DEGRADATION.md", + "ms": 3985.3, + "hit1": false, + "hit5": false, + "top1": "src/core/interfaces/__init__.py" + }, + { + "id": "F5S-10", + "gold": "docs/en/SEARCH_PIPELINE.md", + "ms": 317.2, + "hit1": false, + "hit5": false, + "top1": "src/mcp/server_tools.py" + }, + { + "id": "F5S-11", + "gold": "docs/TRUST_BOUNDARY.md", + "ms": 3906.0, + "hit1": false, + "hit5": false, + "top1": "src/core/search/agentic_search.py" + }, + { + "id": "F5S-12", + "gold": "docs/en/TELEMETRY.md", + "ms": 253.8, + "hit1": false, + "hit5": false, + "top1": "src/main.py" + }, + { + "id": "F5S-13", + "gold": "docs/adr/0003-verify-on-read.md", + "ms": 4607.2, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-14", + "gold": "docs/en/HANDFOFF.md", + "ms": 296.3, + "hit1": false, + "hit5": false, + "top1": "src/core/platform_utils.py" + }, + { + "id": "F5S-15", + "gold": "docs/en/ZED_WINDOWS_QUIRKS.md", + "ms": 3153.7, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/verify_on_read.py" + }, + { + "id": "F5S-16", + "gold": "docs/en/SYSTEM_REQUIREMENTS.md", + "ms": 292.9, + "hit1": false, + "hit5": false, + "top1": "src/core/embedder/onnx_client.py" + }, + { + "id": "TR-R1", + "gold": "src/core/indexing/db_writer.py", + "ms": 3121.8, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_project_runner.py" + }, + { + "id": "TR-R2", + "gold": "src/core/artifact_gc.py", + "ms": 283.8, + "hit1": false, + "hit5": true, + "top1": "src/core/log_manager.py" + }, + { + "id": "TR-R3", + "gold": "src/providers/reranker/llama_install.py", + "ms": 2660.9, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/llama_install.py" + }, + { + "id": "TR-R4", + "gold": "src/providers/reranker/llama_runner.py", + "ms": 365.0, + "hit1": false, + "hit5": true, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "TR-R5", + "gold": "src/providers/embedder/remote_embedder.py", + "ms": 3652.0, + "hit1": true, + "hit5": true, + "top1": "src/providers/embedder/remote_embedder.py" + }, + { + "id": "TR-R6", + "gold": "src/core/graph.py", + "ms": 370.6, + "hit1": true, + "hit5": true, + "top1": "src/core/graph.py" + }, + { + "id": "TR-R8", + "gold": "src/core/search/cypher_sql.py", + "ms": 3918.0, + "hit1": true, + "hit5": true, + "top1": "src/core/search/cypher_sql.py" + }, + { + "id": "TR-R9", + "gold": "src/core/bootstrap_tests.py", + "ms": 328.5, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/a2_link_rate.py" + }, + { + "id": "TR-R10", + "gold": "src/core/indexing/freshness.py", + "ms": 3383.1, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/freshness.py" + }, + { + "id": "TR-P1", + "gold": "src/core/indexing/parser.py", + "ms": 395.6, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/parser.py" + }, + { + "id": "TR-P2", + "gold": "src/core/search/engine.py", + "ms": 5062.9, + "hit1": false, + "hit5": true, + "top1": "docs/zh/SEARCH_PIPELINE.md" + }, + { + "id": "E2E-02", + "gold": "src/core/indexing/db_manager.py", + "ms": 325.5, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_guard.py" + }, + { + "id": "E2E-05", + "gold": "src/core/intelligence/verify_on_read.py", + "ms": 2824.4, + "hit1": true, + "hit5": true, + "top1": "src/core/intelligence/verify_on_read.py" + }, + { + "id": "E2E-07", + "gold": "src/core/indexing/project_indexer_registry.py", + "ms": 242.1, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/project_indexer_registry.py" + }, + { + "id": "E2E-08", + "gold": "src/mcp/tools/indexing_tools.py", + "ms": 4077.0, + "hit1": false, + "hit5": false, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "E2E-09", + "gold": "src/providers/reranker/search_result_reranker.py", + "ms": 244.3, + "hit1": false, + "hit5": false, + "top1": "install.py" + }, + { + "id": "E2E-10", + "gold": "src/core/search/bm25.py", + "ms": 2891.5, + "hit1": true, + "hit5": true, + "top1": "src/core/search/bm25.py" + } + ] +} \ No newline at end of file diff --git a/src/config/settings.py b/src/config/settings.py index c7dc81cc..cb8aae67 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -188,6 +188,16 @@ class SearchConfig: default_factory=lambda: int(os.getenv("MSCODEBASE_ONDEMAND_RERANK_TOP_N", "10")) ) + # Dense-off (E28, Фаза 3): true → НЕ выполнять dense-тир (эмбеддинг запроса + + # векторный поиск в LanceDB). Кандидаты остаются из BM25/FTS5/graph. + # Off по умолчанию — поведение поиска неизменно без флага. + dense_off: bool = field( + default_factory=lambda: os.getenv( + "MSCODEBASE_DENSE_OFF", "false" + ).lower() + == "true" + ) + @dataclass class IndexConfig: diff --git a/src/core/search/engine.py b/src/core/search/engine.py index f2816d60..9f77de00 100644 --- a/src/core/search/engine.py +++ b/src/core/search/engine.py @@ -445,6 +445,11 @@ def __init__(self, indexer, embedder): self._ondemand_rerank_top_n = int( getattr(get_config().search, "ondemand_rerank_top_n", 10) or 10 ) + # Dense-off (E28, Фаза 3): MSCODEBASE_DENSE_OFF=true → dense-тир выключен, + # кандидаты только из BM25/FTS5/graph. Off по умолчанию. + self._dense_off = bool( + getattr(get_config().search, "dense_off", False) + ) self._multi_reranker: Optional[MultiProviderReranker] = None self._multi_reranker_initialized: bool = False self._multi_reranker_lock: Optional[asyncio.Lock] = None # lazy: создаётся при первом async-вызове (привязка к event loop) @@ -933,7 +938,9 @@ async def hybrid_search_async( # Векторный поиск (dense) — с prefilter в LanceDB # (варианты синонимов дают те же эмбеддинги) - if variant == query and not all_dense_results: + # MSCODEBASE_DENSE_OFF=true (E28, Фаза 3): гасит dense-тир целиком — + # и эмбеддинг запроса, и vector search. Default OFF. + if variant == query and not all_dense_results and not self._dense_off: try: # ── Embedding cache ── query_hash = _cache_key(variant) diff --git a/tests/test_dense_off_flag.py b/tests/test_dense_off_flag.py new file mode 100644 index 00000000..f49662d9 --- /dev/null +++ b/tests/test_dense_off_flag.py @@ -0,0 +1,38 @@ +"""E28 (Фаза 3): dense-off флаг — гасит dense-тир, ничего не ломая. + +Контракт: default OFF (плот-ап путь не тронут), флаг гасит и эмбеддинг запроса, +и vector search; BM25/FTS5/graph продолжают работать. +""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + + +def test_flag_default_off(): + from src.config.settings import SearchConfig + + assert os.environ.get("MSCODEBASE_DENSE_OFF", "false") == "false" + assert SearchConfig().dense_off is False + + +def test_env_enables_flag(monkeypatch): + from src.config.settings import SearchConfig + + monkeypatch.setenv("MSCODEBASE_DENSE_OFF", "true") + assert SearchConfig().dense_off is True + monkeypatch.setenv("MSCODEBASE_DENSE_OFF", "false") + assert SearchConfig().dense_off is False + + +def test_searcher_reads_flag(): + from src.core.search.engine import Searcher + + assert hasattr(Searcher, "_dense_off") or True # атрибут ставится в __init__ + src = Path(ROOT / "src/core/search/engine.py").read_text(encoding="utf-8") + assert "not self._dense_off" in src, "dense-тир не гасится флагом" + assert 'get_config().search, "dense_off", False' in src From 38135611c47692bcb43645eeede96d0bdef0ef9d Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Sat, 10 Oct 2026 18:11:04 +0300 Subject: [PATCH 2/2] exp(e29): symbol/NL subset split closes EXP-28 H5, corrects 18% speedup claim EXP-28 left H5 untested: no symbol-vs-NL labelling, and it could not be added after the run (freeze-before-look). This experiment freezes the labelling rule first (snake_case | CamelCase | pathy | single token -> symbolic 15 / NL 18, sha256 7639ba1b), then runs both arms twice. A dense ON : NL 3/18 (16.7%), symbolic 8/15 (53.3%), 0 errors, mean 2559/2555ms B dense OFF: NL 5/18 (27.8%), symbolic 7/15 (46.7%), 0 errors, mean 2375/2354ms NL 2 better / 0 worse; symbolic 0 better / 1 worse (TR-P2). Speedup stable at 7.2% / 7.9%. Verdicts: H1 CONFIRMED (symbolic loses exactly 1 of 15, within the declared tolerance), H2 REFUTED as expected (NL is better), H3 REFUTED as expected (7.5%, below 15%), H4 CONFIRMED (0 errors), H5 CONFIRMED against expectation (two NL cases improved). CORRECTED: EXP-28 published an 18.0% speedup; two runs here show 7.2% and 7.9%. The old number was inflated by one noisy run (21% spread vs 0.2% here). Logged as a new entry with the supersede reference, not a silent edit. Harness incident: the first run from the worktree printed hit1=0/33 with rc=0 and 167ms - a silent zero on an empty index (index is per-project-path). Guard added: all-empty exits 2, and the project root is an explicit manifest parameter. That run is not counted. Phase 3 decision unchanged: no observed harm from dense, no proven gain (1-2 of 33 cases is noise), LanceDB stays as chunk primary store, the flag stays a kill-switch. --- AGENT_DIARY.md | 14 + EXPERIMENTS_LOG.md | 54 +++ experiments/e29_symbol_ab/VERDICTS.md | 36 ++ experiments/e29_symbol_ab/e29_label.py | 52 +++ experiments/e29_symbol_ab/e29_run.py | 92 +++++ .../e29_symbol_ab/frozen/HYPOTHESES.md | 23 ++ .../e29_symbol_ab/frozen/MANIFEST.sha256 | 1 + .../frozen/queries_labeled.jsonl | 33 ++ experiments/e29_symbol_ab/results/e29_A.json | 323 ++++++++++++++++++ experiments/e29_symbol_ab/results/e29_B.json | 323 ++++++++++++++++++ 10 files changed, 951 insertions(+) create mode 100644 experiments/e29_symbol_ab/VERDICTS.md create mode 100644 experiments/e29_symbol_ab/e29_label.py create mode 100644 experiments/e29_symbol_ab/e29_run.py create mode 100644 experiments/e29_symbol_ab/frozen/HYPOTHESES.md create mode 100644 experiments/e29_symbol_ab/frozen/MANIFEST.sha256 create mode 100644 experiments/e29_symbol_ab/frozen/queries_labeled.jsonl create mode 100644 experiments/e29_symbol_ab/results/e29_A.json create mode 100644 experiments/e29_symbol_ab/results/e29_B.json diff --git a/AGENT_DIARY.md b/AGENT_DIARY.md index 59e74494..1e706b17 100644 --- a/AGENT_DIARY.md +++ b/AGENT_DIARY.md @@ -1,4 +1,18 @@ +## [2026-10-10] EXP-29 — H5 закрыт, а число EXP-28 (18%) опровергнуто собственным же экспериментом + +- Разметка symbol/NL детерминированная и **заморожена до прогона** (`e29_label.py`): 15 symbolic / 18 NL. + Иначе H5 остался бы навсегда «не проверен» — постфактум разметку добавлять нельзя (§17). +- **Харнесс поймал собственный баг:** первый прогон из worktree дал `hit1=0/33` при rc=0 и 167ms — + тихий ноль на пустом индексе (индекс per-project-path). Guard: все-пусто → `exit 2`; + `E29_PROJECT_ROOT` вынесен в манифест. Первый прогон не засчитан. +- A dense ON: NL 3/18, symbolic 8/15, 0 ошибок. B dense OFF: NL **5/18**, symbolic 7/15, 0 ошибок. + NL: 2 лучше / 0 хуже; symbolic: 0 лучше / 1 хуже (TR-P2). Ускорение стабильно 7.2%/7.9%. +- **CORRECTED:** EXP-28 публиковал 18.0% — раздуто одним шумным прогоном (разброс 21%). + Действующее число 7.5%; правка внесена новой записью, не тихо (§11). +- Решение по Фазе 3 не изменилось: dense не отключаем, LanceDB не удаляем (primary store чанков), + `MSCODEBASE_DENSE_OFF` остаётся kill-switch. Числа EXP-29 — разрез той же панели, не новый эксперимент. + ## [2026-10-10] EXP-28 dense-off A/B — LanceDB оказался primary store, удаление отклонено - **Phase Zero угадал наполовину:** «LanceDB = единственное хранилище графа» — опровергнуто diff --git a/EXPERIMENTS_LOG.md b/EXPERIMENTS_LOG.md index 3d259771..1a1004e8 100644 --- a/EXPERIMENTS_LOG.md +++ b/EXPERIMENTS_LOG.md @@ -1,4 +1,58 @@ # EXPERIMENTS_LOG.md — Audit Verification (2026-07-22) +## [2026-10-10] — EXP-29 разрез symbol/NL: закрывает H5, корректирует число EXP-28 (PARTIAL) + +**Зачем:** EXP-28 оставил H5 как «⏳ не проверено» — в панели не было разметки symbol-vs-NL. +Разметка не могла быть добавлена постфактум (§17), поэтому потребовался новый эксперимент. + +**Вход (заморожен ДО прогона):** `experiments/e29_symbol_ab/frozen/queries_labeled.jsonl`, +sha256 `7639ba1bda00212f8bceb04ae63ddaacddb897d21ae58f2aceff2430f88a53bf`, 33 кейса панели EXP-28 + колонка `subset`. +Правило разметки детерминированное и заморожено в `e29_label.py`: +`symbolic := snake_case | CamelCase | pathy(:: / слэш / .метод()) | одиночный токен без пробелов` → symbolic 15 / NL 18. +5 гипотез, 3 из них внесены как ожидающе-провальные (H1, H2, H3). + +**Команда (код из worktree, индекс проекта — явный параметр):** +``` +$env:E29_PROJECT_ROOT="" # индекс per-project-path, в worktree его нет +python experiments/e29_symbol_ab/e29_run.py # arm A: dense ON +$env:MSCODEBASE_DENSE_OFF="true"; python experiments/e29_symbol_ab/e29_run.py # arm B: dense OFF +``` +**Сырой вывод (2 прогона на плечо; полные логи в results/e29_A.json / e29_B.json):** +``` +arm=A dense ON : NL 3/18 (16.7%) symbolic 8/15 (53.3%) errors 0 empty_top1 0 mean_ms 2559.2 / 2555.2 +arm=B dense OFF: NL 5/18 (27.8%) symbolic 7/15 (46.7%) errors 0 empty_top1 0 mean_ms 2375.2 / 2354.1 +NL : 2 лучше / 0 хуже / 16 так же +symbolic: 0 лучше / 1 хуже / 14 так же (TR-P2: engine.py -> docs/zh/SEARCH_PIPELINE.md) +ускорение: 7.2% и 7.9% (стабильно, разброс 0.2%) +``` + +**Инцидент харнесса (закрыт):** первый прогон из worktree дал `hit1=0/33` при rc=0 и mean_ms 167 — +тихий ноль на пустом индексе (§19.6). Причина: индекс per-project-path, в worktree его нет. +Исправлено guard'ом: если все запросы вернули пусто → `return 2` с явным сообщением, плюс явный +`E29_PROJECT_ROOT` в манифесте. Непрозрачность устранена, но первый прогон не засчитан. + +**Вердикты:** +- H1 (symbolic не хуже, допуск 1 кейс) — **CONFIRMED**: минус ровно 1 кейс из 15. +- H2 (NL хуже ≥3 п.п.) — **REFUTED, как ожидалось**: NL **лучше** на 2 кейса. +- H3 (ускорение ≥15%) — **REFUTED, как ожидалось**: стабильные 7.2%/7.9%. +- H4 (0 ошибок) — **CONFIRMED** (0/33 × 2 прогона × 2 плеча). +- H5 (≥1 улучшенный NL-кейс) — **CONFIRMED, как ожидалось провалиться**: E2E-05 и E2E-07. + +**Механизм:** оба улучшения — NL-запросы, где dense уводил top-1 в документ +(`docs/adr/0003-verify-on-read.md` и `src/core/multi_project_searcher.py` вытесняли код). +Регрессия одна и лексическая (`docs/zh/SEARCH_PIPELINE.md` вместо `engine.py`). + +**Согласованность с EXP-28:** суммы подмножеств воспроизводят EXP-28 один в один +(A 8+3=11, B 7+5=12) — это разбиение панели, а не независимый эксперимент. + +**CORRECTED к EXP-28 (без тихой правки):** EXP-28 публиковал ускорение **18.0%**. EXP-29 двумя +прогонами даёт **7.2% и 7.9%**; прежнее число было раздуто одним шумным прогоном (разброс 21%). +Прежний вердикт H3=PARTIAL остаётся верным, но **число 18% больше не публикуется**: действующее — 7.5% +(среднее двух прогонов EXP-29). + +**Вывод по Фазе 3:** наблюдаемого вреда dense нет ни на одном подмножестве, но и доказанного выигрыша +в качестве нет (1–2 кейса из 33 = шум по построению). Решение не изменилось: dense не отключаем, + LanceDB не удаляем (он primary store чанков), флаг остаётся как kill-switch. + ## [2026-10-10] — EXP-28 dense-off A/B: гасим dense-тир флагом (PARTIAL) **Вопрос:** можно ли отключить dense (LanceDB vector search) без потери качества — то есть довести Фазу 3 до точки принятия решения об удалении? diff --git a/experiments/e29_symbol_ab/VERDICTS.md b/experiments/e29_symbol_ab/VERDICTS.md new file mode 100644 index 00000000..a1952b4d --- /dev/null +++ b/experiments/e29_symbol_ab/VERDICTS.md @@ -0,0 +1,36 @@ +# EXP-29 — вердикты (HYPOTHESES.md не редактируется — он заморожен) + +Дата: 2026-10-10. Вход: `frozen/queries_labeled.jsonl`, sha256 `7639ba1b…a53bf`, 33 кейса +(symbolic 15 / NL 18). 2 прогона на плечо, один индекс, один день, различается только env. + +| # | Гипотеза | Фальсификатор | Наблюдение | Вердикт | +|---|---|---|---|---| +| H1 | на symbolic dense-off не хуже (падение ≤1 кейса) | падение ≥2 кейсов | symbolic hit@1 8/15 → **7/15** (53.3% → 46.7%) = минус **1** кейс | ✅ CONFIRMED (в пределах заявленного допуска; единственная регрессия — TR-P2) | +| H2 | на NL dense-off хуже ≥3 п.п. | падение <3 п.п. | NL hit@1 3/18 → **5/18** (16.7% → 27.8%) = **плюс** 2 кейса | ❌ REFUTED (ожидаемый провал) | +| H3 | ускорение ≥15% воспроизводится | <5% в обоих прогонах | 7.2% и 7.9% (A 2559/2555ms, B 2375/2354ms) | ❌ REFUTED (ожидаемый провал): стабильно ~7.5%, но <15% | +| H4 | 0 исключений | ≥1 ERROR | 0/33 в обоих плечах, оба прогона | ✅ CONFIRMED | +| H5 | dense-off улучшает ≥1 NL-кейс | 0 улучшенных NL-кейсов | E2E-05 и E2E-07 улучшены (NL, +2) | ✅ CONFIRMED (ожидаемый провал) | + +## Механизм, а не совпадение +Оба улучшения — NL-запросы, где dense уводил top-1 **в документ**: +- E2E-05: `docs/adr/0003-verify-on-read.md` → `src/core/intelligence/verify_on_read.py` +- E2E-07: `src/core/multi_project_searcher.py` → `src/core/indexing/project_indexer_registry.py` +Единственная регрессия TR-P2 — symbolic: `src/core/search/engine.py` → `docs/zh/SEARCH_PIPELINE.md`. +Картина «dense как триггер доканального дрейфа на NL» повторяется в обоих прогонах. + +## Согласованность с EXP-28 (важнее новых цифр) +Суммы подмножеств **воспроизводят** EXP-28 один в один: A = 8 symbolic + 3 NL = 11/33; +B = 7 + 5 = 12/33. Тот же индекс, тот же день — это held-out по разбиению, а не новый эксперимент. + +## CORRECTED к EXP-28 (правило §11/§19.11, не тихой правки) +EXP-28 сообщал «ускорение mean-of-means **18.0%**». EXP-29 двумя прогонами показывает **7.2% и 7.9%**. +Прежнее число было раздуто одним шумным прогоном (разброс 21% у EXP-28 против 0.2% у EXP-29). +Прежний вердикт H3 = PARTIAL остаётся верным, но **числа в нём больше не публикуются** — +действующее число 7.5% (среднее двух прогонов, EXP-29). + +## Ограничения +- N мал: symbolic 15, NL 18. Разница 1–2 кейсов = шум по построению; утверждать «dense мешает на NL» + на 2 кейсах нельзя — можно утверждать только «не видно вреда и есть 2 воспроизводимых улучшения». +- Один хост, один индекс, один день. Перенос не проверялся. +- Разметка — эвристика (snake_case | CamelCase | pathy | одиночный токен); часть NL-запросов может + содержать идентификаторы, и наоборот. Проверялась воспроизводимость правила, а не его точность. \ No newline at end of file diff --git a/experiments/e29_symbol_ab/e29_label.py b/experiments/e29_symbol_ab/e29_label.py new file mode 100644 index 00000000..b64cd86a --- /dev/null +++ b/experiments/e29_symbol_ab/e29_label.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""EXP-29 — разметка панели E28 на symbol/NL ДО прогона (freeze-before-look). + +Правило детерминированное, не подгоняется глазами и не смотрит на результаты: + symbolic := в запросе есть snake_case(_x), CamelCase(xX), путь (:: / . / слэш) + либо одиночный токен без пробелов длиной >= 2 + иначе NL +Метки пишутся В ЗАМОРОЖЕННЫЙ файл — это часть входа, а не постфактум-разметка. +""" +import hashlib +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +SRC = ROOT / "experiments/e28_denseoff/frozen/queries.jsonl" +DST = ROOT / "experiments/e29_symbol_ab/frozen/queries_labeled.jsonl" + +SNAKE = re.compile(r"[A-Za-z_][A-Za-z0-9]*_[A-Za-z0-9_]+") +CAMEL = re.compile(r"\b[A-Z][a-z]+[A-Z][A-Za-z0-9]*\b") +PATHY = re.compile(r"::|[\w]/|\.\w+\(") + + +def label(q: str) -> str: + if SNAKE.search(q) or CAMEL.search(q) or PATHY.search(q): + return "symbolic" + toks = q.split() + if len(toks) == 1 and len(toks[0].strip(".,?")) >= 2: + return "symbolic" + return "NL" + + +rows = [json.loads(l) for l in SRC.read_text(encoding="utf-8").splitlines() if l.strip()] +out = [] +for r in rows: + r = dict(r) + r["subset"] = label(r["query"]) + out.append(r) + +DST.write_text("\n".join(json.dumps(r, ensure_ascii=False, sort_keys=True) for r in out) + "\n", + encoding="utf-8", newline="\n") +sha = hashlib.sha256(DST.read_bytes()).hexdigest() +(DST.parent / "MANIFEST.sha256").write_text(f"{sha} queries_labeled.jsonl\n", encoding="utf-8", newline="\n") + +from collections import Counter # noqa: E402 +c = Counter(r["subset"] for r in out) +print("label rule: snake_case | CamelCase | pathy | single-token") +print("counts:", dict(c), "total:", len(out)) +print("sha256", sha) +if c["symbolic"] < 8: + print("WARNING: symbolic subset < 8 — вывод будет 'N мал', а не 'проверено'", file=__import__("sys").stderr) \ No newline at end of file diff --git a/experiments/e29_symbol_ab/e29_run.py b/experiments/e29_symbol_ab/e29_run.py new file mode 100644 index 00000000..f2ef91bc --- /dev/null +++ b/experiments/e29_symbol_ab/e29_run.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""EXP-29 — тот же измеритель, что E28, плюс разрез по subset (symbolic / NL). + +Различается только env MSCODEBASE_DENSE_OFF. Вход: frozen/queries_labeled.jsonl +(sha7639ba1b — в frozen/MANIFEST.sha256). Exit 2 на пустом входе (§19.6). +""" +import json +import os +import sys +import time +from collections import defaultdict +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.parent +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts")) + +import e2e_quality_search as H # noqa: E402 + +FROZEN = ROOT / "experiments/e29_symbol_ab/frozen/queries_labeled.jsonl" +# Проект с ИНДЕКСОМ может отличаться от дерева с кодом: индекс per-project-path, +# в worktree его нет → все запросы вернули бы 0 результатов (тихий ноль, §19.6). +PROJ = Path(os.getenv("E29_PROJECT_ROOT") or ROOT) + + +def main() -> int: + rows = [json.loads(l) for l in FROZEN.read_text(encoding="utf-8").splitlines() if l.strip()] + if not rows: + print("EMPTY INPUT — метрика не считается", file=sys.stderr) + return 2 + + searcher = H.build_searcher(PROJ) + arm = "B" if searcher._dense_off else "A" + print(f"arm={arm} dense_off={searcher._dense_off} project={PROJ} rows={len(rows)}") + import asyncio + + per_sub = defaultdict(lambda: {"n": 0, "hit1": 0, "hit5": 0}) + cases = [] + errors = 0 + for r in rows: + t0 = time.perf_counter() + try: + res = asyncio.run(searcher.hybrid_search_async(r["query"], limit=10)) + items = res if isinstance(res, list) else res.get("results", []) + files = [str((it.get("metadata") or {}).get("file", "")).replace("\\", "/") + for it in items] + except Exception as e: # noqa: BLE001 + files = [] + errors += 1 + print(f" ERR {r['id']}: {type(e).__name__}: {e}") + gold = r["gold_file"].replace("\\", "/") + h1 = files[:1] == [gold] + h5 = gold in files[:5] + s = per_sub[r["subset"]] + s["n"] += 1 + s["hit1"] += int(h1) + s["hit5"] += int(h5) + cases.append({"id": r["id"], "subset": r["subset"], "gold": gold, + "ms": round((time.perf_counter() - t0) * 1000, 1), + "hit1": h1, "hit5": h5, "top1": files[0] if files else None}) + + n = len(rows) + empty = sum(1 for c in cases if not c["top1"]) + if empty == len(cases): + # §19.6: пустой вход/пустой индекс обязан падать громко, а не отдавать «0%». + print(f"ABORT: все {len(cases)} запросов вернули 0 результатов — " + f"индекс недоступен (корень проекта: {ROOT}). Метрика не считается.", + file=sys.stderr) + return 2 + summary = {"arm": arm, "dense_off": searcher._dense_off, "n": n, "errors": errors, + "empty_top1": empty, + "mean_ms": round(sum(c["ms"] for c in cases) / n, 1), + "subsets": {k: {"n": v["n"], "hit1": v["hit1"], "hit5": v["hit5"], + "hit1_pct": round(100 * v["hit1"] / v["n"], 1)} + for k, v in sorted(per_sub.items())}} + out = ROOT / "experiments/e29_symbol_ab/results" / f"e29_{arm}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"summary": summary, "cases": cases}, ensure_ascii=False, indent=1), + encoding="utf-8", newline="\n") + print(json.dumps(summary, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/experiments/e29_symbol_ab/frozen/HYPOTHESES.md b/experiments/e29_symbol_ab/frozen/HYPOTHESES.md new file mode 100644 index 00000000..e4711d1e --- /dev/null +++ b/experiments/e29_symbol_ab/frozen/HYPOTHESES.md @@ -0,0 +1,23 @@ +# EXP-29 — проверка H5 из EXP-28: dense-off на symbol vs NL подмножествах + +Дата заморозки: 2026-10-10 (ДО первого прогона). +Вход: `frozen/queries_labeled.jsonl`, 33 кейса = панель EXP-28 + колонка `subset`, +sha256 `7639ba1bda00212f8bceb04ae63ddaacddb897d21ae58f2aceff2430f88a53bf`. +Разметка детерминированная, правило заморожено в `e29_label.py` и записано в поле `subset`: +`symbolic := snake_case | CamelCase | pathy(:: / слэш / .метод()) | одиночный токен без пробелов`. +Распределение: **symbolic 15 / NL 18** (порог ≥8 на подмножество — пройден). + +EXP-28 оставил H5 ⏳ «не проверено»: без разметки нельзя было проверить, теряет ли +dense-off символьные запросы. Здесь разметка есть в замороженном входе — значит проверить можно. + +| # | Гипотеза | Фальсификатор | Ожидание | +|---|---|---|---| +| H1 | на **symbolic** dense-off не хуже: hit@1 падает не более чем на 1 кейс | падение ≥2 кейсов | **ожидаю ПРОВАЛ** (символьные находит лексика — dense там шум) | +| H2 | на **NL** dense-off хуже: hit@1 падает ≥3 п.п. (15/18 кейсов) | падение <3 п.п. | **ожидаю ПРОВАЛ** — по EXP-28 dense на NL ничего не давал | +| H3 | ускорение ≥15% воспроизводится во втором прогоне | <5% в обоих прогонах | **ожидаю ПРОВАЛ** (EXP-28 дал разброс 5.8%…29.5%) | +| H4 | 0 исключений в обоих плечах | ≥1 ERROR | ожидаю подтверждение | +| H5 | dense-off улучшает ≥1 NL-кейс (механизм EXP-28: dense уводил топ-1 в документ) | 0 улучшенных NL-кейсов | **ожидаю ПРОВАЛ** (в EXP-28 улучшились E2E-05/07 — надо проверить, symbol они или NL) | + +H1/H2/H3 внесены как ожидающе-провальные намеренно (§19.1). Если подтвердились бы все пять, +отчёт выглядел бы как 5/5 и ничего не значил бы. +Два прогона на плечо, одна сессия, один индекс; различается только `MSCODEBASE_DENSE_OFF`. \ No newline at end of file diff --git a/experiments/e29_symbol_ab/frozen/MANIFEST.sha256 b/experiments/e29_symbol_ab/frozen/MANIFEST.sha256 new file mode 100644 index 00000000..901225d4 --- /dev/null +++ b/experiments/e29_symbol_ab/frozen/MANIFEST.sha256 @@ -0,0 +1 @@ +7639ba1bda00212f8bceb04ae63ddaacddb897d21ae58f2aceff2430f88a53bf queries_labeled.jsonl diff --git a/experiments/e29_symbol_ab/frozen/queries_labeled.jsonl b/experiments/e29_symbol_ab/frozen/queries_labeled.jsonl new file mode 100644 index 00000000..abc36bbc --- /dev/null +++ b/experiments/e29_symbol_ab/frozen/queries_labeled.jsonl @@ -0,0 +1,33 @@ +{"also_ids": [], "gold_file": "src/core/rate_limiter.py", "id": "F5S-01", "query": "Почему в ограничителе скорости используется threading.Lock, а не asyncio.Lock, и с каким инцидентом это связано?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/redact.py", "id": "F5S-02", "query": "Насколько надёжно модуль redact вырезает секреты и по какому принципу он ловит credential-строки?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/gitignore_parser.py", "id": "F5S-03", "query": "Почему парсер .gitignore использует формат 'gitignore', а не 'gitwildmatch'?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/embedder_lease.py", "id": "F5S-04", "query": "Как решается проблема, что idle-watchdog убивает общий embedder во время полной переиндексации?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/reindex_ledger.py", "id": "F5S-05", "query": "Какое главное ограничение наложено на модуль durable reindex ledger, чтобы он не мог сломать индексацию?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/quiet_break_gate.py", "id": "F5S-06", "query": "Как quiet-break gate ведёт себя при инфраструктурном сбое git или графа?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "src/providers/reranker/reranker_scoring.py", "id": "F5S-07", "query": "Что делает validate_scores с неоценёнными чанками, у которых score равен NaN или Inf?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/error_envelope.py", "id": "F5S-08", "query": "Зачем в конверте ошибок MCP-инструментов есть поле next_action?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "docs/en/GRACEFUL_DEGRADATION.md", "id": "F5S-09", "query": "На сколько уровней деградации рассчитана система и что происходит, когда внешние провайдеры недоступны?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/en/SEARCH_PIPELINE.md", "id": "F5S-10", "query": "Почему в RRF-слиянии ранги считаются отдельно для каждого канала, а не общим enumerate?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/TRUST_BOUNDARY.md", "id": "F5S-11", "query": "Как система относится к инструкциям из проиндексированного чужого репозитория — как к командам или как к данным?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/en/TELEMETRY.md", "id": "F5S-12", "query": "Сохраняются ли метрики по инструментам между перезапусками MCP-сервера?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/adr/0003-verify-on-read.md", "id": "F5S-13", "query": "Какой вердикт получает ACTIVE-узел памяти, если резолвер недоступен и проверка не дала результата?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/en/HANDFOFF.md", "id": "F5S-14", "query": "Каким способом MCP-сервер определяет открытый в Zed проект на Windows, если переменные окружения не передаются?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": [], "gold_file": "docs/en/ZED_WINDOWS_QUIRKS.md", "id": "F5S-15", "query": "Что известно о переменной ZED_WORKTREE_ROOT на Windows?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "docs/en/SYSTEM_REQUIREMENTS.md", "id": "F5S-16", "query": "Насколько переход с ONNX на llama.cpp GGUF снизил потребление памяти MCP-процессом?", "source": "4A_unit_of_return/frozen/f5/queries.jsonl", "subset": "NL"} +{"also_ids": ["E2E-03"], "gold_file": "src/core/indexing/db_writer.py", "id": "TR-R1", "query": "begin_write _write_lock RLock reindex freeze", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": ["TR-P3"], "gold_file": "src/core/artifact_gc.py", "id": "TR-R2", "query": "ArtifactGC 30d projects 90d telemetry 7d logs retention", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/providers/reranker/llama_install.py", "id": "TR-R3", "query": "ubatch embed 512 rerank 1024 resolve_ubatch", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/providers/reranker/llama_runner.py", "id": "TR-R4", "query": "_InterProcessLock _is_pid_alive OpenProcess SYNCHRONIZE stale PID", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": ["TR-R7", "E2E-06"], "gold_file": "src/providers/embedder/remote_embedder.py", "id": "TR-R5", "query": "search_code timeout idle embedder _revive_llama_cpp hot-reload", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/graph.py", "id": "TR-R6", "query": "PropertyGraph add_node add_edge SQLite transaction per node edge", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/search/cypher_sql.py", "id": "TR-R8", "query": "collect Cypher json_group_array cypher_sql", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/bootstrap_tests.py", "id": "TR-R9", "query": "TESTS edges verifying tests decoys bootstrap_tests", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": ["E2E-01"], "gold_file": "src/core/indexing/freshness.py", "id": "TR-R10", "query": "FreshnessChecker to_pandas columns lancedb broken dead code", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/indexing/parser.py", "id": "TR-P1", "query": "CodeParser _parse_with_tree_sitter _walk_node AST chunks", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": ["E2E-04"], "gold_file": "src/core/search/engine.py", "id": "TR-P2", "query": "hybrid_search_async reciprocal_rank_fusion FTS5 BM25", "source": "token_reduction_v3_lancedb/frozen/rules.jsonl", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/indexing/db_manager.py", "id": "E2E-02", "query": "миграция схемы добавление колонок file_mtime_ns в таблицу LanceDB", "source": "scripts/e2e_quality_search.py::CASES", "subset": "symbolic"} +{"also_ids": [], "gold_file": "src/core/intelligence/verify_on_read.py", "id": "E2E-05", "query": "ленивая проверка факта памяти verify on read статус ADR", "source": "scripts/e2e_quality_search.py::CASES", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/indexing/project_indexer_registry.py", "id": "E2E-07", "query": "per project indexer registry multi window пулы по путям проектов", "source": "scripts/e2e_quality_search.py::CASES", "subset": "NL"} +{"also_ids": [], "gold_file": "src/mcp/tools/indexing_tools.py", "id": "E2E-08", "query": "переиндексация одного изменённого файла notify change rate limit", "source": "scripts/e2e_quality_search.py::CASES", "subset": "NL"} +{"also_ids": [], "gold_file": "src/providers/reranker/search_result_reranker.py", "id": "E2E-09", "query": "ранжирование результатов реранкером BGE M3 перестановка топ", "source": "scripts/e2e_quality_search.py::CASES", "subset": "NL"} +{"also_ids": [], "gold_file": "src/core/search/bm25.py", "id": "E2E-10", "query": "bm25 ключевые слова медленный но точный полнотекстовый", "source": "scripts/e2e_quality_search.py::CASES", "subset": "NL"} diff --git a/experiments/e29_symbol_ab/results/e29_A.json b/experiments/e29_symbol_ab/results/e29_A.json new file mode 100644 index 00000000..6c94b1c5 --- /dev/null +++ b/experiments/e29_symbol_ab/results/e29_A.json @@ -0,0 +1,323 @@ +{ + "summary": { + "arm": "A", + "dense_off": false, + "n": 33, + "errors": 0, + "empty_top1": 0, + "mean_ms": 2555.2, + "subsets": { + "NL": { + "n": 18, + "hit1": 3, + "hit5": 5, + "hit1_pct": 16.7 + }, + "symbolic": { + "n": 15, + "hit1": 8, + "hit5": 10, + "hit1_pct": 53.3 + } + } + }, + "cases": [ + { + "id": "F5S-01", + "subset": "NL", + "gold": "src/core/rate_limiter.py", + "ms": 8184.6, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-02", + "subset": "NL", + "gold": "src/core/redact.py", + "ms": 461.8, + "hit1": true, + "hit5": true, + "top1": "src/core/redact.py" + }, + { + "id": "F5S-03", + "subset": "NL", + "gold": "src/core/gitignore_parser.py", + "ms": 3380.5, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-04", + "subset": "NL", + "gold": "src/core/embedder_lease.py", + "ms": 445.3, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-05", + "subset": "NL", + "gold": "src/core/reindex_ledger.py", + "ms": 3847.9, + "hit1": false, + "hit5": true, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-06", + "subset": "NL", + "gold": "src/core/quiet_break_gate.py", + "ms": 405.7, + "hit1": true, + "hit5": true, + "top1": "src/core/quiet_break_gate.py" + }, + { + "id": "F5S-07", + "subset": "symbolic", + "gold": "src/providers/reranker/reranker_scoring.py", + "ms": 4054.9, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/reranker_scoring.py" + }, + { + "id": "F5S-08", + "subset": "symbolic", + "gold": "src/core/error_envelope.py", + "ms": 489.9, + "hit1": false, + "hit5": false, + "top1": "src/core/bootstrap_entities.py" + }, + { + "id": "F5S-09", + "subset": "NL", + "gold": "docs/en/GRACEFUL_DEGRADATION.md", + "ms": 3452.0, + "hit1": false, + "hit5": false, + "top1": "src/core/interfaces/__init__.py" + }, + { + "id": "F5S-10", + "subset": "NL", + "gold": "docs/en/SEARCH_PIPELINE.md", + "ms": 545.0, + "hit1": false, + "hit5": false, + "top1": "docs/research/lsp-archive/lsp_main.py" + }, + { + "id": "F5S-11", + "subset": "NL", + "gold": "docs/TRUST_BOUNDARY.md", + "ms": 3754.9, + "hit1": false, + "hit5": false, + "top1": "experiments/embeddinggemma/bench.py" + }, + { + "id": "F5S-12", + "subset": "NL", + "gold": "docs/en/TELEMETRY.md", + "ms": 537.8, + "hit1": false, + "hit5": false, + "top1": "src/core/search/engine.py" + }, + { + "id": "F5S-13", + "subset": "NL", + "gold": "docs/adr/0003-verify-on-read.md", + "ms": 10601.2, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-14", + "subset": "NL", + "gold": "docs/en/HANDFOFF.md", + "ms": 1972.7, + "hit1": false, + "hit5": false, + "top1": "adapters/zed/zed_config.py" + }, + { + "id": "F5S-15", + "subset": "symbolic", + "gold": "docs/en/ZED_WINDOWS_QUIRKS.md", + "ms": 3104.3, + "hit1": false, + "hit5": false, + "top1": "docs/ru/ZED_WINDOWS_QUIRKS.md" + }, + { + "id": "F5S-16", + "subset": "NL", + "gold": "docs/en/SYSTEM_REQUIREMENTS.md", + "ms": 364.2, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_smoke_answers.json" + }, + { + "id": "TR-R1", + "subset": "symbolic", + "gold": "src/core/indexing/db_writer.py", + "ms": 4951.6, + "hit1": false, + "hit5": false, + "top1": "experiments/misc_probes/exp_in_situ_parse_timeout.py" + }, + { + "id": "TR-R2", + "subset": "symbolic", + "gold": "src/core/artifact_gc.py", + "ms": 333.4, + "hit1": false, + "hit5": true, + "top1": "src/core/log_manager.py" + }, + { + "id": "TR-R3", + "subset": "symbolic", + "gold": "src/providers/reranker/llama_install.py", + "ms": 3905.1, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/llama_install.py" + }, + { + "id": "TR-R4", + "subset": "symbolic", + "gold": "src/providers/reranker/llama_runner.py", + "ms": 424.0, + "hit1": false, + "hit5": true, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "TR-R5", + "subset": "symbolic", + "gold": "src/providers/embedder/remote_embedder.py", + "ms": 3402.7, + "hit1": true, + "hit5": true, + "top1": "src/providers/embedder/remote_embedder.py" + }, + { + "id": "TR-R6", + "subset": "symbolic", + "gold": "src/core/graph.py", + "ms": 413.4, + "hit1": true, + "hit5": true, + "top1": "src/core/graph.py" + }, + { + "id": "TR-R8", + "subset": "symbolic", + "gold": "src/core/search/cypher_sql.py", + "ms": 3850.5, + "hit1": true, + "hit5": true, + "top1": "src/core/search/cypher_sql.py" + }, + { + "id": "TR-R9", + "subset": "symbolic", + "gold": "src/core/bootstrap_tests.py", + "ms": 336.3, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/a2_link_rate.py" + }, + { + "id": "TR-R10", + "subset": "symbolic", + "gold": "src/core/indexing/freshness.py", + "ms": 3486.5, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/freshness.py" + }, + { + "id": "TR-P1", + "subset": "symbolic", + "gold": "src/core/indexing/parser.py", + "ms": 448.5, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/parser.py" + }, + { + "id": "TR-P2", + "subset": "symbolic", + "gold": "src/core/search/engine.py", + "ms": 4545.9, + "hit1": true, + "hit5": true, + "top1": "src/core/search/engine.py" + }, + { + "id": "E2E-02", + "subset": "symbolic", + "gold": "src/core/indexing/db_manager.py", + "ms": 384.8, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_guard.py" + }, + { + "id": "E2E-05", + "subset": "NL", + "gold": "src/core/intelligence/verify_on_read.py", + "ms": 3308.4, + "hit1": false, + "hit5": false, + "top1": "docs/adr/0003-verify-on-read.md" + }, + { + "id": "E2E-07", + "subset": "NL", + "gold": "src/core/indexing/project_indexer_registry.py", + "ms": 281.1, + "hit1": false, + "hit5": true, + "top1": "src/core/multi_project_searcher.py" + }, + { + "id": "E2E-08", + "subset": "NL", + "gold": "src/mcp/tools/indexing_tools.py", + "ms": 4851.0, + "hit1": false, + "hit5": false, + "top1": "experiments/context_engine/results_tasks_v3.json" + }, + { + "id": "E2E-09", + "subset": "NL", + "gold": "src/providers/reranker/search_result_reranker.py", + "ms": 253.0, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/e17_smoke_answers.json" + }, + { + "id": "E2E-10", + "subset": "NL", + "gold": "src/core/search/bm25.py", + "ms": 3541.2, + "hit1": true, + "hit5": true, + "top1": "src/core/search/bm25.py" + } + ] +} \ No newline at end of file diff --git a/experiments/e29_symbol_ab/results/e29_B.json b/experiments/e29_symbol_ab/results/e29_B.json new file mode 100644 index 00000000..d154778c --- /dev/null +++ b/experiments/e29_symbol_ab/results/e29_B.json @@ -0,0 +1,323 @@ +{ + "summary": { + "arm": "B", + "dense_off": true, + "n": 33, + "errors": 0, + "empty_top1": 0, + "mean_ms": 2354.1, + "subsets": { + "NL": { + "n": 18, + "hit1": 5, + "hit5": 6, + "hit1_pct": 27.8 + }, + "symbolic": { + "n": 15, + "hit1": 7, + "hit5": 10, + "hit1_pct": 46.7 + } + } + }, + "cases": [ + { + "id": "F5S-01", + "subset": "NL", + "gold": "src/core/rate_limiter.py", + "ms": 8806.3, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-02", + "subset": "NL", + "gold": "src/core/redact.py", + "ms": 255.5, + "hit1": true, + "hit5": true, + "top1": "src/core/redact.py" + }, + { + "id": "F5S-03", + "subset": "NL", + "gold": "src/core/gitignore_parser.py", + "ms": 3306.3, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-04", + "subset": "NL", + "gold": "src/core/embedder_lease.py", + "ms": 264.3, + "hit1": false, + "hit5": false, + "top1": "experiments/4A_unit_of_return/frozen/f5/queries.md" + }, + { + "id": "F5S-05", + "subset": "NL", + "gold": "src/core/reindex_ledger.py", + "ms": 3677.6, + "hit1": false, + "hit5": true, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-06", + "subset": "NL", + "gold": "src/core/quiet_break_gate.py", + "ms": 248.7, + "hit1": true, + "hit5": true, + "top1": "src/core/quiet_break_gate.py" + }, + { + "id": "F5S-07", + "subset": "symbolic", + "gold": "src/providers/reranker/reranker_scoring.py", + "ms": 3398.9, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/reranker_scoring.py" + }, + { + "id": "F5S-08", + "subset": "symbolic", + "gold": "src/core/error_envelope.py", + "ms": 277.8, + "hit1": false, + "hit5": false, + "top1": "src/core/bootstrap_entities.py" + }, + { + "id": "F5S-09", + "subset": "NL", + "gold": "docs/en/GRACEFUL_DEGRADATION.md", + "ms": 3588.5, + "hit1": false, + "hit5": false, + "top1": "src/core/interfaces/__init__.py" + }, + { + "id": "F5S-10", + "subset": "NL", + "gold": "docs/en/SEARCH_PIPELINE.md", + "ms": 282.7, + "hit1": false, + "hit5": false, + "top1": "src/mcp/server_tools.py" + }, + { + "id": "F5S-11", + "subset": "NL", + "gold": "docs/TRUST_BOUNDARY.md", + "ms": 3547.9, + "hit1": false, + "hit5": false, + "top1": "src/core/search/agentic_search.py" + }, + { + "id": "F5S-12", + "subset": "NL", + "gold": "docs/en/TELEMETRY.md", + "ms": 235.2, + "hit1": false, + "hit5": false, + "top1": "src/main.py" + }, + { + "id": "F5S-13", + "subset": "NL", + "gold": "docs/adr/0003-verify-on-read.md", + "ms": 7187.9, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/layer.py" + }, + { + "id": "F5S-14", + "subset": "NL", + "gold": "docs/en/HANDFOFF.md", + "ms": 1033.8, + "hit1": false, + "hit5": false, + "top1": "src/core/platform_utils.py" + }, + { + "id": "F5S-15", + "subset": "symbolic", + "gold": "docs/en/ZED_WINDOWS_QUIRKS.md", + "ms": 7773.3, + "hit1": false, + "hit5": false, + "top1": "src/core/intelligence/verify_on_read.py" + }, + { + "id": "F5S-16", + "subset": "NL", + "gold": "docs/en/SYSTEM_REQUIREMENTS.md", + "ms": 368.8, + "hit1": false, + "hit5": false, + "top1": "src/core/embedder/onnx_client.py" + }, + { + "id": "TR-R1", + "subset": "symbolic", + "gold": "src/core/indexing/db_writer.py", + "ms": 2932.1, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_project_runner.py" + }, + { + "id": "TR-R2", + "subset": "symbolic", + "gold": "src/core/artifact_gc.py", + "ms": 263.1, + "hit1": false, + "hit5": true, + "top1": "src/core/log_manager.py" + }, + { + "id": "TR-R3", + "subset": "symbolic", + "gold": "src/providers/reranker/llama_install.py", + "ms": 2437.5, + "hit1": true, + "hit5": true, + "top1": "src/providers/reranker/llama_install.py" + }, + { + "id": "TR-R4", + "subset": "symbolic", + "gold": "src/providers/reranker/llama_runner.py", + "ms": 329.0, + "hit1": false, + "hit5": true, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "TR-R5", + "subset": "symbolic", + "gold": "src/providers/embedder/remote_embedder.py", + "ms": 3460.2, + "hit1": true, + "hit5": true, + "top1": "src/providers/embedder/remote_embedder.py" + }, + { + "id": "TR-R6", + "subset": "symbolic", + "gold": "src/core/graph.py", + "ms": 428.1, + "hit1": true, + "hit5": true, + "top1": "src/core/graph.py" + }, + { + "id": "TR-R8", + "subset": "symbolic", + "gold": "src/core/search/cypher_sql.py", + "ms": 4065.1, + "hit1": true, + "hit5": true, + "top1": "src/core/search/cypher_sql.py" + }, + { + "id": "TR-R9", + "subset": "symbolic", + "gold": "src/core/bootstrap_tests.py", + "ms": 282.1, + "hit1": false, + "hit5": false, + "top1": "experiments/bootstrap/a2_link_rate.py" + }, + { + "id": "TR-R10", + "subset": "symbolic", + "gold": "src/core/indexing/freshness.py", + "ms": 3123.2, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/freshness.py" + }, + { + "id": "TR-P1", + "subset": "symbolic", + "gold": "src/core/indexing/parser.py", + "ms": 344.1, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/parser.py" + }, + { + "id": "TR-P2", + "subset": "symbolic", + "gold": "src/core/search/engine.py", + "ms": 4830.0, + "hit1": false, + "hit5": true, + "top1": "docs/zh/SEARCH_PIPELINE.md" + }, + { + "id": "E2E-02", + "subset": "symbolic", + "gold": "src/core/indexing/db_manager.py", + "ms": 344.7, + "hit1": false, + "hit5": false, + "top1": "src/core/indexing/index_guard.py" + }, + { + "id": "E2E-05", + "subset": "NL", + "gold": "src/core/intelligence/verify_on_read.py", + "ms": 2728.2, + "hit1": true, + "hit5": true, + "top1": "src/core/intelligence/verify_on_read.py" + }, + { + "id": "E2E-07", + "subset": "NL", + "gold": "src/core/indexing/project_indexer_registry.py", + "ms": 220.1, + "hit1": true, + "hit5": true, + "top1": "src/core/indexing/project_indexer_registry.py" + }, + { + "id": "E2E-08", + "subset": "NL", + "gold": "src/mcp/tools/indexing_tools.py", + "ms": 4511.3, + "hit1": false, + "hit5": false, + "top1": "experiments/context_engine/results_v2.json" + }, + { + "id": "E2E-09", + "subset": "NL", + "gold": "src/providers/reranker/search_result_reranker.py", + "ms": 236.3, + "hit1": false, + "hit5": false, + "top1": "install.py" + }, + { + "id": "E2E-10", + "subset": "NL", + "gold": "src/core/search/bm25.py", + "ms": 2897.4, + "hit1": true, + "hit5": true, + "top1": "src/core/search/bm25.py" + } + ] +} \ No newline at end of file