# backend/ — Go-бэкенд TextMachine Зона сессии «Бэкенд». Контракт — `docs/architecture/05-decisions-log.md` (D1–D21); контракты Фазы 0 — `03-implementation-notes.md`; квирки провайдеров — `docs/experiments/00-provider-quirks.md`. **`.env` не читать.** ## Карта пакетов | Пакет | Что делает | Ключевые файлы | |---|---|---| | `cmd/tmctl` | CLI: `translate` / `report` / `status` (read-only проекция N/M+паспорта глав+деньги, `--json`) / `redrive` (переатака флагнутых: `--chapter/--chunk/--reason/--dry-run`, D15.3) | `main.go`, `internal/pipeline/status.go` | | `internal/llm` | OpenAI-совместимый транспорт + retry/backoff, **capability-слой** (budget_field/temperature/reasoning per-модель), failover (написан, НЕ подключён — D4: только local-роли), нативный Anthropic = DEPRECATED-референс под будущий Gemini | `httpllm.go`, `capability.go`, `failover.go` | | `internal/ledger` | Цены по usage (вкл. reasoning/cache-поля), `PriceForResponse` по фактической модели | `pricing.go` | | `internal/store` | SQLite (modernc, CGO-free), идемпотентные миграции v1–v6, reserve/settle+checkpoint, chunk_status, глоссарий, ruby, retrieval_state, request_log | `ledger.go`, `migrate.go`, `glossary.go` | | `internal/config` | fail-fast загрузка models/pipeline/book; **эхо-мина-гейт** `echoMineViolation`; `CheckRunnable` блокирует неисполнимое (C2/fanout/judge) | `models.go`, `pipeline.go` | | `internal/pipeline` | Раннер (циклы, disposition, single-hop эскалация с ре-гейтом, resume), чанкер v4, ingest txt/epub+ruby, classify/coverage, **банк памяти v2** (Aho-Corasick, спойлер-окна, disposition, post-check), рендер+инъекция | `runner.go`, `memory.go`, `disposition.go`, `coverage.go` | | `internal/obs` | trace_id, структурные логи, safego | | ## Инварианты — ЛОМАТЬ НЕЛЬЗЯ (каждый закреплён тестами) 1. **Деньги:** reserve → call → **settle+checkpoint одной транзакцией**; `committed == SUM(checkpoints)` переживает kill -9 (`kill9_test.go`); цена — по фактически ответившей модели; потолки книга/день считают committed+reserved. **Одно исключение — явный `tmctl redrive`** (D15.3): удаляет чекпоинты сброшенных стадий, НЕ возвращая committed (реально потраченные деньги остаются) → после redrive `committed ≥ SUM(checkpoints)` — безопасное направление (потолок не занижает), задокументировано в `ResetChunkStages`. 2. **Snapshot-дисциплина:** всё, что влияет на wire-байты ИЛИ на вердикты (capability, память content-hash, context-assembly, версии classify/coverage/chunker/maxtok, эскалационные оверрайды), свёрнуто в `snapshotID`; правка = громкий `--resnapshot`. ⚠ Любой resnapshot = переоплата книги (D15) — content-addressed reuse спроектировать до онгоинга. 3. **Эхо-мина DeepSeek:** thinking у deepseek НИКОГДА не отключать — `reasoning:"off"` там осознанный no-op; `echoes_when_thinking_off` + рекурсивный скан extra_body валят конфиг fail-fast. У grok наоборот: off = ЯВНЫЙ `reasoning_effort:"none"`. 4. **Порядок classify:** refusal-blacklist / CJK-echo — ДО length/empty (усечённый отказ не превращается в платный ретрай); retry-flagged только {length, empty} на той же модели; loop-чек ПЕРЕД удвоением max_tokens. 5. **Эскалация:** ровно 1 хоп на другую модель, результат РЕ-гейтится, retry-бюджет не сбрасывается, editor pinned (escalate_to только на translator — принуждается валидацией), канал adult — только permissive-провайдеры (fail-closed). 6. **Детерминизм:** рендер — чистая функция snapshot; сортировки перед записью; никакого map-order в выводе; length-prefixed хэши. Сорс-чанкер lossless (fuzz-тесты). 7. **Нейтральность адаптера:** `internal/llm` не знает про перевод (src/target/coverage) — контент-вердикты живут в раннере (disposition-слой, post-settle). ## Подготовка ja-книги (чек-лист сида) - **Kana-написания имён = matchable.** Ruby-чтения (`鈴木すずき`) захватываются на инжесте и авто-подцепляются как alias соответствующей ручной записи (D16.4, `attachRubyAliasesToManual`), поэтому кана-форма имени, у которого есть кандзи+фуригана, матчится автоматически. **Сверх этого — руками:** имя, которое встречается ТОЛЬКО каной (без кандзи-написания с ruby), не даст авто-alias → внеси кана-написание в сид как `aliases:` вручную, иначе кана-упоминание молча не заматчится («тихо пусто»). - **Двойные чтения (強敵→とも) — под флагом.** Авто-alias ставится для формы «all-Han база + all-kana чтение»; семантическое двойное чтение неотличимо оффлайн (нужен yomi-словарь, B6). Такой alias фаерится AMBIGUOUS (короткий/коллизионный) и проходит post-check, но длинное двойное чтение может дать CONFIRMED-ложь — при подозрении убери его из сида. Полная дизамбигуация — Фаза 2 (B6/Палладий-Поливанов). - **Kana-precision ≥4 не замерена** (minKeyLenPhonetic=3 — консервативный дефолт): кана-ключи НЕ проверяются на границу слова (в отличие от латиницы/кириллицы — D16.3), т.к. у каны нет сегментации (как у Han) — иначе ломается частый кейс «имя+частица» (すずきは). ≥4-кана-компаунд-precision — задача полигона (расширение E1) + токенизатор B6. ## Как гонять ```bash go build ./... && go vet ./... && go test ./... -race # всё зелёное = норма go run ./cmd/tmctl translate --config example/book.yaml # реальные вызовы — нужны ключи в .env go run ./cmd/tmctl report --config example/book.yaml # $0, читает store # live-conformance (реальные провайдеры, платно, вне CI): set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/ ``` Финал каждой вехи — агентское адверсариальное селфревью (несколько дименсий → независимая верификация каждой находки → фиксы mutation-verified: реверт фикса валит именно его тест). Внешнее ревью — оркестратор. ## Известный техдолг (не трогать молча — см. D-лог) F3 at-most-once (после D15.2); Escal.Chains — валидируемый мёртвый конфиг (раннер читает только `escalate_to`, полные цепочки = шаг 7); модели D3-эскалации не заведены в `models.yaml` (интерим-заглушки помечены — пакет №3); min-budget Kimi≥16k/Gemini≥8k — комментарии, не схема; fix-лист №3 (D20.4) + v3-правки спеки D15.2 (D20.1) — промт пакета №3.