| .. | ||
| cmd | ||
| deploy | ||
| docs | ||
| internal | ||
| .golangci.yml | ||
| BACKLOG.md | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
platform — control plane (SaaS-слой)
Зона записи сессии «Платформа». P0 собран 04.08 (скелет: HTTP · сессии · схема read-model ·
интерфейс ингеста), P1 — 05.08 (вход через OIDC · кредитный леджер · админ-CLI · деплой-юнит ·
закрытие регистра дефектов). Направление зоны — docs/PLATFORM_DIRECTION.md, критерии приёмки —
docs/ENGINEERING_STANDARDS.md, дефекты — docs/DEFECT_REGISTER.md, зонный журнал —
docs/platform-PROGRESS.md (весь прогресс зоны здесь, решение владельца 04.08), стек —
docs/STACK_DECISIONS.md.
Батарея зоны: make check (build · vet · fmt · lint · test -race). Тесты со схемой требуют
TM_PLATFORM_TEST_DSN (как поднять Postgres без root — docs/STACK_DECISIONS.md); без него они
пропускаются, и check называет пропуски вслух. Живая проба рендера конфигурации требует второго
гейта — TM_PLATFORM_TEST_ENGINE_BIN + TM_PLATFORM_TEST_BOOK_TEMPLATE. make vuln и make fuzz —
отдельными целями.
⚠ TM_PLATFORM_LANGUAGE_PAIRS — обязательная настройка деплоя (форма zh>ru,ja>ru:unavailable):
её отвечает GET /capabilities, по ней же интейк отклоняет неподдерживаемую пару кодом
unsupported_pair. Какие пары существуют, решают ДАННЫЕ (пакет промптов оператора), а не список в Go.
⚠ Пустой список отклоняет ВСЁ, а не пропускает всё (акт 5 P7): «не объявлено ничего» и «объявлена
эта пара» — разные ответы, и прежнее послабление заставляло деплой принимать книги, которые он мог
только провалить. Инстанс, принимающий загрузки без единой ДОСТУПНОЙ пары, не стартует вовсе — это
второй конец того же правила, и он в силе и в бою, и в рецепте дев-стенда.
Бинари: cmd/tmplatformd (сервис) и cmd/tmplatformctl (админ: гранты, КОРРЕКТИРОВКИ (adjust), баланс, журнал входов,
отзыв сессий, дев-интейк book add, список книг для апгрейда движка books, сид дев-стенда seed,
и exit-marker — его зовёт systemd на конце прогона). Деплой — deploy/.
Метрики — отдельным слушателем (TM_PLATFORM_METRICS_ADDR, дефолт 127.0.0.1:9464), формат
Prometheus; на контрактную поверхность они не выходят и наружу не привязываются (docs/STACK_DECISIONS.md §24).
Эффективная конфигурация печатается на старте с источником каждой настройки; секреты и денежные
суммы — фактом наличия, без значения (PD-114).
⚠ Прогон — транзиентный systemd-юнит, а не ребёнок сервиса (D39.106). Установка требует
loginctl enable-linger, иначе не стартует ни один прогон; почему именно так и что было
измерено — docs/STACK_DECISIONS.md §15–20.
Куда смотреть, чтобы понять состояние зоны
| Вопрос | Ответ лежит здесь |
|---|---|
| что сделано последним паком и зачем | docs/platform-PROGRESS.md, шапка «Текущее состояние» |
| статус конкретного дефекта | docs/DEFECT_REGISTER.md (источник истины; счёт — python3 docs/scripts/counts.py от корня репозитория) |
| правила, которые переживают пак и не выводятся из одной функции | docs/STACK_DECISIONS.md §22 (порядок блокировок), §33–36 (долг материализации, форма пайплайна, атомарность) |
| что зона обязана уметь и по какой норме | docs/PLATFORM_DIRECTION.md, docs/ENGINEERING_STANDARDS.md |
| как это разворачивается и в каком порядке | deploy/README.md |
| незакрытые куски работы | BACKLOG.md |
⚠ Документы вида docs/P7_* — рабочие записи конкретной сессии, а не состояние зоны: они несут
ход работы и полезны той же сессии после компакции. Для лендинга их читать не нужно.
⚠ Git и зона (читать ДО первой строки кода)
Платформенная сессия не коммитит — лендит оркестратор. Канон — ../CLAUDE.md, §Гардрейлы; продублировано здесь, потому что зона живёт своим онбордингом. Направление зоны (вход · деньги · стандарты · скорость) — docs/PLATFORM_DIRECTION.md; стандарты и критерии приёмки — docs/ENGINEERING_STANDARDS.md; дефекты и уязвимости — docs/DEFECT_REGISTER.md (каждая находка получает строку ДО закрытия).
- Писать только внутрь
platform/. Ничего за её пределами — ниdocs/, ниbackend/, ниfrontend/, ни корневых файлов. Нужна правка вне зоны — пинг владельцу, её сделает оркестратор. - Чужие незакоммиченные файлы в дереве не трогать: параллельные сессии — норма.
- НИКАКИХ
git add -A,reset --hard,amend/rebase, перезаписи истории иcheckoutповерх грязного дерева. - ⚠ Ревью-гард модулей (D39.85): путь Go-модуля платформы никогда не вкладывать под путь движка — иначе он получит доступ к
backend/internal/*по правилу префикса.
Что здесь будет
Сервис между фронтом и движком перевода. Всё, что относится к ПОЛЬЗОВАТЕЛЯМ и не относится к переводу:
- аутентификация и аккаунты — есть (P1): вход через OIDC даёт только СОБЫТИЕ входа, сессия
своя; ключ личности
(provider, subject), почта не ключ. Оплаты нет и в бете не будет (владелец 05.08): аккаунты живут на кредитном балансе, фри-тир — записьgrantв леджер; - библиотека книг: чья книга, права доступа, хранение исходников и экспортов — приём есть (P5):
POST /v0/booksпринимает multipart потоково со своим потолком тела и своим дедлайном чтения, кладёт исходник в каталог книги подTM_PLATFORM_BOOKS_DIRи ведёт книгу по статусамuploading → parsing → not_started | rejected; разбор — $0-команда движкаtmctl manifest. Стартовыйbook.yamlновой книги пишет ПЛАТФОРМА — один раз, из деплой-шаблонаTM_PLATFORM_BOOK_TEMPLATE(форма Б, D39.130; построено P6). Дальше файл принадлежит оператору: платформа его не читает и не перезаписывает (D39.110 §2b в силе). Шаблона нет или он битый — книга ЖДЁТ человека, а не отклоняется; - учёт денег на пользователя — схема и операции есть (P1): append-only леджер в целых
микро-долларах, резервации, кэш баланса с инвариантом
balance == SUM(ledger). Защита прогона — холд ДО спавна плюс книжный потолок движку (жёсткий стоп исполняет движок). ⚠ Потолок, который получает движок, — это НАКОПЛЕННЫЙ потолок КНИГИ (committed + reservedза всю её историю плюс купленный прирост), а не бюджет прогона: платформа переводит одно в другое сама (D39.122); - остановка и продолжение перевода — есть (P5):
POST /v0/runs/{id}/stopпишет НАМЕРЕНИЕ стопа в Postgres до сигнала (иначе стоп и авария — один и тот же выход движка, PD-152) и просит systemd;/resumeоткрывает новую попытку с ОСТАТКОМ бюджета прогона, переиспользуя механику перезапуска реконсилятора; - очередь задач и запуск воркеров, статусы прогонов, ретраи — есть (P4): River на том же Postgres; задание очереди выдаёт только РАЗРЕШЕНИЕ стартовать, а жизнь прогона ведёт реконсилятор, который читает мир (Postgres · журнал книги · маркер выхода) и не ждёт процесса;
- читающая поверхность контракта — есть (P7): дерево глав и пары с текстом, замечания, банк и
подпись,
GET /capabilities, машинная модель ошибок (code+request_id), условные чтения (ETag/304) и сжатие JSON,Idempotency-Keyна двух создающих вызовах; - SSE-поток прогресса во фронт — есть (P7): поток на КНИГЕ (не на прогоне), кадры минтит
писатель в свою транзакцию, клиент продолжает по
Last-Event-ID(⚠ денежные суммы на провод и на экран НЕ идут — D39.84; пользователь видит ОСТАТОК процентом — окон со сбросом больше нет, владелец 05.08).
Карта зоны: где что лежит
Читать сверху вниз — это порядок, в котором запрос проходит систему.
| Пакет | Что держит |
|---|---|
cmd/tmplatformd |
демон: сборка зависимостей, очередь, свипы. runner.go — единственное место, где зона склеивается |
cmd/tmplatformctl |
админ-CLI и exit-marker, который systemd зовёт на конце прогона |
internal/httpapi |
ВСЯ контрактная поверхность: v0.go (маршруты, библиотека, интейк, прогоны) · reading.go (главы, пары, замечания, банк) · stream.go (SSE) · capabilities.go · problem.go (модель ошибок) · conditional.go (ETag/304 + gzip; SSE через него НЕ проходит — потому и не сжимается) · idempotency.go · project.go и notes.go (переводы словарей: read-модель → провод) |
internal/auth, internal/login |
сессии, CSRF, вход. Принципал создаётся ТОЛЬКО в мидлваре |
internal/books |
интейк: приём файла, каталог книги, рендер стартового book.yaml, разбор через tmctl manifest |
internal/runs |
жизнь прогона: допуск, спавн транзиентного юнита, реконсилятор, деньги на границе попытки |
internal/readmodel |
материализатор читающей поверхности: манифест + экспорт + сайдкар банка → read-модель. Зовётся на ГРАНИЦАХ работы (конец интейка, конец прогона) — там же, где контракт объявляет свежесть перевода. Он же держит ОЧЕРЕДЬ долгов (Drain): граница ставит долг колонкой на книге, платящий берёт его в аренду, неоплаченный едет в конец очереди |
internal/ingest |
словарь шва с движком: NDJSON-события, коды выхода, декодеры манифеста/экспорта/банка. Здесь же переводятся словари движка в контрактные |
internal/runner |
как зовётся движок: транзиентный юнит, argv команд, маркер выхода, файловые сайдкары |
internal/pgstore |
вся SQL. readmodel.go — чтения и проекции, events.go — буфер кадров потока, sink.go — материализатор шва, credits.go — деньги |
internal/pricing, internal/money |
шкала глав и целые микро-доллары |
internal/metrics, internal/reqid, internal/jobs, internal/config, internal/gates |
телеметрия, id запроса, очередь, конфигурация, гейты тулчейна |
Три канала движка, и других нет: tmctl manifest --json (структура), tmctl export --json --pairs
(ТЕКСТ пар — единственный носитель), <project_db>.bank.json (банк), плюс tmctl status --json как
канал ремонта и events.jsonl как поток. Живой SQLite движка не читается никогда (D39.85).
Чего здесь НЕ будет
Перевода. Движок (../backend/) остаётся как есть: один процесс на книгу, свой SQLite под
эксклюзивным flock (store.Open — «один процесс владеет файлом проекта»). Платформа его
ЗАПУСКАЕТ как воркер, а не поглощает. Причина та же, по которой движок не ветвится по паре
языков: пользователи и квоты ничего не меняют в проводе запроса, значит им нечего делать в
кодовой базе, где каждый байт свёрнут в снапшот-хеш.
Известное требование к движку (не забыть)
⚠ Глобальный брокер конкурентности. pipeline/ratelimit.go строит рейт-гарды НА ПРОГОН,
а лимит провайдера — на весь аккаунт. N параллельных пользователей = N независимых гардов
против общего лимита (в комментарии там зафиксировано: mistral валит ~48% вызовов под
параллелизмом). Воркер должен отпрашиваться у платформы перед вызовом. Это единственная
по-настоящему новая механика на стыке.
Стек
Пины, даты релизов и обоснования — docs/STACK_DECISIONS.md (зонный,
live-сверка 04–05.08); общая записка по обоим новым сервисам — ../frontend/docs/STACK_DECISIONS.md §5.
Коротко: Go 1.26.4 в go.mod — общий с движком floor ЯЗЫКА, при этом toolchain go1.26.6 там же поднимает тулчейн (D39.130; make version-check СРАВНИВАЕТ версии, а не матчит) · стандартный net/http + ServeMux без
роутер-библиотеки · PostgreSQL 18 · pgx v5.10.0 · goose v3.27.3 · очередь River v0.42.0 на том же
Postgres — подключена и работает (P4: задание выдаёт разрешение стартовать, жизнь прогона ведёт реконсилятор) · вход x/oauth2 v0.36.0 + go-oidc/v3 v3.20.0 ·
x/time v0.15.0 для лимита на /auth/login · govulncheck отдельной целью.
Redis не заводим нигде — зафиксировано как архитектурное «нет».
Прогресс наружу — SSE, события пушит воркер, а не фронт опрашивает read-model.
Аутентификация — одна серверная сессия в Postgres, два способа предъявления: __Host-кука для
браузера и Authorization: Bearer для десктопа и CLI; эндпоинты про куки не знают ничего.