Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,69 @@
- *nit 4–6 исправлены в том же проходе:* `None`/отрицательные счётчики теперь
fail-open («неизвестно» ≠ «отсутствует»), мёртвая ветка убрана.
- P5 снят и записан до мержа: requests 10→12/25 при планке ≥10.
- **После мержей 2026-11-12: main собран, доки проверены, аудит чист**
- *Мержи (по указанному порядку):* #116 → `a5135d0`, #115 → `83229e1`,
#117 → `183a5f0`, #118 → `8149cb2`, #120 → `64341de`, #121 → `6f60c51`,
#122 → `2edb807` (стек Goal 2 перебазирован на main перед мержем, без
force-push). Конфликты были только в `AGENT_DIARY.md` (обе записи
сохранены) и один интеграционный G1×G2 в `tests/test_false_abstain.py`:
проверка отсутствия фразы заменена на `<present>=0` (именно это ложно).
- *Планка тестов:* `pytest tests/ -m "not integration and not benchmark"` →
**2607 passed / 9 skipped** (было 2560/10; +47). `ruff_gate` OK,
`smoke_e2e --project .` PASSED (4 проверки, шаг 4 = text — дефолт).
- *Аудит документации:* `stale_detector` давал ложный дрейф на
`onnxruntime==1.27.0` в ru/zh `SYSTEM_REQUIREMENTS` (пин зависимости
сравнивался с версией проекта 3.5.0). Исправлено в
`tools/stale_detector/stale_check.py`: версия после `=`/`@`/`~`/`>`/`<`
не считается версией проекта; guard —
`tests/test_stale_detector_versions.py` (2 теста, red-first).
`stale_detector` → «Docs appear up to date»; negative control пере-пинен
(`negative_controls_runner.py --pin`, причина в `pin_log.json`),
`ALL PROVEN (3)`.
- *Остальные гейты аудита:* `check_docs_vs_installer` 155 флагов / 0 проблем;
`check_docs_vs_config` 66 переменных / 0 проблем; `check_known_issues` OK
(≤300 строк); `check_layer_boundaries` и `architecture_linter` — инварианты
соблюдены.
- *GitHub About:* описание переписано под текущий продукт (local-first,
text-first, BM25/FTS5 + AST graph), добавлены топики `local-first`,
`text-first`, `onnx`.
- *Побочная находка:* прогон тестов создал в корне каталог `MagicMock/`
(1732 файла `mock.project_path/*/chunks.db`, артефакт подменённого пути).
Из-за него `test_real_project_scan_without_venv` упирался в кап 10001.
Каталог удалён; тест, который пишет по `MagicMock`-пути, не найден —
записано как хвост (в CI чистый клон, поэтому зелено).
- **G4-retry — dense-smoke на ONNX e5-small-int8, 384 (12→13.11.2026): DONE**
- *Провайдер в прогоне:* `onnx:multilingual-e5-small-int8`, выход 384-d
(закреплён `MSCODEBASE_EMBEDDER_PROVIDER=onnx`; :8080 и LM Studio в сборке
не участвовали). Сборка: **7307 чанков**, `fixed_size_list<float>[384]`,
**2210.8 s**, артефакт **73.3 MB** (temp data root).
- *Smoke на этом индексе:* `✅ SMOKE E2E: PASSED (4 проверки)`, шаг 4 =
`режим=vector, provider=onnx, dim=384, таблица=codebase_chunks, results=3`.
- *Корневая причина G4 (не Cast error):* в `_open_or_create_table` ширина
читалась из строки-сэмпла, а `_migrate_*` делали `table = table or
self.table`. Пустая LanceDB-таблица **falsy** → `AttributeError
'LanceDBManager' object has no attribute 'table'` → ловился как «таблица не
найдена» → проверка размерности не выполнялась, и 384-схема доживала до
записи 768-векторов.
- *Контракт размерности:* `src/core/indexing/dim_contract.py` — провайдер
объявляет ширину, схема строится от неё, перед записью/поиском
`require_dim_match(provider, provider_dim, schema_dim)`; несовпадение —
`EmbeddingDimMismatch` с именем провайдера и двумя числами. Пустая таблица
пересоздаётся (терять нечего), непустая — отказ (без молчаливого resize).
`db_writer` больше не паддит/обрезает вектор.
- *Ретраи:* `bounded_attempts` — 3 batch + 1 revive + 2 single, затем
`EmbedderUnavailable` с провайдером и последней ошибкой (BLOCKED), без
цикла idle-unload/restart. Покрыто red-first тестом.
- *Внешние эндпоинты:* при закреплённом провайдере сканер и probe :8080
отключены; `auto` сохраняет прежнее поведение. Тумблер —
`MSCODEBASE_EMBEDDER_PROVIDER` (документирован в CONFIGURATION en/ru/zh).
- *Проверка:* `pytest tests/ -m "not integration and not benchmark"` →
**2570 passed / 10 skipped** (было 2560; +10 новых), rc=0; `ruff_gate` OK;
smoke text-only PASSED. Найдено и закрыто 6 регрессий-мока (`provider_dim`
больше не считает `MagicMock` шириной 1).
- *Осталось:* сборка dense на CPU ~37 мин (не оптимизировалась); `auto`
по-прежнему может переключиться на внешний провайдер — пинится только
явным конфигом.
- **G4 — dense-smoke: BLOCKED, минимальный репро (12→13.11.2026):**
- *Диск:* D: 28,5 GB свободно, C: 4,6 GB. Оценка артефакта: 7 278 чанков ×
768 float32 ≈ 22 MB сырых векторов + LanceDB-оверхед — места хватает.
Expand Down
1 change: 1 addition & 0 deletions docs/en/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ path and LSP.
| `LM_STUDIO_HOST` / `LM_STUDIO_PORT` | `127.0.0.1` / `1234` | LM Studio provider |
| `OLLAMA_HOST` / `OLLAMA_PORT` | `127.0.0.1` / `11434` | Ollama provider |
| `EMBEDDING_MODEL` / `MODEL_NAME` / `EMBEDDING_DIMENSION` | `qwen3-embedding` / `multilingual-e5-small-int8` / `384` | Provider model selection |
| `MSCODEBASE_EMBEDDER_PROVIDER` | `auto` | Pin the embedder (`onnx`, `llama_cpp`, `lm_studio`, `ollama`); external endpoints are only used when named |
| `MSCODEBASE_ONNX_PROVIDER` | — | Force a specific ONNX execution provider |
| `MSCODEBASE_REMOTE_TOKEN` / `_REMOTE_RATE_LIMIT_RPS` | — | Remote provider auth and rate limit |
| `MSCODEBASE_LANGUAGE_PACK` | `false` | Enable tree-sitter-language-pack (downloads parsers on first use) |
Expand Down
1 change: 1 addition & 0 deletions docs/ru/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dense-пути и LSP.
| `LM_STUDIO_HOST` / `LM_STUDIO_PORT` | `127.0.0.1` / `1234` | Провайдер LM Studio |
| `OLLAMA_HOST` / `OLLAMA_PORT` | `127.0.0.1` / `11434` | Провайдер Ollama |
| `EMBEDDING_MODEL` / `MODEL_NAME` / `EMBEDDING_DIMENSION` | `qwen3-embedding` / `multilingual-e5-small-int8` / `384` | Выбор модели провайдера |
| `MSCODEBASE_EMBEDDER_PROVIDER` | `auto` | Закрепить эмбеддер (`onnx`, `llama_cpp`, `lm_studio`, `ollama`); внешние эндпоинты используются только по имени |
| `MSCODEBASE_ONNX_PROVIDER` | — | Принудительно задать конкретный execution provider ONNX |
| `MSCODEBASE_REMOTE_TOKEN` / `_REMOTE_RATE_LIMIT_RPS` | — | Аутентификация и лимит частоты для удалённого провайдера |
| `MSCODEBASE_LANGUAGE_PACK` | `false` | Включить tree-sitter-language-pack (скачивает парсеры при первом использовании) |
Expand Down
1 change: 1 addition & 0 deletions docs/zh/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
| `LM_STUDIO_HOST` / `LM_STUDIO_PORT` | `127.0.0.1` / `1234` | LM Studio provider |
| `OLLAMA_HOST` / `OLLAMA_PORT` | `127.0.0.1` / `11434` | Ollama provider |
| `EMBEDDING_MODEL` / `MODEL_NAME` / `EMBEDDING_DIMENSION` | `qwen3-embedding` / `multilingual-e5-small-int8` / `384` | provider 模型选择 |
| `MSCODEBASE_EMBEDDER_PROVIDER` | `auto` | 固定 embedder(`onnx`、`llama_cpp`、`lm_studio`、`ollama`);外部端点仅在显式指定时使用 |
| `MSCODEBASE_ONNX_PROVIDER` | — | 强制指定 ONNX 执行 provider |
| `MSCODEBASE_REMOTE_TOKEN` / `_REMOTE_RATE_LIMIT_RPS` | — | 远程 provider 认证与速率限制 |
| `MSCODEBASE_LANGUAGE_PACK` | `false` | 启用 tree-sitter-language-pack(首次使用时下载 parser) |
Expand Down
4 changes: 2 additions & 2 deletions scripts/negative_controls/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"output_contains": [
"STALE NEGATIVE CONTROL: PASSED"
],
"fixture_digest": "2ccfb294583a201017e9118181cf94e53e5684095944f9e5831d9c33e9752ef0"
"fixture_digest": "80a1ddead46c9e932cff812a126e81a0ef4a29f8c618b85608e07ab22e384fe9"
},
{
"id": "dead_guard_classifier",
Expand All @@ -57,5 +57,5 @@
"fixture_digest": "0913eb0455dad68770669b2aadb3e0759b3539f3ba5b35e0c79ce3bf755484d1"
}
],
"min_accepted_revision": "0846cbda34e2a5f90f0768c3b91ce894e2a26fa2"
"min_accepted_revision": "8294d907f1e631248cfbd04218f2aed991f0e6b8"
}
9 changes: 9 additions & 0 deletions scripts/negative_controls/pin_log.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,14 @@
"stale_detector": "2ccfb294583a201017e9118181cf94e53e5684095944f9e5831d9c33e9752ef0",
"dead_guard_classifier": "0913eb0455dad68770669b2aadb3e0759b3539f3ba5b35e0c79ce3bf755484d1"
}
},
{
"pinned_at_utc": "2026-11-12T19:27:15Z",
"reason": "stale_check.py: dependency pins (pkg==x.y.z) are not project versions - false drift on ru/zh SYSTEM_REQUIREMENTS; mutant still fails (rc=1, marker ok)",
"digests": {
"drift_gate": "a1110ab503285417fba8e5f76d2249f281ccb9a533a018ef827b851a58b3150f",
"stale_detector": "80a1ddead46c9e932cff812a126e81a0ef4a29f8c618b85608e07ab22e384fe9",
"dead_guard_classifier": "0913eb0455dad68770669b2aadb3e0759b3539f3ba5b35e0c79ce3bf755484d1"
}
}
]
121 changes: 93 additions & 28 deletions scripts/smoke_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"""

import argparse
import os
import sqlite3
import sys
import time
Expand Down Expand Up @@ -116,20 +117,53 @@ def check_services() -> dict:
return report


def _active_embedder() -> tuple[str, int, str]:
"""Which embedder the smoke run must ask, from explicit configuration.

The script has no `--dense` flag on purpose: `MSCODEBASE_DENSE_OFF` picks
the tier, and `MSCODEBASE_EMBEDDER_PROVIDER` names the provider so a run
can never accidentally validate a different backend's width.
"""
provider = os.getenv("MSCODEBASE_EMBEDDER_PROVIDER", "auto").strip().lower()
if provider == "onnx":
return "onnx", 384, "multilingual-e5-small-int8"
if provider in ("lm_studio", "ollama"):
return (
provider,
int(os.getenv("EMBEDDING_DIMENSION", "384")),
os.getenv("MODEL_NAME", "multilingual-e5-small-int8"),
)
# auto / llama_cpp — исторический путь smoke: llama-server на :8080.
return "llama_cpp", int(os.getenv("EMBEDDING_DIMENSION", "768")), "embeddinggemma-2"


def _embed_text(text: str) -> tuple[list[float], str, int]:
provider, expected_dim, model = _active_embedder()
if provider == "onnx":
from src.core.embedder.onnx_client import get_onnx_client

client = get_onnx_client(port=9876, model_name=model)
return client.embed(text), provider, expected_dim
url = (
"http://127.0.0.1:8080/v1/embeddings"
if provider == "llama_cpp"
else "http://127.0.0.1:1234/v1/embeddings"
)
r = httpx.post(url, json={"input": [text], "model": model}, timeout=30.0)
r.raise_for_status()
return r.json()["data"][0]["embedding"], provider, expected_dim


def check_embed() -> tuple[bool, str]:
"""Реальный embed через llama.cpp REST API. Проверяет размерность."""
"""Реальный embed через активный провайдер. Проверяет размерность."""
try:
r = httpx.post(
"http://127.0.0.1:8080/v1/embeddings",
json={"input": ["Проверка реального эмбеддинга smoke_e2e"]},
timeout=15.0,
)
r.raise_for_status()
data = r.json()
vec = data["data"][0]["embedding"]
vec, provider, expected = _embed_text("Проверка реального эмбеддинга smoke_e2e")
dim = len(vec)
ok = dim == 768
return ok, f"dim={dim} (embeddinggemma-2: 768) {'✅' if ok else '❌'}"
ok = dim == expected
return ok, (
f"provider={provider}, dim={dim} (expected {expected}) "
f"{'✅' if ok else '❌'}"
)
except Exception as e: # noqa: BLE001 — диагностика
return False, f"embed error: {e}"

Expand Down Expand Up @@ -207,13 +241,18 @@ def check_search(project: Path) -> tuple[bool, str]:
Открываем LanceDB напрямую (lancedb.connect) — чтение не требует нашего
PID-lock (он защищает запись; MCP другого окна держит lock — создание
Indexer через DI упало бы «PID lock held», что и случилось в первом
прогоне). Вектор запроса — реальный embed через llama.cpp (8080).
прогоне). Вектор запроса — real embed активного провайдера
(`MSCODEBASE_EMBEDDER_PROVIDER`), ширина сверяется со схемой таблицы.

Если векторного индекса нет (дефолт `MSCODEBASE_DENSE_OFF=true`), шаг
проверяет текстовый индекс SQLite: падать на отсутствии вектора значило бы
требовать от дефолтной конфигурации чужой режим.
"""
from src.core.artifact_paths import get_db_path
from src.core.indexing.dim_contract import (
EmbeddingDimMismatch,
schema_vector_dim,
)

vector_db = Path(str(get_db_path(project)))
if not vector_db.exists():
Expand All @@ -237,18 +276,33 @@ def check_search(project: Path) -> tuple[bool, str]:
ok, detail = _text_index_search(project)
return ok, f"в векторной БД нет таблиц; {detail}"
table = db.open_table(names[0])
# Реальный embed запроса через llama.cpp
r = httpx.post(
"http://127.0.0.1:8080/v1/embeddings",
json={"input": ["def __init__ class initialization"]},
timeout=15.0,
)
r.raise_for_status()
vec = r.json()["data"][0]["embedding"]
if table.count_rows() == 0:
# An empty vector table is not a dense index; the shipped default is
# text-only, so validate the text index instead of a width mismatch.
ok, detail = _text_index_search(project)
return ok, f"векторная таблица пуста; {detail}"
# Width contract first: a query must match the table's vector width.
# A mismatch is a named failure, not a silent text fallback (G4).
schema_dim = schema_vector_dim(table.schema)
vec, provider, _expected = _embed_text("def __init__ class initialization")
if schema_dim is None:
return False, "vector table has no fixed-size vector column"
if len(vec) != schema_dim:
raise EmbeddingDimMismatch(
f"provider '{provider}' returns {len(vec)}-d vectors, but "
f"the index is {schema_dim}-d; refusing to search a mixed index"
)
df = table.search(vec).limit(3).to_pandas()
n = len(df)
ok = n > 0
return ok, f"режим=vector, таблица={names[0]}, results={n} {'✅' if ok else '❌'}"
return ok, (
f"режим=vector, provider={provider}, dim={len(vec)}, "
f"таблица={names[0]}, results={n} {'✅' if ok else '❌'}"
)
except EmbeddingDimMismatch as e:
# A vector index exists and its width is the contract: do not hide the
# failure behind a text fallback (that was G4's silent path).
return False, f"dimension contract: {e}"
except Exception as e: # noqa: BLE001 — диагностика
ok, detail = _text_index_search(project)
if ok:
Expand All @@ -265,8 +319,19 @@ def main() -> int:

_setup_env()
revision = get_revision()
provider, expected_dim, _model = _active_embedder()
if provider == "onnx":
# A pinned local provider is part of the run, not a precondition: bring
# it up before health so an idle-unloaded server is not a false FAIL.
try:
from src.core.embedder.onnx_client import get_onnx_client

get_onnx_client(port=9876, model_name=_model).ensure_server_running()
except Exception as exc: # noqa: BLE001 — the embed step reports detail
print(f" ⚠️ ONNX start failed: {exc}")
print("🧪 Smoke E2E — реальные сервисы (без моков)")
print(f"revision: {revision}")
print(f"embedder provider: {provider} (expected dim {expected_dim})")
print("━" * 50)
failures = 0

Expand All @@ -275,18 +340,18 @@ def main() -> int:
svc = check_services()
for name, status in svc.items():
print(f" {status} {name}")
# Обязательные live-сервисы (embedder=llama.cpp 8080, reranker 8081) —
# только их отсутствие валит smoke. onnx (9876) — альтернативный embedder-бэкенд,
# не обязателен, когда активен llama.cpp (информационно).
required = [n for n in ("embedder", "reranker") if svc.get(n) == "🔴"]
# The required health entry is the ACTIVE provider, not a fixed llama port:
# an ONNX dense run must not fail because :8080 is intentionally absent.
active_health = "onnx" if provider == "onnx" else "embedder"
required = [n for n in (active_health, "reranker") if svc.get(n) == "🔴"]
if required:
print(f" ⚠️ Критичные сервисы не отвечают ({', '.join(required)}) — embed/rerank проверки могут упасть")
failures += 1
elif svc.get("onnx") == "🔴":
print(" ℹ️ onnx — альтернативный embedder не запущен (активен llama.cpp); необязательный — пропускаю")
elif provider != "onnx" and svc.get("onnx") == "🔴":
print(" ℹ️ onnx — альтернативный embedder не запущен; необязательный — пропускаю")

# 2. Embed
print("2. Реальный embed (llama.cpp):")
print(f"2. Реальный embed ({provider}, expected {expected_dim}-d):")
ok, msg = check_embed()
print(f" {'✅' if ok else '❌'} {msg}")
failures += 0 if ok else 1
Expand Down
Loading
Loading