Cut documentation sediment in the platform zone: agents read each file end to end and removed layered history whose content lives in the decision journal
This commit is contained in:
parent
5b187db0d2
commit
d40e9bf88e
8 changed files with 309 additions and 1390 deletions
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
| ID | Хвост | Вес | Источник |
|
||||
|---|---|---|---|
|
||||
| П-1 | **HTTP/SSE-слой и сервисная обвязка** (экс-строка 96 единого бэклога) — ⚠ **ЧАСТЬ ИСПОЛНЕНА:** аутентификация, сессии и CSRF построены P1; тейлер `events.jsonl` с курсором и карантином проекции, реконсилятор и пять ручек `/v0` — P4/P5; ПОТРЕБИТЕЛЬСКАЯ половина шва (коды выхода, фолд присваиванием, потолки) — P6. **ОСТАЁТСЯ** читающая поверхность: SSE-эндпоинт, ручки глав/юнитов/замечаний, проекция банка — это пак P7. Состав ниже — исходная постановка: read-API поверх готовых `OpenReadOnly`-путей движка; SSE-события ПУШИТ воркер, фронт read-model не опрашивает (каждый read-вызов движка — дорогой ре-ингест, до строки 100 единого); аутентификация ратифицирована D39.84: одна серверная сессия в Postgres — `__Host`-кука браузеру · `Authorization: Bearer` десктопу/CLI · principal создаётся ТОЛЬКО в middleware, CSRF только на cookie-пути; ⚠ порядок деплоя: read-путь движка схему НЕ мигрирует — отказ это exit 13 + токен `schema_mismatch found=N expected=M` (`backend/internal/store/migrate.go`, `SchemaMismatchError`; прежний якорь `store.go:135-143` и цитата «schema vN … expects vM» протухли, D39.134), а лечение — `tmctl migrate` НОВЫМ бинарём (рантбук прогнан живьём P7); **форма потока ратифицирована D39.85 (`docs/research/23`):** воркер-обёртка платформы супервайзит процесс tmctl, ингестит NDJSON-события идемпотентным апсертом (run_id, seq) в Postgres (Reporting Database, не полный CQRS), SSE — из Postgres; ре-синк на обрыве — `tmctl status --json`; живой SQLite движка НЕ читать (анти-паттерн, аргументы в research/23 §4); ревью-гард: путь Go-модуля платформы никогда не вкладывать под `textmachine/backend/*` ⚠ **ДИСПОЗИЦИЯ 22.08 (аудит доков): строка ЗАКРЫТА, живого хвоста нет.** Последний остаток — читающая поверхность — был вынесен отдельной строкой **П-17** и исполнен паком P7 (D39.153), а «часть исполнена» перестала что-либо значить ещё тогда. Норма шапки этого файла требует у каждой петли диспозиции — вот она; ссылку «до строки 100 единого бэклога» держать не нужно, та строка закрыта 09.08. | P7 (остаток: читающая поверхность) | D39.81, D39.84, D39.85, STACK_DECISIONS §5 |
|
||||
| П-1 | **HTTP/SSE-слой и сервисная обвязка** (экс-строка 96 единого бэклога) — ⚠ **ЗАКРЫТА, живого хвоста нет (диспозиция 22.08, аудит доков).** Аутентификация, сессии и CSRF — P1; тейлер `events.jsonl` с курсором и карантином проекции, реконсилятор и пять ручек `/v0` — P4/P5; потребительская половина шва (коды выхода, фолд присваиванием, потолки) — P6. Последний остаток, читающая поверхность, был вынесен отдельной строкой **П-17** и исполнен паком P7 (D39.153), после чего «часть исполнена» перестало что-либо значить. Форма потока и аутентификации ратифицирована D39.84/D39.85 (`docs/research/23-engine-platform-seam.md`); порядок деплоя против schema-mismatch движка — `deploy/README.md`, §«Апгрейд ДВИЖКА» | закрыта | D39.81, D39.84, D39.85, STACK_DECISIONS §5 |
|
||||
| П-2 | **Глобальный брокер рейт-лимитов провайдеров** (экс-строка 97): гарды движка per-процесс (`pipeline/ratelimit.go:11`, mistral ~48% отказов под параллелизмом), а лимит провайдера — на ВЕСЬ аккаунт: N прогонов = N независимых гардов против общего лимита; воркер отпрашивается у платформы перед вызовом | ДО второго параллельного пользователя | D39.81, D39.84 |
|
||||
| П-3 | **Очередь и конкурентность по книге** — ⚠ **ИСПОЛНЕНО P4 в той форме, которая была нужна:** River на том же Postgres, `MaxAttempts: 1` (повтор не доделывает работу, а спавнит второй движок — работу восстанавливает реконсилятор), одновременность по книге закрыта не лизами, а частичным уникальным индексом `runs_one_live_per_book` в БД: двух живых прогонов на книге не бывает, потому что движок держит эксклюзивный лок на файле проекта. **ОСТАЮТСЯ** ровно те части, которые нужны только со ВТОРЫМ хостом: пиннинг книги к хосту и лизы `book_leases` с heartbeat — сегодня хост один, и лиза без второго хоста охраняет от самой себя | до второго хоста | STACK_DECISIONS §5 и §19, D39.84 |
|
||||
| П-4 | **Учёт токенов/денег per-user + бюджет-гейт ДО старта задачи** (сырьё уже считает движок: `request_log` + `internal/ledger`; помнить: леджер = нижняя граница — строка 78 единого) | Ф3-пак платформы | platform/README, D39.84 |
|
||||
|
|
@ -18,15 +18,15 @@
|
|||
| П-7 | **Кредитный баланс и фри-тир** (решения владельца 05.08; ПРОДАЖИ НЕТ — бета на предоплаченных ключах владельца): append-only леджер в целых микро-долларах, грант фри-тира из админки (дефолт $5, настраиваемый), холд ДО спавна прогона + пер-книжный потолок движку (жёсткий стоп исполняет движок), расчёт на границе попытки. ⚠ «дефолт $5» ПРОТУХ: слово владельца 16.08 (D39.138 п.2л, PD-104) — грант по умолчанию НОЛЬ, начисление руками; возврат $5 идёт вместе с суточным агрегатным потолком. ⚠ Окон с обнулением НЕТ — баланс, а не подписка: черновик `usage_windows` в подписочной форме заменяется. Платёжный провайдер не проектируется до решения продавать. Разбор — `docs/PLATFORM_DIRECTION.md` §2 | P1+, после П-6 | владелец 04–05.08 |
|
||||
| П-8 | **Минимальная админ-поверхность**: начислить/списать кредиты аккаунту (одна запись леджера), посмотреть баланс и состояние прогонов. Защищённая ручка либо CLI — форму предлагает сессия. Без неё фри-тир неуправляем | вместе с П-7 | владелец 05.08 |
|
||||
| П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; сырьё у движка есть (`request_log`/`ledger`, потолки конфига), форму API предлагает P0 | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 |
|
||||
| П-9 | **Загрузка книги `POST /books` (multipart)** — единственная ручка контракта 0.2.0, которую пак раннера НЕ взял. Причина названа, а не умолчана: это не «ещё один хендлер», а хранилище (куда лёг файл, кто его чистит при отказе), разбор (статусы `uploading`/`parsing`/`rejected` существуют в схеме и ни одним писателем не заполняются), каталог проекта движка (кто пишет `book.yaml` — платформа его НЕ правит, D39.110) и пер-маршрутный потолок тела вместе с тестом, которого ждёт PD-72. Пока её нет, книги заводятся дев-инструментом `tmplatformctl book add`, и библиотека читает их так же, как читала бы загруженные: read-model один. ⚠ PD-72 закрывается ВМЕСТЕ с этой ручкой, не раньше — **ИСПОЛНЕНО P5 (11.08), с одной НАЗВАННОЙ половиной на ратификации:** ручка построена потоково (`r.MultipartReader`, пер-маршрутный потолок тела, свой дедлайн чтения), хранилище — каталог книги под `TM_PLATFORM_BOOKS_DIR`, статусы `uploading → parsing → not_started \| rejected` получили писателей, разбор зовёт `tmctl manifest`, PD-72 закрыт своим тестом. НЕ построено и вынесено вопросом в журнал: кто пишет стартовый `book.yaml` при интейке (D39.110 §2b) — шов `books.ErrNotProvisioned` назван, без конфигурации книга отказывает разбором **→ развилка РЕШЕНА D39.130: бета = форма Б (стройка = П-14); движковая форма В (`tmctl init`) = строка 170 единого** | Ф3, следующий пак | сессия P4 (границей промта D39.119 §5а), исполнено P5 |
|
||||
| П-9 | **Загрузка книги `POST /books` (multipart)** — **ИСПОЛНЕНО P5 (11.08):** ручка построена потоково (`r.MultipartReader`, пер-маршрутный потолок тела, свой дедлайн чтения), хранилище — каталог книги под `TM_PLATFORM_BOOKS_DIR`, статусы `uploading → parsing → not_started \| rejected` получили писателей, разбор зовёт `tmctl manifest`, PD-72 закрыт своим тестом. Развилка «кто пишет стартовый `book.yaml` при интейке» (D39.110 §2b, шов `books.ErrNotProvisioned`) **РЕШЕНА D39.130:** бета = форма Б (стройка — П-14); движковая форма В (`tmctl init`) = строка 170 единого | Ф3, следующий пак | сессия P4 (границей промта D39.119 §5а), исполнено P5 |
|
||||
| П-10 | **Честная оценка «$/глава» от ДВИЖКА.** Сейчас ставка — константа платформы ($0.03, провенанс exp08 v2 через D30.4, `STACK_DECISIONS` §20), и это осознанная бета-мера: движковой поверхности оценки не существует, а выдумывать её запрещено. Ставка решает только ДЛИНУ шкалы (деньги защищены холдом и потолком движка), но на книге, которая заметно дороже или дешевле средней, шкала врёт пользователю о том, сколько глав он покупает. Нужна оценка от движка по конкретной книге — запрос уходит строкой ЕДИНОГО бэклога через оркестратора, не сюда | когда-нибудь (до первого платящего) | сессия P4 |
|
||||
| П-11 | **Наблюдаемость раннера.** Метрик и трейсинга в зоне нет вовсе (грепнуто: ни prometheus, ни otel, ни expvar, ни pprof), а у раннера появились величины, которые без них не видны: глубина очереди, возраст незакрытых холдов, число прогонов в карантине, отставание тейлера, длительность свипа. Сегодня всё это читается только глазами по логам и SQL. Связано с PD-115 (у зоны нет внешнего эталона ни по одной оси, кроме безопасности) — **ИСПОЛНЕНО P5 (11.08):** `prometheus/client_golang` v1.24.1 на отдельном слушателе `TM_PLATFORM_METRICS_ADDR`; глубина очереди · возраст самого старого открытого холда · карантины · отставание тейлера · книги в интейке · длительность свипа и счётчик недоведённых проходов · запросы и задержки по паттерну маршрута. Ось наблюдаемости получила внешний эталон (практики именования Prometheus + золотые сигналы, `ENGINEERING_STANDARDS` §2) — половина PD-115 | Ф3 | сессия P4, исполнено P5 |
|
||||
| П-12 | **Квота интейка и ретеншен отклонённых книг.** `POST /books` даёт аутентифицированному аккаунту писать на диск оператора: один аплоад ограничен (64 МиБ), число аплоадов — ничем. Отклонённая по вине источника книга каталог теряет, отклонённая по вине деплоя — сохраняет намеренно, и не чистит их никто. Строка регистра — PD-175 ⚠ **ПРОДУКТОВАЯ ПОЛОВИНА СНЯТА ЦЕЛИКОМ — D39.176 п.1 (слово владельца 30.08), диспозиция записана паком P12 31.08:** продуктовых КВОТ НЕТ и не будет, фри-тир-лимиты не проектируются, живём на покупке API и зачислениях из админки. То есть «сколько книг входит во фри-тир» — больше НЕ развилка и НЕ ждёт владельца; вопроса нет. Остаётся ИНЖЕНЕРНАЯ гигиена, решаемая зоной без чьего-либо слова: ретеншен отклонённых (`rejected`) книг · свип каталогов-сирот · потолок диска. Гейт у неё один и он не продуктовый — открытая регистрация, которой в закрытой бете нет. Диспозиция дописана СЮДА и в `PD-175`, потому что доставлена она была пингом, а пинг уезжает в архив | инженерная гигиена, до открытой регистрации | сессия P5; продуктовая половина снята D39.176 |
|
||||
| П-11 | **Наблюдаемость раннера** — **ИСПОЛНЕНО P5 (11.08):** `prometheus/client_golang` v1.24.1 на отдельном слушателе `TM_PLATFORM_METRICS_ADDR`; глубина очереди · возраст самого старого открытого холда · карантины · отставание тейлера · книги в интейке · длительность свипа и счётчик недоведённых проходов · запросы и задержки по паттерну маршрута. Ось наблюдаемости получила внешний эталон (практики именования Prometheus + золотые сигналы, `ENGINEERING_STANDARDS` §2) — половина PD-115, вторая половина строки остаётся открытой | Ф3 | сессия P4, исполнено P5 |
|
||||
| П-12 | **Квота интейка и ретеншен отклонённых книг.** `POST /books` даёт аутентифицированному аккаунту писать на диск оператора: один аплоад ограничен (64 МиБ), число аплоадов — ничем. Отклонённая по вине источника книга каталог теряет, отклонённая по вине деплоя — сохраняет намеренно, и не чистит их никто. Строка регистра — PD-175 ⚠ **ПРОДУКТОВАЯ ПОЛОВИНА СНЯТА ЦЕЛИКОМ — D39.176 п.1 (слово владельца 30.08), диспозиция записана паком P12 31.08:** продуктовых КВОТ НЕТ и не будет, фри-тир-лимиты не проектируются, живём на покупке API и зачислениях из админки. То есть «сколько книг входит во фри-тир» — больше НЕ развилка и НЕ ждёт владельца; вопроса нет. Остаётся ИНЖЕНЕРНАЯ гигиена, решаемая зоной без чьего-либо слова: ретеншен отклонённых (`rejected`) книг · свип каталогов-сирот · потолок диска. Гейт у неё один и он не продуктовый — открытая регистрация, которой в закрытой бете нет. | инженерная гигиена, до открытой регистрации | сессия P5; продуктовая половина снята D39.176 |
|
||||
| П-13 | **Оценка размера книги в символах — у ДВИЖКА.** `character_count` контракта платформа считает потоково на приёме (байты, не являющиеся продолжением UTF-8), что точно для UTF-8 и приблизительно для GB18030/UTF-16, которые движок принимает и декодирует сам. Манифест несёт `source_bytes` и `encoding`, но не число символов. Запрос уходит строкой ЕДИНОГО бэклога через оркестратора; строка регистра — PD-177 **→ строка 171 единого заведена 14.08** | когда-нибудь | сессия P5 |
|
||||
| П-15 | **ИСПОЛНЕНО P6 (14.08).** Платформенная половина шва эмиттера (движковая залендена D39.131, `9cfe080`; события уже пишутся — StreamVersion 1.1):** (а) маппинг exit-кодов движка: 4 = потолок (`paused`, не `failed`), 5 = graceful stop, 10–19 = полоса отказов — сегодня outcome() знает только 0/2/3 и читает `paused_reason` из stale-снапшота ДО drain (ceiling-стоп материализуется `failed`, Resume 409), а `refusedTheSource()` не смотрит на код вовсе (опечатка `book.yaml`/лок ведут к удалению загрузки — PD-196 у потребителя не закрыт); (б) фолд `unit_done` ПРИСВАИВАНИЕМ по тройке (chapter, unit, wave), не инкрементом — ратифицировано D39.131 п.2г (at-least-once; ⚠ уточнение дофикса P6: присваивание самолечит ПОВТОРНУЮ доставку, а не пропущенную — недодрейненный хвост новая попытка не перечитывает, её курсор начинается с размера журнала на допуске; строка PD-219); (в) принять `Ceiling.Scope` (book\|day — диагностика PD-157; resume дневного потолка не гонять в цикл) и outcome `ceiling\|stopped`; (г) dev-супервизор `outcomeOf` стейл (0/2/3); (д) порядок деплоя против деадлока v15 — строка 174 единого (решение ДО деплоя эмиттер-бинаря); watch: foreign-hello adoption при пре-существующем журнале в workdir | ИСПОЛНЕНО P6: (а) коды 4/5/10–19 через `ingest.OutcomeOf`, потолок = `paused` двумя каналами (PD-113 закрыт), интейк судит по коду (PD-196 закрыт), exit 5 без намерения = прерывание и перезапуск (PD-152 закрыт); (б) фолд присваиванием по `unit_resolutions` (миграция 00015); (в) StreamVersion 1.1, `Ceiling.Scope`, внутренняя причина `daily_ceiling`, резюм дневного потолка = 409 (PD-157 половина); (г) dev-супервизор читает ту же таблицу; watch воспроизведён и закрыт (PD-200); порядок деплоя — `deploy/README.md` + `tmplatformctl books --migratable` | приёмка D39.131 |
|
||||
| П-14 | **ИСПОЛНЕНО P6 (14.08).** Стройка интейка формы Б (ратификация D39.130): при создании книги платформа рендерит стартовый `book.yaml` из деплой-шаблона (шов `books.ErrNotProvisioned` уже назван P5); требования к полям — читать `backend/internal/config/book.go` как справочник, шаблон — деплой-артефакт оператора, не код; при приходе формы В (строка 170 единого, `tmctl init`) рендер заменяется вызовом движка | ИСПОЛНЕНО P6: рендер в ОДНОМ месте (`books.provision`), `TM_PLATFORM_BOOK_TEMPLATE`, работа с YAML-узлом (комментарии и незнакомые ключи оператора живы, значения — строки), `O_EXCL` (никогда не перезаписываем), битый шаблон = класс, который ЖДЁТ. Проверено настоящим `tmctl manifest` (3 главы) | ратификация D39.130 (приёмка P5) |
|
||||
| П-16 | **ИСПОЛНЕНО P6 (14.08).** Дев-сид стенда: тестовый пользователь, чтобы фронт разрабатывался на живой платформе, а не на своих моках** (слово владельца 14.08): одна команда/цель — тестовый аккаунт + грант админ-CLI + демо-книга через живой интейк, рецепт в `deploy/`; дев-вход БЕЗ внешнего OIDC-провайдера — сверить статус dev-профиля (П-6/PD-8; вставка сессии в Postgres руками, как в пробах, рецептом не считается); фронтовая сторона готова (дев-прокси `TM_PLATFORM`). Границы: фикстуры фронта остаются батарее его гейтов (детерминированные состояния — не работа стенда); полный снос `frontend/src/mock/` — триггер Ф-29, после читающей поверхности (SSE П-1 · главы/юниты · банк-экспорт = строка 169 единого) | ИСПОЛНЕНО P6: `tmplatformctl seed` (дев-вход → аккаунт → грант → загрузка через ЖИВОЙ `POST /v0/books` → ожидание конца интейка), дев-вход `TM_PLATFORM_DEV_LOGIN` с четырьмя гардами непроходимости в проде, рецепт страницей в `deploy/README.md`. Прогнано на стенде целиком | владелец 14.08 |
|
||||
| П-17 | **ИСПОЛНЕНО P7 (16–17.08).** **Читающая поверхность (пак P7)** — остаток П-1, названный отдельной строкой, потому что П-1 стала «частично исполнена» и её вес перестал что-либо значить: SSE-эндпоинт (события ПУШИТ воркер, фронт read-model не опрашивает), ручки глав/юнитов/замечаний, проекция банка. Хранилище под первую половину уже есть: `unit_resolutions` (миграция 00015) несёт диспозицию каждого разрешённого юнита — `shipped`/`flagged`/`reason`, — и проецировать её будет этот пак. ⚠ Ревизия области читается одной транзакцией со страницей (PD-163 закрыт P6) — это была предпосылка любого потока | ИСПОЛНЕНО P7: `listChapters`/`listUnits`/`listNotes`/`listBankTerms`/`streamBookEvents`/`getCapabilities` по канону 0.3.0 (⚠ `submitBankDecisions` СНЯТ 22.08 вместе с пер-термной моделью подписи — D39.144, слово владельца; см. PD-370) · модель ошибок с машинным `code` · условные чтения и сжатие · `Idempotency-Key` · `structure_version` · материализатор `internal/readmodel` (манифест + экспорт + сайдкар банка). НЕ вошло и отложено в P8: `updateBook`/`deleteBook`/`getRun`, экспорт (`createExport`/`getExport`), эскроу (П-18) | П-1, D39.84/85, контракт 14 |
|
||||
| П-15 | **ИСПОЛНЕНО P6 (14.08).** Платформенная половина шва эмиттера (движковая залендена D39.131, `9cfe080`) | ИСПОЛНЕНО P6: коды выхода движка 4/5/10–19 через `ingest.OutcomeOf`, потолок = `paused` двумя каналами (PD-113 закрыт), интейк судит по коду (PD-196 закрыт), exit 5 без намерения = прерывание и перезапуск (PD-152 закрыт); фолд `unit_done` ПРИСВАИВАНИЕМ по тройке (chapter, unit, wave) через `unit_resolutions` (миграция 00015; ратифицировано D39.131 п.2г, at-least-once); StreamVersion 1.1, `Ceiling.Scope` (book\|day), внутренняя причина `daily_ceiling`, резюм дневного потолка = 409 (PD-157 половина); dev-супервизор читает ту же таблицу; foreign-hello adoption воспроизведён и закрыт (PD-200); порядок деплоя — `deploy/README.md` + `tmplatformctl books --migratable`. ⚠ Граница присваивания (PD-219): оно самолечит ПОВТОРНУЮ доставку, а не пропущенную — недодрейненный хвост новая попытка не перечитывает, её курсор начинается с размера журнала на допуске | приёмка D39.131 |
|
||||
| П-14 | **ИСПОЛНЕНО P6 (14.08).** Стройка интейка формы Б (ратификация D39.130) | ИСПОЛНЕНО P6: рендер в ОДНОМ месте (`books.provision`), `TM_PLATFORM_BOOK_TEMPLATE`, работа с YAML-узлом (комментарии и незнакомые ключи оператора живы, значения — строки), `O_EXCL` (никогда не перезаписываем), битый шаблон = класс, который ЖДЁТ. Требования к полям — `backend/internal/config/book.go` как справочник; шаблон — деплой-артефакт оператора, не код. При приходе формы В (строка 170 единого, `tmctl init`) рендер заменяется вызовом движка. Проверено настоящим `tmctl manifest` (3 главы) | ратификация D39.130 (приёмка P5) |
|
||||
| П-16 | **ИСПОЛНЕНО P6 (14.08).** Дев-сид стенда: тестовый пользователь, чтобы фронт разрабатывался на живой платформе, а не на своих моках (слово владельца 14.08). Границы: фикстуры фронта остаются батарее его гейтов (детерминированные состояния — не работа стенда); полный снос `frontend/src/mock/` — триггер Ф-29, после читающей поверхности | ИСПОЛНЕНО P6: `tmplatformctl seed` (дев-вход → аккаунт → грант → загрузка через ЖИВОЙ `POST /v0/books` → ожидание конца интейка), дев-вход `TM_PLATFORM_DEV_LOGIN` с четырьмя гардами непроходимости в проде, рецепт страницей в `deploy/README.md`. Прогнано на стенде целиком | владелец 14.08 |
|
||||
| П-17 | **ИСПОЛНЕНО P7 (16–17.08).** Читающая поверхность — остаток П-1, названный отдельной строкой | ИСПОЛНЕНО P7: `listChapters`/`listUnits`/`listNotes`/`listBankTerms`/`streamBookEvents`/`getCapabilities` по канону 0.3.0 · модель ошибок с машинным `code` · условные чтения и сжатие · `Idempotency-Key` · `structure_version` · материализатор `internal/readmodel` (манифест + экспорт + сайдкар банка). ⚠ `submitBankDecisions` СНЯТ 22.08 вместе с пер-термной моделью подписи — D39.144, слово владельца; см. PD-370. **НЕ вошло и отложено в P8:** `updateBook`/`deleteBook`/`getRun`, экспорт (`createExport`/`getExport`), эскроу (П-18) | П-1, D39.84/85, контракт 14 |
|
||||
| П-18 | **Эскроу денег шва: write-ahead intent · `uncertain` · `closing`** (строка 136 единого бэклога) — денежный промт, сознательно НЕ взятый ни P5, ни P6. Здесь же закрывается PD-154 (`settled_at` между `Settle` и `MarkSettled`): половина эскроу рядом с проектируемым целым — второй, более слабый ответ на тот же вопрос | денежный промт | строка 136, PD-154 |
|
||||
| П-19 | **Конверсия свободного от склейки блока под `sqlc`** (решение владельца 20.08 «БЕРЁМ»; граница названа P8-FIX, PD-44). Пять файлов без единой склейки — `credits.go`(15) · `identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) = **42 места / 41 текст / 40 КОНВЕРТИРУЕМЫХ** (⚠ испр. D39.172: `observe.go` структурно не конвертируется — River мигрирует `river_job` сам; худший позиционный дрейф набора, семь `int64` подряд, остался рукописным): это весь кусок пакета, где инструмент силён и ничего не ломает. Остальные 25 склеенных мест недостижимы по построению (`lastRun` — девять потребителей, `nextRevisionOfThisBooksLibrary` — восемь), и это ровно та часть, где рантайм-ошибки и случались. Состав пака: `sqlc.yaml` + пин версии инструмента (как у `golangci-lint`) + генерённый код в дереве + гейт «сгенерённое актуально» + `WithTx` для одиннадцати денежных запросов, идущих внутри ЧУЖОЙ транзакции. ⚠ Класс «нет такой колонки» УЖЕ закрыт постоянным гейтом (`TestEverySQLStatementParsesAgainstTheMigratedSchema`, 162 оператора, включая склеенные), поэтому этот пак покупает типизированные скан-структуры и раннюю обратную связь, а не корректность. **РЕШЕНО владельцем 22.08: ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — изменений много, мешать их с чем-либо нельзя | отдельная сессия | PD-44, фикс-лист приёмки P7 п. 4, решение владельца 22.08 |
|
||||
| П-20 | **Хвост PD-162 после P8-FIX: холд прогона, чей движок не ответит НИКОГДА, возвращается только рукой.** Пак дал оператору и диагноз (`tmplatformctl runs --stalled`, гейдж `tm_platform_runs_stalled`), и терминальный вердикт (`run abandon [--release-hold]`), и это сознательная граница: автоматически решать про деньги прогона, о котором нельзя спросить, — та же ошибка «я не смог спросить = его нет», только со счётчиком впереди. Автоматический ответ на этот вопрос и есть эскроу — write-ahead intent · `uncertain` · `closing` (строка 136 единого бэклога, П-18). Пока эскроу нет, ручка остаётся ручной, и это записано, а не подразумевается | вместе с П-18 | P8-FIX |
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# platform — control plane (SaaS-слой)
|
||||
|
||||
Зона записи сессии «Платформа». P0 собран 04.08 (скелет: HTTP · сессии · схема read-model ·
|
||||
интерфейс ингеста), P1 — 05.08 (вход через OIDC · кредитный леджер · админ-CLI · деплой-юнит ·
|
||||
закрытие регистра дефектов). Направление зоны — `docs/PLATFORM_DIRECTION.md`, критерии приёмки —
|
||||
Зона записи сессии «Платформа». Что построено и каким паком — секция «Что здесь будет» ниже и
|
||||
шапка «Текущее состояние» зонного журнала. Направление зоны — `docs/PLATFORM_DIRECTION.md`, критерии приёмки —
|
||||
`docs/ENGINEERING_STANDARDS.md`, дефекты — `docs/DEFECT_REGISTER.md`, зонный журнал —
|
||||
`docs/platform-PROGRESS.md` (весь прогресс зоны здесь, решение владельца 04.08), стек —
|
||||
`docs/STACK_DECISIONS.md`.
|
||||
|
|
@ -10,13 +9,11 @@
|
|||
Батарея зоны: `make check` (build · vet · fmt · lint · test -race). `make vuln` и `make fuzz` —
|
||||
отдельными целями.
|
||||
|
||||
⚠ **Сколько у батареи условий — НЕ ЗДЕСЬ, и это правка 29.08.** Число жило в трёх местах сразу
|
||||
(здесь — два, `docs/ENGINEERING_STANDARDS.md` — три, `docs/STACK_DECISIONS.md` — четыре), и это самый
|
||||
дешёвый способ получить ложную приёмку: сессия, честно исполнившая §3.1 по этому файлу, объявит
|
||||
«скипов ноль» при красном тесте, о котором её версия списка не знала. **Единственный носитель —
|
||||
`docs/STACK_DECISIONS.md`, раздел «Гейты батареи»**; здесь его сознательно не дублируем. Коротко и
|
||||
без числа: без переменных окружения батарея МОЛЧА пропускает около трёхсот тестов и остаётся
|
||||
зелёной, поэтому «зелено» без сверки со списком условий не значит ничего.
|
||||
⚠ **Сколько у батареи условий — НЕ ЗДЕСЬ. Единственный носитель — `docs/STACK_DECISIONS.md`,
|
||||
раздел «Гейты батареи»**; здесь число сознательно не дублируем. Разошедшиеся копии дают ложную
|
||||
приёмку: сессия, честно исполнившая §3.1 по устаревшему списку, объявит «скипов ноль» при красном
|
||||
тесте, о котором её версия не знала. Без переменных окружения батарея МОЛЧА пропускает около
|
||||
трёхсот тестов и остаётся зелёной, поэтому «зелено» без сверки со списком условий не значит ничего.
|
||||
|
||||
⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` — обязательная настройка деплоя** (форма `zh>ru,ja>ru:unavailable`):
|
||||
её отвечает `GET /capabilities`, по ней же интейк отклоняет неподдерживаемую пару кодом
|
||||
|
|
@ -95,13 +92,12 @@ Prometheus; на контрактную поверхность они не вы
|
|||
Postgres; задание очереди выдаёт только РАЗРЕШЕНИЕ стартовать, а жизнь прогона ведёт
|
||||
реконсилятор, который читает мир (Postgres · журнал книги · маркер выхода) и не ждёт процесса;
|
||||
- читающая поверхность контракта — **есть (P7)**: дерево глав и пары с текстом, замечания, ЧТЕНИЕ
|
||||
банка (⚠ ЗАПИСЬ в банк снята 22.08 вместе с пер-термной моделью подписи — `PD-370`, D39.144: подпись
|
||||
это `resume`. ⚠⚠ **А вот «ручки поправить/добавить термин нет» — УЖЕ НЕВЕРНО, и это правка 29.08:**
|
||||
дверь `POST /books/{bookId}/bank/corrections` построена паком P9 и смонтирована — `httpapi/v0.go`
|
||||
в `contractSurface`, обработчик `httpapi/bank.go`, канон `14-api-contract/openapi.yaml`, акты
|
||||
D39.161/162/166, монтаж по `Capabilities.bank_corrections_enabled`. Снята была ПЕР-ТЕРМНАЯ ПОДПИСЬ,
|
||||
а не правка термина, и предложение выше склеивало две разные вещи), `GET /capabilities`, машинная модель ошибок (`code` + `request_id`), условные чтения
|
||||
(`ETag`/304) и сжатие JSON, `Idempotency-Key` на двух создающих вызовах;
|
||||
банка, `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; пользователь видит ОСТАТОК процентом — окон со сбросом больше нет,
|
||||
|
|
@ -126,12 +122,10 @@ Prometheus; на контрактную поверхность они не вы
|
|||
| `internal/pricing`, `internal/money` | шкала глав и целые микро-доллары |
|
||||
| `internal/metrics`, `internal/reqid`, `internal/jobs`, `internal/config`, `internal/gates` | телеметрия, id запроса, очередь, конфигурация, гейты тулчейна |
|
||||
|
||||
Каналы движка, которыми зона пользуется, — СЕМЬ (⚠ счёт в этой фразе отставал от списка ТРИЖДЫ: до
|
||||
29.08 она начиналась «три канала», перечисляя пять; на 30.08 говорила «пять» при шести; 31.08 —
|
||||
«шесть» при семи, забыв `bank-apply`. Поэтому здесь больше нет слов «и других нет»: список ниже
|
||||
СЧИТАЕТСЯ при правке, а притязание на исчерпывающесть снято как трижды не оправдавшееся —
|
||||
исчерпывающий перечень с атомарностью живёт в `docs/STACK_DECISIONS.md`, «Инвентарь каналов движка»,
|
||||
и правится вместе с ним): `tmctl manifest --json` (структура), `tmctl export --json --pairs`
|
||||
Каналы движка, которыми зона пользуется, — СЕМЬ. ⚠ Слов «и других нет» здесь нет намеренно:
|
||||
исчерпывающий перечень с атомарностью живёт в `docs/STACK_DECISIONS.md`, «Инвентарь каналов
|
||||
движка», и список ниже СЧИТАЕТСЯ при правке вместе с ним. `tmctl manifest --json` (структура),
|
||||
`tmctl export --json --pairs`
|
||||
(ТЕКСТ пар — единственный носитель), `<project_db>.bank.json` (банк), `tmctl status --json` (канал
|
||||
ремонта; с лендингом `6ec9f8a` он ещё и оценивает пере-проход ДО покупки), `events.jsonl` (поток) и —
|
||||
с лендингом пака «писатель книги» (D39.175) — `tmctl build` с файловым сайдкаром
|
||||
|
|
@ -166,7 +160,7 @@ live-сверка 04–05.08); общая записка по обоим нов
|
|||
|
||||
Коротко: 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 ·
|
||||
Postgres — **подключена и работает** (P4) · вход `x/oauth2` v0.36.0 + `go-oidc/v3` v3.20.0 ·
|
||||
`x/time` v0.15.0 для лимита на `/auth/login` · `govulncheck` отдельной целью.
|
||||
**Redis не заводим нигде** — зафиксировано как архитектурное «нет».
|
||||
|
||||
|
|
|
|||
|
|
@ -106,24 +106,25 @@ TM_PLATFORM_ENGINE_KEYS_PATH=/etc/tmplatform/engine-keys # KEY=VALUE, е
|
|||
грант по умолчанию НОЛЬ и начисляется руками — `tmplatformctl grant`. Строка `=5` в этом блоке
|
||||
включала обратно ровно тот самообслуживаемый безлимитный грант, который дефолт выключает.
|
||||
|
||||
⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` обязателен, и с акта 5 P7 правило ЖЁСТЧЕ, чем описывала прежняя
|
||||
редакция этого абзаца** (испр. 22.08 аудитом доков — она утверждала обратное: «интейк принимает
|
||||
любую»). Инстанс, который ПРИНИМАЕТ ЗАГРУЗКИ и не объявил ни одной ДОСТУПНОЙ пары, **не стартует
|
||||
вовсе** (`internal/config/config.go`, проверка `IntakeEnabled() && len(AvailablePairs()) == 0`);
|
||||
объявленная, но недоступная пара отклоняется интейком кодом `unsupported_pair`. То есть «не объявлено
|
||||
ничего» и «объявлена эта пара» — разные ответы, и деплой больше не может принять книгу, которую он
|
||||
способен только провалить.
|
||||
⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` обязателен, и правило ЖЁСТКОЕ** (акт 5 P7). Инстанс,
|
||||
который ПРИНИМАЕТ ЗАГРУЗКИ и не объявил ни одной ДОСТУПНОЙ пары, **не стартует вовсе**
|
||||
(`internal/config/config.go`, проверка `IntakeEnabled() && len(AvailablePairs()) ==
|
||||
0`); объявленная, но недоступная пара отклоняется интейком кодом `unsupported_pair`. То
|
||||
есть «не объявлено ничего» и «объявлена эта пара» — разные ответы, и деплой больше не
|
||||
может принять книгу, которую он способен только провалить.
|
||||
|
||||
## Шаблон книги (`TM_PLATFORM_BOOK_TEMPLATE`) — артефакт ОПЕРАТОРА
|
||||
|
||||
Загруженной книге нужен `book.yaml`, и пишет его платформа — ОДИН раз, при первом разборе, из этого
|
||||
шаблона (форма Б, D39.130). Дальше файл принадлежит оператору: платформа его не читает и никогда не
|
||||
перезаписывает (`O_EXCL`), поэтому правка потолка или пайплайна в конкретной книге переживает всё.
|
||||
Загруженной книге нужен `book.yaml`, и пишет его платформа — ОДИН раз, при первом
|
||||
разборе, из этого шаблона (форма Б, D39.130). Дальше файл принадлежит оператору:
|
||||
платформа его не читает и никогда не перезаписывает (`O_EXCL`), поэтому правка потолка
|
||||
или пайплайна в конкретной книге переживает всё.
|
||||
|
||||
Платформа подставляет ровно то, что знает только она: `book_id` · `title` · `source_lang` ·
|
||||
`target_lang` · `source_file`. Всё остальное едет из шаблона нетронутым — включая ключи, о которых
|
||||
эта версия платформы не слышала, и включая `genre`: он ушёл из контракта и из интейка в 0.3.0
|
||||
(Б-23), поэтому теперь это значение ОПЕРАТОРА и платформа его не трогает.
|
||||
Платформа подставляет ровно то, что знает только она: `book_id` · `title` ·
|
||||
`source_lang` · `target_lang` · `source_file`. Всё остальное едет из шаблона нетронутым
|
||||
— включая ключи, о которых эта версия платформы не слышала, и включая `genre`: он ушёл
|
||||
из контракта и из интейка в 0.3.0 (Б-23), поэтому теперь это значение ОПЕРАТОРА и
|
||||
платформа его не трогает.
|
||||
|
||||
```yaml
|
||||
# /srv/textmachine/book-template.yaml
|
||||
|
|
@ -139,77 +140,81 @@ ceilings:
|
|||
book_usd: 25
|
||||
```
|
||||
|
||||
⚠ **Пути внутри шаблона — АБСОЛЮТНЫЕ.** Движок резолвит относительный путь от каталога КНИГИ, а не
|
||||
от каталога шаблона, поэтому `../configs/pipeline.yaml` в шаблоне указывает на разные файлы для
|
||||
каждой книги и ни на один из них — правильно.
|
||||
⚠ **Пути внутри шаблона — АБСОЛЮТНЫЕ.** Движок резолвит относительный путь от каталога
|
||||
КНИГИ, а не от каталога шаблона, поэтому `../configs/pipeline.yaml` в шаблоне указывает
|
||||
на разные файлы для каждой книги и ни на один из них — правильно.
|
||||
|
||||
⚠ **`ceilings.day_usd` в шаблоне НЕ НУЖЕН — и в примере его нет намеренно** (решение владельца
|
||||
15.08). Трату прогона уже жёстко ограничивает купленный пользователем объём: платформа держит холд и
|
||||
передаёт движку `--ceiling-usd`, флаш к этому холду. Дневная ось поверх этого — второй лимит на ту же
|
||||
трату, и стоит он дороже, чем даёт: остановленный им прогон приезжает `paused` (не `failed`), но
|
||||
продолжить его платформа не может — `resume` отвечает 409, потому что следующая попытка упёрлась бы в
|
||||
тот же лимит в тот же день, а пользователю эта пауза необъяснима: лимит не его и он его не видит.
|
||||
⚠ **`ceilings.day_usd` в шаблоне НЕ НУЖЕН — и в примере его нет намеренно** (решение
|
||||
владельца 15.08). Трату прогона уже жёстко ограничивает купленный пользователем объём:
|
||||
платформа держит холд и передаёт движку `--ceiling-usd`, флаш к этому холду. Дневная
|
||||
ось поверх этого — второй лимит на ту же трату, и стоит он дороже, чем даёт:
|
||||
остановленный им прогон приезжает `paused` (не `failed`), но продолжить его платформа
|
||||
не может — `resume` отвечает 409, потому что следующая попытка упёрлась бы в тот же
|
||||
лимит в тот же день, а пользователю эта пауза необъяснима: лимит не его и он его не
|
||||
видит.
|
||||
|
||||
Движка это не касается — `ceilings.day_usd` у него остаётся, и вписать его в конкретную книгу руками
|
||||
оператор вправе. Именно на этот случай платформенная обработка `daily_ceiling`/409 из P6 и остаётся
|
||||
предохранителем: она делает такую остановку различимой и честной, а не дефолтом, который её вызывает.
|
||||
Движка это не касается — `ceilings.day_usd` у него остаётся, и вписать его в конкретную
|
||||
книгу руками оператор вправе. Именно на этот случай платформенная обработка
|
||||
`daily_ceiling`/409 из P6 и остаётся предохранителем: она делает такую остановку
|
||||
различимой и честной, а не дефолтом, который её вызывает.
|
||||
|
||||
Битый или отсутствующий шаблон — **беда деплоя, а не книги**: загрузки не отклоняются и не удаляются,
|
||||
книги ждут в `parsing` (метрика `tm_platform_books_in_intake{status="parsing"}`), в логе — ERROR с причиной. Починили файл — ближайший
|
||||
свип разбирает всё накопившееся.
|
||||
Битый или отсутствующий шаблон — **беда деплоя, а не книги**: загрузки не отклоняются и
|
||||
не удаляются, книги ждут в `parsing` (метрика
|
||||
`tm_platform_books_in_intake{status="parsing"}`), в логе — ERROR с причиной. Починили
|
||||
файл — ближайший свип разбирает всё накопившееся.
|
||||
|
||||
## Апгрейд ПЛАТФОРМЫ: миграция и бинарь едут вместе
|
||||
|
||||
⚠ **Схема и бинарь платформы — один шаг, а не два.** Миграция 00016 УДАЛЯЕТ колонки, которые прежний
|
||||
бинарь читает (`books.note_count`, `books.genre`, `chapters.heading`, таблица `notes`), поэтому
|
||||
инстанс старой сборки рядом с уже мигрированной базой отвечает 500 на КАЖДОЕ чтение книги.
|
||||
⚠ **То же повторяет 00021** (акт 5 P7): она сносит `chapters.units_done`, который прежний бинарь
|
||||
читает в проекции книги. Правило, значит, не «про 00016», а постоянное: любая миграция, снимающая
|
||||
колонку, делает перекрытие версий невозможным, и проверять это надо на каждой. Порядок:
|
||||
остановить старую версию → накатить миграцию (`TM_PLATFORM_MIGRATE=1` на одном инстансе или
|
||||
`goose` руками) → поднять новую. Роллинг-апгрейд с перекрытием версий на этом шаге не поддерживается;
|
||||
дефолт `TM_PLATFORM_MIGRATE` выключен именно поэтому — накат должен быть решением, а не побочным
|
||||
эффектом старта реплики.
|
||||
⚠ **Схема и бинарь платформы — один шаг, а не два.** Любая миграция, снимающая колонку,
|
||||
делает перекрытие версий невозможным: инстанс старой сборки рядом с уже мигрированной
|
||||
базой отвечает 500 на КАЖДОЕ чтение книги. Так было у 00016 (`books.note_count`,
|
||||
`books.genre`, `chapters.heading`, таблица `notes`) и у 00021 (`chapters.units_done`,
|
||||
акт 5 P7) — проверять надо на КАЖДОЙ. Порядок: остановить старую версию → накатить
|
||||
миграцию (`TM_PLATFORM_MIGRATE=1` на одном инстансе или `goose` руками) → поднять
|
||||
новую. Роллинг-апгрейд с перекрытием версий на этом шаге не поддерживается; дефолт
|
||||
`TM_PLATFORM_MIGRATE` выключен именно поэтому — накат должен быть решением, а не
|
||||
побочным эффектом старта реплики.
|
||||
|
||||
⚠ **Отредактированная миграция не перезапускается.** goose ключуется НОМЕРОМ и не хранит ни имени, ни
|
||||
контрольной суммы, так что правка уже применённого файла невидима: на стенде это лечится сносом базы,
|
||||
в проде — гейтом `internal/pgstore/migrations.sha256`, который делает правку выпущенной миграции
|
||||
видимой ревьюеру. Поймано живой пробой P7 (правка 00016 после её применения на стенде).
|
||||
⚠ **Отредактированная миграция не перезапускается.** goose ключуется НОМЕРОМ и не
|
||||
хранит ни имени, ни контрольной суммы, так что правка уже применённого файла невидима:
|
||||
на стенде это лечится сносом базы, в проде — гейтом
|
||||
`internal/pgstore/migrations.sha256`, который делает правку выпущенной миграции видимой
|
||||
ревьюеру. Поймано живой пробой P7 (правка 00016 после её применения на стенде).
|
||||
|
||||
## Апгрейд ДВИЖКА: порядок против деадлока (строка 174 единого бэклога)
|
||||
|
||||
Read-only команды движка отказывают файлу проекта СТАРЕЕ бинаря — **exit 13 и токен
|
||||
`schema_mismatch found=N expected=M` на stderr** (форма финализирована D39.134; прежняя цитата
|
||||
«schema vN … expects vM» в этом доке была МЁРТВОЙ — движок её не печатает), а платформа зовёт
|
||||
`status` перед каждым спавном и на расчёте денег. Значит выкат нового движка на хост с существующими
|
||||
книгами запирает их до миграции файла проекта.
|
||||
`schema_mismatch found=N expected=M` на stderr** (форма финализирована D39.134; прежняя
|
||||
цитата «schema vN … expects vM» в этом доке была МЁРТВОЙ — движок её не печатает), а
|
||||
платформа зовёт `status` перед каждым спавном и на расчёте денег. Значит выкат нового
|
||||
движка на хост с существующими книгами запирает их до миграции файла проекта.
|
||||
|
||||
⚠ **Проверено исполнением на стенде (P7):** проектная БД, отведённая на схему v14 при бинаре v15,
|
||||
даёт `tmctl status --json` → exit 13 с этим токеном; `tmctl migrate --config <book.yaml>` делает
|
||||
пред-миграционный бэкап и переводит v14 → v15; тот же `status` после неё — exit 0.
|
||||
⚠ **Проверено исполнением на стенде (P7):** проектная БД, отведённая на схему v14 при
|
||||
бинаре v15, даёт `tmctl status --json` → exit 13 с этим токеном; `tmctl migrate
|
||||
--config <book.yaml>` делает пред-миграционный бэкап и переводит v14 → v15; тот же
|
||||
`status` после неё — exit 0.
|
||||
|
||||
⚠ **Правило БЫЛО: эмиттер-бинарь не выкатывается, пока `tmctl migrate` не заленден** ($0-команда
|
||||
движка, открывающая файл на запись без прогона). ⚠⚠ **УСЛОВИЕ ВЫПОЛНЕНО — правка 29.08.** Глагол
|
||||
существует и заленджен: `backend/cmd/tmctl/migrate.go`, в диспетчере `main.go` он стоит в списке
|
||||
команд, и у него есть свой код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»). Правило
|
||||
блокировало выкат по причине, которой больше нет, — и это худший род блокировки, потому что снаружи
|
||||
он неотличим от осторожности. Порядок ниже действует; читать его как ДЕЙСТВУЮЩИЙ, а не как «когда
|
||||
приедет».
|
||||
⚠ **`tmctl migrate` СУЩЕСТВУЕТ и заленден** — $0-команда движка, открывающая файл
|
||||
проекта на запись без прогона: `backend/cmd/tmctl/migrate.go`, в диспетчере `main.go`,
|
||||
свой код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»). Порядок ниже —
|
||||
ДЕЙСТВУЮЩИЙ, а не «когда приедет».
|
||||
|
||||
⚠⚠ **ВТОРАЯ, НЕЗАВИСИМАЯ причина не выкатывать движок раньше платформы, дописана 31.08 (дофикс приёмки
|
||||
P12): подъём ФОРМЫ МАНИФЕСТА движка требует платформенного билда, иначе выбывает КАЖДАЯ новая книга.**
|
||||
Платформа с P12 сверяет `manifest_version` с известной ей формой (`ingest.KnownManifestVersion`,
|
||||
зеркало `manifestVersion` движка) и на незнакомую отвечает НЕ-деструктивным классом
|
||||
`parser_unavailable` — файл пользователя цел, и это осознанный выбор: без сверки переименованный ключ
|
||||
декодируется в нули, а ноль глав интейк читает как «источник прочли, книги нет», то есть УДАЛЯЕТ
|
||||
аплоад (`PD-213`). Но класс всё равно ТЕРМИНАЛЕН по бюджету попыток: движок впереди платформы ⇒ каждая
|
||||
загруженная книга уходит в `rejected` после пяти попыток. Симптом — интейк массово отклоняет при
|
||||
здоровом на вид движке; лечение — выкатить платформенный билд, знающий новую форму. Правило то же, что
|
||||
у схемы хранилища: **сначала платформа, потом движок**, и оба конца этого правила теперь записаны.
|
||||
⚠⚠ **ВТОРАЯ, НЕЗАВИСИМАЯ причина не выкатывать движок раньше платформы: подъём ФОРМЫ
|
||||
МАНИФЕСТА движка требует платформенного билда, иначе выбывает КАЖДАЯ новая книга.**
|
||||
Платформа с P12 сверяет `manifest_version` с известной ей формой
|
||||
(`ingest.KnownManifestVersion`, зеркало `manifestVersion` движка) и на незнакомую
|
||||
отвечает НЕ-деструктивным классом `parser_unavailable` — файл пользователя цел, и это
|
||||
осознанный выбор: без сверки переименованный ключ декодируется в нули, а ноль глав
|
||||
интейк читает как «источник прочли, книги нет», то есть УДАЛЯЕТ аплоад (`PD-213`). Но
|
||||
класс всё равно ТЕРМИНАЛЕН по бюджету попыток: движок впереди платформы ⇒ каждая
|
||||
загруженная книга уходит в `rejected` после пяти попыток. Симптом — интейк массово
|
||||
отклоняет при здоровом на вид движке; лечение — выкатить платформенный билд, знающий
|
||||
новую форму. Правило то же, что у схемы хранилища: **сначала платформа, потом движок**,
|
||||
и оба конца этого правила теперь записаны.
|
||||
|
||||
Порядок (действующий). ⚠ Ключевое: **`migrate` гоняется НОВЫМ бинарём** — старый уводит
|
||||
файл в свою же схему, то есть не делает ничего, и деадлок остаётся. Поэтому бинарь кладётся ДО
|
||||
миграции, а `TM_PLATFORM_ENGINE_BIN` переключается ПОСЛЕ.
|
||||
файл в свою же схему, то есть не делает ничего, и деадлок остаётся. Поэтому бинарь
|
||||
кладётся ДО миграции, а `TM_PLATFORM_ENGINE_BIN` переключается ПОСЛЕ.
|
||||
|
||||
```sh
|
||||
# 0. Закрыть приём. Между списком и миграцией окно, в которое пользователь может стартовать прогон
|
||||
|
|
@ -235,31 +240,29 @@ done
|
|||
systemctl start tmplatformd
|
||||
```
|
||||
|
||||
Почему шаг 1 — про три факта, а не про один. Остановленная попытка ждёт резюма на бинаре, к которому
|
||||
она ЗАПИНЕНА (строка 139): миграция уводит файл вперёд, старый бинарь его больше не откроет, и резюм
|
||||
пришлось бы форсить на новую сборку — риск пере-оплаты уже купленных вызовов. По той же причине
|
||||
блокирует НЕЗАКРЫТЫЙ холд: расчёт денег читает `committed_usd` тем же запиненным бинарём, и после
|
||||
миграции цифру уже не прочитать. Все три предиката запинены тестом
|
||||
(`TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList`) — каждый по отдельности, потому
|
||||
что «блокер, который тихо перестал блокировать» виден только на уже прошедшем апгрейде.
|
||||
Почему шаг 1 — про три факта, а не про один. Остановленная попытка ждёт резюма на
|
||||
бинаре, к которому она ЗАПИНЕНА (строка 139): миграция уводит файл вперёд, старый
|
||||
бинарь его больше не откроет, и резюм пришлось бы форсить на новую сборку — риск
|
||||
пере-оплаты уже купленных вызовов. По той же причине блокирует НЕЗАКРЫТЫЙ холд: расчёт
|
||||
денег читает `committed_usd` тем же запиненным бинарём, и после миграции цифру уже не
|
||||
прочитать. Все три предиката запинены тестом
|
||||
(`TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList`) — каждый по
|
||||
отдельности, потому что «блокер, который тихо перестал блокировать» виден только на уже
|
||||
прошедшем апгрейде.
|
||||
|
||||
Книга, застрявшая на `daily_ceiling` или на вечно незакрытом холде, блокирует апгрейд бессрочно, и
|
||||
выхода для оператора сегодня нет (строка регистра PD-217): такую книгу придётся либо дождаться, либо
|
||||
разрешить вручную в БД. Форсирующего флага у команды нет намеренно — он бы и был тем самым способом
|
||||
пере-оплатить купленное.
|
||||
Книга, застрявшая на `daily_ceiling` или на вечно незакрытом холде, блокирует апгрейд
|
||||
бессрочно, и выхода для оператора сегодня нет (строка регистра PD-217): такую книгу
|
||||
придётся либо дождаться, либо разрешить вручную в БД. Форсирующего флага у команды нет
|
||||
намеренно — он бы и был тем самым способом пере-оплатить купленное.
|
||||
|
||||
⚠ **Проверка исполнением ЖДЁТ.** Порядок записан, `tmplatformctl books --migratable` проверен на
|
||||
стенде (P6), но сам шаг `tmctl migrate` не гонялся: движковой команды ещё нет. Пометить сделанным
|
||||
можно только после прогона на стенде с настоящим `migrate`.
|
||||
|
||||
⚠ **Хвост, который НЕ построен и построен вслепую не будет:** движковый `migrate` придёт с
|
||||
машиноразличимой ошибкой «версия не совпала». Когда придёт — платформа сможет лечиться сама (поймала
|
||||
ошибку → `migrate`, если у книги нет открытых попыток → повтор), без стоп-мира. Строка регистра
|
||||
PD-201.
|
||||
⚠ **Хвост, который НЕ построен:** самолечение платформы — поймала `schema_mismatch` →
|
||||
зовёт `migrate`, если у книги нет открытых попыток → повтор, без стоп-мира. Строка
|
||||
регистра PD-201.
|
||||
|
||||
## Застрявшая работа: что оператор делает, когда свип не справляется
|
||||
|
||||
Пак P8-FIX завёл диагноз и вердикт для двух вещей, которые до него повторялись вечно и молча.
|
||||
Диагноз и терминальный вердикт для двух состояний, которые свип не закрывает сам
|
||||
(P8-FIX).
|
||||
|
||||
**Прогон, который реконсилятор не может закончить.** Признак: растёт
|
||||
`tm_platform_sweep_unfinished_total`, гейдж `tm_platform_runs_stalled` больше нуля.
|
||||
|
|
@ -270,50 +273,47 @@ tmplatformctl runs --stalled # только те, что свип не д
|
|||
# когда следующая попытка, что сказал движок, сколько держит
|
||||
```
|
||||
|
||||
⚠ **Колонка `PHASE` говорит, какая это половина, и лечение у них разное.** `live` — прогон ещё идёт,
|
||||
и первый вопрос к systemd. `settling` — прогон УЖЕ кончился, движка нет, а его расчёт не сходится:
|
||||
деньги пользователя заморожены в открытой резервации, и вопрос к systemd бессмыслен. До `PD-385`
|
||||
второй половины не было ни в этой таблице, ни в гейдже, ни у `run abandon`, — а ERROR на пороге
|
||||
слал оператора именно сюда.
|
||||
⚠ **Колонка `PHASE` говорит, какая это половина, и лечение у них разное.** `live` —
|
||||
прогон ещё идёт, и первый вопрос к systemd. `settling` — прогон УЖЕ кончился, движка
|
||||
нет, а его расчёт не сходится: деньги пользователя заморожены в открытой резервации, и
|
||||
вопрос к systemd бессмыслен.
|
||||
|
||||
Дальше решает человек, а не платформа: сперва спросить systemd (`systemctl --user status <юнит>`).
|
||||
Если процесс жив — остановить его и дать реконсилятору закрыть прогон штатно, это лучше во всём.
|
||||
Если движок не ответит уже никогда (каталог книги унесён, маунт отвалился):
|
||||
Дальше решает человек, а не платформа: сперва спросить systemd (`systemctl --user
|
||||
status <юнит>`). Если процесс жив — остановить его и дать реконсилятору закрыть прогон
|
||||
штатно, это лучше во всём. Если движок не ответит уже никогда (каталог книги унесён,
|
||||
маунт отвалился):
|
||||
|
||||
```sh
|
||||
tmplatformctl run abandon --run <id> --reason "почему" [--release-hold]
|
||||
```
|
||||
|
||||
Команда ОТКАЖЕТ, если попытка ещё называет юнит или несёт базовую линию траты, — закрывать прогон
|
||||
над живым движком нельзя. `--reason` обязателен: это единственная запись о том, почему оплаченный
|
||||
прогон объявлен законченным. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только
|
||||
попытка, не дошедшая до движка, значит она ничего не потратила); флаг решает лишь КОГДА — сразу или
|
||||
на ближайшем свипе. Он нужен, когда демон остановлен и свипа не будет.
|
||||
Команда ОТКАЖЕТ, если попытка ещё называет юнит или несёт базовую линию траты, —
|
||||
закрывать прогон над живым движком нельзя. `--reason` обязателен: это единственная
|
||||
запись о том, почему оплаченный прогон объявлен законченным. Холд возвращается ЦЕЛИКОМ
|
||||
в обоих случаях (к abandon допускается только попытка, не дошедшая до движка, значит
|
||||
она ничего не потратила); флаг решает лишь КОГДА — сразу или на ближайшем свипе. Он
|
||||
нужен, когда демон остановлен и свипа не будет.
|
||||
|
||||
⚠ **«На ближайшем свипе» стало правдой только с `PD-391`.** Раньше вердикт не снимал отсрочку
|
||||
попытки, а список расчёта по ней и фильтрует, — то есть для ВСЕЙ популяции, ради которой команда
|
||||
написана (пять неудач ⇒ отсрочка упёрлась в потолок 30 минут), холд оставался закрытым до получаса
|
||||
ПОСЛЕ действия оператора, и `tm_platform_oldest_open_hold_seconds` продолжал расти. Читалось это как
|
||||
«я сделал, не помогло».
|
||||
⚠ **Оговорка `PD-418`: ветвление `run abandon` идёт по `runs.finished_at`, а не по
|
||||
наличию осиротевшей попытки.** Значит команда лечит `settling`-строку только у прогона,
|
||||
который УЖЕ кончился. Живой прогон с нерассчитанной ПРЕДЫДУЩЕЙ попыткой ушёл бы в живую
|
||||
ветку: осиротевший холд команда не тронет, а ответит про процесс. Сегодня это состояние
|
||||
недостижимо — единственный не-тестовый путь ко второй открытой резервации, `reopen`,
|
||||
отказывается стартовать следующую попытку, пока холд предыдущей открыт, — и пак P12 его
|
||||
достижимее НЕ сделал (блокированная расплата теперь считается неудачей и видна, но
|
||||
рестарт по-прежнему не происходит, `PD-424`). Оговорка стоит здесь, потому что документ
|
||||
иначе обещает оператору то, чего код не делает, и разойдётся заметно, если этот
|
||||
инвариант когда-нибудь ослабнет. Долговечное лечение — ветвить по НАЛИЧИЮ осиротевшей
|
||||
попытки вместо `finished_at`, как уже делает settling-ветвь `StalledRuns`.
|
||||
|
||||
⚠ **Оговорка, дописанная паком P12 30.08 (`PD-418`): ветвление `run abandon` идёт по
|
||||
`runs.finished_at`, а не по наличию осиротевшей попытки.** Значит команда лечит `settling`-строку
|
||||
только у прогона, который УЖЕ кончился. Живой прогон с нерассчитанной ПРЕДЫДУЩЕЙ попыткой ушёл бы в
|
||||
живую ветку: осиротевший холд команда не тронет, а ответит про процесс. Сегодня это состояние
|
||||
недостижимо — единственный не-тестовый путь ко второй открытой резервации, `reopen`, отказывается
|
||||
стартовать следующую попытку, пока холд предыдущей открыт, — и пак P12 его достижимее НЕ сделал
|
||||
(блокированная расплата теперь считается неудачей и видна, но рестарт по-прежнему не происходит,
|
||||
`PD-424`). Оговорка стоит здесь, потому что документ иначе обещает оператору то, чего код не делает,
|
||||
и разойдётся заметно, если этот инвариант когда-нибудь ослабнет. Долговечное лечение — ветвить по
|
||||
НАЛИЧИЮ осиротевшей попытки вместо `finished_at`, как уже делает settling-ветвь `StalledRuns`.
|
||||
|
||||
**Строка `PHASE = settling` — та же команда, другой исход.** Прогон уже кончился, свипа, который
|
||||
довёл бы его расчёт, не будет никогда, поэтому `run abandon` закрывает деньги В СВОЕЙ транзакции
|
||||
независимо от флага: резервация закрывается, холд возвращается ЦЕЛИКОМ, прогон помечается
|
||||
рассчитанным. Целиком — потому что не хватает как раз движковой цифры (это и есть определение
|
||||
состояния), и суммы, которую платформа могла бы обосновать, не существует; альтернатива — деньги
|
||||
заморожены навсегда. Статус самого прогона НЕ переписывается: он уже закончился со своим исходом.
|
||||
Повторный вызов отвечает «its money is already closed», а не «нет такого прогона».
|
||||
**Строка `PHASE = settling` — та же команда, другой исход.** Прогон уже кончился,
|
||||
свипа, который довёл бы его расчёт, не будет никогда, поэтому `run abandon` закрывает
|
||||
деньги В СВОЕЙ транзакции независимо от флага: резервация закрывается, холд
|
||||
возвращается ЦЕЛИКОМ, прогон помечается рассчитанным. Целиком — потому что не хватает
|
||||
как раз движковой цифры (это и есть определение состояния), и суммы, которую платформа
|
||||
могла бы обосновать, не существует; альтернатива — деньги заморожены навсегда. Статус
|
||||
самого прогона НЕ переписывается: он уже закончился со своим исходом. Повторный вызов
|
||||
отвечает «its money is already closed», а не «нет такого прогона».
|
||||
|
||||
**Книга, чью читательскую поверхность не удаётся построить.** После пяти неудач долг списывается,
|
||||
книга остаётся с прежним текстом и попадает в список:
|
||||
|
|
@ -426,10 +426,6 @@ install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine/books
|
|||
Оператору, которому это нужно, юнит называет ровно две строки замены (`ProtectHome=tmpfs` +
|
||||
`BindPaths=`); других изменений не требуется.
|
||||
|
||||
⚠ Выбирающего путь кода ещё нет: `Supervisor.Workdir` задаёт вызывающий, а вызывающий — воркер,
|
||||
которого нет (строка 103 единого бэклога). Когда он появится, корень становится настройкой, и её
|
||||
дефолт — этот каталог.
|
||||
|
||||
## Чего здесь ещё нет
|
||||
|
||||
TLS и домен (перед юнитом предполагается edge-прокси), ограничитель соединений на edge,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -56,23 +56,17 @@
|
|||
|
||||
**Контракт-первичность:** поверхность = `docs/architecture/14-api-contract/openapi.yaml`; расхождение
|
||||
кода со спекой = дефект чей-то один: либо спека правится через ратификацию, либо код. Генерация
|
||||
типов/стабов из спеки (oapi-codegen — кандидат, ратификация при P1) предпочтительнее ручного дрифта.
|
||||
типов/стабов из спеки предпочтительнее ручного дрифта (`oapi-codegen` — КАНДИДАТ, решение за паком,
|
||||
который его возьмёт: `PLATFORM_DIRECTION.md` §3).
|
||||
|
||||
## 3. Критерии приёмки сессии (Definition of Done)
|
||||
|
||||
1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **СКОЛЬКО условий — смотреть в
|
||||
`docs/STACK_DECISIONS.md`, раздел «Гейты батареи», он единственный носитель** (правка 29.08:
|
||||
число жило в трёх файлах и они расходились — два здесь, три там, четыре в третьем, и сессия,
|
||||
честно исполнившая этот пункт по устаревшей копии, объявляла приёмку выполненной при красном
|
||||
тесте). Список ниже оставлен как ИСТОРИЯ поправки 22.08, а не как действующий счёт.
|
||||
⚠ **«Скипов ноль» требует ТРЁХ условий, а не
|
||||
одного** (испр. 22.08 — прежняя редакция называла только DSN и позволяла объявить приёмку
|
||||
выполненной с молча пропущенной третью батареи): `TM_PLATFORM_TEST_DSN` (схема) ·
|
||||
`TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (живой рендер конфигурации) ·
|
||||
ДОСТИЖИМЫЙ пользовательский менеджер systemd (`internal/runner`, `systemdOrSkip` — на хосте без
|
||||
logind-сессии три теста скипаются и «скипов 0» недостижимо в принципе, `PD-374`). Условие, которое
|
||||
на этом хосте не выполнено, сессия НАЗЫВАЕТ вместе с числом скипов — «зелёная батарея» без него не
|
||||
значит ничего. Порядок подъёма PG без root — `STACK_DECISIONS.md`.
|
||||
1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **СКОЛЬКО условий и какие — смотреть в
|
||||
`docs/STACK_DECISIONS.md`, раздел «Гейты батареи», он единственный носитель** (число жило в трёх
|
||||
файлах, они разошлись, и сессия, честно исполнившая этот пункт по устаревшей копии, объявляла
|
||||
приёмку выполненной при красном тесте). Условие, которое на этом хосте не выполнено, сессия
|
||||
НАЗЫВАЕТ вместе с числом скипов — «зелёная батарея» без него не значит ничего. Порядок подъёма
|
||||
PG без root — там же.
|
||||
2. Каждый деливерабл проверен ИСПОЛНЕНИЕМ (сервер поднят и опрошен, миграции применены дважды,
|
||||
констрейнты сработали поимённо, супервизор гонял настоящий процесс) — не чтением.
|
||||
3. **Мутации сажаются в КОПИИ дерева, и копия несёт КАНОН.** Норма изоляции — `D39.113`: правка в
|
||||
|
|
@ -86,9 +80,7 @@
|
|||
выходит «выжившей» и даёт ЛОЖНУЮ находку «константа не запинена». С каноном копия зелёная.
|
||||
Вторая половина того же правила: **вердикт посадки судится по ДЕЛЬТЕ против чистой базовой линии
|
||||
ТОЙ ЖЕ копии и по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи** — иначе известный флейк даёт
|
||||
ложное «пойман» и тихо теряет находку о недостающем пине. Записано сюда паком `P8-REVIEW` 24.08
|
||||
(`PD-395`) именно потому, что прежде рецепт жил ТОЛЬКО в промте ревью-пака, а промты после
|
||||
лендинга уезжают в `archive/` — и ловушка взводилась заново для каждой следующей сессии.
|
||||
ложное «пойман» и тихо теряет находку о недостающем пине (`PD-395`).
|
||||
|
||||
4. Заявленное в отчёте свойство несущего пути ОБЯЗАНО быть запинено тестом, который ловит мутацию
|
||||
этого свойства; сессия сама называет в отчёте, какой тест что пинит. Приёмка оркестратора сажает
|
||||
|
|
@ -100,8 +92,8 @@
|
|||
8. **Построил механизм — грепни ОТКРЫТЫЕ строки регистра по своим файлам.** Каждое совпадение
|
||||
получает диспозицию в отчёте: закрыть, сузить, пере-формулировать или оставить с причиной.
|
||||
Класс «строка `open`, а лекарство уже в дереве» стоил зоне порядка 10% открытых строк и
|
||||
отправлял следующий пак чинить построенное. Греп — по ПОЛНЫМ путям, не по именам файлов:
|
||||
замер приёмки 27.08 дал по именам 22 совпадения, почти все ложные (`main.go`, `runner.go`,
|
||||
`config.go` есть и в `platform/`, и в `backend/`), по полным путям — 2, и оба настоящие.
|
||||
отправлял следующий пак чинить построенное. Греп — по ПОЛНЫМ путям, не по именам (замер 27.08:
|
||||
по именам 22 совпадения, почти все ложные — `main.go`/`runner.go`/`config.go` есть и в
|
||||
`platform/`, и в `backend/`; по полным путям — 2, оба настоящие).
|
||||
Ратифицировано `D39.159` п. 7. ⚠ Правило симметрично: якорь строки, УБИТЫЙ твоим переездом,
|
||||
чинит тот, чей переезд его убил, и называет причину — не следующий читающий пак.
|
||||
|
|
|
|||
|
|
@ -34,10 +34,9 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
1. Ключ личности — пара `(provider, sub)` в таблице `identities`. Почта — только ПОДСКАЗКА для
|
||||
связывания и только при `email_verified` (Google прямо предупреждает: почта меняется, primary
|
||||
identifier из неё делать нельзя).
|
||||
2. ⚠ **Коллизия почты — дыра, найденная скептиком:** в `00001_identity.sql` `users.email` NOT NULL
|
||||
с уникальным индексом по `lower(email)`. Что происходит, когда `(google, subX)` приносит почту,
|
||||
уже занятую другим пользователем, и обновляем ли мы `users.email` при каждом входе — обязано быть
|
||||
решено ДО первого входа: именно здесь тихий баг связывания становится захватом аккаунта.
|
||||
2. Коллизия почты решена ДО первого входа (миграция `00005`): `users.email` — NULLABLE и БЕЗ
|
||||
уникального индекса, неизвестная пара всегда создаёт НОВЫЙ аккаунт. Цена и разбор —
|
||||
`STACK_DECISIONS.md` §9.
|
||||
3. Ротация идентификатора сессии на границе входа (защита от фиксации) — обязательна.
|
||||
4. ⚠ **Журнал входов** (время, провайдер, класс устройства/IP) и ручка «отозвать все мои сессии» —
|
||||
день-один для сервиса, продающего токены: таблица сессий чистится свипом и аудитом не является.
|
||||
|
|
@ -49,8 +48,8 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
**Оплаты нет и в бете не будет.** Первые аккаунты — пробные, ключи провайдеров
|
||||
предоплачены владельцем. ⚠ **Автоматического фри-тира на бете НЕТ** (слово владельца 16.08, D39.138
|
||||
п.2л, PD-104): дефолт `TM_PLATFORM_SIGNUP_GRANT_USD` = **0**, кредит начисляется руками
|
||||
(`tmplatformctl grant`). Прежние «$5 по умолчанию» в этом разделе — протухший текст: возврат гранта
|
||||
идёт ВМЕСТЕ с суточным агрегатным потолком, который его ограничивает, а тот принадлежит платежам. Платёжный провайдер не выбирается, не проектируется и в бэклог зоны как
|
||||
(`tmplatformctl grant`); возврат автоматического гранта идёт ВМЕСТЕ с суточным агрегатным потолком,
|
||||
который его ограничивает, а тот принадлежит платежам. Платёжный провайдер не выбирается, не проектируется и в бэклог зоны как
|
||||
работа не заходит; вернуться — когда появится решение продавать. ⚠ Прежняя редакция этого раздела
|
||||
несла вывод «покупателям из России платить нечем» — он был ВЫВЕДЕН из целевого языка перевода, а не
|
||||
установлен, и снят как необоснованный (владелец 05.08). Язык книги о географии плательщика не говорит.
|
||||
|
|
@ -65,9 +64,7 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
- Вопрос «продолжать ли автоматически после сброса лимитов» отпадает вместе с окнами. Осталось:
|
||||
после пополнения баланса прогон возобновляется явным действием (кнопка/админ), а не сам.
|
||||
|
||||
**Фри-тир = ГРАНТ в леджер, управляется из админки** (владелец 05.08). ⚠ Дефолт на бете — **НОЛЬ**
|
||||
(PD-104, слово владельца 16.08): начисление руками, возврат автоматических $5 — вместе с суточным
|
||||
агрегатным потолком, когда появятся платежи. Настраивается пер-аккаунт; «накинуть кредитов» = одна запись леджера типа `grant`, отдельного кода фри-тира не
|
||||
**Фри-тир = ГРАНТ в леджер, управляется из админки** (владелец 05.08). Настраивается пер-аккаунт; «накинуть кредитов» = одна запись леджера типа `grant`, отдельного кода фри-тира не
|
||||
существует. Это и есть стандартная механика (Modal и другие делают так же): начисление и есть весь
|
||||
фри-тир. Нужна минимальная админ-поверхность — защищённая ручка или CLI-команда, пишущая грант.
|
||||
|
||||
|
|
@ -98,33 +95,31 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
движок→платформа в приватную таблицу; наружу по-прежнему уходит только процент остатка.
|
||||
⚠ **Запрет, который обязан ехать вместе с механизмом:** ЭНФОРСМЕНТ на событии строить нельзя —
|
||||
иначе корректность квоты повиснет на доставке потока. Событие только показывает; останавливает
|
||||
потолок. **Носитель работы движка — строка 103 единого бэклога** (словарь событий); заводится
|
||||
оркестратором отдельной строкой, платформа её не пишет.
|
||||
потолок. **Построено:** событие `spend` в словаре потока — `internal/ingest/events.go` `TypeSpend`.
|
||||
|
||||
## 3. Стандарты и стек: что взять, что не брать
|
||||
|
||||
> ⚠⚠ **ВЕРХНИЙ СЛОЙ 22.08 — ЧИТАТЬ ПРЕЖДЕ ВСЕГО НИЖЕ. По `sqlc` действует НЕ то решение, что записано
|
||||
> в этом разделе:** владелец 20.08 сказал **БЕРЁМ** (D39.153 п.6а), а 22.08 уточнил — **отдельной
|
||||
> сессией, не внутри содержательного пака** (D39.154 п.10): правок много, мешать их с другой работой
|
||||
> нельзя. Граница взятия НАЗВАНА паком P8-FIX и она узкая: пять файлов без единой склейки —
|
||||
> `credits.go`(15) · `identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) =
|
||||
> **41 запрос**; остальное (25 склеек из 147, 15 фрагментов-констант, read-модель) для sqlc
|
||||
> недостижимо по построению. Носитель работы — `BACKLOG.md` **П-19**. Отказ P7 ниже остаётся как
|
||||
> история пака, а не как действующее решение. ⚠ Тем же паком построена ЗАМЕНА того, ради чего sqlc
|
||||
> ⚠⚠ **ВЕРХНИЙ СЛОЙ — ЧИТАТЬ ПРЕЖДЕ ВСЕГО НИЖЕ. По `sqlc` действует НЕ то решение, что записано
|
||||
> в этом разделе:** владелец 20.08 сказал **БЕРЁМ** (D39.153 п.6а), 22.08 уточнил — отдельной
|
||||
> сессией (D39.154 п.10), и 29.08 пак **принят и заленджен** (`D39.172`): сорок запросов пяти файлов
|
||||
> `pgstore` вынесены в `internal/pgstore/queries/`, пин 1.31.1, `sqlc diff` в `make check`. Граница
|
||||
> узкая по построению, а не по вкусу: свободный от склейки блок; остальное (25 склеек из 147, 15
|
||||
> фрагментов-констант, read-модель) для sqlc недостижимо. Устройство, оговорки и цена —
|
||||
> `STACK_DECISIONS.md`, строка «Кодоген SQL». Отказ P7 ниже остаётся как история пака, а не как
|
||||
> действующее решение. ⚠ Тем же паком построена ЗАМЕНА того, ради чего sqlc
|
||||
> звали: гейт `pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema` планирует ЖИВЫМ Postgres
|
||||
> каждый собранный запрос пакета против мигрированной схемы — то есть покрывает и склейки, которых
|
||||
> sqlc не видит, и именно в них случились оба рантайм-падения зоны.
|
||||
>
|
||||
> ⚠ **ПЕРЕ-ПОДПИСАНО D39.132 п.2б** (баннер «ратификация за оркестратором» снят как отработанный):
|
||||
> `oapi-codegen` — КАНДИДАТ, решение за паком, который его берёт; `sqlc` привязан к P7; факт про River
|
||||
> поправлен. **Решение P7 по обоим — НЕ БРАТЬ, с доводом, а не молчанием:**
|
||||
> ⚠ **ПЕРЕ-ПОДПИСАНО D39.132 п.2б:** `oapi-codegen` — КАНДИДАТ, решение за паком, который его
|
||||
> берёт. **Решение P7 по обоим — НЕ БРАТЬ, с доводом, а не молчанием (история пака P7, 20.08):**
|
||||
>
|
||||
> - **`sqlc`.** Читающая поверхность добавила ~15 запросов, и ровно они — те, которые sqlc не
|
||||
> генерирует: запрос страницы и её ревизии живёт в ОДНОЙ транзакции с курсором, водяным знаком и
|
||||
> агрегатами первой страницы (`internal/pgstore/readmodel.go`), то есть выигрыш был бы на
|
||||
> `select … where id = $1`, которых в паке единицы. Цена реальна: денежные типы требуют `overrides`,
|
||||
> а генерённый слой пришлось бы всё равно оборачивать проекцией. Строка PD-44 остаётся открытой —
|
||||
> это отказ ЭТОГО пака, а не отмена направления.
|
||||
> а генерённый слой пришлось бы всё равно оборачивать проекцией. Строка PD-44 тогда осталась
|
||||
> открытой — это был отказ ЭТОГО пака, а не отмена направления.
|
||||
> - **`oapi-codegen`.** Довод «на P7 ручек станет больше, значит дешевле сейчас» проверен фактом: 20
|
||||
> операций контракта, из них построено 14, и КАЖДАЯ требует рукописной проекции read-модели в
|
||||
> контрактные словари (`internal/httpapi/project.go`) — генератор даёт имена полей, а переводит
|
||||
|
|
@ -132,22 +127,6 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
> генератор игнорирует, и исполнителем правила остаётся констрейнт БД. Взамен пак поставил ДРУГОЙ
|
||||
> гейт от дрейфа — пофайловый дифф проводных структур против required-списка канона, исполненный
|
||||
> тестами (`httpapi.Test*CarriesEveryRequiredField`).
|
||||
>
|
||||
> - **`oapi-codegen` («ВЗЯТЬ — доказано исполнением»): НЕ ВЗЯТ.** Ручки P4 и P5 (`internal/httpapi/v0.go`)
|
||||
> написаны руками, проводные структуры выписаны полем в поле. `ENGINEERING_STANDARDS:59` при этом
|
||||
> называет его «кандидатом при P1» — то есть два дока зоны говорят про один инструмент разное.
|
||||
> Довод зоны ЗА пере-подпись в «кандидат»: доказательство исполнением 05.08 нашло и половину
|
||||
> против — генератор слеп к 3.1-условиям (`if action=promote → dst`), поэтому исполнителем правила
|
||||
> всё равно остаётся констрейнт БД, а сгенерированные типы всё равно требуют рукописной проекции.
|
||||
> На пяти ручках выигрыш — сверка имён полей; он реален, но это НЕ «доказано, берём». Довод ПРОТИВ
|
||||
> пере-подписи: читающая поверхность P7 добавит больше ручек, чем есть сейчас, и там цена дрейфа
|
||||
> растёт — тогда взять его дешевле сейчас, чем потом.
|
||||
> - **`sqlc` («до первого хендлера»): НЕ ВЗЯТ** — PD-44 открыт с P1, хендлеры P4/P5 приехали без него.
|
||||
> Довод зоны: срок «до первого хендлера» уже нарушен фактом, и повторять его — значит писать
|
||||
> заведомо неисполняемое; честная форма — либо привязать к П-1/P7 (там и появляется масса запросов,
|
||||
> ради которых он брался), либо снять.
|
||||
> - **`River (в go.mod не заводить, пока не подключён)`: УСТАРЕЛО как факт** — подключён с P4 и
|
||||
> мигрируется своим мигратором (`STACK_DECISIONS` §19). Правится ниже как факт, а не как решение.
|
||||
|
||||
Стек менять не надо: правильные куски уже взяты (pgx · goose · River · stdlib ServeMux/CSRF · slog ·
|
||||
опаковые сессии). Добавить четыре вещи, пока репозиторий — скелет:
|
||||
|
|
@ -155,25 +134,16 @@ hosted IdP (связка PII + доступность, а свою сессию
|
|||
| Добавление | Пин | Статус |
|
||||
|---|---|---|
|
||||
| OIDC-вход | x/oauth2 v0.36.0 + go-oidc/v3 v3.20.0 | ратифицировано (§1) |
|
||||
| Кодоген сервера из ратифицированной спеки OpenAPI 3.1 | `oapi-codegen/v2` v2.8.0 (17.07.2026) | **НЕ БРАТЬ — решение P7, пере-подписано `D39.132` п.2б** (баннер выше несёт довод). ⚠ **Правка 29.08:** в этой клетке стояло «**ВЗЯТЬ** — доказано исполнением 05.08», и это ПРОТИВОРЕЧИЛО баннеру того же файла, который говорит «Решение P7 по обоим — НЕ БРАТЬ». Замер 05.08 остаётся верным и лежит ниже — он доказал, что инструмент РАБОТАЕТ, а не что его берут; слово вердикта было прочитано из замера. Сегодняшний статус: КАНДИДАТ, решение за паком, который его возьмёт |
|
||||
| ~~`sqlc` для денежных/квотных таблиц~~ → `sqlc` для поверхности контрактных ручек и read-model | v1.31.1 | **ПЕРЕСМОТРЕНО приёмкой P1 (05.08), доказано исполнением** — см. абзац ниже. Денежный пакет остаётся рукописным. ⚠ **НЕ ИСПОЛНЕНО на 14.08**, срок «до первого хендлера» пройден — см. баннер статуса выше (PD-44) |
|
||||
| Кодоген сервера из ратифицированной спеки OpenAPI 3.1 | `oapi-codegen/v2` v2.8.0 (17.07.2026) | **КАНДИДАТ** — решение за паком, который его возьмёт (`D39.132` п.2б; отказ P7 и его довод — в баннере выше). Замер 05.08 ниже доказал, что инструмент РАБОТАЕТ, а не что его берут |
|
||||
| `sqlc` на свободный от склейки блок `pgstore` (включая денежный) | v1.31.1 | **ВЗЯТ и заленджен 29.08** (`D39.172`, PD-44 закрыт) — устройство и границы в `STACK_DECISIONS.md`, строка «Кодоген SQL» |
|
||||
| `golang.org/x/time/rate` | v0.15.0 | лимиты в процессе; долговечные пер-пользовательские — в Postgres |
|
||||
|
||||
**Пересмотр по `sqlc` (приёмка P1, 05.08, доказано исполнением вне репозитория).** Первая редакция
|
||||
этой строки требовала взять `sqlc` ДО денежных таблиц. Таблицы приехали без него (PD-44), и вопрос
|
||||
пришёл на ратификацию. Проверено: sqlc v1.31.1 читает все goose-миграции зоны (на момент пробы их было семь; `00008` приехала позже и пробу не проходила) и генерирует под
|
||||
`pgx/v5` код, почти совпадающий с рукописным (`:execrows` → `RowsAffected`, параметры структурой) —
|
||||
инструмент на этой схеме РАБОТАЕТ. Две трения, обе увидены исполнением: (1) его анализатор отвергает
|
||||
запрос, который Postgres принимает (неквалифицированный `user_id` в коррелированных подзапросах) —
|
||||
то есть переход означает правку существующего SQL, а не обёртку; (2) колонки типизуются как
|
||||
`pgtype`/`int64`, поэтому `money.MicroUSD` на границе теряется без блока `overrides` — а единый
|
||||
денежный тип и есть то, ради чего заведены PD-15/PD-39.
|
||||
|
||||
**Решение: денежный пакет НЕ переписывать.** Он только что прошёл ревью и имеет батарею против
|
||||
живой БД; обмен отревьюенного кода на сгенерированный без единого нового теста ничего не покупает.
|
||||
`sqlc` берётся на поверхность контрактных ручек и read-model (П-1), где запросов много и они
|
||||
меняются вместе со схемой — там он ловит именно свой класс: запрос ссылается на колонку, которую
|
||||
унесла миграция. Блок `overrides` для денежных колонок пишется тогда же, до первого хендлера.
|
||||
⚠ **Две трения `sqlc`, увиденные исполнением на пробе 05.08 (тот же пин 1.31.1, что взят):** его анализатор
|
||||
отвергает запрос, который Postgres принимает (неквалифицированный `user_id` в коррелированных
|
||||
подзапросах) — переход означает правку существующего SQL, а не обёртку; и колонки типизуются как
|
||||
`pgtype`/`int64`, поэтому `money.MicroUSD` на границе теряется без блока `overrides` (он и стоит в
|
||||
`platform/sqlc.yaml` подстановкой `*.*_micro_usd`) — а единый денежный тип и есть то, ради чего
|
||||
заведены PD-15/PD-39.
|
||||
|
||||
**Доказательство исполнением по кодогену (05.08, $0, вне репозитория):** `oapi-codegen` v2.8.0 в
|
||||
режиме `std-http-server` + `strict-server` на ратифицированной копии `openapi.yaml` (983 строки,
|
||||
|
|
@ -262,8 +232,8 @@ cgroup-поддереве платформы, `KillMode=mixed`, `Restart=on-fail
|
|||
К-12) — это пользовательский поток, а не книжный.
|
||||
9. **SSE поверх HTTP/1.1** упирается в лимит ~6 соединений на источник при нескольких вкладках —
|
||||
на dev-стенде это выглядит как загадочное зависание; одна строка в доке стенда снимает класс.
|
||||
10. **Персист манифеста глав (строка 100 единого бэклога) — несущий для латентности чтения:** только
|
||||
он держит чтения на read-модели вместо 1.4–1.5 с ре-ингеста движка на книге 23 МБ.
|
||||
10. **Персист манифеста глав — ИСПОЛНЕН** (таблица `chapters`, миграция `00002_readmodel.sql`):
|
||||
только он держит чтения на read-модели вместо 1.4–1.5 с ре-ингеста движка на книге 23 МБ.
|
||||
|
||||
Бюджеты (в батарею фронта по мере появления экранов): DOM ≤1400 узлов на тяжёлом экране ·
|
||||
виртуализованный список ≤60 строк независимо от размера коллекции · медиана кадра прокрутки ≤17 мс,
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@
|
|||
> таблица уходит оркестратору вместе с деревом.
|
||||
>
|
||||
> Общая записка по обоим новым сервисам — `frontend/docs/STACK_DECISIONS.md` §5 (02.08). Здесь —
|
||||
> платформенная часть с датами релизов и сверкой. Что изменилось за два дня: три библиотечных пина
|
||||
> §5 (pgx · goose · River) на 04.08 всё ещё последние; по Go последним патчем стенда идёт **1.26.6**
|
||||
> (13.08, поднят security-адвизори — см. таблицу) — floor `go.mod` оставлен общим с движком.
|
||||
> платформенная часть с датами релизов и сверкой; floor `go.mod` оставлен общим с движком, тулчейн
|
||||
> сборки поднят выше — см. таблицу.
|
||||
|
||||
## Пины
|
||||
|
||||
| Что | Пин | Релиз пина | Зачем нам |
|
||||
|---|---|---|---|
|
||||
| Go (язык, `go.mod`) | **1.26.4** | 02.06.2026 | Тот же floor, что у движка (`backend/go.mod`) — общий стенд собирает оба модуля одним тулчейном |
|
||||
| Go (тулчейн сборки, `make version-check` + `toolchain` в `go.mod`) | **≥1.26.6** | 13.08.2026 | Поднят с 1.26.5 не по вкусу, а по `make vuln`: база адвизори опубликовала пять уязвимостей stdlib против 1.26.5 — `net/http`, `crypto/tls`, `net/url`, `encoding/xml`, `encoding/asn1` (GO-2026-6218/6090/6089/6088/5972), все закрыты в 1.26.6, и две трассируются в пути, которые эта служба зовёт (`pgstore.Open → pgx.ParseConfig → asn1.Unmarshal`). На 1.26.6 батарея зелёная и скан чист. ⚠ Как floor РЕАЛЬНО держится (первая редакция этого подъёма не держала ничего — регекс `go1\.26\.([5-9]…)` принимал ту самую 1.26.5, а `GO_MIN_VERSION` жил только в echo): `make version-check` СРАВНИВАЕТ версии (`sort -V`, префиксы `rc`/`devel` отвергаются), и `go.mod` несёт `toolchain go1.26.6` — его читает всякая сборка, даже мимо make: при `GOTOOLCHAIN=auto` хост скачает нужный тулчейн, при `=local` остановится с ошибкой. Само сравнение запинено (`internal/gates`) |
|
||||
| Go (тулчейн сборки, `make version-check` + `toolchain` в `go.mod`) | **≥1.26.6** | 13.08.2026 | Поднят с 1.26.5 не по вкусу, а по `make vuln`: база адвизори опубликовала пять уязвимостей stdlib против 1.26.5 — `net/http`, `crypto/tls`, `net/url`, `encoding/xml`, `encoding/asn1` (GO-2026-6218/6090/6089/6088/5972), все закрыты в 1.26.6, и две трассируются в пути, которые эта служба зовёт (`pgstore.Open → pgx.ParseConfig → asn1.Unmarshal`). На 1.26.6 батарея зелёная и скан чист. ⚠ Как floor РЕАЛЬНО держится: `make version-check` СРАВНИВАЕТ версии (`sort -V`, префиксы `rc`/`devel` отвергаются), и `go.mod` несёт `toolchain go1.26.6` — его читает всякая сборка, даже мимо make: при `GOTOOLCHAIN=auto` хост скачает нужный тулчейн, при `=local` остановится с ошибкой. Само сравнение запинено (`internal/gates`) |
|
||||
| PostgreSQL | **18.x** (проверено на 18.4), floor **16** | 18.4 — май 2026 | 18 — текущая мажорная (19 в бете, в прод не берём); floor 16, потому что River тестируется на трёх последних мажорных |
|
||||
| HTTP | stdlib `net/http` + `ServeMux` | — | Роутер-библиотека не нужна: `ServeMux` с 1.22 умеет метод+wildcards, а `Request.Pattern` даёт лог по маршруту, не по пути |
|
||||
| CSRF | stdlib `http.CrossOriginProtection` | Go 1.25 | Ровно тот механизм, что описан в §5 (Sec-Fetch-Site → Origin), теперь в тулчейне — свой велосипед не пишем |
|
||||
|
|
@ -53,14 +52,10 @@
|
|||
|
||||
## Что решено сессией P1 (05.08)
|
||||
|
||||
8. **Миграции append-only, БЕЗ исключений — включая «до первого деплоя».** Первая редакция этого
|
||||
пункта разрешала править их на месте, пока «ни одна среда их не применяла». Это опровергнуто
|
||||
исполнением: goose записывает только НОМЕР (ни имени, ни хеша), поэтому база, доехавшая до
|
||||
версии 3, на новом наборе рапортует «migrations applied» и не получает ни одной новой таблицы,
|
||||
а `DownTo` на ней ломается навсегда. Дев-воркфлоу из этого же документа создаёт ровно такую
|
||||
среду. Поэтому выпущенные `00001`–`00003` возвращены байт-в-байт, а всё новое приехало
|
||||
отдельными номерами (`00004` индексы · `00005` вход · `00006` снятие черновика `usage_windows` ·
|
||||
`00007` кредиты · `00008` `auth_states.issuer` и `.start_id`). Гейт, которого не хватало:
|
||||
8. **Миграции append-only, БЕЗ исключений — включая «до первого деплоя».** Причина: goose
|
||||
записывает только НОМЕР (ни имени, ни хеша), поэтому база, доехавшая до версии 3, на новом
|
||||
наборе рапортует «migrations applied» и не получает ни одной новой таблицы, а `DownTo` на ней
|
||||
ломается навсегда. Дев-воркфлоу из этого же документа создаёт ровно такую среду. Гейт:
|
||||
`migrations.sha256` + тест
|
||||
`TestReleasedMigrationsAreUnchanged` — чтобы изменить выпущенную миграцию, надо осознанно
|
||||
изменить строку в манифесте, где это видно ревьюеру. Апгрейд со старого релиза проверен
|
||||
|
|
@ -151,13 +146,6 @@
|
|||
сессия жила бы до свипа) — поэтому сессия, протухшая по бездействию и подметённая, теряет поток
|
||||
с опозданием до часа. К этому моменту любой другой её запрос — 401.
|
||||
|
||||
⚠⚠ **ЭРРАТА 24.08 (`P8-REVIEW`, `PD-379`) СНЯТА 29.08 паком P11: её собственное условие
|
||||
наступило.** Эррата объявляла обещание «мгновенный отзыв» ложным для этого канала и говорила,
|
||||
что абзацы политики остаются как есть **до решения по `PD-379`**. Дефект закрыт кодом и доказан
|
||||
двумя раздельными живыми сценариями, поэтому размен «нет лимита одновременных сессий В ОБМЕН на
|
||||
мгновенный отзыв» оплачен, и абзац 7.1.2 выше снова описывает дерево. Галочка `ASVS 5.0 7.4.1` в
|
||||
`docs/archive/platform-PROGRESS-P0-P3.md` снимается этим же — как и обещал `D39.159` §6.
|
||||
|
||||
**7.1.3 / 7.6.1, согласование с федеративной сессией.** Наша сессия живёт СВОЕЙ жизнью:
|
||||
RP-initiated logout и back-channel logout не реализованы. Следствия названы прямо: выход из
|
||||
Google не завершает нашу сессию, и отзыв доступа на стороне Google — тоже. Единственные границы
|
||||
|
|
@ -242,7 +230,7 @@
|
|||
|
||||
Порядок утверждается ПРЯМЫМ пином, а не конкурентным прогоном: тест держит замок книги, дожидается, пока операция реально заблокируется, и проверяет строку попытки через `for update nowait`. Конкурентная проба оставлена, но она пробует — посадка «снять книгу-первой из `RestartRun`» её пережила, потому что рестарт берёт замок один раз за прогон.
|
||||
|
||||
23. **«Ошибка материализации» — два разных факта, и различает их `runs.quarantines`.** Транзиентное — повтор следующим свипом; сюда входят не только дедлок и сериализация (`40P01`/`40001`), но и всё, во что превращается ШТАТНЫЙ рестарт управляемого Postgres: класс 08, `57P01`/`57P02`/`57P03`, `pgconn.SafeToRetry`, любой `net.Error`. Первая редакция знала только первые два, и рестарт базы карантинил проекцию живого платного прогона навсегда — снятия карантина в дереве нет (найдено ре-чеком V2, исполнением). Граница держится на типах: ошибка чтения ФАЙЛА — `*fs.PathError`, а он `net.Error` не удовлетворяет; пропасть, конфликт payload, битая строка — карантин ПОПЫТКИ (её проекции), прогон при этом продолжается и продолжает платить. До различения дедлок, который разрешился сам, ослеплял проекцию живого платного прогона навсегда.
|
||||
23. **«Ошибка материализации» — два разных факта, и различает их `runs.quarantines`.** Транзиентное — повтор следующим свипом; сюда входят не только дедлок и сериализация (`40P01`/`40001`), но и всё, во что превращается ШТАТНЫЙ рестарт управляемого Postgres: класс 08, `57P01`/`57P02`/`57P03`, `pgconn.SafeToRetry`, любой `net.Error`. ⚠ Снятия карантина в дереве нет, поэтому ошибочный карантин необратим и ослепляет проекцию живого платного прогона навсегда. Граница держится на типах: ошибка чтения ФАЙЛА — `*fs.PathError`, а он `net.Error` не удовлетворяет; пропасть, конфликт payload, битая строка — карантин ПОПЫТКИ (её проекции), прогон при этом продолжается и продолжает платить.
|
||||
|
||||
## Что решено сессией P5 (11.08) — загрузка книги, стоп/резюм, наблюдаемость
|
||||
|
||||
|
|
@ -340,8 +328,7 @@
|
|||
безусловная запись возвращает любой из трёх.
|
||||
|
||||
35. **«Есть ли у пайплайна редактор» — свойство КНИГИ, от объявления движка; но фактов ДВА, и в этом
|
||||
вся правка (пере-подписано паком P12, 30.08, по решению владельца D39.165 §2; прежняя редакция
|
||||
ниже).** Движок называет форму сам: пайплайн без редактора даёт волне `edit` знаменатель ноль
|
||||
вся правка (пере-подписано по решению владельца D39.165 §2).** Движок называет форму сам: пайплайн без редактора даёт волне `edit` знаменатель ноль
|
||||
(`beginWaves`). Читать это с последнего ПРОГОНА по-прежнему НЕЛЬЗЯ — последний прогон самый
|
||||
новый, а только что допущенный ещё ничего не объявил, и `chapters_done` падал в ноль в момент
|
||||
допуска (это часть правила не менялась).
|
||||
|
|
@ -374,19 +361,8 @@
|
|||
(`PD-404`). Решение владельца 28.08: смена формы конвейера — СОБЫТИЕ КНИГИ, как пере-нарезка, и
|
||||
счёт легально пересчитывается на границе; `shape_epoch` — то, чем клиент отличает законный
|
||||
пересчёт от хода назад (канон **0.9.0**, поле непрозрачное, саму форму на провод не выносим).
|
||||
⚠ **Прежняя редакция ЭТОГО абзаца (30.08) утверждала обратное — «полосу прогона это НЕ ломает,
|
||||
эпоха всегда объявление её собственного прогона» — и была НЕВЕРНА; заменена 31.08 по замеру.**
|
||||
Довод звучал убедительно и не выдержал исполнения: у одного прогона две попытки, и вторая
|
||||
объявляет СВОЮ форму, поэтому эпоха между ними ходит вниз. `PD-401` при этом действительно был
|
||||
не про то, какой флаг выбирает волну, а про парность числителя и базлайна своей колонки — она не
|
||||
тронута, базлайны по-прежнему берутся в `StartRun` и не пере-снимаются. Но парности мало:
|
||||
монотонность полосы держит именно МОНОТОННЫЙ флаг.
|
||||
> Прежняя формулировка (до 30.08), оставлена с датой и причиной: «свойство КНИГИ
|
||||
> (`books.edit_wave`), монотонное… Флаг только растёт: книга, прошедшая редактирующий пайплайн,
|
||||
> остаётся такой, иначе полу-сделанные главы начали бы считаться сделанными. От этого же флага
|
||||
> зависит БАЗЛАЙН полосы прогона». Причина замены: правило было верно для СОХРАННОСТИ счёта и
|
||||
> ложно для его ПРАВИЛЬНОСТИ — монотонность защищала направление «редактора добавили» и морозила
|
||||
> «редактора убрали», и обе стороны стояли открытыми строками регистра.
|
||||
⚠ Парности числителя и базлайна (`PD-401`, базлайны берутся в `StartRun` и не пере-снимаются)
|
||||
для полосы МАЛО: её монотонность держит именно монотонный флаг.
|
||||
|
||||
36. **Утверждение про АТОМАРНОСТЬ пишется через `xmin`.** Пин, проверяющий конечное состояние, не
|
||||
видит выноса записи из транзакции во второй оператор — состояние то же, меняется окно. `xmin`
|
||||
|
|
@ -396,8 +372,7 @@
|
|||
|
||||
## Инвентарь каналов движка (собран паком P8-FIX 22.08 ЧТЕНИЕМ кода движка)
|
||||
|
||||
> Перенесено оркестратором №18 при лендинге P8-FIX из отчёта пака: это был его артефакт §4.4, и
|
||||
> ДРУГОГО носителя у таблицы в репозитории нет (грепом — `research/23` описывает ФОРМУ шва, а не
|
||||
> ДРУГОГО носителя у этой таблицы в репозитории нет (грепом — `research/23` описывает ФОРМУ шва, а не
|
||||
> перечень каналов с их атомарностью). Собран чтением `backend/`, не по нашим докам. Ценность в двух
|
||||
> колонках, которых нельзя получить из кода платформы: **атомарность записи** (какие сайдкары можно
|
||||
> читать на живом прогоне, а какие рвутся) и **какие каналы движка платформа не потребляет вовсе**.
|
||||
|
|
@ -413,9 +388,9 @@
|
|||
| `<project_db>.auto-bank.yaml` | `pipeline/mining.go` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же правило |
|
||||
| `mined_delta` (путь из `book.yaml`) | **писателя в движке НЕТ** — только читатель `loadMinedDelta`; формат `seed.File` (`terms:`), грузится `membank.LoadGlossarySeed`, `Source` пере-штампуется на `"mined"` | — | **писателя НЕТ и у платформы** | ❌ **разрыв — это строка 199(а) единого бэклога**, развилка ждёт ратификации |
|
||||
| `mined_rejects` | читатель `loadMinedRejects`; формат `rejects: [{src, note}]` — это ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит | — | писателя нет | ❌ тот же разрыв |
|
||||
| `tmctl bank-apply` + документ решений | зона ПИШЕТ (`internal/runs/bank.go` `decisionsFile` → файл во временном каталоге), движок отвечает отчётом `tm-bank-report-v1` на stdout | документ пишется целиком до вызова; отчёт — одноразовая выдача на вызов | `internal/runner/bankapply.go` → `ingest.DecodeBankReport` → `internal/runs/bank.go` `bankVerdict` | ⚠ **СЕДЬМОЙ канал, дописан 31.08 дофиксом приёмки P12** (в первой редакции этой таблицы его не было, и `platform/README.md` трижды подряд объявлял список исчерпывающим при неполном счёте). ЕДИНСТВЕННЫЙ канал, по которому платформа ПИШЕТ в проект движка, — отсюда и своя дверь (`POST /books/{bookId}/bank/corrections`), и свой класс отказов, и класс `write_incomplete` = exit 15. ⚠ Пост-verb факт этого канала (`bank_moved_at`) пишется на ОТДЕЛЬНОМ, отцепленном контексте — иначе обрыв клиента теряет его навсегда (`PD-425`) |
|
||||
| `tmctl build` + сайдкар `<project_db>.book.<fmt>` | движок (`pipeline`, пак «писатель книги», D39.175) | файл пишется целиком до публикации пути; ПУТИ публикуются в `StatusArtifacts.book_files` (`status --json` / `manifest --json`), stdout — конверт `tm-build-v1` | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ **ШЕСТОЙ канал, дописан паком P12 31.08 по пингу аудита доков.** Его будет читать дверь выдачи `createExport`/`getExport`, и она обязана СТРОИТЬ — звать `tmctl build` — а НЕ подбирать файл, лежащий рядом с БД: там копия ПРЕЖНЕЙ сборки (D39.175 п.2, слово владельца). Сверять `BuildReport` (`config_drift`/`stale_unknown`). ⚠ Новый класс отказа движка: exit **16** `book_incomplete` — книга с дырами без `--partial`; раскладка на провод — при постройке двери. ⚠ И ловушка на будущее: интейк на exit **11** действует ДЕСТРУКТИВНО, а `tmctl build` книги из нуля юнитов выходит именно 11 — сегодня недостижимо (интейк зовёт `manifest`, не `build`), учесть при подключении `build` к автоматике |
|
||||
| `tmctl export --json --pairs` | движок (`pipeline/export.go`) | — (одноразовая выдача на вызов) | `internal/runner/engine.go` `ExportArgs`/`Export` → `ingest.DecodeExport` → `internal/readmodel` | ✅ ⚠ **Единственный канал, несущий ТЕКСТ пары** — исходник и перевод; манифест несёт только структуру. Пропущен в первой редакции этой таблицы (найдено аудитом доков в тот же день) |
|
||||
| `tmctl bank-apply` + документ решений | зона ПИШЕТ (`internal/runs/bank.go` `decisionsFile` → файл во временном каталоге), движок отвечает отчётом `tm-bank-report-v1` на stdout | документ пишется целиком до вызова; отчёт — одноразовая выдача на вызов | `internal/runner/bankapply.go` → `ingest.DecodeBankReport` → `internal/runs/bank.go` `bankVerdict` | ⚠ ЕДИНСТВЕННЫЙ канал, по которому платформа ПИШЕТ в проект движка, — отсюда и своя дверь (`POST /books/{bookId}/bank/corrections`), и свой класс отказов, и класс `write_incomplete` = exit 15. ⚠ Пост-verb факт этого канала (`bank_moved_at`) пишется на ОТДЕЛЬНОМ, отцепленном контексте — иначе обрыв клиента теряет его навсегда (`PD-425`) |
|
||||
| `tmctl build` + сайдкар `<project_db>.book.<fmt>` | движок (`pipeline`, пак «писатель книги», D39.175) | файл пишется целиком до публикации пути; ПУТИ публикуются в `StatusArtifacts.book_files` (`status --json` / `manifest --json`), stdout — конверт `tm-build-v1` | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ **Читателя ещё нет.** Его будет читать дверь выдачи `createExport`/`getExport`, и она обязана СТРОИТЬ — звать `tmctl build` — а НЕ подбирать файл, лежащий рядом с БД: там копия ПРЕЖНЕЙ сборки (D39.175 п.2, слово владельца). Сверять `BuildReport` (`config_drift`/`stale_unknown`). ⚠ Новый класс отказа движка: exit **16** `book_incomplete` — книга с дырами без `--partial`; раскладка на провод — при постройке двери. ⚠ И ловушка на будущее: интейк на exit **11** действует ДЕСТРУКТИВНО, а `tmctl build` книги из нуля юнитов выходит именно 11 — сегодня недостижимо (интейк зовёт `manifest`, не `build`), учесть при подключении `build` к автоматике |
|
||||
| `tmctl export --json --pairs` | движок (`pipeline/export.go`) | — (одноразовая выдача на вызов) | `internal/runner/engine.go` `ExportArgs`/`Export` → `ingest.DecodeExport` → `internal/readmodel` | ✅ ⚠ **Единственный канал, несущий ТЕКСТ пары** — исходник и перевод; манифест несёт только структуру |
|
||||
| `events.jsonl` (NDJSON эмиттера) | движок, StreamVersion 1.1 | append-only | `internal/ingest/tail.go` + `pgstore.RunSink` | ✅ |
|
||||
| exit-коды `tmctl` | контракт движка | — | `ingest.OutcomeOf`, `internal/runs/reconcile.go` `outcome` | ✅ |
|
||||
| `tmctl status --json` | движок | — | `internal/runner/engine.go` → `ingest.StatusReport`; расчёт денег | ✅ форму P8-FIX не менял; изменил лишь то, СКОЛЬКО раз его зовут при неудаче (бэкофф отсрочки) |
|
||||
|
|
@ -429,11 +404,7 @@
|
|||
|
||||
## Стенд разработчика — воспроизводимый рецепт
|
||||
|
||||
> ⚠ Перенесено оркестратором №18 21.08 из рабочего хендоффа приёмки P7 при его архивации. Первая
|
||||
> редакция архивного баннера утверждала, что рецепт «уже живёт здесь», — это было НЕВЕРНО: переехали
|
||||
> только две грабли, а сборка, шаблон и гейты батареи остались единственным экземпляром в документе
|
||||
> под баннером «инструкции НЕ исполнять». `platform/README.md` при этом отсылает за рецептом именно
|
||||
> сюда. Класс — живая инструкция, убитая архивацией.
|
||||
> ⚠ `platform/README.md` отсылает за рецептом стенда именно сюда — единственный экземпляр.
|
||||
|
||||
**Собрать бинари и шаблон книги.** Каталог — ЛЮБОЙ вне `/tmp` (иначе не переживёт уборку):
|
||||
|
||||
|
|
@ -452,17 +423,16 @@ sed -e 's#^pipeline: ../configs/#pipeline: <repo>/backend/configs/#' \
|
|||
⚠ Относительные `pipeline:`/`models:` — единственное отличие шаблона от репо-оригинала; без правки
|
||||
движок не находит конфиги из чужого рабочего каталога.
|
||||
|
||||
**Гейты батареи — их ТРИ, и без них `make check` МОЛЧА скипует ~290 тестов** (вся читающая модель,
|
||||
миграции и шов). «Зелёная батарея» без них не значит ничего, поэтому `check` сам печатает имена
|
||||
скипнутых: `TM_PLATFORM_TEST_DSN` (Postgres) · пара `TM_PLATFORM_TEST_ENGINE_BIN` +
|
||||
**Гейты батареи — их ЧЕТЫРЕ, и без них `make check` МОЛЧА скипует ~290 тестов** (вся читающая
|
||||
модель, миграции и шов). «Зелёная батарея» без них не значит ничего, поэтому `check` сам печатает
|
||||
имена скипнутых: `TM_PLATFORM_TEST_DSN` (Postgres) · пара `TM_PLATFORM_TEST_ENGINE_BIN` +
|
||||
`TM_PLATFORM_TEST_BOOK_TEMPLATE` (живой рендер конфигурации и живой прогон движка) · ДОСТИЖИМЫЙ
|
||||
пользовательский менеджер systemd (`/run/user/<uid>`; без него три теста `internal/runner`
|
||||
скипаются молча — `PD-374`) ⚠ **и ЧЕТВЁРТОЕ условие, которого здесь не было до 29.08: хост обязан
|
||||
РЕАЛЬНО применять `MemoryMax` к транзиентному юниту.**
|
||||
скипаются молча — `PD-374`) · **хост обязан РЕАЛЬНО применять `MemoryMax` к транзиентному юниту.**
|
||||
|
||||
⚠⚠ **Движковый бинарь второго гейта обязан быть СОБРАН ИЗ ТЕКУЩЕГО `backend/`, а не переиспользован
|
||||
со стенда** (`cd <repo>/backend && go build -o $W/tmctl ./cmd/tmctl`) — дописано паком P12 30.08 по
|
||||
строке `PD-432`. Цена пропуска названа замером: стендовый `tmctl` от 24.08 против сегодняшнего
|
||||
со стенда** (`cd <repo>/backend && go build -o $W/tmctl ./cmd/tmctl`, `PD-432`). Цена пропуска
|
||||
названа замером: стендовый `tmctl` от 24.08 против сегодняшнего
|
||||
`backend/configs/models.yaml` дал ТРИ красных теста в `internal/books` и `internal/runner` с
|
||||
сообщением `tmctl: config: parse …/models.yaml: yaml: unmarshal errors: line 137: field
|
||||
system_messages not found in type config.CapabilitiesConfig`. Диагноз стоит времени именно потому,
|
||||
|
|
@ -485,7 +455,7 @@ system_messages not found in type config.CapabilitiesConfig`. Диагноз с
|
|||
состояние `cgroup.subtree_control` среза `tm-runs.slice` в момент прогона — назван КАНДИДАТОМ и
|
||||
только: диагноз `PD-423` не установлен, и вносить его в рецепт как проверку нельзя.
|
||||
|
||||
Ожидание при всех трёх: 18 пакетов, exit 0, **скипов 0**, линтер «0 issues». Замерено 29.08: с гейтами — 0 скипов на обоих деревьях; без них — exit 0 и **287 скипов на HEAD
|
||||
Ожидание при всех четырёх: 18 пакетов, exit 0, **скипов 0**, линтер «0 issues». Замерено 29.08: с гейтами — 0 скипов на обоих деревьях; без них — exit 0 и **287 скипов на HEAD
|
||||
`fbe6cf3`**, **304 на дереве пака P11** (пак добавил 17 пинов, гейченных тем же DSN). Число зависит
|
||||
от дерева, и переносить его между ними нельзя.
|
||||
|
||||
|
|
@ -625,7 +595,3 @@ export TM_PLATFORM_TEST_DSN='postgres://postgres@/postgres?host=/tmp&port=55432&
|
|||
роняет соединение — `pg_ctl … status` скажет «no server running». Поднимать заново по разделу выше.
|
||||
- **Замер страницы библиотеки** (когда трогаете проекции книги): `go test ./internal/pgstore/
|
||||
-bench LibraryPage -run xxx` — корпус 40 книг × 500 глав собирается сам, `vacuum analyze` внутри.
|
||||
|
||||
⚠ Postgres на стенде отсутствует как системный пакет и sudo нет. Схема и запросы этой сессии
|
||||
проверены на ЖИВОМ PostgreSQL **18.4**, поднятом без root из бинарников zonky
|
||||
(`io.zonky.test.postgres`, Maven Central) в скрэтчпаде — вне репозитория и вне зависимостей модуля.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue