164 lines
20 KiB
Markdown
164 lines
20 KiB
Markdown
# platform — control plane (SaaS-слой)
|
||
|
||
Зона записи сессии «Платформа». Что построено и каким паком — секция «Что здесь будет» ниже и
|
||
шапка «Текущее состояние» зонного журнала. Направление зоны — `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). `make vuln` и `make fuzz` —
|
||
отдельными целями.
|
||
|
||
⚠ **Сколько у батареи условий — НЕ ЗДЕСЬ. Единственный носитель — `docs/STACK_DECISIONS.md`,
|
||
раздел «Гейты батареи»**; здесь число сознательно не дублируем. Разошедшиеся копии дают ложную
|
||
приёмку: сессия, честно исполнившая §3.1 по устаревшему списку, объявит «скипов ноль» при красном
|
||
тесте, о котором её версия не знала. Без переменных окружения батарея МОЛЧА пропускает около
|
||
трёхсот тестов и остаётся зелёной, поэтому «зелено» без сверки со списком условий не значит ничего.
|
||
|
||
⚠ **`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` §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` |
|
||
|
||
⚠ Рабочие записи конкретной сессии (`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)**: дерево глав и пары с текстом, замечания, ЧТЕНИЕ
|
||
банка, `GET /capabilities`, машинная модель ошибок (`code` + `request_id`), условные чтения
|
||
(`ETag`/304) и сжатие JSON, `Idempotency-Key` на двух создающих вызовах. ⚠ Снята ПЕР-ТЕРМНАЯ
|
||
МОДЕЛЬ ПОДПИСИ (22.08, `PD-370`, D39.144: подпись — это `resume`), а НЕ правка термина: дверь
|
||
`POST /books/{bookId}/bank/corrections` построена паком P9 — `httpapi/bank.go`, маршрут в
|
||
`contractSurface` (`httpapi/v0.go`), канон `14-api-contract/openapi.yaml`, акты D39.161/162/166,
|
||
монтаж по `Capabilities.bank_corrections_enabled`;
|
||
- 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` ·
|
||
`tmctl build` · `tmctl bank-apply`. ⚠ Слов «и других нет» здесь нет намеренно, и второй копии
|
||
перечня здесь тоже нет: атомарность каждого канала, колонка «потребляет ли платформа» и все
|
||
оговорки лежат ЕДИНСТВЕННЫМ носителем в `docs/STACK_DECISIONS.md`, «Инвентарь каналов движка». Оттуда же два правила, которые
|
||
дороже перечня: `tmctl build` (D39.175) в зоне ещё НИКЕМ не читается, и его будущая дверь выдачи
|
||
`createExport`/`getExport` обязана СТРОИТЬ, а не подбирать лежащий рядом файл — там копия прежней
|
||
сборки (D39.175 п.2); `tmctl bank-apply` — ЕДИНСТВЕННЫЙ канал, по которому зона ПИШЕТ в проект
|
||
движка, и потому у него своя дверь (`POST /books/{bookId}/bank/corrections`) и свой класс отказов.
|
||
Живой SQLite движка не читается никогда (D39.85).
|
||
|
||
## Чего здесь НЕ будет
|
||
|
||
Перевода. Движок (`../backend/`) остаётся как есть: один процесс на книгу, свой SQLite под
|
||
эксклюзивным flock (`store.Open` — «один процесс владеет файлом проекта»). Платформа его
|
||
ЗАПУСКАЕТ как воркер, а не поглощает. Причина та же, по которой движок не ветвится по паре
|
||
языков: пользователи и квоты ничего не меняют в проводе запроса, значит им нечего делать в
|
||
кодовой базе, где каждый байт свёрнут в снапшот-хеш.
|
||
|
||
## Известное требование к движку (не забыть)
|
||
|
||
⚠ **Глобальный брокер конкурентности** — единственная по-настоящему новая механика на стыке:
|
||
`pipeline/ratelimit.go` строит рейт-гарды НА ПРОГОН, а лимит провайдера — на весь аккаунт.
|
||
Постановка, замер и вес — строка `BACKLOG.md` П-2.
|
||
|
||
## Стек
|
||
|
||
Пины, даты релизов и обоснования — [`docs/STACK_DECISIONS.md`](docs/STACK_DECISIONS.md) (зонный,
|
||
live-сверка 04–05.08); общая записка по обоим новым сервисам — `../frontend/docs/STACK_DECISIONS.md` §5.
|
||
|
||
Коротко: floor ЯЗЫКА в `go.mod` общий с движком, `toolchain` там же поднимает тулчейн выше
|
||
(D39.130; `make version-check` СРАВНИВАЕТ версии, а не матчит) · стандартный `net/http` + `ServeMux`
|
||
без роутер-библиотеки · PostgreSQL 18 + pgx · goose · очередь River на том же Postgres —
|
||
**подключена и работает** (P4) · вход `x/oauth2` + `go-oidc/v3` · `x/time` для лимита на
|
||
`/auth/login` · `govulncheck` отдельной целью. ⚠ Номера версий здесь намеренно не дублируются: их
|
||
единственные носители — `go.mod` и таблица пинов строкой выше.
|
||
**Redis не заводим нигде** — зафиксировано как архитектурное «нет».
|
||
|
||
Прогресс наружу — SSE, события **пушит воркер**, а не фронт опрашивает read-model.
|
||
Аутентификация — одна серверная сессия в Postgres, два способа предъявления: `__Host`-кука для
|
||
браузера и `Authorization: Bearer` для десктопа и CLI; эндпоинты про куки не знают ничего.
|