textmachine/platform
2026-09-06 02:22:07 +03:00
..
cmd Let a person order part of a book: price and verdict come from the engine's projection before the click, and the order is stored as an identity 2026-09-06 00:43:49 +03:00
deploy Close the holes in the backup: a book still in intake keeps its source, a rejected one with a directory is no hole, and the runbook stops inverting two symptoms 2026-09-05 09:58:09 +03:00
docs Close five register rows the landing fixed, two of them major 2026-09-06 02:22:07 +03:00
internal Remove from the alarm baseline the two rows the landing closed, as the gate's own instruction requires 2026-09-06 02:21:24 +03:00
.gitignore Land the platform fix pack: the sweep starvation that froze settlement for a whole installation is closed by four mechanisms 2026-08-22 17:33:27 +03:00
.golangci.yml Land platform P0 skeleton: module, HTTP surface with security headers, server-side sessions with CSRF, read-model migrations, NDJSON ingest interface, zone battery and stack pins 2026-08-04 23:55:30 +03:00
BACKLOG.md Let a person in: backups that restore, a token the door can issue, renaming a book, and a runbook that survives a clean machine 2026-09-05 07:07:10 +03:00
go.mod Land platform P6 and its dofix: consumer half of the event seam, form-B intake, dev stand seed, engine upgrade order, register through PD-245 2026-08-15 04:45:23 +03:00
go.sum Land platform P6 and its dofix: consumer half of the event seam, form-B intake, dev stand seed, engine upgrade order, register through PD-245 2026-08-15 04:45:23 +03:00
Makefile Land the platform P13 dofix: the tailer parks with a named error the sweep repairs, the battery gate checks every host condition against the host, and the class gate closes its three gates 2026-09-04 02:37:48 +03:00
README.md Let a person order part of a book: price and verdict come from the engine's projection before the click, and the order is stored as an identity 2026-09-06 00:43:49 +03:00
sqlc.yaml Land the sqlc pack: the coverage answer said almost nothing, and six planted mutations surviving a green battery said otherwise — the SQL gate never looks at the Go side of a call 2026-08-29 22:38:50 +03:00

platform — control plane (SaaS-слой)

Зона записи сессии «Платформа». Что построено и каким паком — секция «Что здесь будет» ниже и ВЕРХ зонного журнала: он обратно-хронологический, свежее — выше (раздел «Состояние эры P8» в конце файла — ИСТОРИЯ, не состояние). Направление зоны — 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 — отдельными целями. make conditions печатает условия хоста, которые читают тесты (переменные, бинари на PATH, менеджер systemd), с их состоянием здесь и пакетами, которые каждое открывает; check печатает то же над списком скипов.

Сколько у батареи условий — НЕ ЗДЕСЬ. Единственный носитель — 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), снятие карантина проекции (run unquarantine, P13: колонка QUARANTINE в runs говорит, что снимать), сид дев-стенда 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): дерево глав и пары с текстом, замечания, ЧТЕНИЕ банка, 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;
  • ВЫДАЧА КНИГИ ФАЙЛОМ — есть (04.09, пак «закрыть цикл»): POST /books/{bookId}/exports принимает формат из export_formats, отвечает 202 с адресом статуса в Location и строит асинхронно (River); GET .../exports/{exportId} — поллинг, который ВСЕГДА кончается (pendingreadyexpired, либо pendingfailed) и несёт Retry-After, пока сборка идёт; сам файл лежит по третьему адресу, .../content. ⚠ Дверь СТРОИТ, а не подбирает: зовёт tmctl build --format <f> --out <свой путь> --partial и никогда не отдаёт файл, лежащий рядом с БД движка (там копия ПРЕЖНЕЙ сборки — D39.175 п.2). ⚠ И строит ВСЕГДА (D39.178 п.1, слово владельца 30.08): книга с дырами уходит С ПОМЕТКОЙ на первой странице и знаком на каждой дыре, отказ читателю не отдаётся; отказ по умолчанию (exit 16) остаётся операторской ручкой CLI. ⚠ Ссылка АУТЕНТИФИЦИРОВАННАЯ, а не подписанная — довод в шапке internal/httpapi/exports.go: она строго сильнее капабилити-токена (утёкшая ссылка бесполезна никому, кроме владельца) и не требует деплойного секрета, который никто не ротирует. Артефакт живёт TM_PLATFORM_EXPORT_TTL, потом свип забирает и строку, и байты; BuildReport (дрейф конфигурации, непроверяемая свежесть) уходит ОПЕРАТОРУ в лог, не читателю;
  • SSE-поток прогресса во фронт — есть (P7): поток на КНИГЕ (не на прогоне), кадры минтит писатель в свою транзакцию, клиент продолжает по Last-Event-ID (⚠ испр. 05.09: запрет «денег на проводе» ОТОЗВАН владельцем, D39.196 п.2 — баланс, потолок заказа и холд выходят ДЕНЬГАМИ на форме заказа; процент остатка живёт рядом как СИГНАЛ «мало/пусто», а не как замена суммы. Запрещены по-прежнему цены моделей, стоимость стадий и вызовов и структура наших расходов — ПТ-33/ПТ-35).

Карта зоны: где что лежит

Читать сверху вниз — это порядок, в котором запрос проходит систему.

Пакет Что держит
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 (переводы словарей: read-модель → провод) · bank.go (дверь правок банка) · exports.go (дверь выдачи книги файлом)
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/backup точки восстановления: pg_dump денежного реестра + движковый tmctl backup на каждую книгу + манифест с sha256 каждого файла, публикация переименованием из .partial-. Отдельный пакет и СВОЯ горутина, а не пасс свипа: такт реконсилятора последователен, и копирование целых баз задержало бы расчёт денег на всю свою длину. Восстановление — руками по рантбуку, команды restore здесь нет намеренно
internal/exports дверь выдачи: приём заказа, сборка книги файлом через tmctl build --out, жизнь артефакта и его GC. Отдельный пакет, а не угол runs: здесь ничего не стоит денег и не держит кредит
internal/pricing, internal/money форма заказа поверх ПРОЕКЦИИ ДВИЖКА (шкалы глав и per-chapter ставки больше нет — строка 280) и целые микро-доллары
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) читается с 04.09 — internal/runner/build.gointernal/exports, — и дверь createExport/getExport СТРОИТ, а не подбирает лежащий рядом файл: там копия прежней сборки (D39.175 п.2); tmctl backup читается с 05.09 — internal/runner/backup.gointernal/backup, — и это ВОСЬМОЙ движковый канал зоны; ⚠ единственный, чей ответ разбирается из ЧЕЛОВЕЧЕСКОЙ строки stdout, потому что у глагола нет ни --out, ни JSON-выхода (PD-449). 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 (зонный, live-сверка 0405.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 не заводим нигде — зафиксировано как архитектурное «нет», иначе он приползёт по частям: очередь, лизы и рейт-лимиты живут в том же Postgres.

Прогресс наружу — SSE, события пушит воркер, а не фронт опрашивает read-model. Аутентификация — одна серверная сессия в Postgres, два способа предъявления: __Host-кука для браузера и Authorization: Bearer для десктопа и CLI; эндпоинты про куки не знают ничего. ⚠ Откуда БЕРЁТСЯ Bearer (с 05.09): его выдаёт оператор — tmplatformctl token issue --user <id>; это обычная серверная сессия с теми же двумя сроками и тем же отзывом (revoke --user), плейнтекст печатается ОДИН раз, в БД только дайджест. До этого схему принимал сервер и не выдавал никто, и канон писал это про себя прямым текстом — то есть не-браузерный клиент войти не мог вообще (строка бэклога 270). Самообслуживаемой выдачи нет намеренно: кнопку нажимать некому, пока фронт заморожен.