Actualize backend README to the D39.6 head with every claim execution-verified: new CLI surfaces, v8 store, pair seam, trust-gate, export contract, honest tech-debt ledger
This commit is contained in:
parent
5d752e93e0
commit
fd4f93947c
1 changed files with 34 additions and 23 deletions
|
|
@ -1,49 +1,60 @@
|
||||||
# backend/ — Go-бэкенд TextMachine
|
# backend/ — Go-бэкенд TextMachine
|
||||||
|
|
||||||
Зона сессии «Бэкенд». Контракт — `docs/architecture/05-decisions-log.md` (D1–D23); контракты Фазы 0 — `03-implementation-notes.md`; квирки провайдеров — `docs/experiments/00-provider-quirks.md`. **`.env` не читать.**
|
Зона сессии «Бэкенд». Контракт — `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` / `report` / `status` (read-only проекция N/M+паспорта глав+деньги, `--json`; работает ПРИ живом прогоне — store без flock) / `redrive` (переатака флагнутых: `--chapter/--chunk/--reason/--dry-run`, D15.3). main — тонкая обвязка: разбор аргументов/exit-коды/.env/рендеры вынесены в тестируемые функции (пакет №4) | `main.go`, `invocation.go`, `render.go`, `dotenv.go`, `internal/pipeline/status.go` |
|
| `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-референс под будущий Gemini | `httpllm.go`, `capability.go`, `failover.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/ledger` | Цены по usage (вкл. reasoning/cache-поля), `PriceForResponse` по фактической модели | `pricing.go` |
|
||||||
| `internal/store` | SQLite (modernc, CGO-free), идемпотентные миграции v1–v7, reserve/settle+checkpoint, chunk_status, глоссарий, ruby, retrieval_state (вкл. стиль-флаги v7), request_log; `OpenReadOnly` — без flock/миграций/recovery для status/report | `ledger.go`, `migrate.go`, `glossary.go`, `store.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) | `models.go`, `pipeline.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` | Раннер, декомпозированный по связным единицам (пакет №4): сетап `runner.go` → снапшот `snapshot.go` → сид `seeding.go` → цикл книги `bookrun.go` → петля чанка `chunkrun.go` → стадия `stagerun.go` → эскалация с ре-гейтом `escalation.go` → resume `resume.go`; чанкер v4, ingest txt/epub+ruby, classify/coverage, **банк памяти v2** (Aho-Corasick, спойлер-окна, disposition, post-check), рендер+инъекция. Ф2-стройки добавляются файлом: канал B → `escalation.go`, annotator/voice-инъекция → `chunkrun.go` | `bookrun.go`, `stagerun.go`, `memory.go`, `disposition.go`, `coverage.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 | |
|
| `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 (реально потраченные деньги остаются) → после redrive `committed ≥ SUM(checkpoints)` — безопасное направление (потолок не занижает), задокументировано в `ResetChunkStages`.
|
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, context-assembly, версии classify/coverage/chunker/maxtok, эскалационные оверрайды), свёрнуто в `snapshotID`; правка = громкий `--resnapshot`. ⚠ Любой resnapshot = переоплата книги (D15) — content-addressed reuse спроектировать до онгоинга.
|
2. **Snapshot-дисциплина:** всё, что влияет на wire-байты ИЛИ вердикты (capability, память content-hash + `memoryMatchVersion`, context-assembly, версии classify/coverage/chunker/maxtok/**sanitizer (только при enabled)**, эскалационные оверрайды, **биндинг пара→промпт** через BriefHash+PromptSHA256), свёрнуто в `snapshotID`; правка = громкий `--resnapshot` = переоплата книги (D15).
|
||||||
3. **Эхо-мина DeepSeek:** thinking у deepseek НИКОГДА не отключать — `reasoning:"off"` там осознанный no-op; `echoes_when_thinking_off` + рекурсивный скан extra_body валят конфиг fail-fast. У grok наоборот: off = ЯВНЫЙ `reasoning_effort:"none"`.
|
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.
|
4. **Порядок classify:** refusal-blacklist / CJK-echo — ДО length/empty; retry-flagged только {length, empty} на той же модели; loop-чек ПЕРЕД удвоением max_tokens. Санитайзер — ТОЛЬКО на финальной стадии (isFinal) и только при ru-target.
|
||||||
5. **Эскалация:** ровно 1 хоп на другую модель, результат РЕ-гейтится, retry-бюджет не сбрасывается, editor pinned (escalate_to только на translator — принуждается валидацией), канал adult — только permissive-провайдеры (fail-closed).
|
5. **Эскалация:** ровно 1 хоп, результат РЕ-гейтится, retry-бюджет не сбрасывается, editor pinned, канал adult — только permissive (fail-closed).
|
||||||
6. **Детерминизм:** рендер — чистая функция snapshot; сортировки перед записью; никакого map-order в выводе; length-prefixed хэши. Сорс-чанкер lossless (fuzz-тесты).
|
6. **Детерминизм:** рендер — чистая функция snapshot; сортировки перед записью; никакого map-order в выводе; length-prefixed хэши; сорс-чанкер lossless (fuzz).
|
||||||
7. **Нейтральность адаптера:** `internal/llm` не знает про перевод (src/target/coverage) — контент-вердикты живут в раннере (disposition-слой, post-settle).
|
7. **Нейтральность адаптера:** `internal/llm` не знает про перевод — контент-вердикты в раннере (disposition-слой, post-settle).
|
||||||
|
8. **Экспорт-контракт (D39 слой 6, D39.5):** `exportNormalize` — эфемерная проекция (НЕ входит в request_hash/чекпоинты/деньги), применяется к КАЖДОМУ финальному тексту на всех выдающих поверхностях; **любая внешняя экстракция (полигон!) обязана читать `tmctl export`**, не сырые чекпоинты store — иначе видит ненормализованные байты и молча-неполную книгу (export делает manifest-join + drift-гард, сырое чтение — нет).
|
||||||
|
|
||||||
## Подготовка ja-книги (чек-лист сида)
|
## Подготовка ja-книги (чек-лист сида)
|
||||||
|
|
||||||
- **Kana-написания имён = matchable.** Ruby-чтения (`<ruby>鈴木<rt>すずき</rt></ruby>`) захватываются на инжесте и авто-подцепляются как alias соответствующей ручной записи (D16.4, `attachRubyAliasesToManual`), поэтому кана-форма имени, у которого есть кандзи+фуригана, матчится автоматически. **Сверх этого — руками:** имя, которое встречается ТОЛЬКО каной (без кандзи-написания с ruby), не даст авто-alias → внеси кана-написание в сид как `aliases:` вручную, иначе кана-упоминание молча не заматчится («тихо пусто»).
|
- **Kana-написания имён = matchable.** Ruby-чтения захватываются на инжесте и авто-подцепляются как alias ручной записи (D16.4, `attachRubyAliasesToManual`). Имя, встречающееся ТОЛЬКО каной, внеси в `aliases:` руками — иначе «тихо пусто».
|
||||||
- **Двойные чтения (強敵→とも) — под флагом.** Авто-alias ставится для формы «all-Han база + all-kana чтение»; семантическое двойное чтение неотличимо оффлайн (нужен yomi-словарь, B6). Такой alias фаерится AMBIGUOUS (короткий/коллизионный) и проходит post-check, но длинное двойное чтение может дать CONFIRMED-ложь — при подозрении убери его из сида. Полная дизамбигуация — Фаза 2 (B6/Палладий-Поливанов).
|
- **Двойные чтения (強敵→とも) — под флагом:** длинное двойное чтение может дать CONFIRMED-ложь — при подозрении убери из сида. Полная дизамбигуация — Ф2 (B6).
|
||||||
- **Kana-precision ≥4 не замерена** (minKeyLenPhonetic=3 — консервативный дефолт): кана-ключи НЕ проверяются на границу слова (в отличие от латиницы/кириллицы — D16.3), т.к. у каны нет сегментации (как у Han) — иначе ломается частый кейс «имя+частица» (すずきは). ≥4-кана-компаунд-precision — задача полигона (расширение E1) + токенизатор B6.
|
- **Kana-precision ≥4 не замерена** (minKeyLenPhonetic=3): кана-ключи не проверяются на границу слова (нет сегментации) — задача полигона + токенизатор B6.
|
||||||
|
- ⚠ **ja-книга требует ja-ru записи в `prompts:`-паке стадий** — pair-сеам fail-loud'ится без неё (пример: закомментированный ja-блок в `example/book.yaml`). Контент пакета = данные под реальную книгу (инвариант общности §0.1), не сочинять.
|
||||||
|
|
||||||
## Как гонять
|
## Как гонять
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build ./... && go vet ./... && go test ./... -race # всё зелёное = норма
|
go build ./... && go vet ./... && go test ./... -race # всё зелёное = норма
|
||||||
go run ./cmd/tmctl translate --config example/book.yaml # реальные вызовы — нужны ключи в .env
|
go run ./cmd/tmctl translate --config example/book.yaml # реальные вызовы — ключи в .env (пример: zh→ru)
|
||||||
go run ./cmd/tmctl report --config example/book.yaml # $0, читает store
|
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):
|
# live-conformance (реальные провайдеры, платно, вне CI):
|
||||||
set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/
|
set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Golden-гард детерминизма** (`internal/pipeline/golden_test.go` + `testdata/golden/`): на статичной фикстурной книге пинит бит-в-бит snapshotID, request_hash всех вызовов (вкл. эскалацию), wire-тела, вердикты и resume-байты. Красный golden = рефакторинг изменил wire/вердикты = `--resnapshot` = переоплата книги (D15). Обновлять ТОЛЬКО на осознанной, ратифицированной смене поведения: `TM_UPDATE_GOLDEN=1 go test ./internal/pipeline/ -run TestGolden`.
|
**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 — **маскированный структурный дифф** (хеши/версии → плейсхолдер) обязан быть пустым, если смена версий не меняла вердиктов.
|
||||||
|
|
||||||
Финал каждой вехи — агентское адверсариальное селфревью (несколько дименсий → независимая верификация каждой находки → фиксы mutation-verified: реверт фикса валит именно его тест). Внешнее ревью — оркестратор.
|
Финал каждой вехи — агентское адверсариальное селфревью (мандат CLAUDE.md 12.07); внешнее ревью — оркестратор.
|
||||||
|
|
||||||
## Известный техдолг (не трогать молча — см. D-лог)
|
## Известный техдолг (не трогать молча — см. D-лог / ledger `08-sync-audit-ledger.md`)
|
||||||
|
|
||||||
F3 at-most-once (после D15.2); Escal.Chains — валидируемый мёртвый конфиг (раннер читает только `escalate_to`, полные цепочки = шаг 7); D3-цепочка/канал B ЗАВЕДЕНЫ в `models.yaml` (пакет №3, слаги live-фактчекнуты 2026-07-10 — gemini ИМЕННО `-preview`); `escalation.budget_usd>0` требуется, чтобы single-hop `escalate_to` черновика стрелял (приёмка); per-model min-budget теперь СХЕМА (`capabilities.min_max_tokens`: deepseek 8000 / gemini 8000 / kimi 16000 — D24.3, флор по МОДЕЛИ вызова, у хопа своя; glm/grok без флора), не комментарий; реализация content-addressed resume (D15.2) — после обязательных v3.1-правок (D22.2: langs→verdictSnapshotID, развязка cache_ttl).
|
- **Слой 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 первый-отказ и пр.).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue