# backend/ — Go-бэкенд TextMachine

Зона сессии «Бэкенд». Контракт — `docs/architecture/05-decisions-log.md` (D1–D39.6); целевая архитектура арх-ресета (7 слоёв, инвариант общности §0.1 — любая книга/пара/структура) — `docs/architecture/09-target-architecture.md`; контракты Фазы 0 — `03-implementation-notes.md`; квирки провайдеров — `docs/experiments/00-provider-quirks.md`. **`.env` не читать.**

*(Актуализировано оркестратором 17.07 по мандату владельца, факты сверены по коду; предыдущая ревизия была эры D23.)*

## Карта пакетов

| Пакет | Что делает | Ключевые файлы |
|---|---|---|
| `cmd/tmctl` | CLI: `translate` / `status` (read-only N/M+паспорта глав+деньги, `--json`; работает ПРИ живом прогоне — store без flock) / `report` (request-log+леджер книги **+ per-run quality-report**: структурный KPI предл./нарратив-абзац, тире, cosmetic-strip/echo-rates, глоссарий-промахи, trust-gated — наблюдаемость, НЕ гейт; D39.2-T4) / `export` (**экспорт-поверхность для полигона**: final_hash→checkpoint→exportNormalize, manifest-join c pending/ghost-учётом, ConfigDrift-поле, `--plaintext`; D39.5) / `redrive` (переатака флагнутых: `--chapter/--chunk/--reason/--dry-run`, D15.3). main — тонкая обвязка: разбор/exit-коды/.env/рендеры в тестируемых функциях | `main.go`, `invocation.go`, `render.go`, `dotenv.go` |
| `internal/llm` | OpenAI-совместимый транспорт + retry/backoff, **capability-слой** (budget_field/temperature/reasoning per-модель), failover (написан, НЕ подключён — D4: только local-роли), нативный Anthropic = DEPRECATED-референс | `httpllm.go`, `capability.go`, `failover.go` |
| `internal/ledger` | Цены по usage (вкл. reasoning/cache-поля), `PriceForResponse` по фактической модели | `pricing.go` |
| `internal/store` | SQLite (modernc, CGO-free), идемпотентные миграции **v1–v8** (v8 = trust-gate поля retrieval_state, D39.2-T1), reserve/settle+checkpoint, chunk_status, глоссарий, ruby, retrieval_state (стиль-флаги v7 + `n_trust_gated_suppress`/detail v8), request_log; `OpenReadOnly` — без flock/миграций/recovery для status/report/export | `ledger.go`, `migrate.go`, `glossary.go`, `store.go` |
| `internal/config` | fail-fast загрузка models/pipeline/book; **эхо-мина-гейт** `echoMineViolation`; `CheckRunnable` блокирует неисполнимое (C2/fanout/judge); **pair-keyed промпт-слой** (D39 слой 2): `Stage.Prompts{«zh-ru»: path}` + `PromptPathFor` — **fail-loud на отсутствующую пару** (ja-книга больше не едет молча через zh-промпт); legacy одиночный `prompt:` жив (XOR-валидация) | `models.go`, `pipeline.go` |
| `internal/pipeline` | Раннер по связным единицам: сетап `runner.go` → снапшот `snapshot.go` → сид `seeding.go` → цикл книги `bookrun.go` → петля чанка `chunkrun.go` → стадия `stagerun.go` → эскалация с ре-гейтом `escalation.go` → resume `resume.go`; чанкер v4 (⚠ бюджет = const 1500, логическая граница НЕ построена — слой 1 гейтится exp15); classify/coverage; **банк памяти v2, memmatch-v4**: Aho-Corasick, спойлер-окна, disposition, **trust-gated longest-match** (draft-длиннее НЕ съедает вложенный approved; отказ = громкая телеметрия — терм-дрейф закрыт в КОДЕ, D39.2-T1), post-check; **санитайзер v6**: fold-first детекция всех 6 классов (обход полноширинными сигнатурами закрыт, D39.5-F1) + CJK-глосс-whitelist «терм (漢字)» (bounded, D39.5-F2) + `exportNormalize` (**export-contract слой 6**: каждый FinalText нормализуется; санитайзер = чистый детектор); **реестр `roleInjectionRenderers`** (новая роль = данные+рендерер, не правка switch); readability-гейты за `isRuTarget`; `export.go`/`quality.go` — read-only проекции | `bookrun.go`, `stagerun.go`, `memory.go`, `sanitizer.go`, `export.go`, `quality.go`, `disposition.go` |
| `internal/obs` | trace_id, структурные логи (contextHandler несёт book/chapter/chunk/stage/role из ctx), safego | |

## Инварианты — ЛОМАТЬ НЕЛЬЗЯ (каждый закреплён тестами)

1. **Деньги:** reserve → call → **settle+checkpoint одной транзакцией**; `committed == SUM(checkpoints)` переживает kill -9 (`kill9_test.go`); цена — по фактически ответившей модели; потолки книга/день считают committed+reserved. **Одно исключение — явный `tmctl redrive`** (D15.3): после него `committed ≥ SUM(checkpoints)` — безопасное направление.
2. **Snapshot-дисциплина:** всё, что влияет на wire-байты ИЛИ вердикты (capability, память content-hash + `memoryMatchVersion`, context-assembly, версии classify/coverage/chunker/maxtok/**sanitizer (только при enabled)**, эскалационные оверрайды, **биндинг пара→промпт** через BriefHash+PromptSHA256), свёрнуто в `snapshotID`; правка = громкий `--resnapshot` = переоплата книги (D15).
3. **Эхо-мина DeepSeek:** thinking НИКОГДА не отключать — `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. Санитайзер — ТОЛЬКО на финальной стадии (isFinal) и только при ru-target.
5. **Эскалация:** ровно 1 хоп, результат РЕ-гейтится, retry-бюджет не сбрасывается, editor pinned, канал adult — только permissive (fail-closed).
6. **Детерминизм:** рендер — чистая функция snapshot; сортировки перед записью; никакого map-order в выводе; length-prefixed хэши; сорс-чанкер lossless (fuzz).
7. **Нейтральность адаптера:** `internal/llm` не знает про перевод — контент-вердикты в раннере (disposition-слой, post-settle).
8. **Экспорт-контракт (D39 слой 6, D39.5):** `exportNormalize` — эфемерная проекция (НЕ входит в request_hash/чекпоинты/деньги), применяется к КАЖДОМУ финальному тексту на всех выдающих поверхностях; **любая внешняя экстракция (полигон!) обязана читать `tmctl export`**, не сырые чекпоинты store — иначе видит ненормализованные байты и молча-неполную книгу (export делает manifest-join + drift-гард, сырое чтение — нет).

## Подготовка ja-книги (чек-лист сида)

- **Kana-написания имён = matchable.** Ruby-чтения захватываются на инжесте и авто-подцепляются как alias ручной записи (D16.4, `attachRubyAliasesToManual`). Имя, встречающееся ТОЛЬКО каной, внеси в `aliases:` руками — иначе «тихо пусто».
- **Двойные чтения (強敵→とも) — под флагом:** длинное двойное чтение может дать CONFIRMED-ложь — при подозрении убери из сида. Полная дизамбигуация — Ф2 (B6).
- **Kana-precision ≥4 не замерена** (minKeyLenPhonetic=3): кана-ключи не проверяются на границу слова (нет сегментации) — задача полигона + токенизатор B6.
- ⚠ **ja-книга требует ja-ru записи в `prompts:`-паке стадий** — pair-сеам fail-loud'ится без неё (пример: закомментированный ja-блок в `example/book.yaml`). Контент пакета = данные под реальную книгу (инвариант общности §0.1), не сочинять.

## Как гонять

```bash
go build ./... && go vet ./... && go test ./... -race        # всё зелёное = норма
go run ./cmd/tmctl translate --config example/book.yaml      # реальные вызовы — ключи в .env (пример: zh→ru)
go run ./cmd/tmctl status --config example/book.yaml --json  # $0, N/M+паспорта+деньги, живой прогон ок
go run ./cmd/tmctl report --config example/book.yaml         # $0, quality-report (KPI/rates)
go run ./cmd/tmctl export --config example/book.yaml         # $0, экспорт-JSON для полигона (--plaintext для человека)
# live-conformance (реальные провайдеры, платно, вне CI):
set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/
```

**Golden-гард детерминизма** (`golden_test.go` + `testdata/golden/`): пинит бит-в-бит snapshotID, request_hash, wire-тела, вердикты и resume-байты. Красный golden = wire/вердикты изменились = `--resnapshot` = переоплата. Обновлять ТОЛЬКО на ратифицированной смене поведения: `TM_UPDATE_GOLDEN=1 go test ./internal/pipeline/ -run TestGolden`; при re-capture — **маскированный структурный дифф** (хеши/версии → плейсхолдер) обязан быть пустым, если смена версий не меняла вердиктов.

Финал каждой вехи — агентское адверсариальное селфревью (мандат CLAUDE.md 12.07); внешнее ревью — оркестратор.

## Известный техдолг (не трогать молча — см. D-лог / ledger `08-sync-audit-ledger.md`)

- **Слой 1 (нарезка) НЕ построен:** `targetChunkTokens=1500` — const в единице оценки исходника; логическая граница/декаплинг edit-единицы/когезия — гейтятся exp15 (research/19 §D). **`STMDepth`/`OverlapTokens` — мёртвые заглушки** (в конфиге+снапшоте, исполнение их НЕ читает) — оживают со слоем 1, не удалять и не «подключать» мимоходом.
- **D15.2:** READ-половина `tmctl export` ПОСТРОЕНА (D39.5, минимальная форма); annotation/override-половина и content-addressed resume — отложены (v3.1-спека = дизайн-оф-рекорд, `docs/D15.2-…spec.md`).
- **Retries.RegenerateBeforeEscalate НЕ в снапшоте** (pre-existing, D39.4-LOW): в крэш-окне пониженный ретрай-бюджет молча бросает оплаченные OK-чекпоинты. Дизайн-фикс при следующем заходе в stagerun.
- F3 at-most-once (после D15.2); Escal.Chains — валидируемый мёртвый конфиг (раннер читает только `escalate_to`); флоры min_max_tokens = схема (D24.3).
- `prompts/editor-mono.md` — референс-вариант D13.1 (не боевой, тестом закреплён); боевой = `editor.md` v3-discourse (БИЛИНГВ). Глосс-капы санитайзера (6 CJK / 24 лат-токен) — tunable-эвристики (D39.5), ревизия по чтению пере-прогона.
- Хвост LOW/NOTE-находок адверсариала D39.4 — в ledger-очереди (golden-ячейка глосса, escalated+stripped тест, trustGateEvent первый-отказ и пр.).
