# Заметки реализации (сессия «Бэкенд», 2026-07-04) > **⚠ Точка-во-времени 04.07 (ревизия D31).** §3-контракты Фазы 0 — живой референс бэкенда (request_hash охраняется golden-гардом D23); §5 «[НУЖНО РЕШЕНИЕ]» — все пункты развязаны D-логом (премиум-бюджет → D11/exp08; TM-политика → Р6/D-лог; канал B → D14.1/D19.1; стриминг → keep-alive Ф2); онлайн-факты §3.9 датированы 04.07 — цены/слаги сверять с models.yaml/quirks. > **⚠ СТАТУС-АПДЕЙТ 25.07 (оркестратор №7): код-география §3 перечёркнута паком-15 (D39.23).** Контракты §3 живы семантикой, но монолита `internal/pipeline` больше нет: движок разложен на `internal/{text,seed,chunk,miner,checks,membank}` + композит-корень `pipeline`; конфиг-слой §3.8 расширен `configs/pairs/<пара>` + `configs/langpacks/` + промпт-конвенцией `prompts/<пара>/<роль>.md` и строгим KnownFields-декодом (паки 15/16). Что из §6-техдолга закрыто — перечислено там же, с грепами; снапшот-дисциплина и golden-гард D23 живы в новой раскладке (golden байт-в-байт, парити EXACT — D39.23). Вердикт шага 0 — перевалидации решений v2 глазами реализатора перед Фазой 0 (донорский отчёт — `../research/10-vojo-ai-bot-review.md`). **Живое здесь — §3: контракты, которые Фаза 0 приняла на себя.** Разделы «общий вердикт исполнимости» и «упущения передавшей сессии» СНЯТЫ как прожитые: их предмет либо стал контрактом §3 (cache-write в `Usage` и формула стоимости — §3.5 · таймауты/ретраи параметрами клиента и чтение `Retry-After` вместо трёх захардкоженных · мини-раннер C1), либо ушёл в свои дома — прокси-ловушка (Go stdlib не проксирует loopback, опасны не-loopback локальные адреса вроде WSL-gateway 172.x; в коде `llm.NoProxyClient`) и мёртвый Grok 4.1 Fast живут в `docs/experiments/00-provider-quirks.md` и `backend/configs/models.yaml`, оценка переписывания `store.go` и счётчики донора — в `../research/10-vojo-ai-bot-review.md`, хроника — `../archive/PROGRESS-2026-07-04-10.md`. > ⚠ **§2 СНЯТ — а код на него ещё ссылается, поэтому вот куда он уехал** (заглушка, испр. 02.09): **п. 2 (таймауты/ретраи параметрами клиента, чтение `Retry-After` вместо трёх захардкоженных)** → параметры клиента в `backend/internal/llm/httpllm.go`, греп `03-implementation-notes §2 item 2`; **п. 5 (single-connection write pool + `BEGIN IMMEDIATE`)** → §3.6 «SQLite-рецепт» ниже, носитель кода — `backend/internal/store/store.go`, греп `03-implementation-notes §2 item 5`. Комментарии в коде НЕ трогались — их адресат теперь описан здесь. ## 3. Контракты Фазы 0 (обязательства реализации) ### 3.1 Детерминизм и request-hash (Р6) Рендер промпта — **чистая функция** от `(snapshot джобы, закоммиченные чекпоинты предыдущих чанков, индекс чанка)`. Запрещены timestamps/UUID в промпте; все выборки из памяти — с `ORDER BY`; все map сортируются по ключу перед рендером. Проверяется unit-тестом «два рендера байт-в-байт идентичны». `request_hash = sha256(book_id | chapter | chunk_index | stage | role | model+sampling | snapshot_id | sha256(rendered_messages))`. Противоречие буквы Р6 («ключ: request-hash» vs «волатильные части исключаются») разрешаю так: хэш считается от запроса, **отрендеренного из snapshot**, — волатильность заморожена снапшотом, исключать нечего. ### 3.2 Snapshot контекста джобы Материализованная запись с собственным `snapshot_id`: `brief_hash`, версии промпт-шаблонов per-role, model+sampling per-role, версия чанкера, зафиксированное состояние approved-глоссария (версия-счётчик) *(→ superseded D8: content-hash материализации, НЕ счётчик — drift-proof; реализовано, memory_f1_test)*, style sheet, резюме книги/арок на момент старта джобы. **STM в snapshot не входит** — он детерминированно пересобирается из чекпоинтов. Батч-подтверждение терминов (Фаза 1) применяется только на границе джоб. ### 3.3 Деньги: атомарность settle+checkpoint Дыра «settle прошёл — kill -9 — чекпоинт не записан» = двойная оплата вызова. → **Settle и запись сырого ответа — одна SQLite-транзакция в одном файле БД** (ledger и чекпоинты живут в проектной БД). Тогда неустранимая потеря — только in-flight вызов (провайдер списал, ответ не дошёл) — это и есть честные «≤1 вызов» приёмки. На старте процесса — recovery-проход: снять зависшие reserve джоб без чекпоинта. ### 3.4 Ключ TM (Фаза 1) Действующий ключ — ратифицированный оркестратором `(chunk_src_hash, lang_pair, model_id, prompt_version, brief_hash, context_snapshot_hash)`: **`model_id` и `prompt_version` ВХОДЯТ в ключ**, а не в метаданные (тюнинг промпта и смена модели легитимно инвалидируют TM; перечанкование — by design). Носитель — Р6, `01-decisions.md`, греп `Ключ TM подтверждён`. Нормализация src перед хэшем — только whitespace/Unicode NFC и **до** pre-replace терминов, иначе подтверждение термина инвалидирует TM через чёрный ход (прямой запрет Р6). В Фазе 0 это неважно: cross-book TM не реализован, есть только checkpoint/resume request-hash, который `model`+`prompt_version` уже включает через snapshot. ### 3.5 Нейтральный интерфейс LLM (расширения к типам vojo) - `Message.CacheBoundary bool` — конец стабильного блока префикса. Anthropic-адаптер вешает `cache_control` на соответствующий блок (максимум 4 брейкпоинта); OpenAI-совместимые адаптеры игнорируют (автокэш по префиксу). Единственная точка, где Р5 материализуется в коде, — раскладку задаёт сборщик контекста, не адаптеры. - `Usage{Prompt, Cached, CacheCreation, Completion, Reasoning}` — семантика reasoning per-adapter (xAI аддитивно, OpenAI-спека/ollama subset → 0, Anthropic thinking внутри output → 0) документируется в каждом адаптере, как в доноре. - `ReasoningEffort`: значение `"off"` — явное выключение thinking; маппится адаптером в провайдер-специфичный флаг. Для нестандартных ручек (GLM `thinking.type=disabled`, Qwen `enable_thinking=false`) — `extra_body` per-модель в models.yaml, мёрджится в JSON запроса OpenAI-совместимым адаптером (эмпирика полигона: thinking GLM-4.6 — таймауты ×3, Gemini молча жрёт max_tokens на thinking). - Стриминг в Фазе 0 НЕ реализуется, но интерфейс проектируется под него (второй метод в будущем не ломает адаптеры); длинные генерации закрываются длинными attempt-таймаутами из профиля; провайдерский стриминг отнесён к Ф2. - DeepSeek-адаптер парсит **оба** варианта cache-полей usage (`prompt_tokens_details.cached_tokens` и `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens`) — иначе эксперимент Фазы 0 по кэшу покажет ложный ноль. - Ответ транспорта читается через `LimitReader` (у донора кап есть только в gemini-native). - ⚠ **Failover-декоратор донора СНЯТ, а не портирован** (`backend/internal/llm/failover.go` нет): изоляцию канала B держит механизм лейблов — `content_labels` книги ⊆ `accepts_labels` модели, действия `route|allow|terminal`, fail-closed на пустых лейблах (D39.26 дизайн · D39.27 стройка · D39.28 дельта). ### 3.6 SQLite-рецепт (modernc.org/sqlite) - На файл БД два пула: write (`MaxOpenConns=1`, транзакции `BEGIN IMMEDIATE`) и read (N соединений). - Прагмы на соединение: `journal_mode(WAL)`, `busy_timeout(5000)`, `foreign_keys(1)`, `synchronous(NORMAL)` (для kill -9-приёмки достаточно; отключение питания — вне модели угроз MVP). - Замена advisory-локов: сериализация check-and-reserve через write-пул + `BEGIN IMMEDIATE`. - Регистр/леммы кириллицы нормализуются на стороне Go до записи (NOCASE в SQLite — ASCII-only); сравнения в SQL — только по нормализованным колонкам. - FTS5 в горячий путь v1 не тащить (unicode61 не сегментирует CJK; селективной инъекции хватает точных ключей/алиасов по обычным индексам). - Файлы БД — только на ext4 (WSL: не /mnt/c — WAL-локи ломаются на drvfs); задокументировать для установок. ### 3.7 Coverage-гейт v1 (Фаза 1, спека фиксируется сейчас) Портировать классификацию исходов из `eval/refusal_bench.py` (hard_refusal / soft_refusal / excision_suspect / ok) 1:1, не изобретать заново. Метрика — **символы без пробелов** (не токены!); пороги per-пара из эксперимента 02: нижние zh<2.2 / ja<1.4 / en<0.70 (вырезание), верхние >4.2 / >2.6 / >1.4 (аномалия), sent_cov<0.75; минимальная длина применения (калибровка на 500–2500 симв.). Сегментация — класс `[。!?.!?…]` со схлопыванием последовательностей терминаторов и поглощением закрывающих кавычек/скобок. Отдельные ветки по `finish_reason`: `content_filter` = refusal; `length` = обрезание бюджетом → ретрай с бОльшим max_tokens, НЕ эскалация (реальный кейс Gemini из эксп. 02). Blacklist refusal-паттернов — en/ru/zh/ja. ### 3.8 Конфиги (граница Р2 — закрытие 6 недоопределённостей) Три файла: `book.yaml` (translation brief + ссылки) → `configs/pipeline-c1.yaml` (ядро) → `configs/models.yaml` (модели/цены/таймауты, с датой проверки). В конфиг ядра входят: состав/порядок стадий, роль→модель, версии промптов, пороги гейтов, **режим инъекции глоссария (selective | full_prefix)** (Р5 требует обе схемы), токен-бюджеты сборки контекста (инъекция, STM, overlap), лимит регенераций до эскалации, **именованные эскалационные цепочки** (список моделей; channel-aware фильтр применяет раннер), fan-out N для C2, cache TTL per-стадия. В models.yaml: цены (+cache write/read), профиль таймаутов/ретраев per-модель, `extra_body`. Зашито в раннер: циклы по главам/чанкам, ветвление по гейтам, механика эскалации/ретраев, семантика «эскалация → re-gate → флаг» (диаграмма pipeline.puml перегейтовку не рисует — реализую re-gate с ограничением попыток, иначе эскалированный вывод коммитится непроверенным). ### 3.9 Онлайн-факты — СНЯТЫ, кроме механики (снимок был на 2026-07-04) ⚠ **Цены, слаги и даты проверки живут ТОЛЬКО в `backend/configs/models.yaml`** (поле `prices_checked`), провайдерские ловушки — в `docs/experiments/00-provider-quirks.md` (там же атрибуция находок 04.07: Grok-retired · cache-write Anthropic · per-attempt-60 с · localhost-прокси). Июльский срез цен снят как протухший — с 16.08 действует пере-пин D39.179 п.1 (множитель ×4.47 к июльским деньгам). Что из того среза — МЕХАНИКА, а не цена, и потому остаётся здесь (проверено 04.07 по официальным докам вендоров): **минимальный кэшируемый префикс зависит от модели** (короче лимита кэш молча НЕ создаётся, у Anthropic — 1024/4096 токенов по моделям) · **thinking биллится как output** полным объёмом, даже когда отображается summary · **TTL кэша истекает между вызовами одной стадии** (латентности чанков 63–278 с + гейты) ⇒ TTL — параметр стадии, а телеметрия обязана показывать `cache_read=0` при повторных вызовах, иначе тихая инвалидация невидима · **DeepSeek-кэш best-effort** — экономику считать только по фактическому `usage`. ## 5. [НУЖНО РЕШЕНИЕ] — СНЯТО, всё развязано Пять вопросов 04.07 закрыты, карта развязок — в баннере вверху (тела D11/D14/D19/D23 — в слайсе `../archive/architecture/05-decisions-D1-D38.md`); пятый, не попавший в карту, — пункты оркестратору (фазовые пометки диаграмм, владелец задачи cache-hit), отработан арх-ресетом D39. Формулировки вопросов — `../archive/PROGRESS-2026-07-04-10.md`. ## 6. Что осталось от Фазы 0 живым долгом Селфревью Фазы 0 (скелет `7954325` → волна 1 `e861541` → волна 2 `214e005`) и независимое внешнее ревью **приняты, блокеров ноль**; хроника волн и диспозиция находок F1–F8/D1–D3 — `../archive/PROGRESS-2026-07-04-10.md` (§«Фаза 0 принята»). Из тогдашнего техдолга ЗАКРЫТО: гонка `Runner.clients` (клиенты строятся eager-проходом ДО волн, дальше map read-only — `internal/pipeline/runner.go`, греп `the clients map is READ-ONLY in the waves`; D39.13) · F4 «finish=length с обрезанным черновиком» (флаг диспозиции `FlagLength` — `internal/pipeline/disposition.go`, греп `FlagLength FlagReason`) · F3-класс оплаченных ретраев (кап пере-биллинга: второй нечитаемый оплаченный 2xx ТЕРМИНАЛЕН, ре-биллы капятся одним — `internal/llm/httpllm.go`, греп `Cap billed-decode re-bills at ONE`) · D1/D2 (билингв-редактор ратифицирован D30.1 — требование «убрать `{{text}}`» ОТМЕНЕНО, не исполнять; per-chunk skip+flag построен диспозиция-классификатором) · D3 (изоляция канала B: `failover.go` удалён, механизм лейблов `content_labels`×`accepts_labels` — D39.26/27/28). **Живой остаток (носители — бэклог-строка 146 `docs/PROGRESS.md`, греп `Два техдолга движка без строки трекера`):** - **Дневной потолок — пер-книжный** (ledger в файле проекта одной книги): `day_usd` сбрасывается в UTC-полночь и ограничивает burn-rate одной книги, а не «оператора за день по всем проектам»; общий потолок потребует shared spend-файла (раскладка «файл БД на книгу» из Р3/Р6). Платформенная половина — `PD-157`. - **`prices_checked` (120 д) гейтит read-only `report` и $0-resume** (F6): платных вызовов они не делают, но падают на устаревших ценах. ⚠ Приёмка D39.134: этот же гейт работает ДЕТЕКТОРОМ самолечения деплоя — снимать его бездумно нельзя. - **`cache_ttl` ↔ `cache_write_per_m`**: цена записи в кэш в models.yaml задана под ОДИН TTL (боевой 5m). При `cache_ttl: 1h` реальная запись стоит ×2, а не ×1.25 — недоучёт; вторая цена/валидация нужны при осознанном переходе на 1h.