30 KiB
Бэклог платформы (зонный)
Ведёт зона
platform/(решение владельца 02.08, D39.84: фронт и платформа держат СВОИ бэклоги; единый бэклогdocs/PROGRESS.mdостаётся трекером движка/полигона/доков и фронт/платформа-строк не принимает). Нормы те же: ID стабилен навсегда, каждая петля получает диспозицию. Запросы к ДВИЖКУ сюда не пишутся — они заходят строками единого бэклога через оркестратора (пример: строки 99–102). Засеян оркестратором при лендинге D39.84 — дальше правит платформа-сессия.
Диспозиции после P1 (05.08) — в журнале зоны, раздел «Диспозиции бэклога зоны» (
docs/platform-PROGRESS.md). Коротко: П-6 и П-8 ЗАКРЫТЫ, П-7 закрыт по схеме и операциям (постановка холда воркером — часть П-1), П-4 отменён и поглощён П-7, П-5 переопределён под кредитную модель и ждёт правки спеки, П-1 продолжена, П-2/П-3 не трогали. Дублировать их здесь не стали — у строки один источник истины.
| 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 |
| П-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 |
| П-6 | OAuth-вход популярных провайдеров (Google первым) + модель аккаунта (направление владельца 04.08): authorization code + PKCE, валидация ID-токена, связывание аккаунта по ПОДТВЕРЖДЁННОЙ почте; собственная серверная сессия сохраняется (D39.84) — OAuth даёт только СОБЫТИЕ входа. Форма и библиотеки — по ресёрч-пакету направления (ратифицирует оркестратор); писатель куки с __Host-атрибутами и dev-профиль — здесь же (PD-8) |
P1, вместе с П-1 | владелец 04.08 |
| П-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 |
| П-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 |
| П-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 |
| П-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 |
| П-21 | Ключ идемпотентности под конкуренцией даёт 500 (PD-369). ⚠ Не работа пака P8-FIX и файл им не тронут — найдено попутно, прогоном батареи: собственный тест TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError ФЛЕЙКОВЫЙ, 1–2 падения на ~80 прогонов. Причина не в тесте: ClaimIdempotency ретраит проигранную гонку ровно claimRounds = 3 раза, а восемь одновременных попыток одного ключа, каждая из которых сразу отдаёт ключ назад (как делает любой 4xx), могут отобрать гонку у одного проигравшего трижды подряд — и он получает внутреннюю ошибку вместо одного из четырёх контрактных ответов. Направление, а не решение: граница по ВРЕМЕНИ вместо числа раундов либо ErrKeyInFlight при исчерпании — это решение о контрактном поведении |
до первого чужого пользователя | попутная находка сессии P8-FIX |