diff --git a/platform/BACKLOG.md b/platform/BACKLOG.md index b618839e..44c79814 100644 --- a/platform/BACKLOG.md +++ b/platform/BACKLOG.md @@ -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/*` | P7 (остаток: читающая поверхность) | D39.81, D39.84, D39.85, STACK_DECISIONS §5 | +| П-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 | diff --git a/platform/README.md b/platform/README.md index 46969ef8..2f93e2ea 100644 --- a/platform/README.md +++ b/platform/README.md @@ -106,7 +106,7 @@ Prometheus; на контрактную поверхность они не вы |---|---| | `cmd/tmplatformd` | демон: сборка зависимостей, очередь, свипы. `runner.go` — единственное место, где зона склеивается | | `cmd/tmplatformctl` | админ-CLI и `exit-marker`, который systemd зовёт на конце прогона | -| `internal/httpapi` | ВСЯ контрактная поверхность: `v0.go` (маршруты, библиотека, интейк, прогоны) · `reading.go` (главы, пары, замечания, банк) · `stream.go` (SSE) · `capabilities.go` · `problem.go` (модель ошибок) · `conditional.go` (ETag/304 + gzip; SSE через него НЕ проходит — потому и не сжимается) · `idempotency.go` · `project.go` и `notes.go` (переводы словарей: read-модель → провод) | +| `internal/httpapi` | ВСЯ контрактная поверхность: `v0.go` (маршруты, библиотека, интейк, прогоны) · `reading.go` (главы, пары, замечания, банк) · `stream.go` (SSE) · `capabilities.go` · `problem.go` (модель ошибок) · `conditional.go` (ETag/304 + gzip; SSE через него НЕ проходит — потому и не сжимается) · `idempotency.go` · `project.go` и `reading.go` (переводы словарей: read-модель → провод) | | `internal/auth`, `internal/login` | сессии, CSRF, вход. Принципал создаётся ТОЛЬКО в мидлваре | | `internal/books` | интейк: приём файла, каталог книги, рендер стартового `book.yaml`, разбор через `tmctl manifest` | | `internal/runs` | жизнь прогона: допуск, спавн транзиентного юнита, реконсилятор, деньги на границе попытки | diff --git a/platform/deploy/README.md b/platform/deploy/README.md index 35d8ecfe..e00662d3 100644 --- a/platform/deploy/README.md +++ b/platform/deploy/README.md @@ -87,8 +87,13 @@ TM_PLATFORM_METRICS_ADDR=127.0.0.1:9464 грант по умолчанию НОЛЬ и начисляется руками — `tmplatformctl grant`. Строка `=5` в этом блоке включала обратно ровно тот самообслуживаемый безлимитный грант, который дефолт выключает. -⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` обязателен.** Пустой список делает `/capabilities` и интейк -противоположными: провод говорит «этот деплой не умеет ни одной пары», а интейк принимает любую. +⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` обязателен, и с акта 5 P7 правило ЖЁСТЧЕ, чем описывала прежняя +редакция этого абзаца** (испр. 22.08 аудитом доков — она утверждала обратное: «интейк принимает +любую»). Инстанс, который ПРИНИМАЕТ ЗАГРУЗКИ и не объявил ни одной ДОСТУПНОЙ пары, **не стартует +вовсе** (`internal/config/config.go`, проверка `IntakeEnabled() && len(AvailablePairs()) == 0`); +объявленная, но недоступная пара отклоняется интейком кодом `unsupported_pair`. То есть «не объявлено +ничего» и «объявлена эта пара» — разные ответы, и деплой больше не может принять книгу, которую он +способен только провалить. ## Шаблон книги (`TM_PLATFORM_BOOK_TEMPLATE`) — артефакт ОПЕРАТОРА @@ -131,7 +136,7 @@ ceilings: предохранителем: она делает такую остановку различимой и честной, а не дефолтом, который её вызывает. Битый или отсутствующий шаблон — **беда деплоя, а не книги**: загрузки не отклоняются и не удаляются, -книги ждут в `parsing` (метрика `books_parsing`), в логе — ERROR с причиной. Починили файл — ближайший +книги ждут в `parsing` (метрика `tm_platform_books_in_intake{status="parsing"}`), в логе — ERROR с причиной. Починили файл — ближайший свип разбирает всё накопившееся. ## Апгрейд ПЛАТФОРМЫ: миграция и бинарь едут вместе diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 3399ff3c..2dcb4b7f 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -45,7 +45,7 @@ | PD-298 | bug | info | `internal/pgstore/readmodel.go` `ListNotes`, `internal/pgstore/sink.go` `unitDone` | **Снятие флага с замечания дельта-чтение выразить не может.** Резолюция, пере-разрешённая как не-`flagged` (редрайв), обновляет строку и двигает `revision`, но дельта фильтруется предикатом `ur.flagged` — строка не возвращается, и клиент никогда не узнаёт, что замечание снято: оно остаётся на экране навсегда. Канон §AfterVersion: «A DELETION cannot be expressed this way», и требует одного из двух ответов — `resync_required` либо `400 version_too_old`; здесь не даётся ни один. ⚠ НЕ подтверждено, что движок вообще пере-издаёт `unit_done` для той же тройки (глава, юнит, волна) с `flagged=false` — комментарий `sink.go` это УТВЕРЖДАЕТ («a redrive re-attacks a flagged one»), но чтением движка не сверено. Порядок: сначала сверка у движка, потом либо счётчик замены для замечаний, либо строка «переход недостижим» ⚠ **ДИСПОЗИЦИЯ АКТА 5 (сверка с движком контрактной сессией 20.08): переход НЕДОСТИЖИМ и механизма не строим.** Движок объявляет вердикт юнита один раз на волну, его announce-once-леджер не пере-announce-ит, поэтому единственная пере-доставка — ТОТ ЖЕ вердикт. Инвариант записан в коде (`sink.go` `unitDone`): если что-то научится снимать флаг, фолд обязан выдать кадр — иначе счётчик разъедется со списком молча. Строка держится открытой этим долгом, а не живым дефектом | open | приёмка P7 → доработка 20.08 (сверка) | | PD-299 | standards | info | `internal/httpapi/conditional.go` `acceptsGzip` | **`Accept-Encoding: identity;q=0` не отвечает `406`.** Клиент, потребовавший ЛЮБОГО кодирования кроме identity, получает identity. Половина RFC 9110 §12.5.3, которую правка PD-268 не закрыла: gzip-сторона (именованное кодирование выигрывает у `*`, нулевой вес — отказ) закрыта и пиньётся, эта — нет. Достижимо только специально сконструированным клиентом; ни один генерённый по контракту клиент так не делает | open | доработка 20.08 (сверка находок против дерева) | | PD-368 | hardening | info | `internal/config/config.go`, `internal/runs/reconcile.go` `phaseBudget` | **Пара `TM_PLATFORM_SWEEP_BUDGET`/`TM_PLATFORM_RUN_BUDGET` не проверяется на когерентность на буте, и поднять ОДИН из них — тихий no-op:** фазе достаётся половина прохода, поэтому любое значение `RunBudget` от половины прохода и выше наблюдаемо неотличимо от дефолта. Ревью заходило сюда как в major («поднять бюджет = объявить здоровый прогон застрявшим») и **это опровергнуто исполнением**: поднятие ручки не меняет вообще ничего, вердикт при дефолтах существует и без оператора, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами шиппящиеся дефолты (60 с против 60 с). Остаётся эргономика: поднимать надо `SweepBudget`, и об этом не сказано нигде, кроме доккоммента | open | воркфлоу-ревью волны 2 (P8-FIX), находка опровергнута, остаток зафиксирован | -| PD-373 | doc | info | `internal/readmodel/readmodel.go:64`=`const maxAttempts = 5`, `deploy/README.md:248`=`После пяти неудач` | **У числа попыток материализации ДВА носителя и ничего между ними.** Код держит `const maxAttempts = 5`, рантбук оператора пишет «После пяти неудач долг списывается». **Посажена мутация оркестратором вне списка автора:** `maxAttempts` 5 → 500000, батарея зелёная — пин `readmodel_test.go` ездит `for attempts := range maxAttempts`, то есть доказывает МЕХАНИЗМ при любом значении константы, что само по себе правильно. Незакрытым остаётся другое: подняли константу — рантбук молча начал лгать оператору о том, когда платформа сдаётся. ⚠ Тот же ход у `runs.StalledAfter` проверен и НАРУШЕНИЯ НЕ ДАЛ: там носитель ровно один (рантбук пишет «сколько неудач подряд» без числа), мутация тоже выжила и это законно. Лечение — либо гейт на второй носитель ровно той формы, что пак построил для `ContractVersion` (`internal/gates/contract_test.go` читает канон, а не копию числа), либо число уходит из прозы | open | приёмка P8-FIX (посадка мутации оркестратором №18) | +| PD-373 | doc | info | `internal/readmodel/readmodel.go:64`=`const maxAttempts = 5`, `deploy/README.md:253`=`После пяти неудач` | **У числа попыток материализации ДВА носителя и ничего между ними.** Код держит `const maxAttempts = 5`, рантбук оператора пишет «После пяти неудач долг списывается». **Посажена мутация оркестратором вне списка автора:** `maxAttempts` 5 → 500000, батарея зелёная — пин `readmodel_test.go` ездит `for attempts := range maxAttempts`, то есть доказывает МЕХАНИЗМ при любом значении константы, что само по себе правильно. Незакрытым остаётся другое: подняли константу — рантбук молча начал лгать оператору о том, когда платформа сдаётся. ⚠ Тот же ход у `runs.StalledAfter` проверен и НАРУШЕНИЯ НЕ ДАЛ: там носитель ровно один (рантбук пишет «сколько неудач подряд» без числа), мутация тоже выжила и это законно. Лечение — либо гейт на второй носитель ровно той формы, что пак построил для `ContractVersion` (`internal/gates/contract_test.go` читает канон, а не копию числа), либо число уходит из прозы | open | приёмка P8-FIX (посадка мутации оркестратором №18) | | PD-374 | doc | info | `docs/STACK_DECISIONS.md` «Гейты батареи», `internal/runner/systemd_test.go` `systemdOrSkip`, `Makefile` цель `check` | **Рецепт объявляет у батареи ДВА гейта и ждёт «скипов 0» — а условий три, и третье не названо.** Кроме `TM_PLATFORM_TEST_DSN` и пары `TM_PLATFORM_TEST_ENGINE_BIN`/`_BOOK_TEMPLATE` есть `systemdOrSkip`: без ДОСТИЖИМОГО пользовательского менеджера systemd три теста `internal/runner` скипаются. Поймано пере-прогоном батареи при приёмке: на этом хосте `/run/user/1000` не существует (сессия logind не поднята), поэтому «скипов 0» недостижимо в принципе — `make check` дал 18 пакетов, exit 0, линтер 0 issues и **3 скипа**. Мимо: сама цель `check` печатает над списком скипов «set TM_PLATFORM_TEST_DSN», отправляя читателя к ручке, которая тут ни при чём. Отчёт пака честен и это подтверждает — он мерил отдельно и получил 0, что верно на хосте с живым менеджером. Лечение: назвать третий гейт в рецепте вместе с двумя и не обещать «скипов 0» без него; заодно сделать сообщение цели `check` не называющим одну переменную из трёх | open | приёмка P8-FIX (пере-прогон батареи оркестратором №18) | | PD-6 | hardening | info | `internal/auth/csrf.go:51` | GET освобождён от CSRF (верно), но SSE-хендшейк — GET с амбиентной кукой: origin-чек хендшейка потока (STACK §5) не покрыт ничем. Закрыть при постройке SSE (P1) | open | приёмка P0 (security-линза) | | PD-23 | hardening | info | `internal/pgstore/migrations/00001_identity.sql` | Журнал входов растёт без ретенции и чистится только каскадом при удалении аккаунта. Нужен свип по возрасту (год?) — вопрос политики, не кода | open | самопроверка P1 | diff --git a/platform/docs/ENGINEERING_STANDARDS.md b/platform/docs/ENGINEERING_STANDARDS.md index e6fce48d..321d85e0 100644 --- a/platform/docs/ENGINEERING_STANDARDS.md +++ b/platform/docs/ENGINEERING_STANDARDS.md @@ -60,8 +60,14 @@ ## 3. Критерии приёмки сессии (Definition of Done) -1. `make check` зелёный офлайн; с `TM_PLATFORM_TEST_DSN` — та же батарея плюс живая схема - (порядок подъёма PG без root — `STACK_DECISIONS.md`); скипы названы вслух и их ноль при DSN. +1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **«Скипов ноль» требует ТРЁХ условий, а не + одного** (испр. 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`. 2. Каждый деливерабл проверен ИСПОЛНЕНИЕМ (сервер поднят и опрошен, миграции применены дважды, констрейнты сработали поимённо, супервизор гонял настоящий процесс) — не чтением. 3. Заявленное в отчёте свойство несущего пути ОБЯЗАНО быть запинено тестом, который ловит мутацию diff --git a/platform/docs/PLATFORM_DIRECTION.md b/platform/docs/PLATFORM_DIRECTION.md index 7042a5f1..b2adedf1 100644 --- a/platform/docs/PLATFORM_DIRECTION.md +++ b/platform/docs/PLATFORM_DIRECTION.md @@ -103,6 +103,18 @@ hosted IdP (связка PII + доступность, а свою сессию ## 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 +> звали: гейт `pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema` планирует ЖИВЫМ Postgres +> каждый собранный запрос пакета против мигрированной схемы — то есть покрывает и склейки, которых +> sqlc не видит, и именно в них случились оба рантайм-падения зоны. +> > ⚠ **ПЕРЕ-ПОДПИСАНО D39.132 п.2б** (баннер «ратификация за оркестратором» снят как отработанный): > `oapi-codegen` — КАНДИДАТ, решение за паком, который его берёт; `sqlc` привязан к P7; факт про River > поправлен. **Решение P7 по обоим — НЕ БРАТЬ, с доводом, а не молчанием:**