| .. | ||
| cmd | ||
| configs | ||
| docs | ||
| example | ||
| internal | ||
| prompts/zh-ru | ||
| .env.example | ||
| .gitignore | ||
| .golangci.yml | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
backend/ — Go-бэкенд TextMachine
Зона сессии «Бэкенд». Контракт — docs/architecture/05-decisions-log.md (свежая голова — с хвоста файла); целевая архитектура (7 слоёв, инвариант общности §0.1 — любая книга/пара/структура; статус стройки — шапка-таблица) — docs/architecture/09-target-architecture.md; норматив общности — 12-go-style-notes.md; контракты Фазы 0 — 03-implementation-notes.md (через баннер). Язык-данные майнера — вне пайплайна: internal/lang+configs/langpacks/ (D39.16); пар-калибровки — configs/pairs/<пара>.yaml, промпты — prompts/<пара>/<роль>.md по конвенции (D39.23). Подсистемы (chunk/miner/checks/membank) не импортируют друг друга и не импортируют pipeline — драйвер является композит-корнем (D39.23; проверяется go list -deps). .env не читать.
(Актуализировано оркестратором 01.08 по мандату владельца, факты сверены по коду.)
Карта пакетов
| Пакет | Что делает | Ключевые файлы |
|---|---|---|
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→checks.ExportNormalize, manifest-join c pending/ghost-учётом, ConfigDrift-поле, --plaintext; D39.5) / build (писатель книги: EPUB 3 + чистый txt из той же экспорт-проекции в <project_db>.book.<fmt> рядом с БД, пути — в artifacts.book_files; книга с ЛЮБОЙ дырой — pending/withheld/incomplete/ghost — по умолчанию отказ exit 16 с перечнем, --partial пишет с пометкой на первой странице и маркером на каждой дыре; слова читателя — данные configs/langpacks/<цель>/reader.txt, вне снапшота; --format epub,txt, --out) / redrive (переатака флагнутых: --chapter/--chunk/--reason/--dry-run, D15.3) / manifest ($0-производитель персиста манифеста глав/чанков: нужен ДО первого прогона — дерево глав разобранной, но не запущенной книги) / migrate ($0-write-open без прогона (D39.134): read-only команды схему НЕ мигрируют. Грузит ТОЛЬКО book.yaml — ни цен, ни ключей на $0-пути (строка 146); restore point берёт ПОД ЛОКОМ и только когда шаг реально применяется (имя <метка>-pre-migrate.db — вне секундного неймспейса платного пути, строка 173); несовпадение схемы у read-only путей — типизированный отказ exit 13 с машинным токеном schema_mismatch found=N expected=M) / bank-apply (входная дверь шва, D39.156/D39.158: приём документа решений владельца по банку, $0; отчёт v2 на stdout, отказ по существу — exit 14, незавершённая запись — exit 15) / backup (VACUUM INTO + integrity_check, он же pre-flight боевого прогона) / seed-lint ($0-проверка сида и дельты владельца). ДВА потолка прогона, и они ортогональны. --ceiling-usd — ДЕНЬГИ: перекрывает книжный ceilings.book_usd ТОЛЬКО на этот прогон, в book.yaml не пишется, ноль/отрицательное = отказ запуска; каппит КУМУЛЯТИВНУЮ трату книги, а не приращение прогона. --max-units (D39.165 §1б; механика и денежный отчёт — D39.170) — ОБЪЁМ: не больше N ВЫХОДНЫХ ЮНИТОВ (гранулярность units_total манифеста, та самая, в которой платформа продаёт главы) возьмут слот в этом прогоне — новые юниты, которые он начинает оплачивать, и уже отгруженные, которые он пере-делывает под сдвинутым снапшотом. $0-юниты (резюм, ре-пин) потолок не тратят; юнит, который прежний прогон НАЧАЛ и не отгрузил (обрыв между волнами или внутри волны), дописывается ВНЕ гранта, чтобы один юнит не брал слот дважды (строка 232) — но не больше N таких за прогон, в порядке книги, а остальные ждут как любой недоставленный юнит. Принимает только translate. Остановка по объёму — ЗАВЕРШЕНИЕ (exit 0), а не пауза: различение живёт в отчёте прогона и в логе. ⚠ На книге, которая МАЙНИТ банк, вторая покупка требует --resnapshot (авто-банк растёт между покупками и двигает edit-снапшот) — прогон предупреждает об этом в логе. main — тонкая обвязка: разбор/exit-коды/.env/рендеры в тестируемых функциях (полный перечень глаголов — dispatchCommands в cmd/tmctl/main.go) |
main.go, invocation.go, render.go, dotenv.go, bankapply.go, backup.go |
internal/llm |
OpenAI-совместимый транспорт + retry/backoff, capability-слой (budget_field/temperature/reasoning per-модель), провайдеры openai/local (no-proxy)/anthropic (DEPRECATED-референс); маршрутизация лейблами живёт в config/pipeline (канал B, один хоп chain[0], fail-closed; D4/D39.26) |
httpllm.go, capability.go, provider_*.go |
internal/ledger |
Цены по usage (вкл. reasoning/cache-поля), PriceForResponse по фактической модели |
pricing.go |
internal/store |
SQLite (modernc, CGO-free), цепочка миграций schema_version, reserve/settle+checkpoint, chunk_status, глоссарий (+подписной цикл терминолога), ruby, retrieval_state, request_log; OpenReadOnly — без flock/миграций/recovery для status/report/export (схема не совпала — типизированный *store.SchemaMismatchError{Found,Expected}, обе стороны); Migrate (+шов beforeApply под локом), SchemaHead — та самая Expected; write-open БД новее бинаря — ОТКАЗ (D39.134) |
ledger.go, migrate.go, glossary.go, store.go |
internal/config |
fail-fast загрузка models/pipeline/book; эхо-мина-гейт echoMineViolation; CheckRunnable блокирует неисполнимое (C2/fanout/judge); пар-слой (D39.23): configs/pairs/<пара>.yaml — калибровка нарезки + коридор длины пары; промпт-резолв КОНВЕНЦИЕЙ prompts/<пара>/<роль>.md из Book.LangPair()+роли, fail-loud на пару без пакета (ja-книга не едет молча через zh-промпт), осознанное исключение — prompt_override на стадии |
models.go, pipeline.go, pair.go |
internal/text |
Нормализация и рун-примитивы, общие для банка/майнера/чекеров (script-agnostic слой, lang НЕ импортирует): NormalizeSourceKey/NormalizeTargetForm (версионированный артефакт NormVersion(), фолдится в снапшот), NormalizeSource, TokenizeScript (алфавит цели из данных, Mn-фолд U+0301; TokenizeCyrillic = ru-дефолт-обёртка), DenseScript — единый дом sizing-таксономии (D39.64) |
norm.go, runes.go, source.go |
internal/seed |
Схема сид-YAML книги (File/Term/Decl/Alias) — общий артефакт: майнер её ПИШЕТ (mined-дельта), банк ЧИТАЕТ (LoadGlossarySeed); тип общий, поэтому эмиссия не может разъехаться с загрузчиком |
seed.go |
internal/lang |
Языковые ДАННЫЕ и пак-механизм: эмбед-плоскость data/*.txt (хешируется в EmbeddedVersion() → снапшот, D39.64) · langpack Load/книго-overlay (аллоулист файлов, fail-loud) · манифест-по-каналам (manifest.txt источника объявляет source-morphology/transliteration; манифест-less пак = все каналы, байт-идентичен) · единый реестр письменностей script.go (LangScripts/IsCJKScriptLang из lang-script.txt) · реестр целей из data/target-<tgt>.txt |
langpack.go, embedded.go, script.go |
internal/terminology |
Роль терминолога (пак-20): смердженный банк + KWIC-контексты → батчи консолидации dst той же дешёвой моделью; языковой экран target_script; durable-деньги подписного цикла |
terminology.go, script.go |
internal/chunk |
Ингест (txt/epub, кодировки, ruby) + чанкер: Ingest/IngestEncoded → Document, SplitChunks → []Chunk по SegBudget; структура источника (главы, CJK-числительные, heading-правило) — ДАННЫЕ из lang |
ingest.go, chunker.go, chunktest/ |
internal/membank |
Банк памяти v2, memmatch-v4: Aho-Corasick, спойлер-окна, disposition, trust-gated longest-match (draft-длиннее НЕ съедает вложенный approved; отказ = громкая телеметрия — терм-дрейф закрыт в КОДЕ, D39.2-T1), post-check, рендереры инъекции, загрузка/валидация сида (SeedLint) |
memory.go, memseed.go, mempostcheck.go |
internal/miner |
Детерминированный оффлайн-майнер банка (WS3, $0): MineBank над нормализованными чанками + контраст-корпус → []Term, DeltaYAML — сид-дельта на подпись владельца; все пар-данные приходят *lang.Pack |
miner.go, miner_*.go |
internal/checks |
Детерминированные $0-вердикты над текстом чанка, target-aware ПО ДАННЫМ (D39.64): Checkers компилируется из цель-данных, TargetActive()/TargetScriptNonLatin() гейтят всё — цель без данных инертна, Go-веток по паре нет: санитайзер v7 (fold-first, CJK/Hangul-leak, CJK-глосс-whitelist) + ExportNormalize (export-contract слой 6, no-op без цель-данных), cheap-гейты и DC-чекеры (пар-данные из пака), coverage-гейт эксцизии, regression-guard. Флаг-вокабуляр НЕ импортирует: возвращает факт, диспозицию назначает драйвер |
sanitizer.go, cheapgates.go, checkers.go, coverage.go |
internal/pipeline |
Композит-корень (драйвер): сетап runner.go → снапшот snapshot.go → сид seeding.go → цикл книги bookrun.go → волны waverun.go → петля чанка chunkrun.go → стадия stagerun.go → эскалация с ре-гейтом escalation.go → resume resume.go; classify/disposition; реестр roleInjectionRenderers (новая роль = данные+рендерер, не правка switch); export.go/quality.go/status.go — read-only проекции (общая derivation member-drop = memberDrops); read-out файлы для платформы (D39.85 — приватный SQLite движка ей закрыт): manifest.go (структура глав/чанков + стабильный id главы, строка 100), bankexport.go (весь банк тремя статусами, 125); вместе с картой подписи и авто-банком в mining.go (signatureMapPath · autoBankPath) — все ЧЕТЫРЕ пишутся через artifact.go (write-then-rename — их читают ПОКА идёт прогон); человеческая стоп-таблица .bank-stop.txt — обычным os.WriteFile, БЕЗ этой гарантии, а машинной .bank-stop.json больше нет (снесена D39.158) |
bookrun.go, waverun.go, stagerun.go, export.go, manifest.go, disposition.go |
internal/bookfile |
Писатель книги: EPUB 3 (контейнер/OPF/навигация) и чистый txt из экспорт-проекции; формат знает только он, tmctl build даёт ему данные |
epub.go, txt.go, container.go, model.go |
internal/runevents |
ДВИЖКОВАЯ сторона шва кадров прогона (строка 103): конверт и payload'ы NDJSON-потока events.jsonl + append-only журнал; словарь исходов finished. Форма — здесь, КОГДА событие случилось — у драйвера, долговечная нумерация — у store.events_outbox |
runevents.go, journal.go |
internal/standdata |
Резолв путей корпусных данных ОТ КОРНЯ РЕПОЗИТОРИЯ по маркеру (<репозиторий>/books, D39.157 п.2); $HOME не читается вовсе, env-переопределения TM_* старше дефолтов |
standdata.go |
internal/obs |
trace_id, структурные логи (contextHandler несёт book/chapter/chunk/stage/role из ctx), safego |
Инварианты — ЛОМАТЬ НЕЛЬЗЯ (каждый закреплён тестами)
- Деньги: reserve → call → settle+checkpoint одной транзакцией;
committed == SUM(checkpoints)переживает kill -9 (kill9_test.go); цена — по фактически ответившей модели; потолки книга/день считают committed+reserved. Одно исключение — явныйtmctl redrive(D15.3): после негоcommitted ≥ SUM(checkpoints)— безопасное направление. - Snapshot-дисциплина: всё, что влияет на wire-байты ИЛИ вердикты (capability, память content-hash +
memoryMatchVersion, context-assembly, версии classify/coverage/chunker/maxtok/sanitizer (только при enabled), эмбед-данныеlang.EmbeddedVersion()(D39.64), эскалационные оверрайды, биндинг пара→промпт через BriefHash+PromptSHA256), свёрнуто вsnapshotID; правка = громкий--resnapshot= переоплата книги (D15). Новый фолд обязан быть УСЛОВНЫМ по присутствию данных (правило §6.2, D39.60). - Эхо-мина DeepSeek: thinking НИКОГДА не отключать —
reasoning:"off"там осознанный no-op;echoes_when_thinking_off+ рекурсивный скан extra_body валят конфиг fail-fast. У grok off = ЯВНЫЙreasoning_effort:"none". - Порядок classify: refusal-blacklist / эхо источника (
sourceScriptShareпо ОБЪЯВЛЕННОМУ письму, D39.64) — ДО length/empty; retry-flagged только {length, empty} на той же модели; loop-чек ПЕРЕД удвоением max_tokens. Санитайзер — ТОЛЬКО на финальной стадии (isFinal) и только при активной цели (TargetActive). - Эскалация: ровно 1 хоп, результат РЕ-гейтится, retry-бюджет не сбрасывается, editor pinned, канал adult — только permissive (fail-closed).
- Детерминизм: рендер — чистая функция snapshot; сортировки перед записью; никакого map-order в выводе; length-prefixed хэши; сорс-чанкер lossless (fuzz).
- Нейтральность адаптера:
internal/llmне знает про перевод — контент-вердикты в раннере (disposition-слой, post-settle). - Экспорт-контракт (D39 слой 6, D39.5):
checks.ExportNormalize— эфемерная проекция (НЕ входит в request_hash/чекпоинты/деньги), применяется к КАЖДОМУ финальному тексту на всех выдающих поверхностях; любая внешняя экстракция (полигон!) обязана читатьtmctl export, не сырые чекпоинты store — иначе видит ненормализованные байты и молча-неполную книгу.
Подготовка ja-книги (чек-лист сида)
- Kana-написания имён = matchable. Ruby-чтения захватываются на инжесте и авто-подцепляются как alias ручной записи (D16.4,
membank.AttachRubyAliasesToManual). Имя, встречающееся ТОЛЬКО каной, внеси вaliases:руками — иначе «тихо пусто». - Двойные чтения (強敵→とも) — под флагом: длинное двойное чтение может дать CONFIRMED-ложь — при подозрении убери из сида. Полная дизамбигуация — Ф2 (B6).
- Kana-precision ≥4 не замерена (minKeyLenPhonetic=3): кана-ключи не проверяются на границу слова (нет сегментации) — задача полигона + токенизатор B6.
- ⚠ ja-книга требует каталога
prompts/ja-ru/(по файлу на роль) — промпт-резолв конвенцией fail-loud'ится без него, называя ожидаемый путь (пример: закомментированный ja-блок вexample/book.yaml). Контент пакета = данные под реальную книгу (инвариант общности §0.1), не сочинять.
Как гонять
Стенд: WSL2 (localhost из-под прокси = 403 — для local-вызовов no-proxy транспорт). Книга-стенд: <репозиторий>/books/gu-zhenren/ (адрес по D39.157 п.2; GB18030; тексты и производные версионируются ОТДЕЛЬНЫМ git-репозиторием).
Батарея — ОДНА команда, make из backend/ (Makefile):
make battery # build · vet (оба тег-набора, + архитектурные анализаторы) · gofmt · lint · test -race
# в конце НАЗЫВАЕТ пропущенные тесты — вклеивать в отчёт, не опускать
make battery-stand # то же + корпусные тесты (TM_MINER_PARITY=1 TM_CHECKER_LABELS=1): отсутствие данных ПАДАЕТ
make lint # golangci-lint 2.12.2 (версия пиновата в Makefile; конфиг — .golangci.yml)
Линтер ставится отдельно (бинарь вне репо, make его НЕ доустанавливает — ссылку печатает tools-check).
Архитектурные инварианты живут в internal/archguard и запускаются двумя путями: go vet -vettool из
make vet и обычный go test ./internal/archguard/ (он прогоняет их по всему дереву).
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 для человека)
go run ./cmd/tmctl manifest --config example/book.yaml # $0, пере-строить персист манифеста глав/чанков (--json — сам документ)
go run ./cmd/tmctl migrate --config example/book.yaml # $0, довести схему проекта до головы бинаря (деплой-порядок — D39.134 §3)
go run ./cmd/tmctl translate --config example/book.yaml --ceiling-usd 0.5 # ДЕНЕЖНЫЙ потолок ТОЛЬКО на этот прогон, book.yaml не пишется
go run ./cmd/tmctl translate --config example/book.yaml --max-units 10 # ОБЪЁМНЫЙ потолок: не больше 10 выходных юнитов возьмут слот; стоп = завершение (exit 0)
# live-conformance (реальные провайдеры, платно, вне CI):
set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/
⚠ «Зелёное» ≠ «всё проверено»: четыре теста — ИЗМЕРЕНИЯ на корпусе, а не юнит-тесты, и на чистом клоне они молча скипаются. Голая батарея выше их НЕ выполняет, поэтому «батарея зелёная» без списка SKIP — неполный отчёт, а не приёмка.
| Что тёмное без данных | Флаг | Данные |
|---|---|---|
TestMinerFullBookParity — единственная сверка Go-порта против Python-эталона на полной книге |
TM_MINER_PARITY=1 |
jieba-словарь по внутрирепозиторному ПУТИ eval/exp16/data/, но ВНЕ git (eval/.gitignore:4) — клон его не получает, регенерируется из jieba 0.42.1 (SHA в experiments/16-bank-mining.md:183); плюс <репозиторий>/books/gu-zhenren/{rerun/records.json, guzhenren-seed-v2.yaml} |
TestCheckerLabelsBaseline, TestCheckerLabelsCandidates, TestK6LabelsBaseline — единственный замер precision/recall $0-гейтов |
TM_CHECKER_LABELS=1 |
<репозиторий>/books/gu-zhenren/labels/ |
Пути дефолтятся от КОРНЯ РЕПОЗИТОРИЯ, найденного по маркеру (internal/standdata; $HOME не читается вовсе — D39.157 п.2), так что клон под любым пользователем и по любому пути находит то, что есть; точечные переопределения — TM_CHECKER_LABELS_DIR, TM_MINER_PARITY_{CONTRAST,RECORDS,SEED}.
Golden-гард детерминизма (golden_test.go + testdata/golden/): пинит бит-в-бит snapshotID, request_hash, wire-тела, вердикты и resume-байты. Обновлять ТОЛЬКО на ратифицированной смене поведения: TM_UPDATE_GOLDEN=1 go test ./internal/pipeline/ -run TestGolden; при re-capture — маскированный структурный дифф (хеши/версии → плейсхолдер) обязан быть пустым, если смена версий не меняла вердиктов.
Известный техдолг (не трогать молча; ЕДИНЫЙ трекер — таблица «Бэклог» в docs/PROGRESS.md, номера строк = она)
- D15.2: READ-половина
tmctl exportпостроена (D39.5); annotation/override-половина и content-addressed resume отложены (строка 49). F3 at-most-once — после D15.2 (строка 50). RetriesНЕ фолдится в снапшот (pre-existing, D39.4-LOW; сознательно так же для ручкиRegenerateEchoBeforeEscalate, D39.64 — дефолт 0 = текущее поведение): в крэш-окне пониженный ретрай-бюджет молча бросает оплаченные OK-чекпоинты.- ⚠ ALTER-шаги миграций v8+ НЕ идемпотентны вопреки шапке
migrate.go— полу-применённая БД не сходится (строка 49а). - Открытый остаток строки 79 — граница ПЛОТНОСТИ письма Go-константой
cjkScriptNames(internal/lang/script.go). prompts/zh-ru/editor-mono.md— DORMANT-референс D13.1 (не боевой, тестом закреплён); боевой =prompts/zh-ru/editor.mdv3-discourse (БИЛИНГВ). Глосс-капы санитайзера (6 CJK / 24 лат-токен) — tunable-эвристики (D39.5). Флоры min_max_tokens = схема (D24.3).- Хвост LOW/NOTE-находок адверсариала D39.4 (14 LOW + 14 NOTE) — диспозиция «не чинить», носитель D39.4/D39.5, текст —
docs/archive/reports/SMALLPACK_TAILS_2026-08-02.md(грепD39.4 ledger-очередь); живые пересечения — строки 43/78. ⚠ Прежний указатель на строку 53 мёртв: она закрыта D39.82 (putUniqueDC).