textmachine/platform/docs/archive/platform-PROGRESS-P8.md

472 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Слайс зонного журнала: эра пака P8-FIX (2122.08.2026)
> ⚠ **Архив. Инструкции отсюда НЕ исполнять, при онбординге НЕ читать** — только по конкретной ссылке.
> Вынесено оркестратором №18 22.08 при актуализации доков. Состояние зоны — шапка живого
> `../platform-PROGRESS.md`; дефекты — `../DEFECT_REGISTER.md`; правила, пережившие пак, —
> `../STACK_DECISIONS.md`; ратификация — **D39.154** в `docs/architecture/05-decisions-log.md`.
>
> **Что этот пак сделал:** закрыл фикс-лист приёмки P7 (пункты 17 и 913) и врезанный первым
> блокер свипа `PD-169` — голодание, из-за которого расчёт денег вставал для ВСЕЙ инсталляции.
> Плюс спил пер-термного write-пути банка по слову владельца (`PD-370`, ратификация D39.144).
> Лендинг — `31f1f82`.
>
> ⚠ **Что из этого отчёта ПЕРЕНЕСЕНО в долговечные носители и там первично:**
> инвентарь каналов шва с движком → `../STACK_DECISIONS.md` §«Инвентарь каналов движка» (и там он
> ПОЛНЕЕ: дописан канал текста `tmctl export --json --pairs`, которого в этой таблице не было) ·
> развилка владения `book.yaml` с четырьмя вариантами → строка **199(а)** единого бэклога ·
> поимённый разбор канона под минор 0.5.0 → строка **206** · граница sqlc (41 запрос) → `BACKLOG.md`
> **П-19** · ловушка `.check.log` и третий гейт батареи → `PD-374`.
- **P8-FIX — ФИКС-ЛИСТ ПРИЁМКИ P7 ЗАКРЫТ. Дерево готово к приёмке и лендингу (22.08).**
Батарея: 18 пакетов, exit 0, **скипов 0**, линтер 0 issues, скипов 0, тестов **561** (HEAD 532). Регистр —
**370** строк, открытых **68** (14 minor, 53 info, **1 major**) — сверено командой на момент сдачи
(`python3 docs/scripts/counts.py --check`, exit 0; `--lint` exit 0; ⚠ вес регистра этим скриптом НЕ
проверяется — числа сняты его печатью, а не гейтом). **Открытый major — `PD-370`**, контрактная
половина спила пер-термной подписи банка: она не работа зоны, закрывать её здесь было бы подгонкой
под критерий приёмки. Миграция пака: **00025**. Дерево не закоммичено и не застейджено.
**Удалённых имён тестов ТРИ, и все три — по решению владельца 22.08** (сверено исполнением, не
памятью: имена из `git show HEAD:<файл>` по всем `_test.go` против дерева):
`TestABadDecisionRefusesTheWholeBatchAndNamesTheItem` · `TestAnUnknownTermRefusesTheBatch` ·
`TestTheBankIsReplacedWhileTheDecisionsSurvive` — они описывали пер-термную подпись банка, которую
`D39.144` отменил, а этот пак снял из кода (`PD-370`). Тест снятого кода — не потеря покрытия;
третий переписан на СОХРАНИВШЕЕСЯ свойство (`TestTheBankIsReplacedFromTheEnginesReadOut`).
Плюс один ПЕРЕИМЕНОВАН по факту изменения смысла (`…GivesTheHoldBackOnlyWhenAsked`
`…AlwaysGivesTheHoldBack`, `PD-361`) — имя заведено этим же паком.
**Пинг оркестратору №1 — ЕГО зона, сверить при лендинге:** `docs/architecture/05-decisions-log.md:542`
держит якорь на `internal/pgstore/perf_test.go:13` с токеном «96%». Пак ЭТОТ комментарий правил
(`PD-366`: он лгал про свой же бенчмарк), токен я сохранил намеренно и `--lint` зелёный — но док
чужой, поэтому говорю, а не считаю вопрос закрытым.
**Пинг оркестратору №2 — попутная находка ВНЕ зоны пака:** `PD-369` / `П-21``ClaimIdempotency`
под конкуренцией отвечает 500 (12 падения на ~80 прогонов собственного теста). Файл в диффе пака
не участвует, не трогал.
**Пинг оркестратору №3 (`PD-370`, требует контрактной сессии):** канон 0.4.0 несёт пер-термный
`BankDecision{action: approve|decline}`, а `D39.144` эту модель отменил 16.08 — подписывается весь
банк одним ОК. Зона канон исполняет, снять в одностороннем порядке не может. Правка канона →
правка зоны, в этом порядке.
**Сдача идёт ПОСЛЕ второй волны ревью**, которая нашла в самом паке блокер (`PD-353`) и два
дефекта, делавших его собственные механизмы недостижимыми (`PD-354`, `PD-355`) — раздел «Волна 2».
### Записка-план по §4 промта: пункт → где исполнено / отказ с причиной
| Пункт | Исход | Где |
|---|---|---|
| **Блокер свипа** (врезан первым) | **исполнен четырьмя механизмами** | ниже, отдельным разделом; `PD-169` пере-открыт и закрыт заново |
| 1. `ContractVersion` 0.3.0 при формах 0.4.0 | исполнен | `internal/httpapi/capabilities.go` + гейт `internal/gates/contract_test.go` (`PD-328`) |
| 2. Пины на два поля `EventBase` | исполнен, посадка сперва воспроизведена | `internal/pgstore/events_test.go` (`PD-329`) |
| 3. Бюджет попыток материализации | исполнен | `internal/readmodel/readmodel.go`, миграция 00025 (`PD-330`) |
| 4. sqlc + гейт разбора SQL | **гейт построен; sqlc — граница названа, решение владельца** | `internal/pgstore/sqlgate_test.go`; разбор — `PD-44`, строка бэклога **П-19** |
| 5. `PD-297` (round-trip под блокировкой) | **отказ с причиной** | нечем и не на чем мерить — ниже |
| 6. Труба доставки решений банка (строка 199а) | **не строил, сдаю вопросом** — как промт и требует | ниже, отдельным разделом |
| 7. Мусор: задвоенная строка доккомментария | исполнен | `internal/books/books.go` `canTranslate` (`PD-335`) |
| 8. Ревью-пак четырёх осей | **НЕ мой** — заказ пака P8-REVIEW | — |
| 9. Один носитель на факт для замера 00016→00022 | исполнен, с названным расхождением с буквой пинга | `internal/pgstore/perf_test.go` (`PD-336`) |
| 10. Пол на пустой манифест у материализатора | исполнен | `internal/ingest/manifest.go` `Whole()`, `internal/readmodel/readmodel.go` (`PD-332`) |
| 11. `PD-327` стоит open при ратифицированном каноне | исполнен | регистр, `fixed(P8-FIX)` |
| 12. Изоляция читающих чтений не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/isolation_test.go` (`PD-333`) |
| 13. `CreditHeldBy`: оговорка не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/credits_test.go` (`PD-334`) |
| `PD-255` (норма комментариев сменилась) | закрыт решением владельца 21.08 | регистр |
### Блокер свипа: почему лечение — не «поднять константу»
Диагноз пинга подтверждён построчно и **воспроизведён исполнением**. Живое свойство: проход 2 мин
против 60 с на прогон ⇒ двух медленных прогонов хватает, чтобы съесть проход целиком, после чего
`ctx.Err()` обрывает цикл и `UnsettledRuns` — единственный ретрай отложенного расчёта — не
вызывается ВООБЩЕ, для всей инсталляции, каждый проход. Построено:
1. **Проход разделён на фазы.** Реконсиляция получает половину остатка, расчёт — свой кусок
(`runs.phaseBudget`). Выбранный бюджет фазы больше не ошибка свипа: очередь просто ждёт тика.
2. **Отсрочка с растущим бэкоффом на ПОПЫТКЕ** (`run_attempts.reconcile_after`, миграция 00025).
Свип читает `RunsToReconcile`, а не `ListLiveRuns`; порядок «старейший первым» не тронут — он
верен для всего, что просто медленно, — а из ГОЛОВЫ заклиненный прогон убирает именно отсрочка.
Телеметрия (`Lag`) по-прежнему видит всё.
3. **Терминальное состояние после N неудач** (`runs.StalledAfter` = 5, ≈ четверть часа попыток):
прогон считается ЗАСТРЯВШИМ, гейдж `tm_platform_runs_stalled`, одна ERROR-строка на пересечении
порога — не на каждом тике.
4. **Ручка у ОПЕРАТОРА, которой не было вовсе:** `tmplatformctl runs [--stalled]` называет строку,
число неудач, следующую попытку, сумму холда и его возраст, юнит и последнюю ошибку;
`tmplatformctl run abandon --run <id> --reason <text> [--release-hold]` — терминальный вердикт.
Плюс `books --abandoned` и `book refresh` для второй половины (материализация).
**Границу «дальше не иду» называю явно:** автоматически решать про деньги прогона, о котором нельзя
спросить, этот пак НЕ стал. Правило реконсилятора везде — «я не смог спросить» ≠ «прогона нет»;
прогон, упавший N раз, это ровно прогон, о котором никто не смог спросить, и счётчик перед той же
ошибкой её не чинит. Автоматический ответ на этот вопрос и есть эскроу (строка 136, П-18), который
промт §4.6 из пака исключает. Что счётчик покупает — что человека ИЗВЕЩАЮТ; что покупает ручка —
что извещение чего-то стоит. Хвост записан строкой **П-20**, а не подразумевается.
Оба числа стали ручками: `TM_PLATFORM_SWEEP_BUDGET` и `TM_PLATFORM_RUN_BUDGET`. Второе —
отдельная находка пака: поле `runs.Config.RunBudget` было объявлено, задокументировано и **никем не
присваивалось** (`PD-331`), то есть ручка существовала в структуре и не делала ничего.
### Пункт 4: что построено и где проходит граница sqlc
**Построен гейт, который доходит туда, куда sqlc не дойдёт:** `TestEverySQLStatementParsesAgainstTheMigratedSchema`
сворачивает КАЖДЫЙ SQL пакета прямо из исходника (литералы, конкатенации, именованные константы,
плюс оба внутренних помощника, принимающих SQL параметром) и планирует его живым Postgres'ом
(`explain (generic_plan)`) против мигрированной схемы. **162 оператора, все планируются.**
Несворачиваемый SQL — ОШИБКА гейта, а не пропуск: единственное исключение выписано таблицей в самом
гейте (`store.go` `Ready`, где имя таблицы принадлежит goose). Тем же гейтом закрыт вопрос
фикс-листа «есть ли в read-модели запрос, которого не касается ни один тест»: теперь его касаются
все, на каждом прогоне батареи.
**Граница sqlc — мой выбор, аргумент:** склеек в пакете 25 из 147, фрагментов-констант 15, у
`lastRun` девять потребителей — read-модель для sqlc недостижима по построению, и это ровно та
часть, где рантайм-ошибки и случались. Свободных от склейки файлов целиком пять: `credits.go`(15) ·
`identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) = **41 запрос**.
Это единственный кусок, где инструмент силён; конверсия — отдельный пак (одиннадцать денежных
запросов идут внутри чужой транзакции, генерённый код коммитится, нужен пин версии и гейт
«сгенерённое актуально»). Взять его здесь на две-три ручки — ровно то, от чего предостерёг
фикс-лист: купить инструмент туда, где не болит. Заведено строкой **П-19**.
**Вопрос закрыт владельцем 22.08: sqlc берётся ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — изменений
много и мешать их с содержательной работой нельзя. Граница остаётся названной: 41 запрос.
### Пункт 5, `PD-297`: отказ с причиной
Мерить нечем и не на чем. Сама строка регистра объявляет замер на здешнем стенде НЕпредставительным
(unix-сокет против управляемого PG по TCP — разница на два порядка), а корпусной книги нет: она
появляется на холодном прогоне движка, которым гейчена строка 202 (решение владельца 20.08). Правка
самого опасного на запись пути без замера — ровно то, что эта строка запрещает. Берётся вместе с
холодным прогоном.
### Пункт 9: расхождение с буквой пинга, названное вслух
Пинг просил свести условия В миграцию 00022. **Она РЕЛИЗНАЯ**, и гейт неизменности
(`migrations.sha256` + `TestReleasedMigrationsAreUnchanged`) её правку запрещает; правка гейта ради
этого была бы подгонкой под зелень, что промт и `CLAUDE.md` запрещают прямо. Поэтому 00022 остаётся
нормативным носителем вакуумированных цифр как есть, а условия дописаны в носители, которые править
законно. «96%» не удалено и получило своё условие: это ХОЛОДНЫЙ корпус акта 4 (636 мс против 24),
тогда как 8083% — вакуумированный. Носители мерили разное и все честны.
### Строка 199(а): развилку НЕ решал — сдаю вопросом, как требует §4.2 промта
**ПОПРАВКА ВЛАДЕЛЬЦА 22.08, и она сносит посылку, на которой я строил разбор.** Я описывал трубу
как доставку ПЕР-ТЕРМНЫХ решений («approve/decline на термин»). **Такой модели нет и не было
с 16.08:** D39.144 ратифицировал ОДИН ОК над ВСЕМ банком — «пер-термная подпись = сотни кликов —
НЕ модель продукта», «в модели одного ОК сценария „какие 17 из 300 решены“ не существует».
Пер-термно существует не подпись, а **правка**: поправить/добавить термин до подписи, и — после
прочтения — поправить термин с пере-генерацией задетых глав (вторая половина гейчена строкой 192).
Труба от этого не отменяется, но её ГРУЗ другой: не «решения», а **правки терминов** (`src` → новый
`dst`, плюс добавленные) в `mined_delta`. Развилка ниже (кто объявляет ключи и кто создаёт файлы)
от этого не меняется ни в одном пункте — она про владение `book.yaml`, а не про груз.
**Модель, которую D39.144 отменил, была у зоны ПОСТРОЕНА — 22.08 СНЯТА по слову владельца**
(`PD-370`). Что ушло: маршрут `POST /books/{bookId}/bank/decisions`, хендлер и wire-типы, метод
интерфейса `Library`, `SubmitBankDecisions`, типы `BankDecision`/`BankReceipt`, `UnknownTermError`
и их пины. На месте write-пути — пометка для будущей сессии (`internal/httpapi/reading.go`, блок
«THE WRITE HALF OF THE BANK IS GONE»): что отменено, что ратификация ОСТАВИЛА пер-термным
(правку), и что под неё уже есть в схеме. ⚠ **Контрактная половина остаётся открытой** — канон
0.4.0 несёт путь, три схемы и глагол, и это зона контрактной сессии.
Посылку промта пере-проверил САМ, чтением кода движка, а не по докам. Подтверждается вся:
| Факт | Улика |
|---|---|
| движок читает `mined_delta`, только если путь объявлен | `backend/internal/pipeline/mining.go` `loadMinedDelta`: `if r.Book.MinedDelta == "" { return nil, nil }` — ни одного чтения |
| то же для `mined_rejects` | там же, `loadMinedRejects` |
| объявленный ключ с НЕсуществующим файлом валит загрузку конфига целиком | `backend/internal/config/book.go:199-207`: `bad("mined_delta %s is not readable: %v")` |
| ключей нет в шаблоне оператора | `backend/example/book.yaml` — ноль вхождений `mined_delta`/`mined_rejects` |
| платформа пишет `book.yaml` ОДИН раз и фиксированным набором из пяти ключей | `platform/internal/books/render.go``O_EXCL`, «writes the file once and never again»; ключи `book_id · title · source_lang · target_lang · source_file` |
| **новое, чего в промте не было:** декодер движка СТРОГИЙ — незнакомый ключ это жёсткая ошибка | `backend/internal/config/book.go:142-144`, `dec.KnownFields(true)` — то есть платформа не может «на всякий случай» дописать ключ, которого сборка движка не знает |
| относительные пути резолвятся от каталога КНИГИ | `backend/internal/config/book.go:148-162`, `resolve()` — значит один шаблон на все книги даёт пер-книжные пути без подстановки |
| форматы файлов | `mined_delta` = `seed.File` (`terms:` со схемой `internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed` и пере-штампуется `Source:"mined"`; `mined_rejects` = `rejects: [{src, note}]` (`mining.go` `minedRejectFile`), это ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит |
| на стороне платформы решения write-only | `internal/pgstore/readmodel.go` `SubmitBankDecisions` пишет `action`/`dst`; ни один SELECT их не читает, воркер описан комментарием собственной миграции `00002_readmodel.sql:172-174` и не построен |
**Варианты с ценой (решает владелец, не я):**
- **A. Платформа объявляет два ключа при рождении книги и сразу создаёт два пустых файла.**
Работает без единой правки движка и без действий оператора; пустой файл законен (`terms:` пуст ⇒
ноль записей, `os.Stat` проходит). **Цена:** платформа начинает знать два ключа СХЕМЫ движка, а
форма Б ратифицирована ровно наоборот (D39.110 §2б: шаблон — деплой-артефакт оператора, платформа
подставляет только личность, языки и имя файла). Плюс строгий декодер: сборка движка, которая
этих ключей ещё/уже не знает, перестанет грузить КАЖДУЮ новую книгу.
- **B. Ключи несёт шаблон оператора.** Платформа схемы не узнаёт, пути пер-книжные достаются даром.
**Цена:** сам по себе вариант НЕ работает — движок отказывается грузить конфиг с объявленным, но
отсутствующим файлом, а создавать файлы всё равно некому, кроме платформы. То есть B требует
половины A или варианта D.
- **C. Двигается ратифицированная граница владения:** платформе разрешено писать в каталог книги эти
два файла (и только их), ключи несёт шаблон. **Цена:** правка ратификации D39.110; зато платформа
по-прежнему не знает схемы — она пишет файлы по путям, которые прочитала из уже существующего
`book.yaml`.
- **D. Движок терпит объявленный, но отсутствующий `mined_delta`/`mined_rejects` как пустой.**
Одна правка `backend/internal/config/book.go`, после чего работает чистый B: платформа пишет файлы
только когда решения есть, и ничего не обязано существовать заранее. **Цена:** ослабляется
намеренный гард — опечатка оператора в пути становится тихой. Смягчается тем, что послабление
только для двух ключей и с громким WARN. Это строка ЕДИНОГО бэклога (движок), не моя.
**Рекомендация зоны: C или D+B.** Обе оставляют платформу в неведении о схеме движка — а это и есть
то свойство, ради которого форма Б ратифицировалась. A решает быстрее всех и покупает это тем, что
платформа впервые начинает знать схему чужой стороны, причём под строгим декодером.
**Строить начну только после ратификации.** Границы, если разрешат, приняты как в §4.2: писать не
«перед resume», а только на том резюме, которое СНИМАЕТ стоп подписи (`l.VerifyBank &&
!l.BankReleased`); `--verify-bank` не трогать; пост-ридинговый цикл (строка 192) не проектировать.
`PD-277` в регистре относил эту работу к отложенной 192. ⚠ **22.08 он ПЕРЕ-ДИСПОЗИРОВАН** — не по
развилке, а по спилу глагола: остаток «честность `decline`» умер вместе с ним. Ниже — прежняя
формулировка обязательства, оставленная как след решения: правлю его только после ратификации, как
промт и велит.
### Как это проверялось
**Батарея:** `cd platform && make check` с ОБОИМИ гейтами (`TM_PLATFORM_TEST_DSN` на живом
PostgreSQL 18.4, `TM_PLATFORM_TEST_ENGINE_BIN` + `_BOOK_TEMPLATE`) — 18 пакетов, EXIT=0, скипов 0,
линтер «0 issues». Дифф имён тестов — исполнением: `grep -rho '^func Test[A-Za-z0-9_]*'` по дереву
против `git show HEAD:<файл>` по всем `_test.go`. ⚠ **Замер пере-снят при сдаче** и его исход
изменился: `comm -23` даёт ТРИ удалённых имени — все три описывали пер-термную подпись банка,
снятую 22.08 по слову владельца (`PD-370`, разбор в шапке), — а `comm -13` даёт 29 новых.
Прежняя формулировка «удалённых нет» была верна до спила и снята, а не оставлена рядом с новой.
**Посадки мутаций волны 1 — 20, поймано 20** (перечень ниже; счёт пере-снят при сдаче — прежняя
цифра «16» не сходилась с собственным списком в той же фразе). Каждая ставилась в код, пин обязан был упасть, код возвращался:
снятие `payload["revision"]` ✅ · снятие `payload["structure_version"]` ✅ · `ContractVersion` назад
на 0.3.0 ✅ · `inReadTx` без обеих опций ✅ · без изоляции ✅ · без read-only ✅ · `ListBooks` на
`inTx` ✅ · снятие `and book_id <> $2` ✅ · снятие пола самоописания манифеста ✅ · снятие пола «ноль
глав» ✅ · обе фазы свипа на ОДНОМ дедлайне ✅ · ранний возврат фазы из `Sweep` ✅ · свип читает
`ListLiveRuns` вместо списка «пора» ✅ · отсрочка только по ошибке ✅ · `DeferReadModelDebt` в
`now()` ✅ · долг никогда не списывается ✅ · новая граница наследует потраченный бюджет ✅ · колонка,
которой нет в схеме, в СКЛЕЕННОМ фрагменте (гейт SQL) ✅ · то же в локальной `const q` ✅ ·
несворачиваемый SQL ✅.
**Три посадки из этого списка сперва ПЕРЕЖИЛИ** мои же пины и вскрыли настоящие дефекты пака —
разбор в `PD-337`/`PD-338`/`PD-339`; пины переписаны, мутации пере-посажены.
**Воспроизведение ПЕРЕД пином — там, где промт этого требовал (пп. 2, 12, 13):** три мутации
посажены разом в HEAD-код и прогнана ПОЛНАЯ батарея — 18 пакетов, exit 0, скипов 0. То есть все три
дыры подтверждены исполнением, а не прочитаны в пинге.
**Живые пробы — настоящий `tmctl`, живой Postgres, дев-демон на 127.0.0.1:8098:**
- `GET /v0/capabilities``"contract_version": "0.4.0"` (пункт 1 на проводе);
- `tmplatformctl seed` — аккаунт, грант, книга через ЖИВОЙ `POST /v0/books`: интейк прошёл, движок
нарезал 3 главы, `GET /chapters` отдаёт 3, долг материализации погашен, `read_model_attempts = 0`.
**Это отрицательная проверка нового пола: законный манифест живого движка он НЕ отвергает;**
- **PD-162 воспроизведён живьём** — каталог книги унесён, прогон принят (202), спавн падает.
`tmplatformctl runs` печатает строку с числом неудач `1`, временем следующей попытки, суммой холда
`0.090000`, его возрастом, `(none)` вместо юнита и текстом ошибки движка. **До этого пака оператор
видел только растущий счётчик `sweep_unfinished_total` и не мог сделать ничего;**
- `run abandon` без флага: прогон `failed`/`service_error`, деньги не тронуты, книга РАЗМОРОЖЕНА —
следующий прогон принят (202). С флагом `--release-hold`: холд вернулся целиком;
- **деньги пере-считаны из сырого леджера двумя путями и сошлись до цента:** кэш
`account_balances.balance_micro_usd` = 5000000, `sum(amount_micro_usd)` по `credit_ledger` =
5000000, открытых холдов 0; строки леджера — `grant · hold · hold_release · hold · hold_release`;
- метрики на проводе: `tm_platform_runs_stalled 0`, `tm_platform_reading_surfaces_abandoned 0`;
- обе новые ручки конфигурации печатаются на буте с источником (`TM_PLATFORM_SWEEP_BUDGET` `2m0s`
`default`, `TM_PLATFORM_RUN_BUDGET` `1m0s` `default`) — норма PD-114.
**Гейты доков:** `python3 docs/scripts/counts.py --check` — exit 0, литералы сходятся;
`--lint` — exit 0.
**Раскрываю правку ЧУЖОГО документа:** пак убил два якоря в `PLATFORM_P8_REVIEW_SESSION_PROMPT.md`
(константы `sweepBudget`/`defaultRunBudget` переехали), и `--lint` был из-за этого КРАСНЫМ. Якоря
пере-нацелены на новые места, абзац оставлен как описание НАЙДЕННОГО, а не текущего кода. Правил
чужой промт сознательно и говорю об этом прямо: якоря умерли по моей вине, а красный гейт не
обходят. Оркестратор вправе откатить эту правку — содержательного текста она не трогает.
⚠ Первая моя проверка этого гейта дала EXIT=0 и была ЛОЖНОЙ: я мерил в момент, когда файла с
якорями в дереве не было, поэтому линт его не сканировал. Критик полноты был прав, я нет.
### Спил пер-термной подписи банка (слово владельца 22.08) и что осталось контракту
**Заказ:** модель отменена `D39.144` ещё 16.08, реализации нет — спилить в зоне, оставить пометку
будущим сессиям. Перед спилом — аудит воркфлоу: 5 линз (канон · код зоны · потребители ·
ратификация · будущая пометка) + адверсариальная проверка каждого рискованного вывода, **31 агент**.
**Снято из зоны:** маршрут `POST /books/{bookId}/bank/decisions` · хендлер `bankDecisions` и
wire-типы (`wireBankDecision`/`wireBankDecisions`/`wireBankReceipt`) · метод `SubmitBankDecisions`
из интерфейса `Library` и из стора · типы `BankDecision`/`BankReceipt` · `ErrUnknownTerm` /
`UnknownTermError` и их отображение в контрактный код · пины, которые их описывали.
**НЕ тронуто, и это проверено:** чтение банка (`GET /bank`, `BankTerm`, оси статуса и происхождения)
· вокабула ДВИЖКА `proposed|in_progress|approved` в сайдкар-инжесте — она чужая и не отменялась ·
сам стоп `awaiting_bank` и подпись как `resume` (D39.144 снимала ГЕЙТ, не СТОП) · таблица
`bank_decisions` и её констрейнты: миграции релизные, а колонки `term_id`/`dst` — ровно то, что
нужно будущей ПРАВКЕ термина. Данных под снос не было: на стенде ноль строк решений.
**Пометка на месте write-пути** (`internal/httpapi/reading.go`, «THE WRITE HALF OF THE BANK IS
GONE»): что именно отменено, что ратификация ОСТАВИЛА пер-термным (правку и добавление термина),
чего нет ни на одной поверхности, и что под это уже есть в схеме и в движке (`mined_delta`).
**Гейт размера поверхности сработал**`len(contractSurface) < 15` поймал снятие маршрута. Пол
сдвинут 15 → 14 **с причиной в комментарии**: пол, опущенный под ратифицированное сокращение, — не
подгонка; опущенный без неё — подгонка. Два теста, стоявшие на снятой ручке как на «любом POST»,
пере-нацелены на живой маршрут, а не удалены.
#### Насколько страдает контракт (главный вопрос владельца)
Канон **не лжёт про модель**`D39.144` в него внесена и записана в четырёх местах верно
(`openapi.yaml:443-448`, `:539-541`, `:707`, `:2498-2499`). Беда в другом: **рядом с новой моделью
в полном рабочем обмундировании стоит старая** — путь, три схемы, четыре счётчика, — и обе зовутся
словом «signing». Файл несёт ДВЕ модели подписи.
- **Прямая ложь — шесть мест** (четыре в каноне, два в компаньоне). Худшие: `:1830-1832`
(«статус подтянется на следующей пересборке» — не подтянется, решения до движка не доезжают, что
говорит сам же канон на `:1959-1963`) и компаньон `:672``POST /bank/decisions` НЕ ПОСТРОЕНО» —
было построено в P7 и снято сегодня).
- **Самопротиворечия — три.** Самое явное: `:443` «Signing the bank is ONE act over the whole of
it» против `:477` «Signing is not a row edit» — заголовок описания ручки называет подписью САМУ
РУЧКУ. Тридцать четыре строки между ними, и оба текста компилируются в сгенерированный клиент.
Плюс `:1918-1922` («строка может быть решена и НЕ подписана, потому что отказ — тоже решение») —
дословно отменённая модель — соседствует с `:1929-1932` («INFORMATIONAL, never a gate»).
- **Дыра, которую спил вскрывает и которую надо назвать вслух:** `approve` + непустой `dst` был
ЕДИНСТВЕННЫМ живым носителем того, что D39.144 оставила пер-термным — «поправить термин». А
«добавить свой термин» модель называет, и в контракте его **нет вообще**: на 17 путей канона
запись в банк была ровно одна. То есть после спила у собственной фразы канона `:539-541`
(«correct what they care to, then release it with ONE resume») **нет ручки**.
- **Счётчики вырождаются.** `pending_decisions` считается как «строки `proposed`, на которые НЕТ
записи в `bank_decisions`»; писателя больше нет ⇒ это «сколько строк предложено», а `complete`
истинно ровно тогда, когда read-out движка не несёт ни одной `proposed` (не константа: сайдкар
несёт и `in_progress`, и `approved`). Поля обязательные на проводе (`EventBank.required`), поэтому оставлены как есть:
менять их смысл — правка канона, а не зоны.
- **Объём и вес:** ~17 мест в `openapi.yaml` (~150 строк из 2592) + ~8 в компаньоне. Вес
**ломающий минор 0.4.0 → 0.5.0**; мажора нет ни по политике («0.x ломает миноры весь бета-период»),
ни по факту. Цена фронту нулевая ДОПОЛНИТЕЛЬНО: его зеркало ратифицированно стоит на 0.2.3.
**Пока канон не поправлен, деплой обслуживает на одну ручку меньше, чем объявляет.** Это записано
строкой `PD-370` (зонная половина закрыта, контрактная открыта), и это осознанная цена спила, а не
недосмотр.
### Волна 2: воркфлоу-ревью и что оно нашло в САМОМ паке
Заказ владельца: после возврата ревьюеров волны 1 и починки их находок — воркфлоу пошире, 1030
агентов, один Fable 5 на тяжелейший участок. Исполнено: **11 линз поиска + адверсариальный рефутер
на КАЖДУЮ находку + синтез, 47 агентов, 0 ошибок**. Fable 5 — один, на свипе и деньгах.
**36 находок, 21 пережила рефутера, 15 опровергнуто.** После склейки дублей (голодовку расчётной
фазы назвали пять линз, срез по байту — две) это **17 строк регистра**: `PD-353…PD-366` закрыты,
`PD-367`/`PD-368`/`PD-369` открыты — они не мои решения и переданы как вопросы.
⚠ **Итоговая строка самого воркфлоу («ни одна находка не пережила проверку») — БАГ МОЕГО скрипта
синтеза, а не вердикт.** Она противоречила сама себе (пустыми были и «выжившие», и «убитые»), и
таблица пере-собрана из `journal.jsonl` — по одной записи на агента. Говорю прямо: доверься я
сводке, пак уехал бы на приёмку с блокером внутри.
**Блокер, найденный ПЯТЬЮ независимыми линзами (включая Fable 5) и подтверждённый мной по коду
до их вердикта — `PD-353`.** Пак разделил проход на фазы, чтобы одна не могла съесть другую, и
оставил ВНУТРИ второй фазы то же самое голодание: `UnsettledRuns` упорядочен старейшим-первым,
каждый элемент стоит целый бюджет, а `settle` при нечитаемой трате возвращает nil — то есть клин
рапортует УСПЕХ, потратив проход. Одна недоступная книга замораживала холды ВСЕХ аккаунтов, каждый
проход. Это ровно та формулировка, которую шапка `Sweep` объявляла побеждённой.
**Ещё два, которые делали механизмы пака недостижимыми:**
- `PD-354` — счётчик неудач обнулялся ТИШИНОЙ: дорогой вопрос движку задаётся раз в `ResyncEvery`,
остальные проходы ничего не устанавливают, и `ClearRunDeferral` на них сбрасывал счёт. 1,0,1,0 —
`StalledAfter` недостижим, гейдж пуст, оператору не говорят никогда.
- `PD-355` — колонка SPENT в операторской таблице показывала пожизненную трату КНИГИ. Моя же
починка G8 волны 1 взяла не то число, на единственной таблице, заведённой ради решения о деньгах.
**Чему меня научили РЕФУТЕРЫ (15 опровержений, каждое куплено исполнением, а не рассуждением):**
- «Поднять `RunBudget` = объявить здоровый прогон застрявшим» — **опровергнуто замером**: поднятие
ручки не меняет вообще ничего, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами
шиппящиеся дефолты. Я уже держал эту «починку» в плане и не построил её. Остаток — `PD-368`.
- «Интейк обязан применить `Whole()`» — **опровергнуто контрактом и батареей**: вся батарея интейка
ездит на манифестах без списка глав, контракт интейка — только счётчики. Механика находки верна
(`PD-367` открыт), лекарство — нет.
- «Пропуск тела помощника в гейте SQL = дыра» — **опровергнуто исполнением**: мутация в теле `bump`
краснит десятки поведенческих тестов; спрятать там непроверяемый оператор нельзя.
- «Стойл-детектор слеп к форме PD-162» (находка Fable 5) — опровергнуто.
**Две линзы разошлись во ВЕСЕ одной находки** (`PD-358`, срез по байту в операторской таблице):
одна назвала микрополировкой вне рамки, вторая довела цепочку до многобайтового stderr движка.
Починил как дешёвое и бесспорное, расхождение раскрыл в строке.
### Самопроверка (артефакт §5: имена агентов и по одной команде или цитате из каждого)
Исходов три, «не проверено» ≠ «опровергнуто». Каждому агенту выдан СВОЙ каталог песочницы явным
путём, мандат читающий, state-changing git запрещён перечислением (D39.113). В рабочее дерево не
писал никто: оба ревьюера волны 1 ответили на прямой вопрос уликами.
**Волна 1 — три изолированных ревьюера по финальному диффу (author≠reviewer):**
| Агент | Ось | Цитата или команда ИЗ ЕГО отчёта | Находок | Исход |
|---|---|---|---|---|
| `agent-afec473415b228cdf` | контракт-конформность против канона 0.4.0 + свои посадки | «в песочнице создан симлинк `<scratch>/docs -> …/docs` — новый гейт `internal/gates/contract_test.go` тянется за канон ЗА пределы модуля, и без него копия дерева его не находит» | 5 + 3 пережившие мутации | все 8 починены |
| `agent-a394edded388676bc` | шов с движком и деньги, свой инвентарь каналов | «`repo*/platform/cmd/rev2probe\|rev2ctl\|rev2seam\|rev2ceiling\|rev2ids/main.go` и `repo*/platform/internal/runs/rev2_starve_test.go` — мои пробы (только в копиях)» | 8 + 3 пережившие мутации | все 11 починены; G1 — БЛОКЕР в моём коде |
| `agent-a1e4407d89e52128d` | полнота сдачи: пункт заказа → носитель в дереве | «Snapshot taken at 2026-08-22 00:49:24. ⚠ The tree was moving while I read it: `platform/BACKLOG.md` gained rows П-19/П-20 at 00:48, mid-review» | 6 | 5 починены, 1 (К-4) вскрыла МОЮ ошибку измерения |
Две разведки (read-only, не ревью): `agent-acf370d564663617e` — инвентарь SQL для границы sqlc;
`agent-a1cb3d1a5c59f2f2e` — инвентарь каналов движка.
**Волна 2 — воркфлоу `wf_f8d1f3e6-85a`, 11 линз (одна на Fable 5) + рефутер на каждую находку:**
| Линза (агент) | Предмет | Цитата или команда ИЗ ЕГО отчёта | Находок | Пережило |
|---|---|---|---|---|
| `a2fdcd13144058b57` **Fable 5** | свип и деньги — тяжелейший участок | «Стойл-детектор слеп к клину „юнит исчез, workdir недоступен“ (форма PD-162): реконсиляция каждый проход рапортует успех» | 3 | 2 |
| `ae8f856fe08ee9e61` | фазы и отсрочка свипа | «Счётчик отсрочек не может дорасти до StalledAfter для того самого клина, ради которого написан» | 4 | 3 |
| `a3c4dc82a7d62f8a6` | архитектурный стиль, велосипеды | «Фаза расчёта денег осталась без отсрочки: head-of-line starvation, от которой пак и построен, на списке `UnsettledRuns` не починена» | 3 | 1 |
| `a62786a00eb5bbae4` | каноны Go и каноны зоны | «RunBudget молча обрезан половиной SweepBudget» (опровергнуто) + голодовка расчётной фазы | 4 | 2 |
| `af8620a80b7e12b06` | материализация и пол манифеста | «Провалившийся экспорт на смене кадра затирает весь оплаченный текст книги» | 3 | 3 |
| `ada2420ac7c576856` | CLI, метрики, конфиг | «SPENT в операторской таблице — пожизненный счётчик КНИГИ, а не траты этой попытки» | 3 | 3 |
| `ac92e0a8acba148ec` | сам гейт SQL: даёт ли ложную зелень | «`unresolvable` ключёвана функцией, а не сайтом: второй несворачиваемый запрос в той же функции молча проверяется текстом первого» | 2 | 1 |
| `a21f43459f3f12f7e` | комментарии: вода против несущего | «perf_test.go копирует „нормативные“ цифры из миграции 00022 и в той же фразе утверждает, что не копирует» | 4 | 1 |
| `a8faf63f1dde50903` | скоуп: сверка построенного против БУКВЫ промта | «Пункт 9: новое условие замера в perf_test.go ложно для бенчмарка этого же файла» | 4 | 2 |
| `a4dad647c0eb8310f` | регрессии: что пак сломал в уже работавшем | «Отмена контекста сообщается как дедлайн: остановка демона посреди прохода поднимает счётчик голодания» | 3 | 2 |
| `a178aca863af19915` | контракт-конформность построенного против канона 0.4.0 | «Пункт 1: подъём константы оставил в коде утверждение, что деплой объявляет 0.3.0» (опровергнуто) | 3 | 1 |
Рефутеры (по одному на находку, 36 штук) считаются наравне с находками — три примера того, чем
куплены их вердикты:
- `ad12d9bcca05b883a`: «`WITH the index n_tup_upd=3942 n_tup_hot_upd=3909 (HOT 99.2%) idx size=8192`
против `WITHOUT the index n_tup_upd=1994 n_tup_hot_upd=1984 (HOT 99.5%)`» — из этого замера
выросла `PD-364` (индекс удалён);
- `a337318688be20d30`: «`finished_at = now()` → `books.go:313: column "finished_at" of relation
"books" does not exist (42703)` … на всех 5 сайтах»;
- `a7fadd3c541e9a87d`: «пять проходов подряд дают „the sweep settled 2 of 3 finished runs: context
deadline exceeded“, третий, чистый прогон не достигается никогда, reserved держится на $0.90» —
это подтверждение блокера `PD-353` исполнением.
**Посадки мутаций волны 2 — 6, поймано 6:** фаза расчёта не отсрочивает ✅ · `UnsettledRuns`
игнорирует отсрочку ✅ · тишина снова считается здоровьем ✅ · `RunBudget` снова не присваивается ✅ ·
срез по байту в операторской таблице ✅ · управляющие символы не вычищаются ✅.
⚠ Две первые попытки посадить последние две дали НЕ падение теста, а ошибку сборки (осиротевший
импорт) — это слабее, и они пере-посажены компилирующимися.
### Релей §4.7: не приходил
Аддендум пака P8-REVIEW должен был доехать **только релеем оркестратора с эхо-подтверждением**.
За сессию релея не поступало: ни находок по четырём осям, ни эхо. Отчёт читающего пака я не читал
и не гадал по нему — промт это прямо запрещает, состав пака определяет оркестратор.
Единственное касание того пака — техническое и раскрыто выше: два его якоря умерли от МОИХ правок
кода, `--lint` покраснел, якоря пере-нацелены (`docs/PLATFORM_P8_REVIEW_SESSION_PROMPT.md:62`,
плюс третье пере-нацеливание 22.08 после спила). Содержательного текста чужого промта не трогал.
### Obstacle: что не удалось и что не проверено
- **Сквозного живого перевода через платформу не было** и в этом паке быть не могло: строка 202
гейчена холодным прогоном движка (решение владельца 20.08). Всё после «прогон стартовал и
остановился» доказано батареей против живого Postgres и настоящего `tmctl`, а не переводом.
- **Труба 199(а) не построена и форматы сверены ЧТЕНИЕМ**, а не записью файла: `mined_delta` и
`mined_rejects` прочитаны в коде движка (`membank.LoadGlossarySeed`, `mining.go`), но ни одного
файла платформа не писала — писать было бы строительством нератифицированной развилки.
- **Замер `PD-297` не снят.** Отказ осознанный: путь самый опасный на запись, править его надо с
замером на корпусной книге до и после, а корпусной книги в паке не было.
- **Финальный дифф ПОСМОТРЕН** независимым ревьюером (author≠reviewer) уже после спила — он и
закрыл эту строку obstacle. Три его major были настоящими дырами в МОЁМ коде, и все три —
отсутствие пина, а не ошибка логики: пол `ErrTextUnknownForANewCut` (охраняет единственную
запись, способную стереть оплаченный текст) · «стоп перевешивает отсрочку» в `RunsToReconcile` ·
«пересборка банка не трогает `bank_decisions`» (свойство, на которое опирается пометка, ради
которой спил и делался). Каждая закрыта пином, каждый пин проверен посадкой мутации. Плюс семь
минорных: комментарии `SaveBank` и `bankCountsTx` описывали отменённую модель как живую,
пометка врала про `action NOT NULL`, снятие NUL не было запинено. Не найдено дефектов там, где
он искал отдельно: пол поверхности 15→14 равен реальному размеру таблицы, пере-нацеленные тесты
проверяют то же свойство, лишнего не срезано.
- **Число скипов батареи** снято рецептом `make check`, который пишет `.check.log` в дерево; при
параллельных прогонах агентов лог обрезался. Отдельная сверка двух пакетов с условными скипами
(`internal/runner`, `internal/gates`) дала `--- SKIP` = 0.
- **Контрактная половина `PD-370` открыта**: пока канон не поправлен, деплой обслуживает на одну
ручку меньше, чем объявляет. Это цена спила, названная заранее, а не обнаруженная при приёмке.
### Инвентарь каналов движка (артефакт §4.4 — собран ЧТЕНИЕМ кода движка, не по нашим докам)
Пак коснулся канала движка (пункт 10: `ingest.Manifest.Whole()` построен как зеркало движкового
правила), поэтому инвентарь сдаётся таблицей, а не словами «я прочитал код движка».
| Канал | Писатель в движке | Атомарность | Читатель/писатель платформы | Согласовано? |
|---|---|---|---|---|
| `<project_db>.manifest.json` | `pipeline/manifest.go:379` `writeFileAtomic` | **атомарно** (temp+`Sync`+rename, `pipeline/artifact.go:24-59`) | `internal/runner/engine.go` (`tmctl manifest`) → `ingest.DecodeManifest` → `internal/books/parse.go`, `internal/readmodel` | ✅ **и стало строже**: `Manifest.Whole()` теперь зеркалит `BookManifest.selfConsistent` (`pipeline/manifest.go:460-483`) поле в поле по тем правилам, которые платформа вообще читает (главы, юниты, нумерация). Верхняя граница чанков движка у нас counterpart'а не имеет намеренно — платформа счётчиков чанков не берёт |
| `<project_db>.bank.json` | `pipeline/bankexport.go:154` `writeFileAtomic` | **атомарно** | `internal/runner/artifacts.go:37` → `ingest.DecodeBank` → `pgstore.SaveBank` | ✅ |
| `<project_db>.bank-stop.json` | `pipeline/mining.go:393` `writeFileAtomic` | **атомарно** | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ канал существует и не потребляется; полная таблица подписи платформе сегодня не нужна, она проецирует банк |
| `<project_db>.bank-stop.txt` | `pipeline/mining.go:195` `os.WriteFile` | **НЕ атомарно** (усечение первым делом) | читателя нет | ✅ и это важно: брать его на живом прогоне нельзя — исправление пинга №15 оркестратором подтверждается кодом |
| `<project_db>.mined-signature.yaml` | `pipeline/mining.go:188` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `<project_db>.auto-bank.yaml` | `pipeline/mining.go:741` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `mined_delta` (путь из `book.yaml`) | **писателя в движке НЕТ** — только читатель `pipeline/mining.go` `loadMinedDelta`; формат `seed.File` (`internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed`, `Source` пере-штампуется на `"mined"` | — | **писателя НЕТ и у платформы** | ❌ **разрыв — это и есть строка 199(а)** |
| `mined_rejects` | читатель `pipeline/mining.go` `loadMinedRejects`; формат `rejects: [{src, note}]`; ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит | — | писателя нет | ❌ тот же разрыв |
| `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`; расчёт денег | ✅ пак не менял форму; изменил лишь то, СКОЛЬКО раз его зовут при неудаче (бэкофф) |
| `book.yaml` | оператор (шаблон) + **платформа один раз** (`internal/books/render.go`, `O_EXCL`) | — | платформа ПИШЕТ пять фиксированных ключей | ⚠ **декодер движка СТРОГИЙ** (`config/book.go:142-144`, `KnownFields(true)`): незнакомый ключ = жёсткая ошибка. Значит любое расширение набора ключей платформой — ратификация, а не правка. Прямо относится к развилке 199(а) выше |