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

443 lines
66 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.

# Архив журнала зоны «Платформа» — эра пака P7 (1620.08.2026)
> ⚠ **СРЕЗ, выселен оркестратором №18 при лендинге P7 (20.08, D39.153)** — по образцу срезов `platform-PROGRESS-P0-P3.md` и `-P4-P6.md`. Эра ЗАКРЫТА: пак исполнен пятью актами, принят с фикс-листом и заленден (`9b23e8c`), сессия закрыта владельцем.
>
> **Не читать при онбординге.** Живое состояние зоны — шапка `platform-PROGRESS.md`; статус каждого дефекта — `DEFECT_REGISTER.md` (источник истины); правила, пережившие пак, — `STACK_DECISIONS.md` §3336; фикс-лист приёмки и то, что ушло в единый бэклог, — пинг оркестратора №18 там же в живом журнале.
## Сессия P7 (1617.08): читающая поверхность контракта 0.3.0
Промт — `PLATFORM_P7_SESSION_PROMPT.md`. Ниже: записка-план по §4 (каждый пункт с диспозицией),
команды, живые пробы, находки, obstacle.
### Записка-план: пункт → исполнено / отказ → улика
| § | Пункт | Статус | Улика |
|---|---|---|---|
| 0 | Рантбук деплоя живым `migrate` | **исполнен** | БД проекта отведена на v14 при бинаре v15 → `tmctl status --json` = exit 13, токен `schema_mismatch found=14 expected=15`; `tmctl migrate` → бэкап + v14→v15; тот же `status` = exit 0. Мёртвая цитата вычищена (`deploy/README.md`, П-1 бэклога), пример в `tmplatformctl/runs.go` переписан на ВЕРСИОНИРОВАННЫЙ путь |
| 0 | `tmplatformctl books --migratable``tmctl migrate` end-to-end | **исполнен** | 5 книг стенда, все `MIGRATE=yes`, цикл из деплой-ноты отработал |
| 1а | Читать `BookIntake.title` | **исполнен** | `books.Intake.Title`; пустая строка = «назови по файлу» (`books.title()`), непустая не перезаписывается разбором |
| 1б | Проецировать `reject_reason` | **исполнен** | `httpapi.contractRejectReason`; `parser_unavailable``processing_failed` (ФБ-9: имя значения — то, на что клиент вешает фразу) |
| 2 | PD-104: грант → 0 + тест | **исполнен** | `config.go` дефолт 0; пин `config.TestSignupGrantIsParsedNotGuessed` пинит ИМЕННО ноль; протухшие «$5» вычищены из `PLATFORM_DIRECTION.md` §2, `BACKLOG.md` П-7, регистра |
| 3 | Модель ошибок по карте А-2 | **исполнен** | `httpapi/problem.go`: 16 корневых кодов, статус ВЫВОДИТСЯ из кода (пара «код+статус» непредставима расходящейся), `cause` открытый, `errors[]`, `request_id` из `reqid`; `WWW-Authenticate` на 401; две причины 403 разведены в `cause` при одном корневом `forbidden`; класс 2 — один код без вариации |
| 4 | `listChapters` · `listUnits` (пер-главно, 410) · `listNotes` · `listBankTerms` · `submitBankDecisions` · дельты · SSE · `structure_version` · gzip · ETag/304 | **исполнен** | `httpapi/{reading,stream,conditional}.go`, `pgstore/{readmodel,events}.go`; ручки «вся книга юнитами» НЕТ; `Unit.notes` массивом; агрегаты банка на ПЕРВОЙ странице; дельты ВКЛЮЧИТЕЛЬНЫЕ; `version_too_old` и `cursor_invalid` — причинами |
| 4 | Ловушка `heading` | **исполнен** | поле не декодируется ВОВСЕ (`ingest.ManifestChapter`), колонка `chapters.heading` удалена миграцией, проекция отдаёт `null`; пин `readmodel.TestTheManifestsRenderedHeadingHasNowhereToLand` |
| 4 | Демонтаж построенного гейта полноты банка | **исполнен** | `runs/reconcile.go`: `awaiting_bank` продолжается при ЛЮБОМ состоянии решений; пин `TestResumeLiftsABankStopWithTheDecisionsAsTheyStand`. Граница паузы движка сверена чтением (`pipeline/mining.go`: авто-продолжение с неподписанным банком — дефолт) |
| 4 | Пофазность у главы НЕ строить | **соблюдён** | у главы один счётчик `units_done`; пин «фазовый счётчик не доехал до строки главы» |
| 4 | `Note.code` из ◆-карты | **исполнен с ОДНИМ отступлением** | `httpapi/notes.go`; отступление — незнакомая причина проецируется как `unspecified` (в карте колонка кода пуста, а поле обязательно). Заведена строка PD-246, ждёт ратификации вместе с фразами |
| 5 | `Progress` с книги на `Run`, одна полоса в ГЛАВАХ | **исполнен** | `pgstore.Progress{Done,Total,ETASeconds}`, знаменатель — `ceiling_chapters`; сегмент выбирается `verify_bank && !bank_released` |
| 5 | `Book`: +`revision`, +`chapters_done`, `progress`, `genre` | **исполнен** | `genre` снят и с ПРОВОДА, и из формы интейка, и из колонки: платформа его больше не знает, значение шаблона оператора остаётся его |
| 5 | `Run`: +`book_id`, +`failure_reason`, `verify_bank``stop_for_signing` | **исполнен** | словарь из трёх значений мапится с полосы 1019 и веток `outcome()` (`runs.failureReason`) |
| 5 | `Usage`: `halt_reason` со своим словарём | **исполнен** | `httpapi.contractHaltReason`; прогонная причина больше не может доехать до аккаунтного поля |
| 5 | `GET /capabilities` | **исполнен** | все шесть полей; пары — из конфигурации (`TM_PLATFORM_LANGUAGE_PAIRS`), не список в Go |
| 5 | `limit` подрезать, а не отвергать | **исполнен** | `pgstore.clampPage`; отвергается только значение, которое не является размером страницы |
| 5 | Трейлинг-части интейка → 400 | **исполнен, с находкой** | сначала книга ОСТАВАЛАСЬ при 400 (живая проба); теперь часть после файла ПРОВАЛИВАЕТ чтение, и интейк откатывает загрузку своим же путём |
| 5 | `Location` на 201 | **исполнен** | `/v0/books/<id>` |
| 5 | `Idempotency-Key` на `createBook` и `startRun` | **исполнен** | таблица 00017, повтор отдаёт ИСХОДНЫЙ ответ, другой запрос — 409 `key_reused`, гонка — `key_in_flight` + `Retry-After`, >255 → 400 |
| 5 | `blocked {code, book_id}` на run-options и 409 | **исполнен** | `pgstore.CreditHeldBy` + `runs.CreditHeldError` |
| 5 | Отказ по неподдерживаемой паре на интейке | **исполнен** | `books.ErrUnsupportedPair``errors[].code: unsupported_pair`; пустой список пар = деплой не объявляет ничего и пару не проверяет |
| 5 | `finalizing` вон из словарей | **исполнен** | Go-свитчи и CHECK-констрейнты обеих таблиц |
| 6 | PD-172/173/174/180/199 привести · PD-201 · PD-241 | **исполнен** | все закрыты либо сужены с уликой; PD-241 — перевёрнутый порядок в `outcome()` с пином |
| 6 | Кандидаты PD-219/217/162 | **отсуждены** | PD-217 — половина ЗАКРЫТА (paused больше не блокирует апгрейд движка); PD-219 и PD-162 — не брались, с названной причиной |
| 6 | Находки §9 research/28 | **отсуждены** | три взяты строками (PD-249/250/251/252), две сведены к существующим (PD-203, PD-162/244) |
| 6 | Инструменты: sqlc / oapi-codegen | **отказ с доводом** | оба НЕ взяты; довод — `PLATFORM_DIRECTION.md` §3, и он опирается на замер этого пака, а не на вкус |
### Команды (каждое число отчёта — этой командой)
```sh
# батарея, оба гейта, -race
export TM_PLATFORM_TEST_DSN='postgres://postgres@/postgres?host=/tmp&port=55433&sslmode=disable'
export TM_PLATFORM_TEST_ENGINE_BIN=<путь к tmctl> TM_PLATFORM_TEST_BOOK_TEMPLATE=<шаблон>
make check # EXIT=0, 18 пакетов, скипов 0, линтер 0 issues
grep -rh '^func Test' --include='*_test.go' . | wc -l # 458 (HEAD: 416)
git diff --stat platform | tail -1 # 46 файлов, +2329 646
python3 docs/scripts/counts.py # регистр: 256 строк, 63 открытых
```
⚠ Стенд Postgres на этой машине пришлось поднять с нуля (его не было): рецепт `STACK_DECISIONS.md`
§«Postgres без root», micromamba + `postgresql=18.4` в `~/.local/pgsql`, порт 55433 — отработал
дословно.
### Живые пробы на дев-стенде (демон + сид + настоящий `tmctl`)
Сид P6 отработал целиком: дев-вход → грант → загрузка через ЖИВОЙ `POST /v0/books` → разбор →
`not_started, 3 главы`. Дальше — курлом по новым ручкам:
- `GET /capabilities` → все шесть полей, `contract_version: 0.3.0`, пары `zh>ru available` +
`ja>ru unavailable`;
- `GET /books` → `revision · reject_reason: null · structure_version: 1 · chapters_done: 0 ·
character_count: 987`, **никакого `progress` и никакого `genre`**;
- `GET /books/{id}/chapters` → три главы, `heading: null` у каждой;
- `GET /books/{id}/chapters/{ch}/units` → пара с ИСХОДНЫМ ТЕКСТОМ (канал `tmctl export --pairs`
работает), `target: ""`, `state: pending`, `notes: []`;
- `GET /books/{id}/notes`, `/bank` → пустые с агрегатами и `structure_version`;
- SSE на книге в покое → `hello id=4` → `end id=4` → закрытие; реконнект с `Last-Event-ID: 6` → **204**;
- `ETag` → `304` на `If-None-Match`; `Accept-Encoding: gzip` → `Content-Encoding: gzip` + `Vary`;
тот же заголовок на `/events` → **не сжато**;
- ошибки: 404 → `code: not_found` + `request_id`; 401 → `WWW-Authenticate: Bearer realm="textmachine"`;
POST без `X-TM-Client` → 403 `forbidden` · `cause.code: client_header_missing`;
- интейк: пара `ja→ru` → 400 `unsupported_pair` на обоих полях; часть после файла → 400
`missing_or_late` **и книга не остаётся** (проверено счётом библиотеки до/после);
- `Idempotency-Key`: тот же ключ дважды → один и тот же `Location`; тот же ключ с другим `title` →
409 `idempotency_conflict` · `cause.code: key_reused`.
### Что нашли ПРОБЫ, чего не нашли тесты
**Часть формы после файла оставляла книгу.** Отказ 400 приезжал (правило соблюдалось), но
`intake.Accept` к тому моменту уже создал строку и каталог — пользователь получал ошибку И книгу в
библиотеке. Починено не новым кодом отката, а ПРОВАЛОМ чтения: часть после файла возвращает ошибку
из ридера, и интейк отменяет загрузку тем же путём, которым отменяет оборванную. Пин
`httpapi.TestAPartAfterTheFileIsRefusedAndTheBookIsNotKept`, пере-проверено живьём.
### Адверсариальное селф-ревью финалом: три ревьюера, таблица находок
Механизм по §5 промта: три независимых субагента по ФИНАЛЬНОМУ диффу, author≠reviewer, **два из
трёх — другой модели** (кросс-семейная линза, а не вторая своя). Ноль находок не случился — и это
хорошо: две HIGH нашли ОБА контрактных ревьюера независимо, что и есть подтверждение.
| # | Линза | Находка | Вердикт | Что сделано |
|---|---|---|---|---|
| Р-1 | контракт (кросс-модель) · деньги/шов | **Кадр `note` уносил движковую причину** (`glossary_miss`, `hard_refusal`) и не нёс обязательных `severity`/`code`; `unit_id: null` вместо отсутствия | ПОДТВЕРЖДЕНА, HIGH | Карта переехала в `internal/ingest/notes.go` — один дом на оба потребителя; кадр строится через неё; `unit_id` опускается. Пин `TestTheNoteFrameCarriesTheContractsNoteAndNotTheEnginesReason` |
| Р-2 | контракт (кросс-модель) | **Кадр `status` уносил внутренние словари** (`daily_ceiling`, `parser_unavailable`): та же величина читалась `null` на карточке и внутренним словом в потоке | ПОДТВЕРЖДЕНА, HIGH | Переводы переехали в `internal/ingest/vocabulary.go`, кадр строит `emitStatus`; при ошибке чтения кадр НЕ шлётся (вместо `status: null` вне схемы). Пин `TestTheStatusFrameCarriesTheContractsVocabularyOrNothing` |
| Р-3 | деньги/шов | **DOWN-путь миграции неисполним при первой же строке банка** (воспроизведено живым PG): три словаря переписывались без перевода данных, а goose гоняет миграцию одной транзакцией | ПОДТВЕРЖДЕНА, HIGH | Порядок drop → перевод → add в ОБЕ стороны (UP тоже: он должен переживать пере-накат после отката). Пин `TestARollbackSurvivesTheDataTheNewVocabulariesWrote` — откат до документированного пола (версия 5) на базе С данными |
| Р-4 | деньги/шов | **Четыре писателя меняли статус без кадра** (`RestartRun`, `PauseRun`, `FinishUnspawnedStop`, `ReleaseBankStop`); двое из них ставят `finished_at`, то есть книга «в покое» — и следующий реконнект получает **204 «не переподключайся»** при устаревшем статусе у клиента | ПОДТВЕРЖДЕНА, HIGH | Кадр у всех трёх писателей статуса; четвёртый (`ReleaseBankStop`) перестал быть отдельным писателем — см. Р-5 |
| Р-5 | деньги/шов | **`ReleaseBankStop` коммитился ДО `reopen`**: резюм, который ничего не сделал (расчёт в полёте, бюджет исчерпан), всё равно переключал полосу с N/N на 0/N | ПОДТВЕРЖДЕНА, MED | Снятие стопа перенесено ПОСЛЕ успешного `reopen` |
| Р-6 | деньги/шов | **`Refresh` тратил бюджет прохода ПЕРЕД `settle`**: на большой книге два полных ре-чанка съедали проход, расчёт уходил в «отложено», холд висел свип | ПОДТВЕРЖДЕНА, MED | Деньги первыми: `settle` → затем `refreshReadModel` на СВОЁМ ограниченном контексте, ошибка громкая |
| Р-7 | деньги/шов | **`failureReason` игнорировал маркер**, который ему передавали: OOM и kill по стоп-таймауту приезжали `interrupted` («повтори») | ПОДТВЕРЖДЕНА, MED | Читает `m.Result`; пин `TestWhyARunFailedDecidesWhetherARetryIsWorthOffering` (7 веток) |
| Р-8 | контракт (кросс-модель) | **Квитанции `Run` несли нулевую полосу** (`202` на старт/стоп/резюм) со свежей ревизией — клиентский гард пропускал её и полоса откатывалась к 0/0 | ПОДТВЕРЖДЕНА, MED | `StartRun`/`RequestStop`/`ReadRun` считают ту же полосу, что карточка |
| Р-9 | контракт (кросс-модель) | **Полоса ВТОРОГО прогона считала главы всей книги**: дробь стартовала выше нуля и могла превысить единицу | ПОДТВЕРЖДЕНА, MED | Колонка `runs.chapters_before` — база сегмента, снимается в том же стейтменте, что создаёт прогон; `least(…, ceiling_chapters)` закрывает второй край. Пин `TestASecondRunsBarStartsAtZeroOverAHalfFinishedBook` |
| Р-10 | контракт (кросс-модель) | **`Last-Event-ID: 0` после подрезки буфера молча пропускал дыру** — а `0` это легальный id, который сервер сам выдаёт | ПОДТВЕРЖДЕНА, MED | Условие `last+1 < Oldest` вместо `last > 0 && last < Oldest` |
| Р-11 | контракт (кросс-модель) · скоуп | **Склейка кадров `chapter` шла по имени события**, теряя счётчик соседней главы внутри одного соединения | ПОДТВЕРЖДЕНА, MED | Склейка по (событие, id сущности) |
| Р-12 | контракт (кросс-модель) | **Порядок банка** был `(src, id)` — то есть по ХЕШУ, а канон объявляет «по поверхности, затем по окну» | ПОДТВЕРЖДЕНА, MED | Порядок и кейсет приведены к канону (`src, since, until, id`) |
| Р-13 | контракт (кросс-модель) | **`limit` всё ещё отвергался снизу** (0, отрицательный, нечисло), тогда как канон освобождает параметр от отказа | ПОДТВЕРЖДЕНА, MED | Никогда не отвергается: сверху подрезка, всё прочее — дефолт деплоя. Пин переписан |
| Р-14 | контракт (кросс-модель) | **Неверная пара языков и слишком длинное поле отказывали без `errors[]`** | ПОДТВЕРЖДЕНА, MED | `ErrMalformedLanguage` и `too_long` с указателем на поле |
| Р-15 | контракт (кросс-модель) | **`SweepIdempotency` не имел вызывающего**: ключи не забывались никогда | ПОДТВЕРЖДЕНА, LOW | Подключён в свип демона, окно 25 часов |
| Р-16 | контракт (кросс-модель) | **Курсор замечаний нёс движковое слово волны** в открытом виде | ПОДТВЕРЖДЕНА, LOW | Пакуется порядковым номером |
| Р-17 | скоуп (кросс-модель) | `acceptsGzip` игнорировал `*`; `pump` спал такт даже после полной пачки; `unknownTermPointer` парсил текст ошибки; алиас `itoa` | ПОДТВЕРЖДЕНЫ, LOW | Все четыре починены (типизированная `UnknownTermError` вместо парсинга строки) |
| Р-18 | скоуп (кросс-модель) | **`sqlc` был привязан к этому паку ратификацией**, а пак от него отказался — это отступление, а не решение зоны | СПРАВЕДЛИВО | Отказ с доводом записан в `PLATFORM_DIRECTION.md` §3 и вынесен ВОПРОСОМ оркестратору (ниже) |
| Р-19 | скоуп (кросс-модель) | **`unspecified` — изобретённый код** там, где промт требовал пинг | ЧАСТИЧНО: разбор с владельцем 17.08 показал, что находка верна по форме и неверна по существу | Значение отдано ветке, которую канон УЖЕ ратифицировал («нейтральная фраза для незнакомого кода»), а не заводит новое правило; настоящий предмет — рукописная копия чужого словаря без гейта, PD-246 пере-сформулирован, движку — пинг про публикацию причин данными |
| Р-20 | контракт (кросс-модель) | `410` отвечает и на главу, которой никогда не было: различение 410/404 без надгробий неисполнимо | ПРИНЯТО КАК ЕСТЬ | Лечение у обоих одно (перечитать дерево); строка PD-253 |
| Р-21 | скоуп (кросс-модель) | **Масса комментариев** выросла против нормы зоны (миграция на 250 строк — половина проза) | ЧАСТИЧНО ПРИНЯТО | Подрезано самое тяжёлое; остальное — предмет пинга, а не тихой правки |
| Р-22 | скоуп (кросс-модель) | **Два писателя ошибок** (`WriteProblem` + `WriteStatusProblem`/`codeForStatus`), три одинаковых структуры пары | НЕ БРАЛОСЬ | Второй писатель обслуживает НЕконтрактную поверхность (`/auth`, `/readyz`), где кода версии нет; сведение в один требует ратификации формы для 405/429 — вопрос оркестратору |
**Что ревьюеры подтвердили как ВЕРНОЕ** (важно не меньше находок): ни одной денежной величины на
проводе ни в каком виде · порядок блокировок не тронут, `emitFrame` не берёт НОВЫХ локов ни в одной
из 14 точек · кадр не может быть выпущен для откаченного состояния · демонтаж гейта банка
соответствует таблице канона дословно · словари шва (шесть значений банка, полоса отказов 1019,
все семь типов событий) сверены по коду движка и полны · `finalizing` снят безопасно · оба гейта
миграций пере-считаны и совпадают.
### Живые пробы ПОСЛЕ фикс-раунда (стенд пере-создан)
Стенд пришлось пере-создать, и это отдельная улика: миграция 00016 правилась ПОСЛЕ того, как была
применена, а goose ключуется номером — отредактированный файл не перезапускается никогда. На стенде
лечится сносом базы; в проде именно от этого стоит гейт `migrations.sha256` (обе строки пере-считаны).
- `limit=0 | -3 | abc | 100000` → **200 во всех четырёх** (канон освобождает параметр от отказа);
- квитанция старта: `status: translating`, `progress {done:0, total:2}`, `book_id`, `failure_reason: null`
— полоса едет с квитанцией, а не только с карточкой;
- поток при ЖИВОМ прогоне: `hello id=5` → `status id=7` с `failure_reason: "service_error"` и
`paused_reason: null` — кадр несёт КОНТРАКТНЫЙ словарь (внутреннего слова в кадре нет);
- карточка после отказа: книга `failed`, прогон `failed` + `service_error` (на стенде нет ключей
провайдера — движок отказывает конфигурацией, и это ровно тот класс, который «повтор не лечит»);
- `resume` упавшего прогона → `run_not_resumable` без `cause` (канон: это не остановленный прогон);
- неверный код языка → `invalid_request` + `errors[]` на оба поля;
- `/usage` → `halt_reason: null` (свой словарь, не прогонный).
### Obstacle: что НЕ удалось и что не проверено
1. **Отложено в P8 явно** (заказ промта): `updateBook` · `deleteBook` · `getRun` · `createExport`
/`getExport` · эскроу (П-18).
2. **`Note.code` для незнакомой причины — отступление**, не решение: `unspecified` (PD-246).
3. **Ни одного живого ЗАМЕЧАНИЯ и ни одного термина банка через настоящий прогон не прошло** —
прогон стоит денег, а сессия $0. Проекции замечаний и банка проверены тестами на живом PG и
ручными строками `unit_resolutions`, но не сквозным прогоном движка.
4. **SSE с ЖИВЫМ прогоном не гонялся** по той же причине: проверены книга в покое, реконнект, 204,
склейка и `resync_required` — тестами и на стенде, но не под настоящим потоком кадров.
5. **Стоимость материализации не замерена на большой книге** (PD-248): два полных ре-чанка на
границу работы; на демо-книге в 3 главы это миллисекунды, на 2283 главах — секунды CPU.
6. **`structure_version` на РЕАЛЬНОЙ пере-нарезке** проверен тестом (смена ключа манифеста), но не
настоящей сменой чанкера движка.
7. **Опрос потока — 2 запроса/с на соединение** (PD-247): `LISTEN/NOTIFY` не строился.
8. **Кадр `note` живьём не наблюдался**: он рождается только на помеченной единице настоящего
прогона. Проверен тестом на живом PG через настоящий `RunSink.Apply` (полный путь материализатора),
но не в потоке под браузером.
9. **Полоса под настоящей работой не наблюдалась**: `progress {0,2}` — это старт; движения счётчика
на стенде без ключей провайдера не бывает.
### Вопросы оркестратору (канал вопросов промта, НЕ тихая интерпретация)
1. **Словарь флаг-причин — рукописная копия чужой зоны без гейта** (PD-246; формулировка исправлена
после разбора с владельцем 17.08 — прежняя, «в карте контракта дыра», была неверной).
Проверено чтением движка: локализованных фраз там НЕТ вовсе — `refusal.go` и `status.go` несут
машинные классы, русских пользовательских строк в продовом коде движка ноль. То есть движок и
контракт построены на одном принципе, и переделывать под «гугловский словарик в движке» нечего:
фраза всегда на языке ИНТЕРФЕЙСА, которого не знает ни движок, ни платформа, а движок ОДИН на все
пары — русская строка в нём была бы таргет-спецификой в общем слое (тот же инвариант, что снял
`ruby` с провода).
**Проводу окно закрыто:** причина вне карты едет кодом `unspecified`, а канон уже предписывает
клиенту нейтральную фразу для незнакомого кода — значение отдано РАТИФИЦИРОВАННОЙ ветке, а не
изобретено правило. На подпись остаются две строки продуктового текста: фраза для `unspecified` и
граница ступеней `attention`/`glance` (сегодня — догадка платформы, колонка «Ступень» в
приложении А пуста у всех 15 строк). Это не блокер.
**Правка контракта — предлагаю, зона не моя (готовые формулировки, вставить как есть).**
*Обязательный минимум — КОМПАНЬОН, приложение А, последняя строка (`README.md:899`).* Пустая
клетка кода в ней и есть то, что заставило платформу выбирать значение самой:
> `| незнакомая причина | 8 (по умолчанию) | `unspecified` стабильный плейсхолдер деплоя: не значение словаря, а ОБЯЗАННОСТЬ сервера ответить одним и тем же кодом на причину, которой его сборка карты не знает (носитель PD-246 платформы) | нейтральная, НЕ «ошибка» | |`
*Желательное — ОДНА фраза в канон*, к описанию `Note.code` (`openapi.yaml:1693-1700`), после
абзаца про нейтральную фразу:
> A deployment that meets a reason its build of the map cannot name answers the stable placeholder `unspecified` rather than the producer's own word, so the branch above is exercised by a real value in ordinary operation and not only in theory.
Форма выбрана намеренно: это ОБЯЗАННОСТЬ СЕРВЕРА, а не запись словаря — канон отказывается
перечислять `Note.code` («freezing a list in the schema before the words exist would make it a
second copy»), и перечисление начинать нельзя. Ценность ровно одна и она процессная: без строки в
нормативном документе значение живёт только в Go платформы, и следующая сессия изобретёт своё.
**Бампа версии не требует, и вот почему точно:** `Note.code` объявлен `type: string`, не enum, —
ни один генерённый тип не сужается и не расширяется; а сама фраза описывает поведение, которое
ЕДИНСТВЕННЫЙ потребитель 0.3.0 (этот пак) уже реализует, то есть сломать клиента, сгенерированного
по 0.3.0, она не может. ⚠ Прецедент D39.144 (микро-правка канона без бампа) здесь опорой НЕ
служит: там довод был «спека ещё никем не потреблена», и он больше не верен. Цена, которую надо
назвать: описания компилируются в исходники клиента, значит фраза доедет до `schema.ts` — то есть
зеркало фронта и регенерация типов при разморозке. И пара «код нормативен, фраза ⬜» должна
остаться видимой, иначе следующий читатель решит, что карта опять сломана.
**⚠ ПИНГ ОРКЕСТРАТОРУ — строка в бэклог ДВИЖКА (сама зона в чужой бэклог не пишет):** публиковать
список флаг-причин ДАННЫМИ — артефакт рядом с манифестом либо `tmctl flag-reasons --json` ($0,
таблица `flagReasonSeverity` уже существует). Тогда тест платформы читает его и ПАДАЕТ, если в
карте нет строки, и класс «словарь разъехался тихо» закрывается насовсем. Со стороны платформы
гейт невозможен: импорт `backend/internal` запрещён ревью-гардом модулей (D39.85). Обратный
вариант — чтобы движок эмитил сразу контрактный код — отвергнут: это зеркальная утечка,
продуктовое слово поехало бы в движок, который о контракте знать не должен.
2. **`sqlc` — ОТЛОЖЕН СЛОВОМ ВЛАДЕЛЬЦА 17.08** («отложим»), строка PD-44 дополнена разбором.
Фиксирую честно, включая довод против себя: пак был обязан его взять (D39.132 п.2б), и sqlc
поймал бы ровно тот класс, который дважды укусил этот пак в рантайме (`stop_for_signing`,
`chapters_before`) — батарея его поймала, но на цикл позже компилятора. Что мешает в read-модели:
запросы собраны склейкой общих фрагментов (одна проекция книги на четыре пути), а sqlc требует
литерального SQL на запрос, и страница с её ревизией — транзакция, которую он не моделирует.
Предложение зоны: взять в P8 на отложенных однооператорных ручках, по итогам решить про
read-модель.
3. **Поверхность входа: РЕШЕНО контрактной сессией 17.08 (релей владельца), механизм исполнен.**
Вердикт — вариант A уточнённый: `/auth/*` отвечает тем же конвертом и **без машинного `code`
вовсе**; клиент показывает одну нейтральную фразу, а единственный пользовательски осмысленный
случай — `429` — уже машинный без всякого словаря, потому что лечение едет в `Retry-After`
(платформа его отдаёт: `login.go:173,233`, `dev.go:138`). Отвергнуты: расширение `ErrorCode`
(значения, недостижимые на поверхности, которую этот словарь описывает, ломают инвариант «код
называет свой статус») и собственный словарь в компаньоне (сделал бы НЕнормативный документ
нормативным с чёрного хода — канон в шапке объявляет нормативным для ФОРМЫ только себя).
**Исполнено в этом дереве:** `codeForStatus` и обратная функция удалены — писатель входа берёт
статус в точке вызова и кода не эмитит, поэтому инвертировать больше нечего. Пин
`httpapi.TestTheSignInSurfaceAnswersTheSameEnvelopeWithoutAVersionedCode` (шесть статусов: конверт
и `request_id` есть, `code` нет ни у одного). PD-254 закрыт.
**Остаётся зоне контракта (релей, не пере-проектирование):** две фразы — в канон рядом с
существующей про вход (`:36-37`): поверхность механики сессии отвечает тем же конвертом без
`code`, клиент диспетчеризует по статусу, особый случай `429` с `Retry-After`, серверный текст не
показывает никогда; и в компаньон §2.14 — восстановить срезанное правило и таблицу шести статусов.
Триггер пересмотра назван явно: второй пользовательски осмысленный случай на входе → своя
маленькая нормативная спека на `/auth/*`, а не рост таблицы в спутнике.
4. **⚠ НАХОДКА КОНТРАКТНОЙ СЕССИИ, которая дороже самого вопроса — передать оркестратору отдельно.**
Правило про вход было РАТИФИЦИРОВАНО и потеряно молча: в компаньоне 0.2.3 §2.14 стояло «Отказ
входа — problem+json, как везде; **различать причины отказа клиент не может по замыслу**», а в
сегодняшней редакции осталась только первая половина — вторую срезал коммит лендинга батча 0.3.0
(`8d82096`, проверено `git log -S`). То есть P7 не переоткрывал спор, а восстанавливал решение,
которое кто-то выкинул. **Класс потери структурно непроверяем существующей приёмкой:** сверка
батча шла по мультимножеству модальных глаголов (MUST/SHOULD/MAY), а в этой фразе модального
глагола нет. Стоит отдельного разбора приёмки — не как дефект контракта, а как дыра в МЕТОДЕ
проверки резки прозы.
5. **Плотность комментариев** (PD-255): пак вышел за норму «одна-две строки почему». Часть прозы
несущая, часть — эссе; прошу решение владельца о норме, а не тихую подрезку в следующем паке.
## ⟶ Дослано 22.08 оркестратором №18: записи акта 5, оставшиеся в живом журнале
> Заголовок живого журнала «Эра пака P7 — в архиве» утверждал, что выселено всё, а ~185 строк
> остались на месте. Досылаю их сюда без правки тел.
- **P7, акт 5 — ЗАКРЫТ. Дерево готово к приёмке и лендингу.**
Батарея: 18 пакетов, exit 0, скипов 0, линтер 0 issues, тестовых функций **532** (⚠ было «533» — испр. оркестратором №18 20.08: канонической командой D39.121 `grep -rh '^func Test' platform --include='*_test.go' | wc -l` выходит 532; расхождение на единицу, ни на что не влияет, но число было заявлено без команды). Регистр — 327
строк, открытых 65 (13 minor, 52 info), **major и BLOCKER нет**. Дерево не закоммичено и не
застейджено. Миграции пака: **0001900024**.
### Что построено и зачем
1. **Долг на материализацию — колонка с арендой** (`books.read_model_owed_at`, 00019).
Ставится в ТОЙ ЖЕ транзакции, что закрывает границу работы (конец разбора и все три конца
прогона), гасится сверкой метки, берётся в аренду тем, кто платит. **Зачем:** очередь жила в
памяти процесса — упавший `Refresh` её терял, рестарт демона терял её всю, и оплаченный текст не
доезжал до читателя НИКОГДА. Плюс «в покое» для потока теперь значит «и материализация не
должна» — это Ф-56: книга, только что нарезанная, получала `end` и 204 «не переподключайся»
ровно в те секунды, пока дерево ещё не легло.
**Почему отошёл от прежней реализации:** удалены очередь в памяти (`runs.deferRefresh`/
`DrainRefresh`/`Reader`) и бэкстоп «дерева нет вовсе» (`books.materializeMissingTrees`,
`pgstore.BooksWithNoTree`) — оба покрывались одним механизмом, а бэкстоп к тому же не видел
книгу, у которой дерево есть, но на границу старше. Дрейн переехал в `readmodel.Drain` — в
пакет, чья это работа.
2. **«Глава сделана» = последний проход, который книга на ЭТОМ деплое реально получает**
(`books.edit_wave`, 00024). **Зачем:** счёт шёл по волне `edit`, и на деплое без редактора ни
одна глава не была сделана никогда — шкала покупки не подрезалась, сервис бесконечно предлагал
купить уже переведённое. Форму объявляет сам движок (пайплайн без редактора даёт волне `edit`
знаменатель ноль). **Почему на КНИГЕ, а не на прогоне:** прочитанная с последнего прогона, она
роняла `chapters_done` в ноль в момент допуска нового прогона — счётчик ходил назад, что канон
запрещает. Флаг монотонный: книга, прошедшая редактирующий пайплайн, остаётся такой.
3. **У клейма идемпотентности появился владелец** (`claim_token`, 00020) и **тождество интейка
решается дайджестом файла** (`content_sha256`, 00023). **Зачем:** строка ключа не несла, какая
попытка её держит — застрявшая попытка удаляла строку преемника (пользователь получал вторую
книгу) или писала свою квитанцию поверх чужой (клиент реплеил `Location` несуществующей книги);
а тождество по `Content-Length` не различало две РАЗНЫЕ книги с одинаковым объявлением.
4. **Остановка АККАУНТА читается с аккаунта**, а не с потолка одного прогона; `resume` различает
«прогон закончен, нужен новый» (`ceiling_reached`) и «пополните счёт» (`credit_unavailable`);
`blocked` называет холд, который реально освободит шкалу. **Зачем:** всё это — «у вас нет
денег» пользователю, у которого деньги есть, и отправка его делать не то.
5. **Счётчик замечаний вернулся на главу** (`chapters.note_count`, 00022), `chapters.units_done`
снесён (00021). **Почему это разворот решения 00016 того же пака:** 00016 снёс колонку с
доводом «её никто не пишет» — здесь она возвращается ВМЕСТЕ с писателем, которого ей не
хватало. Основание замерено: джойн, которым её заменяли, стоил 83% времени страницы библиотеки
(16.8 мс против 3.4 мс на корпусе 40 книг × 500 глав, ВАКУУМИРОВАННОМ); со счётчиком — 6.6 мс.
⚠ Условие замера названо здесь и при каждом другом числе про него (P8-FIX, PD-336): на ХОЛОДНОМ
корпусе тот же джойн даёт 96%, и это не противоречие, а другой замер. Нормативный носитель
вакуумированных цифр — миграция 00022, остальные на неё указывают. Бенчмарк оставлен
в дереве: `go test ./internal/pgstore/ -bench LibraryPage -run xxx`.
### Что форсировано и почему
- **Работа по канону 0.4.0 лежит в дереве до его ратификации.** Основание: ответ контрактной
сессии на записку зоны плюс слово владельца 20.08 про «кончились деньги → сообщить и дать
продолжить». Ломающая правка ровно одна — `Run.stop_requested` (0.3.0 такого поля не объявляет);
`cause.code: credit_unavailable` легален и по 0.3.0, второй уровень там открыт. Зависимость
записана строкой **PD-327** и закрывается лендингом канона, а не работой зоны. Если форма
сдвинется словом владельца — правки отделимы, карта в `archive/P7_ACT5_FIX_PLAN_2026-08-20.md` §5б.
- **Тесты на АТОМАРНОСТЬ сверяют `xmin`** (транзакцию, последней писавшей строку). Это не
экзотика ради экзотики: два пина акта проходили под мутацией «вынести метку из транзакции во
второй оператор» — конечное состояние она не меняет, меняет окно, и утверждать про окно иначе
тест не может.
### Что осталось, названо и не спрятано
- **PD-327** — ждёт лендинга канона 0.4.0 (не работа зоны).
- **PD-297** — round-trip на строку в `SaveStructure` под эксклюзивной блокировкой книги. Не взят
осознанно: путь самый опасный на запись, инструмент штатный (`tx.SendBatch`), но правку надо
мерить на корпусной книге до и после.
- **Четыре оси не смотрел никто и никогда:** деньги и леджер целиком · вход/сессии/CSRF · очередь
и джобы · метрики. Пак их не менял, а все приёмки смотрели дифф. Это отдельный пак.
- **Живого перевода на стенде не было ни разу** — движку нужны ключи провайдера и деньги. Всё
после «прогон стартовал и остановился» (подпись банка, halt на потолке, резюм после пополнения)
покрыто батареей против живого Postgres, а не живым движком.
### Как это проверялось
`make check` с гейтами (скипов 0) · каждая правка проверена ПОСАДКОЙ МУТАЦИИ: код испорчен именно
так, как называет комментарий пина, пин обязан упасть, код возвращён (40 проб, все поймались) ·
живые пробы по ручкам на стенде: интейк и реплей, `409 key_reused` на другой файл под тем же
ключом, часть после файла на реплее, `/chapters` `/units` `/notes` `/bank`, ETag→304 и разные теги
у страниц, 410 на чужой id главы, `after_version` из прошлой жизни, `unsupported_pair`, `/usage`
на пустом счету и после `adjust`, старт и стоп прогона со `stop_requested` на проводе, SSE в
покое и SSE с непогашенным долгом (Ф-56 на проводе), 400 вместо 413 на длинном теле.
### Самопроверка
Акт закрыт адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера + критик полноты, 84
агента, 0 ошибок): **37 находок, 18 пережили рефутеров**, и почти все — дефекты, внесённые
правками самого акта. Строки **PD-315…PD-327**, каждая с механизмом и пином. Два из них —
собственные пины акта, проходившие под мутацией, которую сами называют (PD-318): это третье
появление класса в паке, и он назван в плане §3 п.2 ещё до того, как повторился.
- **P7, акт 5 (начало) — исполнение фиксов по ревью акта 4 + ответ контрактной сессии.**
План и рабочий список — **`platform/docs/archive/P7_ACT5_FIX_PLAN_2026-08-20.md`** (читать первым; §5а — что пришло из
контракта).
⚠ **Контрактная сессия ответила 20.08 на записку зоны `archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md`: канон стал
0.4.0** (лежит в дереве контрактной зоны, НЕ ратифицирован — до лендинга в силе 0.3.0). Все три
вопроса, висевшие на владельце, закрыты. Зоне пришла работа: `resume` исчерпанного прогона — 409 с
двумя РАЗНЫМИ причинами (`ceiling_reached` против нового `credit_unavailable`); «глава сделана» =
последний реально пройденный проход, и это ДЕНЬГИ — `ChaptersLeft` на деплое без волны редактора не
подрезается вовсе, сервис бесконечно предлагает купить уже переведённое; отпечаток интейка — по
содержимому, `Content-Length` из нормы вышел, а невозможность установить тождество обязана давать
409, а не реплей; `Run.stop_requested` обязательным булевым (единственная ломающая правка).
Отклонены: слово под суточный потолок движка (решение владельца 15.08) и предложенный зоной признак
«прогон продолжаем» (три лишних чтения на карточке, которую перечитывают на каждый кадр).
Акт 4 закрылся адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера, 75 агентов):
**33 находки, 27 пережили рефутеров**, и часть из них внесли сами правки акта 4. Тяжелейшая —
четыре независимые линзы сошлись на том, что **реконсилятор снимал стоп подписи банка, которого
никто не подписывал**, и доспавнивал движок без `--verify-bank`: складывая снятие стопа внутрь
общего `reopen`, акт 4 не проверил, что `reopen` зовёт не только `resume`, но и свип. Нарушение
согласия пользователя; починено ключом на вызывающего, пин проверен посадкой мутации.
⚠ **Первый написанный на это пин ПРОШЁЛ под мутацией** — фикстура не доходила до кода, потому что
часы сервиса захвачены при конструировании и `f.now` их не двигает. Урок в плане, §3.
Закрыто также: загрузочная проверка пар — на ДОСТУПНОЙ половине (деплой с одними `:unavailable`
больше не стартует), интейк не трактует пустой список как «пускать всё», теги сравниваются
регистронезависимо. Батарея: 18 пакетов, exit 0, скипов 0, тестов **503**.
- **P7, акт 4 — доработка 20.08: сверка ВСЕХ находок против дерева + триаж §8.2.**
(Пакет релеев `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; три вопроса владельцу §8.1а; вопросы контракта
вынесены в `platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md` для отдельной контрактной сессии.)
Все 74 позиции обеих приёмок сверены поштучно с ТЕКУЩИМ деревом — каждая с цитатой живого кода,
исход «не проверено» отдельно от «опровергнуто».
⚠ **Три места числились закрытыми и закрыты не были — правка не закрывала собственный сценарий:**
(1) подрезка буфера потока сверялась ПОСЛЕ того, как водяной знак перепрыгнул дыру, поэтому
условие не срабатывало никогда и кадры `note` терялись молча (PD-283); (2) `CompleteIdempotency`
писала квитанцию в строку, которая попытке уже не принадлежит — воспроизведено живым PG, клиент
получал квитанцию ЧУЖОГО запроса (PD-284); (3) пин на снятие `--verify-bank` был ПУСТЫМ: фикстура
стартовала прогон без `stop_for_signing` (PD-285). Все три «подтверждались» зелёной батареей.
**Ещё закрыто:** HEAD на потоке больше не держит горутину и ошибка `Flush` не выдаётся за
отправленный кадр (PD-286) · `blocked` называет чужую книгу только когда её холд укорачивает шкалу
(PD-287, пина не было вовсе) · `note_count` и `GET /notes` описывают одно множество (PD-288) ·
устаревшая доставка не рождает кадр (PD-289) · чтения на одном снапшоте (PD-290) · флаг «текст
прочитан» переехал на ПАРУ (PD-291) · снятие стопа банка внутри транзакции переоткрытия, отдельного
писателя больше нет (PD-292) · проход материализации подрезается своим бюджетом (PD-293) ·
блокировка книги в решениях банка (PD-294) · дубликат индекса снят (PD-295) · маршруты и коды
ошибок — по ОДНОЙ таблице, тесты ходят по ней (PD-296) · бэкстоп-свип для книги без дерева
(PD-276 дозакрыт) · интейк не платит за третью ре-нарезку (PD-248 актуализирован).
**Триаж §8.2:** 17 сделано, 5 отклонено с причиной, 1 релей. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 501**; каждая правка проверена посадкой мутации, не рассуждением.
⚠ **Осознанно НЕ сделано, с причиной:** PD-297 (round-trip на строку под блокировкой книги —
мерить прежде правки) · PD-202 (draft-only, вопрос владельцу) · PD-282 (канон §resumeRun не
покрывает «денег не осталось») · PD-298 (сперва сверка у движка) · PD-299.
**Осей, которых не смотрел никто:** деньги и леджер · вход/сессии/CSRF · очередь и джобы · метрики.
- **P7: приёмка → правки → приёмка правок (17.08).**
⚠ Полный хендофф под сессию без контекста — **`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`** (читать §0§3,
дальше по нужде). Там же: принципы приёмки, сверка с исходным промтом, техдолг, план следующей сессии.
Категорийное ревью по 12 осям с адверсариальной верификацией: 80 находок, **54 подтверждено, 16
правдоподобно, 10 опровергнуто**; после дедупа **46 уникальных дефектов** — 3 блокирующих класса,
15 HIGH, 17 MEDIUM. Батарея была зелёная, потому что все три блокирующих дефекта живут в путях,
которые тесты проходили только в безопасной форме.
**Починено в зоне** (регистр PD-257…PD-281): пере-нарезка книги больше не валит читающую модель
навсегда (отложенный констрейнт, миграция 00018) · упавший `tmctl export` больше не затирает текст
пустотой · весь блок идемпотентности (гонка первых попыток 500→409, область ключа, отпечаток с
размером файла, закрытие ключа вне контекста запроса) · база полосы прогресса считает тот же
проход, что числитель · SSE (подрезанные кадры → `resync_required`, дедлайн записи, id из
будущего) · материализация не голодает свип · банк читается на штатном деплое · деплой-нота больше
не включает обратно грант, который PD-104 выключил. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 490**.
⚠ **Приёмка ПРАВОК (9 осей + fable-5) нашла, что восемь дефектов внесли сами правки** — батарея не
увидела ни одного, три были запинены под ошибку автора. Все исправлены; разбор — хендофф §0.
**Б-1 ПОЧИНЕН разбором с владельцем 17.08 (PD-277).** Рамка приёмки была неверна: это не «нужен
новый канал в движок», а невыполненная половина строки единого бэклога **191(в)** (слово владельца
16.08, D39.144) — «проводка `resume = снятие стопа` до движка». Движок при выключенном
`--verify-bank` уже делает модель владельца: неподписанное едет авто-строками с пометкой
⟨проверить⟩ (`mining.go:211-231`, D39.42 п.3), а платформа передавала флаг обратно и роняла прогон
в тот же стоп. Снято: `l.VerifyBank && !l.BankReleased`; пин `TestAResumedRunIsSpawnedWithoutTheSigningStop`.
⚠ Остаток: `decline` пользователя до движка не доезжает (движок читает `mined_rejects`) — строка
бэклога **192**, отложена владельцем.
**PD-273 закрыт решением владельца 17.08:** статус остаётся `awaiting_bank`, лечение — признак
«стоп был запрошен» в контракте; готовая строка для единого бэклога — хендофф §7(и), вставляет
оркестратор (зона `docs/` не наша).
**Разбор, план, обязательства через оркестратора и команды воспроизведения —
`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`.**
⚠ Заявки раздела «Сессия P7» ниже приёмку НЕ прошли — читать их вместе с хендоффом, не вместо.
- **P7 ПОСТРОЕН — заявка САМОЙ сессии написания (17.08).** ⚠ **Приёмку эта заявка НЕ прошла** — см.
верхнюю запись и `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`: 46 уникальных дефектов, три блокирующих класса, из них
ни один батарея не видела. Читать ниже как исходную позицию, а не как состояние дерева.
Читающая поверхность контракта **0.3.0**:
главы · пары с текстом · замечания · банк и подпись · SSE на КНИГЕ · `GET /capabilities` ·
машинная модель ошибок (`code` + `request_id`) · условные чтения и сжатие · `Idempotency-Key` ·
`structure_version`. Плюс форварды (`title`, `reject_reason`), PD-104 (грант → 0), демонтаж
построенного гейта полноты банка (D39.144), PD-241, конформность построенного по §5 промта и
рантбук деплоя, прогнанный ЖИВЫМ `migrate`. Батарея на стенде `~/.local/pgsql` (порт 55433) под
`-race` с ОБОИМИ гейтами: **EXIT=0, 18 пакетов, скипов 0, линтер 0 issues**, тестовых функций
**416 → 458**. Дифф: 46 файлов, +2329/646. Селф-ревью финалом ТРИ независимых ревьюера, два из
них другой модели: **22 находки, 17 подтверждены и починены** (две HIGH нашли оба контрактных
ревьюера независимо), 5 диспозиция с аргументом. Разделы ниже записка-план, живые пробы,
таблица селф-ревью, obstacle, вопросы оркестратору.