textmachine/platform/docs/platform-PROGRESS.md

491 lines
69 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.

# Журнал зоны «Платформа»
> **Что это.** Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше.
> Отработавшие эры вынесены срезами в [`archive/`](archive/) — читать только по конкретной ссылке.
## Состояние зоны на 07.09.2026
| Вопрос | Ответ |
|---|---|
| последний заленджённый пак | «форма заказа перевода» (0506.09, `628cc56`+`36ea8b8`, акты **D39.208**, **D39.211****D39.214**), канон контракта `0.12.0` |
| пак в дереве, не закоммиченный | «деньги и правда на экране» — отчёт ниже |
| открытые дефекты | `DEFECT_REGISTER.md` (счёт — `python3 docs/scripts/counts.py` от корня) |
| нормы и приёмка | `ENGINEERING_STANDARDS.md` · направление — `PLATFORM_DIRECTION.md` · стек и стенд — `STACK_DECISIONS.md` |
| незакрытые куски работы | `../BACKLOG.md` (`П-N`) |
| как разворачивается | `../deploy/README.md` |
## ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (0607.09, `textmachine-bf`)
> Промт `docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md`, вход HEAD `e4097cb`, дерево на входе чисто.
> Зона НЕ коммитит — дерево передано оркестратору №23 (`textmachine-a8`).
### Что построено
| Заказ | Что стало с деревом | Чем предъявлено |
|---|---|---|
| §4.0 триаж журнала | журнал 5029 → **491** строки, из которых 385 — этот отчёт; две эры вынесены срезами в `archive/` | `carriers.py` по живому файлу — **0** находок при 491 осмотренных строках; вынос разобран построчно (ниже) |
| `PD-441` объявленная маржа | каждая строка `settlement` несёт БАЗИС; `runs` печатает `≥` перед суммой и легенду | два независимых пути по деньгам (ниже) · пины `TestEverySettlementSaysThatItsFigureIsAFloor`, `TestOnlyAnEndingTheEngineChoseIsSettledAsComplete`, `TestTheOperatorsTableSaysSpentIsAFloor…` |
| `PD-424` окно гонки | арбитр В ХРАНИЛИЩЕ: `AbandonOrder.ProofAttemptID` + `ProofSpawns`, сверка под книжной блокировкой | пины `TestAProofAboutTheAttemptBeforeThisOneIsRefused`, `TestAnAttemptClaimedWhileSystemdWasBeingAskedIsRefused` |
| `PD-438` видимость парковки | колонка `run_attempts.parked_at`, гейдж `tm_platform_parked_attempts`, ячейка PARKED в `runs` | пины `TestAParkedAttemptIsVisibleInTheRowTheGaugeAndTheListing`, `TestTheOperatorsTableSaysSpentIsAFloor…` |
| строки 285 + 325 | ⛔ **ПОСТРОЕНО, НО НЕ ГОТОВО — сработало правило остановки, см. секцию ниже.** Синхронный $0 `tmctl manifest` на приёме: fail fast и вход сужен по ЧИСЛУ ГЛАВ | 4 пина `failfast_test.go` + `TestOnlyTheHolderOfTheParseClaimCanDeleteARefusedIntake` + пин медленного разреза (круг 3) и три пина круга 4 (деплойные ветви · материализация · очередь) |
| строка 305 | `make check` при таймауте называет пакет, тест и причину | воспроизведено и предъявлено сквозным `make check` на копии |
| §4.4б | комментарий `THE FIX IS NOT IN THIS PACKAGE` приведён к поведению | греп по отозванной формулировке — 0 при 197 осмотренных `.go` |
### Заказ по пунктам — исход каждого
| Пункт промта | Исход |
|---|---|
| §4.0 триаж журнала | **сделано**; независимо пере-проверено оркестратором №23 мультимножеством и `carriers.py` по самим срезам |
| §4.1 `PD-441` | **сделано в достижимой половине**; вторая половина — движковая, названа поимённо и ушла строкой бэклога |
| §4.1 `PD-424` | **сделано**, и предмет оказался шире ряда: окон два |
| §4.1 `PD-438` | **сделано** |
| §4.2 строка 282 | **сознательно не делаю** — снято до выдачи, платформенная половина уже построена |
| §4.3 строки 285 и 325 | ⛔ **построено и ОСТАНОВЛЕНО**: пять кругов самопроверки, девять major из одиннадцати — в этом одном механизме, третий подряд промах бюджета хвоста. Предмет уезжает отдельным паком по правилу остановки оркестратора №23; семь незакрытых пунктов перечислены поимённо |
| §4.4 два малых дока | **не трогаю** — зона оркестратора; расхождения ушли пингами (ниже) |
| §4.4а строка 305 | **сделано**, предъявлено сквозным `make check` на копии |
| §4.4б комментарий | **сделано** |
| §4.5 запреты | **соблюдены**, проверено исполнением: контракт, `ENGINEERING_STANDARDS`, `PLATFORM_DIRECTION`, `backend/` — 0 изменённых файлов; коммитов 0; индекс пуст |
| пинги оркестратора: `PD-458`, `П-10` | **пере-сняты по дереву, оба подтвердились**; `PD-458``fixed`, `П-10` закрыта актом зоны |
### Два круга самопроверки — что нашёл каждый
**Круги сошлись на ТРЕТЬЕМ, а не на втором.** Второй я вела сама и объявила сходимость
преждевременно: адверсариальный субагент нашёл четыре major, три из них — дефекты, которые ввёл ЭТОТ
пак и которых не видел ни один мой пин.
| круг | находка | что сделано | чем предъявлено |
|---|---|---|---|
| 1 (мой, по готовой работе) | посадка «снят сравнитель попытки» ВЫЖИЛА — случай ловился чужим охранником | новой попытке дан тот же счёт заявок + сверка ТЕКСТА отказа | пере-посадка красная, текст называет именно сравнитель попытки |
| 1 | посадка «снят охранник `is null`» ВЫЖИЛА — два охранника прикрывали друг друга | добавлено обращение к записи напрямую | пере-посадка красная: `a second mark moved the stamp` |
| 1 | синхронный разрез добавил хвост, о котором бутовый гейт не знал | `UploadSettle = CutBudget + writeBudget + 30s`, `CutBudget` стал константой | пин + мутация, текст называет оба числа |
| 2 (по заказу и по первому кругу) | «≥» и PARKED были предъявлены ЧТЕНИЕМ КОДА, а не исполнением | заведён пин, читающий настоящий вывод команды | он же поймал мою ошибку в ожидании: `money.USD()` не печатает `$` |
| 2 | охранники `DeleteRefusedIntake` (стадия, claim, отсутствие прогона) не запинены | заведён пин | мутация «охранник claim'а всегда истинен» → `gave <nil>, want ErrNoBook` |
| 2 | дельта контракта врала про `character_count_exact` и `structure` | ⚠ **починка второго круга оказалась НЕВЕРНОЙ и пере-сделана четвёртым:** материализация с интейка снята, поэтому в `201` оба поля отсутствуют ВСЕГДА, а не «когда материализация удалась» | дельта переписана; пин `TestTheIntakeNeitherMaterializesNorEnqueuesWhenItsCutSucceeds` |
| 2 | числа отчёта устаревали четырежды | сняты заново после последней правки | команды и выводы — в разделах ниже |
| **3 (опровергатель, `fable`)** | ⛔ **пере-чтение строки для `201` шло на контексте, открытом ДО тела** (бюджет 30 с), а разрез длится до 90 с ⇒ после медленного разреза `201` нёс `parsing`/0 глав над строкой `not_started`/500 | свежий `writeCtx` для пере-чтения | пин `TestASlowCutStillAnswersWithTheRowAsItStandsAfterIt`; бюджет укорочен сеемым полем, а не ожиданием |
| 3 | ⛔ **три ветви деплойного класса НЕ отдавали claim и тратили попытку**, а одна писала `rejected`, который `201` выносил пользователю | все три при `atIntake` возвращают `errNotConclusive`, не трогая `defer_`/`reject` | пин `TestADeploymentFaultLeavesNoVerdictAboutTheFile`: `parse_attempts = 0`, `parse_started_at is null` |
| 3 | ⛔ **синхронная материализация читательской поверхности** (до 5 мин) держала запрос и не входила в `UploadSettle` | на интейке не материализуем — долг записан, забирает свип; `UploadSettle = CutBudget + 3×writeBudget` с перечнем шагов | гейт `internal/config` читает саму константу |
| 3 | ⛔ **маппинг обоих отказов на HTTP не был запинен** — обмен кодов местами оставлял пакет зелёным | пин по КОДУ, а не по факту 400 | `TestTheIntakesOwnRefusalsReachTheWireWithTheirOwnCodes` |
| 3 | ⚠ **сужение входа действует только на синхронной ветви** | закрыть сегодня нечем: словарь `RejectReason` закрыт, ближайшее значение УДАЛЯЕТ исходник | заведён `PD-463`, пинг ниже |
| 3 | `PD-458` помечен `fixed(в дереве пака…)` — ложная атрибуция | `fixed(0632a30)` | `git merge-base --is-ancestor 0632a30 e4097cb` → да |
| 3 | ряды `PD-441`/`PD-424`/`PD-438` не несли диспозиции пака | дописаны, форма ряда сохранена | `awk -F'|' '{print NF-2}'` по трём рядам → 7, 7, 7 при шапке 7 |
| 3 | `TestTheUploadSettleBudgetCoversTheSynchronousCut` — ТАВТОЛОГИЯ | тест удалён, вместо него перечень шагов в комментарии константы | `CutBudget = 10h` оставлял его зелёным, а `internal/config` давал 12 красных |
| 3 | замер денег не воспроизводился: скратч-БД удалена, команда не названа | стал ПИНОМ, печатающим обе стороны | `TestTheRawLedgerAndTheReadModelAgreeOnWhatWasSpent` |
| 3 | живой `STACK_DECISIONS` отсылал в архив, чей баннер запрещает исполнять инструкции | сказано, что правило целиком стоит на месте, а архив — археология | — |
| 3 | док-комментарий `cutNow` склеен с `cutResult` | разделены | `gofmt` и `go vet` чисты |
| 3 | числа «потеряна 1 строка» и «195 осмотренных `.go`» | пере-сняты: **10** строк (все названы) и **197** | раздел «Команды и их вывод» |
| **4 (опровергатель, `fable`)** | ⛔ **задание очереди ставилось в транзакции `StartParsing` и гонялось с синхронным разрезом за один claim** — выигрывает очередь ⇒ fail-fast молча нет; выигрывает разрез ⇒ job съеден впустую, и после возврата claim'а книгу подбирает только свип через 20 мин | job ставится ТОЛЬКО там, где разрез не идёт; на пути «вердикта нет» — вместе с возвратом claim'а, одной транзакцией (`ReleaseParseClaim`) | пины `TestTheIntakeNeitherMaterializes…` и `TestACutWithNoVerdictGivesTheClaimBackAndEnqueuesTheJob`, обе мутации красные |
| 4 | ⛔ **`UploadSettle` снова не покрывал хвост:** `StartParsing` (30 с) выпал из перечня, а квитанция идемпотентности 10 с, не 30 ⇒ худший путь 190 с при константе 180 | `CutBudget + 4*writeBudget`, перечень шагов пере-написан ПО КОДУ и по бюджету каждого | гейт `internal/config` читает саму константу |
| 4 | ⛔ **из «трёх ветвей деплойного класса» запинена была одна**; мутации на `ErrStorageGone` и `ErrDirectoryGone` выживали | обе ветви решаются ДО вызова движка, поэтому запинены на своём уровне | `TestTheBranchesDecidedBeforeTheEngineAlsoLeaveNoVerdict` |
| 4 | ⛔ **починка «не материализуем на интейке» не имела пина** | заведён | мутация «снят `!atIntake`» → `the intake materialized the reading surface (1 claims, 1 refreshes)` |
| 4 | ⛔ **дельта контракта противоречила починке третьего круга** — обещала `character_count_exact: true` там, где его теперь не бывает | дельта переписана: оба поля отсутствуют в `201` ВСЕГДА | пин выше |
| 4 | `Settle` потерял док-комментарий: мой тип встал между ним и функцией | комментарий возвращён | `go doc ./internal/pgstore Store.Settle` печатает его |
| 4 | `PD-461` сам нёс дефект, который описывает (9 полей), и занижал счёт | ряд переписан: полей 7, строк ЧЕТЫРЕ, а не две | `awk` по разделителю: `PD-461` → 7 |
| 4 | в таблице «Что построено» стоял носитель-призрак — удалённый тест | заменён на настоящие | `grep` по имени → 0 |
| 4 | ⚠ **опровергатель ошибся** в одном числе: `character_count_exact` «в 3 файлах» | пере-снято: **4** (`v0.go` 2 · `project.go` 1 · `v0_test.go` 2 · `books.go` 2); он грепал только строковый вид имени | команда в разделе ниже |
| 4 | ⚠ мутация «снят охранник стадии у `DeleteRefusedIntake`» выживает | **по построению, а не из-за дыры:** каждый терминальный переход (`FinishParse`, `reject`) обнуляет `parse_started_at`, поэтому непустой claim влечёт `status = 'parsing'` — охранник избыточен | `grep -n parse_started_at internal/pgstore/books.go` — все четыре писателя |
### `PD-424`: окон оказалось ДВА, а не одно
Ряд называл одно — заявку на спавн между пробой systemd и коммитом. По коду их два, и второе опаснее:
`AbandonRun` под книжной блокировкой читает **любую живую попытку** (`a.ended_at is null`), поэтому
разблокировавшаяся расплата + `restart` подставляют под доказательство о попытке N **попытку N+1** с
живым процессом. Лечение — доказательство приходит парой (`ProofAttemptID`, `ProofSpawns`) и
сверяется в той же транзакции; расхождение — `ErrProofOvertaken`, отказ, а не применение.
`unit_name` в свидетели не годится: `ReleaseSpawnClaim` возвращает его в NULL. Отсюда монотонный
счётчик `spawns`, инкремент — внутри самого CAS `RecordSpawn`.
### Миграция `00034` (санкционирована оркестратором №23)
`run_attempts`: `spawns integer not null default 0` · `parked_at timestamptz`. Down-путь ГОНЯЕТСЯ
(`pgstore.TestMigrationsRollBackAndReapply`, зелёный на живом Postgres). Существующие строки поведения
не меняют: сверка идёт с ПРОЧИТАННЫМ числом, не с абсолютным. План гейджа снят исполнением: обе
подвыборки (`parked_at`/`quarantine_reason`) идут `Index Scan using run_attempts_live_idx` — новый
индекс не нужен. Запись парковки — только на ПЕРЕХОДЕ, в установившемся состоянии ноль операторов.
### `PD-441`: чем ограничена платформенная половина
Маржу платформа **измерить не может** — биллинга провайдера у зоны нет, а движковый леджер её не
несёт. Что сделано: цифра перестала читаться как цена. Что НЕ сделано и почему: показывать маржу
конечному пользователю нечего — недо-счёт бьёт по ДЕПЛОЮ, баланс пользователя завышен в его же
пользу (решение оркестратора №23, 06.09; формулировка «показать пользователю» из промта снята).
**Движковая половина — заказ следующему паку, поимённо:**
1. `backend/internal/pipeline/stagerun.go`, ветвь `No 2xx ever arrived: nothing was billed` — сеттлить
ОЦЕНКУ резервации, а не ноль, когда отменённый вызов уже ушёл; различитель — факт ухода, порог
обязан быть ЗАМЕРЕН, а не назначен (ряд называет латентность кандидатом: 20 мс против 156 000 мс).
2. `tmctl status --json` — публиковать рядом с `committed_usd` число и сумму строк, чья цена
ОЦЕНОЧНАЯ или неизвестна (движок уже печатает `estimated-cost rows: N`, строка бэклога **78**).
Без этого платформа умеет говорить «≥ X», но никогда «≥ X, до Y».
### Строки 285 и 325: одна правка, предикат — по данным
`tmctl manifest` зовётся синхронно после последнего байта. Четыре исхода:
`глав ≥2``FinishParse` инлайн, `201` несёт `not_started` и число глав · `глав 0``400`
`invalid_request`, `errors[{file, no_book}]`, не принято ничего · `глав 1``400`,
`errors[{file, no_chapter_structure}]` · **деплойный класс или таймаут**`201 parsing`, очередь
доделывает, claim ВОЗВРАЩАЕТСЯ (иначе задание очереди нашло бы книгу занятой и ничего не сделало).
⭐ Отказ **по числу глав, а не по расширению**: выдача кладёт по одному XHTML на главу ДВИЖКА, значит
«книга одним полотном» ⟺ движок нарезал <2 глав. `.epub` движок режет по nav/NCX и он проходит
блокировка по расширению отказала бы тому, что мы умеем доставлять. Go по паре и формату не ветвится.
#### Дельта контракта, которую ратифицирует оркестратор (минор `0.12.0` → `0.13.0`)
**Канон я НЕ трогаю** это зона оркестратора. Здесь лежит ТЕКСТ дельты, чтобы её не выводили заново.
**Что становится ложным:** `docs/architecture/14-api-contract/openapi.yaml`, описание `201` у
`createBook` фраза «**The `201` carries `parsing`, not `uploading`**». После синхронного разбора
`201` несёт `parsing` только на одном из четырёх исходов.
**ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОЙ ДЕЛЬТЫ ОТОЗВАНА (строка 285), и вот что она говорила неверно.** Она обещала, что
`character_count_exact` и `structure` зависят от того, «удалась ли материализация читательской
поверхности», и что при удаче приезжают в том же `201`. Это было верно ровно до починки четвёртого
круга: интейк БОЛЬШЕ НЕ МАТЕРИАЛИЗУЕТ читательскую поверхность (она стоит два движковых прогона и
держала бы запрос загрузившего), поэтому оба поля в `201` отсутствуют **ВСЕГДА** и приезжают
следующей ревизией библиотеки. Сказано вслух, а не заменено молча: при конфликте редакций
действует эта.
**Что несёт ответ `POST /v0/books` в каждом исходе:**
| исход | ответ | тело |
|---|---|---|
| движок нарезал ** 2 глав** | `201`, заголовок `Location` | `Book.status = "not_started"` · `chapter_count` = число глав движка · `character_count` = счёт интейка с **`character_count_exact: false`** и **`structure: null`** ВСЕГДА. Оба поля пишет `SaveStructure` вместе с читательской поверхностью, а интейк её не материализует (уплотняет запрос на два движковых прогона): они приезжают позже, проходом материализатора, и клиент видит их следующей ревизией библиотеки. Правило `null` не меняется |
| движок прочёл и нарезал **0 глав** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_book" }]`; книга НЕ принята ни строки в библиотеке, ни каталога на диске |
| движок прочёл и нарезал **1 главу** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_chapter_structure" }]`; тоже не принято ничего |
| **деплойный класс или таймаут** | `201`, заголовок `Location` | `Book.status = "parsing"` прежнее поведение маршрута целиком; очередь и страховочный свип доделывают |
**Деплойные классы перечнем** (ни один не отказывает пользователю): `not_configured` у книги нет
конфигурации движка, или шаблон деплоя не читается · `storage_unavailable` корень хранилища книг не
смонтирован · `schema_mismatch` проектная БД книги не той схемы, что бинарь · `parser_unavailable`
движок не удалось ЗАПУСТИТЬ, либо он ответил классом, которого эта сборка не знает, либо обычным
выходом 1 · плюс превышение бюджета синхронного разбора (`books.CutBudget`).
**Новых значений `ErrorCode` НЕ заводится.** Оба отказа существующий `invalid_request`; `no_book` и
`no_chapter_structure` живут в `errors[].code`, который сама спека объявляет НЕ закрытым Not closed,
like `cause.code`»). `no_chapter_structure` ВРЕМЕННЫЙ: он снимается, когда построена структура глав
для выдачи (строка бэклога **283**), и в коде ветки стоит этот номер, чтобы её нашли и убрали.
### Батарея, мутации и деньги
- **`make check` MAKE-EXIT=0**, 20 пакетов `ok`, красных 0, скипов **8**. Невыполненное условие хоста
названо: `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (рецепт требует $0-пайплайн
РЯДОМ с `backend/prompts/`, то есть записи в чужую зону или полной копии дерева).
- **Тесты: 853 875 (+22).** Тестов, существовавших ДО пака, не удалено ни одного
(`git diff -- '*_test.go' | grep -c '^-func Test'` 0). Один тест, добавленный ЭТОЙ ЖЕ сменой,
удалён третьим кругом как тавтологичный (`TestTheUploadSettleBudgetCoversTheSynchronousCut`:
`UploadSettle` определён через `CutBudget`, поэтому утверждение выполнялось всегда) в диффе против
`943617a` это не видно, и потому названо здесь. Число снималось ПЯТЬ раз и первые четыре были
неверны: «866 863» (глоб захватил не-тестовые файлы), «853 863», «853 864», «853 866» и
«853 869» каждое снято до пинов, добавленных следующим кругом самопроверки. В отчёте последнее,
после последней правки. Само по себе это и есть измеренная цена преждевременного объявления
сходимости: шесть замеров одного числа, потому что кругов оказалось не два, а пять.
- **Мутации: посажено 18, поймано 14 сразу, выжило 3, одна поимка отозвана** (её тест удалён третьим кругом, см. таблицу). Две выживших дыры в МОИХ пинах,
обе починены и пере-посажены (после починки красные); третья выживает СОЗНАТЕЛЬНО и названа.
Шестнадцатая посадка ОТБРОШЕНА мной как негодная: первая версия мутации охранника `claim`'а
краснила тест ошибкой ТИПИЗАЦИИ Postgres (`could not determine data type of parameter $2`), то есть
давала правый вердикт по неправой причине; пере-посажена корректно типизированной, и в таблице стоит
вторая.
| посадка | текст падения (или почему выжила) | хеш восстановлен |
|---|---|---|
| `report-failures` без строки сводки пакетов | `does not carry "textmachine/platform/internal/money"` | да |
| `report-failures` возвращён к грепу `--- FAIL` (исходный дефект 305) | то же, на всех трёх фикстурах | да |
| `report-failures` без счёта улик | `does not carry "Evidence, 1 lines"` | да |
| `AbandonRun` без сравнителя ПОПЫТКИ | **ВЫЖИЛА**: случай ловился сравнителем ЗАЯВОК правый вердикт по неправой причине. Пин починен (новой попытке даётся тот же счёт заявок + сверка ТЕКСТА отказа), пере-посажена `answered <nil>, want ErrProofOvertaken` | да |
| `AbandonRun` без сравнителя ЗАЯВОК | `answered <nil>, want ErrProofOvertaken: the unit name reads exactly as the proof saw it` | да |
| `RecordSpawn` без инкремента свидетеля | `the claim counter went 0 → 0: a witness that does not move cannot catch the race` | да |
| свип не пишет парковку | `the parked attempt carries no ParkedAt` | да |
| свип пишет парковку каждый проход (снят порог) | **ВЫЖИВАЕТ ПО ПОСТРОЕНИЮ**: охранник записи всё равно не двигает метку. Порог покупает СТОИМОСТЬ, а не свойство; названо в комментарии теста | да |
| `MarkParked` без охранника `is null` | **ВЫЖИЛА**: два охранника прикрывали друг друга. Добавлено обращение к записи НАПРЯМУЮ, пере-посажена `a second mark moved the stamp from … to …` | да |
| снятие метки сделано неисполнимым | `the attempt is materializing again and the row still says parked since …` | да |
| парковка посчитана карантинным гейджем | `parked=0 quarantined=1, want the park counted once and in its own series` | да |
| разрез выпал из `UploadSettle` (состояние ДО находки) | **поимка НЕВОСПРОИЗВОДИМА:** ловивший её тест удалён третьим кругом как тавтологичный, и в «поймано» она больше не считается | да |
| знак `≥` снят с колонки SPENT | `SPENT prints an exact amount; the engine's meter is a lower bound` | да |
| ячейка PARKED слита с QUARANTINE | `a parked attempt shows no elapsed time in PARKED, so «how long has it been quiet» has no answer` | да |
| охранник claim'а у `DeleteRefusedIntake` всегда истинен | `a delete under somebody else's claim gave <nil>, want ErrNoBook` | да |
| пере-чтение снова на контексте, открытом ДО тела | `after a cut that outlived the write budget the response says "parsing" with 0 chapters, want the parsed row` | да |
| ветвь «манифест не читается» снова тратит попытку | `the intake's pass spent 1 attempts of a budget that bounds how often a broken HOST is asked` + `the claim is still held (…)` | да |
| коды двух отказов на HTTP поменяны местами | `item code no_chapter_structure, want "no_book": the two refusals are different answers to the user` | да |
- **Деньги двумя независимыми путями** на настоящих строках: сырой SQL по `credit_ledger` (7 строк,
сумма 4919187 micro) и чтение платформы `ReadAccount` (Balance=LedgerSum=$4.919187) сходятся.
Расхождение до/после починки на тех же строках: было `note=""`, стало `at least: …` и, для
оборванной попытки, `…cut off mid-work… (PD-441)`.
### ⛔ СРАБОТАЛО ПРАВИЛО ОСТАНОВКИ — синхронный разрез НЕ ГОТОВ и должен уехать отдельным паком
Пятый круг дал major **внутри бюджета хвоста загрузки**, а оркестратор 23 отнёс этот бюджет к пути
синхронного разреза именно на этот случай. Правило исполнено: путь больше не чинится, находки ниже
записаны для следующего пака и НЕ закрыты.
**Чем правило сработало (замер, а не оценка).** `UploadSettle` не покрывает хвост ТРЕТИЙ раз подряд, и
каждый раз по новой причине. Сегодняшняя: перечень шагов в комментарии константы называет `FinishParse`
и `ReleaseParseClaim` АЛЬТЕРНАТИВАМИ whichever end the cut reaches»), а код их СКЛЕИВАЕТ ошибка
`FinishParse` уходит наверх (`internal/books/parse.go`, греп `owed, err := s.Store.FinishParse`), и
`cutNow` на любой не-`ErrBadIntake` ошибке зовёт `ReleaseParseClaim` на СВЕЖЕМ `writeCtx`. Худший путь:
`StartParsing 30 + разрез 90 + FinishParse 30 + Release 30 + ReadBook 30 + квитанция 10 = 220 с` при
`UploadSettle = 210 с`.
**Измерено, а не оценено (двоичный поиск по бутовому гейту через `Load()`):** гейт принимает дедлайн
до **26m29s**, ложь начинается с **26m21s** окно шириной восемь секунд, достижимое только ручной
настройкой почти вплотную к потолку самого гейта; на дефолте **10m0s** запас **16m20s**. Наблюдаемое
следствие **дубль книги, а не потеря**: претензия на ключ идемпотентности, пережившая `ClaimStale`,
перехватывается повтором с новым токеном, и человек, повторивший «висящую» загрузку, получает вторую
книгу вместо реплея первой. Достижимость худшего пути без искусственного замедления **не измерена**:
она требует конъюнкции «большая книга» и «три полных `writeBudget` подряд», а гейт живого движка на
этом хосте не закрыт подставное замедление на вопрос «достижимо ли БЕЗ него» не отвечает. Ряд
`PD-464`. Батарея этого не видит по построению: пин на сумму был
тавтологичным и снят третьим кругом, а мутация `4*writeBudget → 3*` оставляет и `internal/config`, и
`internal/books` зелёными.
**Почему это остановка, а не ещё одна починка.** Из одиннадцати major, найденных кругами 35, девять
лежат в одном месте синхронном разрезе на приёме. Это новый механизм в конкурентном платном пути
(claim · очередь · транзакция · бюджет), и он ведёт себя как такой механизм: каждая починка открывает
новую площадь. Три круга подряд одна и та же константа оказывалась короче хвоста, и каждый раз по
другой причине это свойство предмета, а не невнимательности.
**Что остаётся НЕ ЗАКРЫТЫМ в этом пути** (для пака, который его заберёт):
1. `UploadSettle` короче хвоста на 10 с (`PD-464`). Лечение **не поднять константу**: она
выводилась руками трижды и трижды была неверной, каждый раз по новой причине, поэтому четвёртый
вывод руками подпорка (`D39.216`). Границу надо выводить **ИЗ кода пути**.
2. Комментарий в `cutNow` the queue job is already enqueued») противоречит починке, ради которой
функцию правили: на этом пути задание ставит сам `ReleaseParseClaim`.
3. Отказ `ClaimParse` в `cutNow` не различает рутинную гонку (`ErrParseClaimed`) и сбой БД, и во
втором случае молчит: книга остаётся `parsing` без задания и без строки лога до свипа.
4. Параметр `enqueue` у `StartParsing` в бою мёртв (движок настроен всегда), а его doc описывает его
как штатный путь; решение «кто ставит задание» размазано по двум местам на одном предикате.
5. Охранник `RowsAffected() == 0 → задание не ставить` в `ReleaseParseClaim` не запинен ничем.
6. «ВСЕГДА» в дельте контракта гарантия по ТАЙМИНГУ, не по построению: свип материализатора
теоретически успевает между `FinishParse` и `ReadBook`. Практически вероятность близка к нулю, но
тексту контракта полагается либо структурная гарантия, либо оговорка.
7. Свип `StuckIntake` третий претендент на claim, в разборе гонки не назван: при
`TM_PLATFORM_UPLOAD_DEADLINE > claimGrace` он может взять claim раньше разреза.
8. Разрез потерял единственный ограничитель параллелизма: задание на приёме больше не ставится, а
`MaxWorkers: 4` был у очереди одновременных разрезов теперь не ограничивает ничто.
9. После последнего байта тела маршрут МОЛЧИТ до 210220 с; промежуточный прокси об этом не спрашивали.
10. Отказ уходит наружу только машинным кодом (`Detail` не заполняется) «честная причина человеку»
из §4.3 сегодня не доезжает. Фронт заморожен, значит либо причина едет в `Detail`, либо пункт
признаётся неисполненным.
### Замер, который должен пережить пак: очередь выигрывала гонку у разреза 5 раз из 6
Синхронный разрез берёт `ClaimParse` ПОСЛЕ коммита `StartParsing`, а `StartParsing` ставил задание
очереди ВНУТРИ той же транзакции то есть воркер становился видимым тем же коммитом и гонялся с
разрезом за один и тот же claim. Проба (энкьюер зовёт `Parse` сразу после коммита, как это делает
River на видимости строки), шесть прогонов: **очередь выиграла 5 раз, разрез 1 раз**.
Оба исхода стоят книге, и это делает гонку дефектом, а не шероховатостью:
- **выигрывает очередь** синхронного вердикта нет вовсе, пустой файл принимается `parsing`,
попытка бюджета потрачена, отказ приезжает асинхронно и оставляет книгу в библиотеке;
- **выигрывает разрез** задание съедено впустую (`ErrParseClaimed` `nil`), и после возврата
claim'а книгу подбирает только страховочный свип, то есть через `claimGrace` = 20 минут.
задание ставится ТОЛЬКО там, где синхронного разреза не будет, а на пути «вердикта нет» вместе с
возвратом claim'а одной транзакцией. Цена названа: процесс, умерший между строкой и разрезом,
оставляет книгу без задания, и её берёт свип через `claimGrace`, а не сразу.
### Команды и их вывод — числа этого отчёта
Сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются.
```
$ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
20 строк `ok`, ни одной `FAIL`, MAKE-EXIT=0
--- did NOT run: 8 skipped … UNSET: TM_PLATFORM_TEST_BOOK_TEMPLATE, TM_PLATFORM_TEST_ENGINE_BIN
$ git grep -h '^func Test' 943617a -- 'platform/**/*_test.go' | wc -l → 853
$ find platform -name '*_test.go' -exec grep -h '^func Test' {} + | wc -l → 875
$ git diff -- '*_test.go' | grep -c '^-func Test' → 0
$ git diff -- '*_test.go' | grep -c '^[-+]func Fuzz' → 0
$ python3 docs/scripts/carriers.py platform/docs/platform-PROGRESS.md
carriers: 0 помеченных утверждений без носителя · осмотрено строк: 491
$ grep -c '^| PD-' platform/docs/DEFECT_REGISTER.md → 465
$ python3 docs/scripts/counts.py --check
✗ docs/PROGRESS.md: «открытых 106» против пере-счёта 112 · «всего 458» против 465 (зона docs/)
$ grep -rc 'THE FIX IS NOT IN THIS PACKAGE' --include=*.go platform/ | grep -v ':0' | wc -l → 0
контроль: .go файлов осмотрено 197 · 'character_count_exact' найдено в 4 (v0.go, project.go, v0_test.go, books.go)
$ git status --short -- docs/architecture/14-api-contract/ platform/docs/ENGINEERING_STANDARDS.md \
platform/docs/PLATFORM_DIRECTION.md → пусто
$ git status --short -- backend/ | wc -l → 17
⚠ и это НЕ мой след: 17 файлов правит параллельная бэкенд-сессия. Что этот пак в `backend/` не
писал, из дерева НЕ измеримо — измеримо лишь то, что один файл я туда положила по инерции и
убрала (`backend/configs/` чист). Прежняя редакция подавала это как замер; это не замер.
$ git diff --cached --name-only | wc -l → 0
```
### Вынос журнала — что именно потеряно, пере-снято после всех правок
`comm` по непустым строкам: было **4156**, стало **4420** (три файла плюс баннеры срезов), не сошлось
**10** строк и все десять названы, потому что «одна» в прежней редакции этого абзаца была верна лишь
на момент замера и устарела от моей же последующей правки:
- заголовок «Пинги оркестратора (живые ссылки для следующих сессий «…ИСПОЛНЕНЫ» (все пять пингов
под ним отработаны);
- девять строк трёх ХВОСТОВЫХ секций-указателей Эра пака P7 в архиве», «Закрытые эры P0P3 в
архиве» и дублирующий их буллет) они дублировали блок «Архив эр» и слиты в него; обе ссылки
(`-P7.md`, `-P0-P3.md`) в живом файле сохранены и резолвятся.
То есть потеряны ФОРМУЛИРОВКИ дублей, не содержание, и это утверждение проверяемо: все шесть ссылок
на срезы в живом файле разрешаются в существующие файлы. Ссылки ИЗ других файлов в вынесенные
диапазоны пере-нацелены: `sqlc.yaml`, `STACK_DECISIONS.md` (условия стенда там же сказано, что архив
читается как археология, а не как инструкция), `README.md` (шапка и таблица).
### Находка собственного круга: хвост загрузки, о котором бутовый гейт не знал
Синхронный разрез добавил до полутора минут к тому, что загрузка делает ПОСЛЕ тела, а бутовый гейт
(`internal/config`: `UploadDeadline + books.UploadSettle < min(UploadGrace, ClaimStale)`) про этот
участок не знал то есть оператор мог настроить дедлайн, при котором загрузка переживает окно
идемпотентного ключа. Вылечено переносом бюджета в саму константу: `UploadSettle = CutBudget +
writeBudget + 30s`. Существующий бутовый тест читает `UploadSettle`, а не литерал, поэтому подъём
`CutBudget` теперь автоматически ужимает допустимый дедлайн.
**Цена того, что `CutBudget` стал КОНСТАНТОЙ, а не настройкой — явно.** Деградация мягкая: бюджет
работает ПОРОГОМ, а не потолком на хосте, где разрез книги законно дольше полутора минут, загрузка
не падает, она возвращается на прежний асинхронный путь (`201 parsing`, очередь доделывает), и
единственная потеря менее информативный `201`. Ручка убрана не ради чистоты: за ней стоял способ
выстрелить себе в ногу настройка, которой можно вытолкнуть хвост загрузки за окно, в котором её
ключ ещё можно переиграть, а окно это ни один экран не показывает.
### Дофикс по приёмке оркестратора №23 — шесть пунктов, все закрыты
| пункт | что было | что сделано | чем предъявлено |
|---|---|---|---|
| **Д1** | **базис расчёта ВЫВЕРНУТ на самом частом окончании:** `finish()` считает исход в локальную переменную, а `settle()` получает ТОТ ЖЕ до-финишный снапшот у прогона, кончившегося чисто, в леджер уезжал `halted` | снапшот несёт исход, который этот же вызов только что решил (`l.Status, l.PausedReason = status, pausedReason`) | **ИСПОЛНЕНИЕМ, не по коду:** прогон доведён до `ready`, строка леджера прочитана SQL-ом. До починки: `status="ready"`, а `note` «cut off mid-work». Пин `TestACleanEndingIsSettledOnTheBasisOfTheEndingItReached`, мутация возвращает дефект и красит его текстом |
| **Д2** | гейдж `PARKED` не запинен на уровне ЭКСПОЗИЦИИ, и проводка `Observe → metrics.Runner` в демоне не запинена вовсе | ассерт на серию в экспозиции + гейт, читающий композитный литерал демона и требующий, чтобы каждое поле бралось из поля СВОЕГО имени | две мутации: подмена серии `the exposition is missing "tm_platform_parked_attempts 7"`; обмен полей в демоне `one state's number is published under another's name` |
| **Д3** | гейт 305 пинил ТАРГЕТ, а не его использование: откат ветви отказа к голому грепу оставлял батарею зелёной | гейт держит ветвь отказа рецепта `check`: она обязана звать `report-failures` и НЕ грепать `--- FAIL` сама | мутация буквальный откат строки красит обе проверки |
| **Д4** | рантбук утверждал, что у парковки нет ни колонки, ни гейджа, ни строки; после пака это ложь | рантбук переписан: у парковки СВОИ три сигнала, карантинных нет и не будет, потому что снимать её не надо. Плюс `≥` у SPENT и пятый отказ `run abandon` | `deploy/README.md`, греп `tm_platform_parked_attempts` и `ПЯТЫЙ ОТКАЗ` |
| **Д5** | комментарий `PD-424` объявлял исчерпывающие «two ways», а способов ТРИ | комментарий перестал объявлять полноту и называет третий способ с его радиусом; сам способ ряд `PD-465` | заявка коммитится в `RecordSpawn`, юнит поднимается строкой ниже (`spawn.go`, `Runner.Start`) |
| **Д6** | 18 протухших якорей в регистре, сдвинутых этим паком | пере-нацелены ПО ТОКЕНУ, а не по памяти; девятнадцатый мой собственный, я записала токен с многоточием, которого в коде нет | `counts.py --lint`: было 25 проблемных якорей в 120 доках, стало **5**, в регистре **0** |
| **Д7** | не заказан приёмкой, найден её же батареей: новый ряд `PD-465` несёт два маркера тревоги и не был объявлен в `alarmBaseline` | объявлен, с доводом почему он НИЖЕ major (окно в миллисекунды, расход ограничен потолком того же прогона, аккаунтом не эксплуатируем) | гейт `TestOpenRowsBelowMajorThatCarryAlarmMarkers…` красил батарею именно на нём; и назвал его прибор **305** «`check` упал» отличилось от «тест упал» на настоящем падении |
**Один якорь остался и он НЕ мой по зоне:** `docs/architecture/05-decisions-log.md:2678` целит в
`platform/internal/pgstore/books.go:314` по токену `FinishParse records a parsed book`; мой пак сдвинул
цель на **343**. Правка в зоне оркестратора число передано пингом.
**Что приёмка нашла и что чинить НЕ надо** (правило остановки в силе, дописано к семи пунктам
остановленного разреза): разрез потерял единственный ограничитель параллелизма задание на приёме
больше не ставится, а `MaxWorkers: 4` был у очереди · маршрут молчит до 210220 с после последнего
байта, промежуточный прокси об этом не спрашивали · отказ уходит наружу только МАШИННЫМ кодом, `Detail`
не заполняется, то есть «честная причина человеку» из §4.3 сегодня не доезжает.
### Пять кругов самопроверки — что дал каждый
| круг | кто | major | итог |
|---|---|---|---|
| 1 | я, по готовой работе | | две дыры в МОИХ пинах (посадки выживали) + хвост загрузки, о котором бутовый гейт не знал |
| 2 | я, против заказа | | «≥» и парковка были предъявлены чтением кода, а не исполнением; охранники `DeleteRefusedIntake` не запинены; дельта контракта неверна. **Сходимость объявлена здесь ПРЕЖДЕВРЕМЕННО** |
| 3 | опровергатель `fable` | **4** | три дефекты, введённые этим паком: мёртвый контекст пере-чтения · три ветви без возврата claim'а · синхронная материализация; плюс незапиненный маппинг на проводе |
| 4 | опровергатель `fable` | **5** | гонка задания очереди с разрезом за один claim (**очередь выигрывала 5 из 6**) · бюджет снова короче хвоста · две ветви из трёх без пина · `!atIntake` без пина · ложная дельта контракта |
| 5 | опровергатель `fable` | **1** (+6 minor) | бюджет короче хвоста ТРЕТИЙ раз, по третьей причине **правило остановки** |
**Цена преждевременной сходимости, измеренная:** одно число (счёт тестов) снималось ШЕСТЬ раз,
потому что кругов оказалось не два, а пять; и дельта контракта, которую оркестратор собирался
ратифицировать, дважды была ложной первый раз по существу, второй раз после моей же починки.
**Что из этого стоит унести дальше:** восемь major из одиннадцати нашёл ЧУЖОЙ прибор, а не я, и
все восемь в коде, который я только что написала и считала проверенным. Оба круга, которые я вела
сама, дали настоящие находки, но НИ ОДНОГО major в новом механизме: свой код я проверяла по тому,
что он должен делать, а опровергатель по тому, что он делает.
### Что НЕ удалось и что считаю слабым местом
- **Не измерено:** маржа `PD-441` в деньгах приборa нет на этой стороне (см. движковую половину).
- **Не предъявлено живым прогоном:** синхронный разбор гонялся на фикстурах и на живом Postgres, но
не на настоящем `tmctl` гейт `TM_PLATFORM_TEST_ENGINE_BIN` требует записи в `backend/`.
- **Слабое место, которое называю сам:** `settlementBasis` судит по СТАТУСУ попытки, а не по факту
наличия вызовов в полёте. Классы огрублены сознательно (пере-пометка стоит менее точного сигнала,
недо-пометка прячет деньги), но `awaiting_bank` отнесён к «чистым» по чтению кода движка, а не по
замеру: если окажется, что банк-стоп тоже рвёт волну, класс придётся сузить.
- **Третье, названное опровергателем и оставленное сознательно:** если удаление отказанной книги
само упрётся в БД (`discard` `removeIntake`), пользователь получит `400`, а строка останется
`parsing` под claim'ом; свип через `claimGrace` перечитает манифест, потратит бюджет попыток и
оставит книгу `rejected` в библиотеке то есть ровно то состояние, которого отказ и избегает.
Не лечу: путь требует отказа БД РОВНО между двумя её же операциями в одном запросе, а лечение
компенсирующая транзакция, то есть механизм заметно крупнее устраняемого класса. Названо, чтобы
следующий пак не открывал его заново.
- **Второе слабое место:** отказ книге без глав продуктовое сужение. Оно временное и помечено
строкой 283 в коде ветки, но пока владелец его не подтвердил, это решение сессии.
**Работа завершена, править больше не планирую.** Дерево передано оркестратору 23 незакоммиченным:
37 файлов, все внутри `platform/`, индекс пуст. Синхронный разрез построен и ОСТАНОВЛЕН правилом
остановки его семь незакрытых пунктов перечислены выше поимённо и уезжают отдельным паком; всё
остальное закрыто и предъявлено.
## Живые нормы зоны, у которых нет другого носителя
Каждая прошла триаж 06.09: она не выводится из кода и не стоит ни в `ENGINEERING_STANDARDS.md`, ни в
`STACK_DECISIONS.md`. Оркестратору предложено абсорбировать их в нормы зоны до тех пор живут здесь.
- **Ничего не отдавать на лендинг без прогона ПАКЕТА, которого правка касалась.** «Код написан и пин
заведён» не то же, что «прогнано»; числа снимаются после ПОСЛЕДНЕЙ правки, включая комментарные
(генезис `D39.211` п.6, где это зафиксировано как ошибка оркестратора, а не как норма зоны).
- **`go test` без `-v` строк `--- SKIP` не печатает вовсе**, поэтому греп по ним в не-verbose логе даёт
ЛОЖНЫЙ НОЛЬ скипов (`D39.213` п.3).
- **Зелёная батарея это полный список ПАКЕТОВ плюс отсутствие `FAIL`**, а не отсутствие красных строк
в хвосте вывода (`D39.169`).
- **Гейты доков перегоняются ПОСЛЕ последней правки доков, а не кода**, и число из растущего файла
не число (`D39.188`).
- **Шаблон книги второго гейта батареи обязан указывать на СНАПШОТ движковых конфигов того же коммита,
что и бинарь** (`git archive <commit> backend/configs backend/prompts`), а не на рабочее дерево:
иначе живой тест читает файлы параллельной бэкенд-сессии и краснеет без дефекта (`PD-432` покрывает
БИНАРЬ, не шаблон).
- **Механизм, который агрегирует чужой каталог и отдаёт результат наружу, обязан иметь СПИСОК ТОГО,
ЧТО НЕ БЕРЁТ, и список начинается с секретов.** Исключать по расширению ловушка в обе стороны
(`D39.201`).
- **Красный тест, мерящий ВРЕМЯ, на загруженной машине не результат:** прежде чем звать его
дефектом, гонять изолированно и смотреть `uptime` (`D39.194`).
## Вопросы и пинги оркестратору — ОТКРЫТЫ
- **Счёт рядов регистра сдвинут этим паком** и живёт в чужой зоне: `docs/PROGRESS.md` объявляет
«открытых 106 / всего 458», пере-счёт даёт **112 / 465** (`python3 docs/scripts/counts.py --check`).
Заведены `PD-459``PD-465`, `PD-458` переведён в `fixed`.
- **Нужен пятый `RejectReason` в контракте (`PD-463`), либо решение, что асинхронная ветвь приёма
остаётся проницаемой.** Сужение входа до того, что умеет выдача, действует только на синхронной
ветви; на ветви, куда книга уходит при деплойном классе, «книга одним полотном» попадает в
библиотеку молча. Закрыть нечем: словарь закрыт четырьмя значениями, и ближайшее по словам
`source_unreadable` ТЕРМИНАЛЬНО и УДАЛЯЕТ исходник то есть уничтожает файл пользователя из-за
НАШЕГО ограничения.
- **Предложение движка провести `--ceiling-usd` в `status`** (пинг 15, 09.08) не принято и не
отклонено с тех пор; `backend/internal/pipeline/status.go` отвечает «a read path never carries a
run-scoped override». Нужна диспозиция или строка бэклога.
- **`counts.py` держит регистр ОДНИМ путём и слайсы не глобит.** Это предусловие плана нарезки
`DEFECT_REGISTER.md` (`docs/DOC_CLEANUP_PLAN.md`, Б14в): вынос без него молча уронит счёт.
- **§2.12 компаньона контракта** (`docs/architecture/14-api-contract/README.md`) утверждает, что пять
денежных полей не могут доехать, тогда как аллоулист шва несёт `committed_usd`/`reserved_usd`
(`platform/internal/ingest/resync.go`); якоря на `pipeline/status.go` там же мертвы. Зона `docs/`.
- **Зеркало контракта у фронта `0.2.3` против канона `0.12.0`** и несёт отозванное правило «денег в
интерфейсе MVP нет вообще» (`D39.196` п.2 его снял). Зона фронта заморожена; пинг на разморозку.
- **Восстановление из бэкапа не предъявлено на книге в сотни мегабайт, при заполненном диске и при
чужих соединениях** (`PD-462` покрывает соседний класс, этот нет). Нужна строка или ряд.
- **Правило записи адресов в доках, живущее только в архиве и относящееся к прибору из `docs/`:**
адрес пишется БЕЗ якорной нотации там, где текст РАССКАЗЫВАЕТ о форме (линтер `counts.py` разбирает
форму где угодно, включая объяснение этой формы), а мёртвый адрес цитируется ПОРОЗНЬ имя файла в
кавычках, номер словами, иначе цитата сама становится якорем. В `counts.py` этого нет; носитель
нужен в зоне `docs/`.
- **Рантбук деплоя ни разу не прогонялся end-to-end с настоящим `migrate`** названо предусловием
первого выката ещё пингом 17 и носителя не получило.
## Архив эр
- **Паки 0406.09** закрыть цикл» · «пустить внутрь можно» · «форма заказа перевода» · наблюдение за
живым платным потоком) [archive/platform-PROGRESS-2026-09-04-06.md](archive/platform-PROGRESS-2026-09-04-06.md).
Ратификации **D39.194**, **D39.201**, **D39.208**, **D39.211****D39.214**.
- **Эры P9P13 (27.0803.09)** плюс приёмка P8-REVIEW, раздел «Состояние эры P8» и исполненные пинги
оркестраторов 15–№22 [archive/platform-PROGRESS-P9-P13.md](archive/platform-PROGRESS-P9-P13.md).
Ратификации **D39.159**, **D39.162**, **D39.166**, **D39.169**, **D39.172**, **D39.180**, **D39.188**.
- **Эра пака P8-FIX (2122.08) В АРХИВЕ.** Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle [archive/platform-PROGRESS-P8.md](archive/platform-PROGRESS-P8.md). Ратификация D39.154, лендинг `31f1f82`. Живое из этой эры: четыре открытые строки регистра (`PD-370` мажор контрактная половина · `PD-371` · `PD-372` · `PD-373`/`PD-374`), инвентарь каналов шва в `STACK_DECISIONS.md`, граница sqlc в `BACKLOG.md` П-19.
- **Записи акта 5 пака P7, остававшиеся в живом журнале, ДОСЛАНЫ в [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md)** 22.08: заголовок «эра P7 в архиве» стоял, а тела лежали здесь.
- **Паки P4, P5, P6 и их дофиксы (0815.08) приняты и залендены.** Ратификации: **D39.123** (P4 «раннер», `d29e30c`; формула аргумента потолка = `committed + прирост`, PD-158) · **D39.130** (P5, `69d485a`; Go-floor 1.26.6 `toolchain`-директива в `go.mod` плюс сравнивающий гейт `make version-check`; интейк пишет `book.yaml` формой Б) · **D39.131** (эмиттер шва движка; словарь кадров `internal/ingest/events.go`) · **D39.132** (P6 + дофикс; `PD-113` закрыт, у батареи появился второй гейт окружения). Записи паков с дофиксами, живыми пробами и посадками [archive/platform-PROGRESS-P4-P6.md](archive/platform-PROGRESS-P4-P6.md); промт P5 `archive/PLATFORM_P5_SESSION_PROMPT_2026-08-10.md`. Счёт регистра и тестов тех дней устарел на порядок: живые числа берутся `python3 docs/scripts/counts.py --check` и `DEFECT_REGISTER.md`, а не отсюда.
- **Эра пака P7 (1620.08)** пять актов, приёмка и фикс-раунды: [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md).
- **Эры P0P3** (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) исполнены и залендены
(D39.107/109/112/114): [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md) (D39.124).
Решения оттуда живут в D-логе и `DEFECT_REGISTER.md`.