textmachine/platform/README.md

153 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` и список книг, чью
читательскую поверхность построить не удалось (`books --abandoned` / `book refresh`), диагностика и
терминальный вердикт по застрявшим прогонам (`runs [--stalled]` / `run abandon`, P8-FIX), сид
дев-стенда `seed`, и `exit-marker`его зовёт systemd на конце прогона). Что оператор делает, когда
свип не справляется, — `deploy/README.md` §«Застрявшая работа». Деплой — `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` §1520.
## Куда смотреть, чтобы понять состояние зоны
| Вопрос | Ответ лежит здесь |
|---|---|
| что сделано последним паком и зачем | `docs/platform-PROGRESS.md`, шапка «Текущее состояние» |
| статус конкретного дефекта | `docs/DEFECT_REGISTER.md` (источник истины; счёт — `python3 docs/scripts/counts.py` от корня репозитория) |
| правила, которые переживают пак и не выводятся из одной функции | `docs/STACK_DECISIONS.md` §22 (порядок блокировок), §3336 (долг материализации, форма пайплайна, атомарность) |
| что зона обязана уметь и по какой норме | `docs/PLATFORM_DIRECTION.md`, `docs/ENGINEERING_STANDARDS.md` |
| как это разворачивается и в каком порядке | `deploy/README.md` |
| незакрытые куски работы | `BACKLOG.md` |
⚠ Рабочие записи конкретной сессии (`P7_*`, `*_HANDOFF`, планы актов) — **не состояние зоны**: они
несут ход работы и полезны той же сессии после компакции. Все такие файлы уехали в
`docs/archive/` при лендингах, и новые туда же; для лендинга их читать не нужно.
## ⚠ Git и зона (читать ДО первой строки кода)
**Платформенная сессия не коммитит — лендит оркестратор.** Канон — `../CLAUDE.md`, §Гардрейлы; продублировано здесь, потому что зона живёт своим онбордингом. Направление зоны (вход · деньги · стандарты · скорость) — `docs/PLATFORM_DIRECTION.md`; стандарты и критерии приёмки — `docs/ENGINEERING_STANDARDS.md`; дефекты и уязвимости — `docs/DEFECT_REGISTER.md` (каждая находка получает строку ДО закрытия).
1. **Писать только внутрь `platform/`.** Ничего за её пределами — ни `docs/`, ни `backend/`, ни `frontend/`, ни корневых файлов. Нужна правка вне зоны — пинг владельцу, её сделает оркестратор.
2. **Чужие незакоммиченные файлы в дереве не трогать**: параллельные сессии — норма.
3. **НИКАКИХ** `git add -A`, `reset --hard`, `amend`/`rebase`, перезаписи истории и `checkout` поверх грязного дерева.
4. ⚠ Ревью-гард модулей (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)**: дерево глав и пары с текстом, замечания, ЧТЕНИЕ
банка (⚠ ЗАПИСЬ в банк снята 22.08 вместе с пер-термной моделью подписи — `PD-370`, D39.144: подпись
это `resume`, а ручки «поправить/добавить термин» нет ни в зоне, ни в каноне), `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` и `reading.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`](docs/STACK_DECISIONS.md) (зонный,
live-сверка 0405.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; эндпоинты про куки не знают ничего.