Land the contract minor: the canon stops promising the door the owner abolished, declares the correction door derived field-for-field from the landed verb, and the deployment announces the version it now serves

This commit is contained in:
heaven 2026-08-27 18:32:04 +03:00
parent b5b2ab6eb2
commit 243a8c341c
10 changed files with 1030 additions and 130 deletions

View file

@ -0,0 +1,372 @@
# Отчёт контрактной сессии: минор 0.4.0 → 0.5.0
> Сессия по промту `docs/CONTRACT_MINOR_SESSION_PROMPT.md` (оркестратор №19, 27.08.2026).
> Файл ведётся ПО ХОДУ работы (§7): секция 1 написана ДО правок. Зона записи — два файла контракта
> плюс этот отчёт; сессия НЕ коммитит.
## 1. Эхо-протокол (§9) и записка-план — ДО правок
**Скоуп:** снос отменённой пер-термной модели подписи (§3.1) · объявление двери правок банка,
выведенной из словаря `tmctl bank-apply` (§3.2) · судьба счётчиков `pending_decisions`/`complete`
(§3.2-бис) · предупреждение о конверте вне `/v0` (§3.3) · версия `0.5.0`.
**Инварианты:** словарь двери — из `membank/decisions.go` + `pipeline/bankdecisions.go`, не
изобретаю; неизвестный кортеж — НЕ ошибка (форма добавления терма); `undecided` — только вместе с
запретительным контрактом `SignatureState`, обе половины; коды 14/15 различимы на проводе; конверт
ошибки — существующий `Problem`/`ErrorCode`; узость окна, отклонение сид-терма, необслуживаемость —
явно в каноне; машиночитаемый признак «не построено» обязателен.
**Не-делать:** не коммичу; `platform/`/`frontend/`/`backend/`/`eval/` не трогаю; полоса прогресса
(строка 200) не берётся (D39.160); семантика прогресса `openapi.yaml:1293` не меняется; гейт
платформы не правится — его красный ожидаем; в `docs/PROGRESS.md` не пишу.
### Записка-план: что меняю, где, чем проверю
**Канон `openapi.yaml`:**
1. `version: 0.4.0 → 0.5.0`; пример `contract_version``0.5.0`.
2. §3.1 снос: путь `:469-505` · проза `:1830` (`TermStatus`) · схемы `:1939-2008`
(`BankDecision`/`BankDecisionsRequest`/`BankDecisionsResult`). Мой греп подтвердил список промта
и НЕ нашёл иных носителей четырёх имён (команда в §5 ниже).
3. §3.2-бис: снос `pending_decisions`+`complete` из `BankPage` (`:1924-1934`) и — находка МОЕГО
грепа, в списке промта её нет — из `EventBank` (`:2367-2372`). Ратифицированная проза «ONE act»
в `listBankTerms` сохраняется, клауза счётчиков и фраза «marked unverified inside the service»
(снята ещё ФБ-8 как обещание без носителя, но в `:445` уцелела) — уходят.
4. §3.2 дверь: `POST /books/{bookId}/bank/corrections` + схемы (`BankCorrection`,
`BankCorrectionsRequest`, `BankCorrectionsReceipt`, `BankSignatureCount`, `CorrectionRefusal`) +
два корневых кода (`bank_corrections_refused` 409, `bank_corrections_incomplete` 503) + признак
`bank_corrections_enabled` в `Capabilities` (прецедент `intake_enabled`; false ⇒ 404).
5. §3.3: в `info.description` (после `:35-37`) — предупреждение о конверте вне `/v0` без
обязательного `code`, ссылкой на компаньон §2.14 (один носитель на факт).
6. Тег `:127` «Memory bank and term signing» — переформулировать без пер-термной подписи.
**Компаньон `README.md`:** шапка (статус 0.5.0, без самозаписи ратификации) · `:124` и `:724`
(списки мест правила «схема + слова» — преемник) · `:674` (историческая строка сохраняется, статус
дописывается) · `:685` (строка про недоезд `decline` — пере-снята: новая дверь пишет файлы движка) ·
`:358` §2.9 (остаток пере-привязан к живым носителям) · новая секция §2.19 (вывод двери, провенанс) ·
§3 таблица (строка двери: движок построен, платформа — пак 2в) · Приложение А-2 (две строки кодов).
**Чем проверю (§5):** (1) YAML-парс + линтер, если найдётся в окружении; (2) сверка `$ref` против
`components.schemas` в обе стороны + loader-хук на дубли ключей; (3) гейт платформы на копии
`cp -a --parents platform docs/architecture/14-api-contract` — красный ИМЕННО на версии и с
ожидаемым текстом. Плюс опровергатели (§5 промта, заказ): 24 агента, мандат «дверь против словаря
глагола» + оси §6.
## 2. Обоснование формы двери (§3.2)
Полная таблица «провод ← движковый источник, file:line» и решения ◆ — в компаньоне, новая секция
§2.19 (`docs/architecture/14-api-contract/README.md`); здесь — сами решения с разобранными
альтернативами. Всё выведенное (✓) взято из `membank/decisions.go` + `pipeline/bankdecisions.go` +
`cmd/tmctl/main.go:69-75` и в канон попало без пересочинения.
**2.1. Форма поверхности — один `POST /books/{bookId}/bank/corrections` с документом целиком.**
Альтернатива «ресурсная» (`PATCH /bank/terms/{id}`) отвергнута: глагол атомарен и
всё-или-ничего (`ApplyDecisions`, `decisions.go:282-307`), а пер-строчные мутации обещали бы
частичную применимость и правку строк банка, которых этот вызов не меняет (вид пересобирается на
границе прогона — `18-bank-ontology.md`). Альтернатива «двух путей» (`…/preview` отдельным POST)
отвергнута: два пути = два места, где растёт форма, а закон шва п.7 требует, чтобы проекция была
той же операцией, что мутация («a report printed by the same call that already wrote is not a
projection» — `bankdecisions.go:44-45`).
**2.2. Имя `corrections`.** «decisions» вернуло бы имена снесённых схем с другой семантикой
(генерённый дифф читался бы как правка старой двери); «edits» — `edit` в грепе-гейте §5 компаньона
неотличим от имени волны движка. Продуктовое слово владельца — «правка» (D39.144).
**2.3. `book_id` в теле, обязательный, при живом `{bookId}` в пути.** Движок держит ДВЕ разные
проверки: документ обязан НАЗЫВАТЬ книгу (`decisions.go:141-143`) и отдельно сверяется с открытой
книгой (`bankdecisions.go:267-269`) — причина названа в коде: решения несут слова пользователя в
канон книги, и набор, посчитанный для другой книги, даунстрим не заметит. Если бы book_id вводила
платформа из пути, сверка стала бы вакуумной — клиент, POST-нувший набор книги A в путь книги B,
прошёл бы молча. Цена — одно поле.
**2.4. Кортеж требует все четыре члена** (`src`,`sense`,`since_chapter`,`until_chapter`; движок
дефолтит опущенные — `decisions.go:469,479-482`). Провод строже движка сознательно: неизвестный
кортеж здесь легально ДОБАВЛЯЕТ терм, поэтому цена опущенного `sense` — не отказ, а тихая
параллельная строка (клиент хотел править (src, sense="X"), а назвал (src, "")). `null`-семантика
окна — как у `BankTerm` (0.3.0 развёл сентинел `0` на `null`); проекция `null→0` — платформа,
закон шва п.6.
**2.5. `preview` — обязательное булево тела.** Закон шва п.7 (проекция прежде мутации) +
прецеденты тотальной обязательности (`stop_requested` — §6в J компаньона; в запросах —
`RunRequest.stop_for_signing`). Query-параметр на POST отвергнут: в этом каноне тело — единственный
носитель семантики небезопасных вызовов, и опциональность дала бы «мутацию по умолчанию».
**2.6. Раскладка полосы отказов.** Ратифицированная полоса 1019 (`main.go:69-75`) ложится так:
- **14 → `409` `bank_corrections_refused`** + member `refusals[]`. Отдельный корневой код, не общий
`400`: это законный отказ, по которому ПОЛЬЗОВАТЕЛЬ пере-решает (совет промта принят; довод —
разные адресаты ремеди). `409`, не `422`: канон весь класс state-зависимых отказов держит на 409,
а отказ здесь — о книге как она стоит (сид, коллизии набора).
- **15 → `503` `bank_corrections_incomplete`**: «документ ПРИНЯТ, слать ТОТ ЖЕ» — ремеди машинный и
специфичный, потому код свой, а не общий `service_unavailable`; ретрай сходится по байтовому
no-op (`bankdecisions.go:209-221`).
- **12 → `409` `run_in_flight`**: словарь канона уже имеет точное слово («книга уже переводится»);
движковая причина (флок живого прогона, `bankdecisions.go:159-163`) совпадает по смыслу.
- **10/11/13 → без пер-операционных кодов**: это «деплой сломан, чинит человек» — `5xx` общего вида
(500 канон сознательно не документирует — Zalando-правило из А-2).
- **потолок 1 МиБ → `413`** (`payload_too_large` дообъяснён), **потолок 5000 → схема
(`maxItems: 5000`) + слова**, нарушение = `400` `invalid_request` (нарушение объявленной формы;
движковый класс 14 для него — за платформенной валидацией и на провод не выходит).
**2.7. Причины пер-решенческих отказов — `refusals[].detail`, developer-facing, не показываются.**
У движка причины — свободный текст (`RejectedDecision.Reason`); машинного словаря причин нет, и
таблица перевода (закон п.6) переводит только структуры, не прозу. Замораживать в каноне пересказ
двадцати формулировок = вторая копия растущего словаря. Альтернатива «коды причин на платформе»
отвергнута: платформа для этого парсила бы английские предложения движка. Названная щель — в §6.
**2.8. Что НЕ пошло на провод из отчёта глагола** (каждое — решение, не забывчивость): пути файлов
и `written_delta`/`written_rejects` (серверная топология; ремеди клиента один — «слать тот же»);
`canonical_rewrite*` (предупреждение оператору файлов на диске, у клиента продукта нет ни файла, ни
комментариев в нём); ТЕКСТЫ `preexisting_problems` (свободный текст движка) — но их СЧЁТ
(`preexisting_faults`) едет, потому что «книга уже больна, следующий прогон может встать» экрану
нужен; `replaced[]` (свободный текст) — свёрнут в булев `displaced`, несущий ровно то свойство,
ради которого поле существует («перекрыл чужое слово — видь это», `decisions.go:225-228`);
`mode` — раскладывается на `preview`/`changed`/HTTP без остатка; `decisions_version`/
`report_version` — версии документов ШВА, на проводе их роль играет `contract_version`.
**2.9. `depth`: `edit_wave``refinement`.** Движковая константа несёт имя волны — запрещённый на
проводе словарь (гейт §5). Продуктовое слово с тем же смыслом; обе половины смысла («следующий
прогон»; «черновик не пере-формируется» → «не пере-переводится с нуля») продублированы словами.
Поле оставлено (не свёрнуто в прозу), потому что движок сделал его ПОЛЕМ с версией ровно затем,
чтобы будущая смена глубины была наблюдаемой (`decisions.go:51-57`).
**2.10. `signature` — опубликован с ОБЕИМИ половинами контракта** (`bankdecisions.go:88-118`):
положительная (счёт согласован с правилом сворачивания — полезен экрану) и запретительная
(не гейт; не отвечает «погаснет ли стоп» ни в одну сторону; `undecided: 0` ничего не обещает;
решать — ПРАВО; `unreadable` = числа не значат ничего). `map` (путь) не едет; «карты ещё нет»
выражено `signature: null` — иначе `0/0` без карты был бы байтово неотличим от «всё решено», тот же
класс дыры, который движок закрыл флагом `unreadable`.
**2.11. Признак «не построено» — `Capabilities.bank_corrections_enabled: boolean`.** `Capabilities`
годится: плоский документ деплоя, уже дом `intake_enabled` (булев + объявленный `404`) и
`export_formats` («пусто = не построено»). Булев, а не пустой список: у двери нет оси форматов —
нечего перечислять. Поведение при `false` объявлено (`404`), зеркаля интейк.
**2.12. Квитанция — НЕ чтение:** без `revision`/`structure_version`, кадр `bank` не испускается,
строки `GET /bank` не меняются до следующего прогона — сказано в каноне прямо (иначе первый же
экран прочтёт «исправил, а банк не изменился» как баг). Это проекция однонаправленного потока
онтологии (источники → вид → проекции, `18-bank-ontology.md`).
## 3. Решение по счётчикам (§3.2-бис): СНЕСТИ — со всех ТРЁХ носителей
Мой греп нашёл носителей больше, чем названо в промте: `pending_decisions`/`complete` жили в
`BankPage` (`:1924-1934`), в квитанции `BankDecisionsResult` (`:1997-2008`, умерла со схемой) **и в
кадре `EventBank` (`:2367-2372`, required)** — третий носитель в списке промта отсутствовал
(команда: `grep -n "pending_decisions\|'complete'\|complete:" openapi.yaml` до правок — хиты
442, 1924, 1929, 1997, 2000, 2004, 2367, 2371-2372).
Из трёх заказанных исходов выбран **снос**, довод в компаньоне §2.19-бис: пере-определить нельзя —
честный счёт движковый (`SignatureState`), и read-модель его не вычислит, не пере-реализовав
движковый закон (запрещено D39.156 п.6; «Чего эта форма НЕ несёт» онтологии); пометить нельзя —
обязательное вечно-нулевое поле это PD-370 в поле вместо пути. Ратифицированная проза «Signing the
bank is ONE act…» СОХРАНЕНА в `listBankTerms`; попутно из того же абзаца снята фраза «marked
unverified inside the service» — обещание без носителя, снятое ещё ФБ-8 (компаньон §6б) и
уцелевшее в одном месте. Цена сноса названа в каноне и компаньоне: чтение банка больше не отвечает
«сколько осталось»; счёт едет квитанцией двери (`signature`); возврат в чтение — когда закажут
экран подписи и движок опубликует нерешённость проекцией (носитель — `18-bank-ontology.md`).
## 4. Исполнения самопроверки (§5) — команды и вывод
**4.1. Канон парсится + дубли ключей** (loader-хук, § 5 п.1-2; скрипт `check_canon.py` в
скратчпаде сессии):
```
$ python3 .../check_canon.py
PARSE OK, no duplicate keys. version = 0.5.0
```
**4.2. Ссылки, обе стороны** (тот же скрипт: все `$ref` резолвятся; все компоненты на кого-то
ссылаются):
```
refs total: 272; dangling: 0
components.schemas: 63 defined, unreferenced: none
components.responses: 10 defined, unreferenced: none
components.parameters: 11 defined, unreferenced: none
components.headers: 2 defined, unreferenced: none
```
Тем же скриптом пере-ран гейт §5 компаньона (утечка конвейерного словаря) по ФИНАЛЬНОЙ спеке:
`stage` — 1 хит, сама формулировка запрета (легально); `edit` — 2 хита, оба прежние легальные
английские глаголы (`:283` «not an edit», `:1683` «editing the source» — инвентарь 0.4.0, новых
нет; команда: `grep -n "\bedit" openapi.yaml | grep -v credit`); `verdict` — 2 хита БЫЛИ моими в
`CorrectionRefusal`, исправлено на «refusal» до сдачи, пере-ран чист.
**4.3. Настоящий линтер и генератор** (пины зоны фронта, поверх п.1):
```
$ ./frontend/node_modules/.bin/spectral lint --ruleset frontend/.spectral.yaml docs/architecture/14-api-contract/openapi.yaml
No results with a severity of 'error' found!
$ ./frontend/node_modules/.bin/openapi-typescript <канон 0.4.0 из HEAD> → OK (exit 0)
$ ./frontend/node_modules/.bin/openapi-typescript <канон 0.5.0> → OK (exit 0)
```
Дифф генерённых типов 0.4.0 → 0.5.0 — 380 строк, ВСЕ в заявленной поверхности: путь и operationId ·
минус три схемы и счётчики (`BankPage`, `EventBank`) · плюс шесть схем · `ErrorCode` +2 значения ·
`Capabilities` +1 обязательное поле · `Problem.refusals`. Неожиданных сдвигов нет (команда: `diff
types-040.ts types-050.ts`). Замечание генератора: `oneOf`-ветки идентичности `BankCorrection`
сворачиваются в `& (unknown | unknown)` — тип не дискриминирует ветки; это известный класс К-11
(генератор игнорирует и `if`/`then`), правило «схема + слова» применено, слова в описании стоят.
**4.4. Гейт платформы краснеет ПРЕДСКАЗУЕМО** (§5 п.3; копия ВМЕСТЕ с каноном, как велит промт):
```
$ cp -a --parents platform docs/architecture/14-api-contract $SCRATCH/gatecopy/
$ cd $SCRATCH/gatecopy/platform && go test ./internal/gates/ -run TestTheAnnouncedContractVersionIsTheOneTheCanonRatified
--- FAIL: TestTheAnnouncedContractVersionIsTheOneTheCanonRatified (0.00s)
contract_test.go:40: this build announces contract 0.4.0 and the ratified canon is 0.5.0; the
constant is raised in the same change as the code that implements a minor, never afterwards
FAIL
```
Красный ИМЕННО на несовпадении версий и ИМЕННО с ожидаемым текстом; константу поднимает оркестратор
при лендинге тем же коммитом (§4 промта, D39.160).
## 5. Находки опровергателей — все, включая отвергнутые с причиной
Панель: 3 агента (в пределах веера 24, §5 промта), по осям §6 — «словарь двери против словаря
глагола» (заказ) · «холодный потребитель/генератор» · «что читатель узнает о том, чего нет».
Workflow `wf_20c22060-e6e`, 349k токенов, 0 упавших. Каждый ключевой клейм пере-сверен МОИМ чтением
кода до вправления (файлы и строки в диспозициях). **Итог: 21 находка; принято и вправлено 17,
отвергнуто 4 (с причиной ниже).**
### Приняты и вправлены (после каждой — куда легла правка)
**Ось «словарь глагола»:**
1. **MAJOR. Decline сид-поверхности уплощён до безусловного отказа.** Движок отказывает ТОЛЬКО
инертному (`decisions.go:374`, условие `!deltaHoldsSurface`); когда правки книги держат свою
строку той же поверхности, decline принимается и снимает её — ремонт ливлока, ради которого
правило и переписывалось. Мой канон запрещал ремонт. → канон (`BankCorrection`, список
«NOT correctable») + компаньон §2.19 (строка сид-терма).
2. **MAJOR. Decline алиаса уплощён.** Движок судит РЕЗУЛЬТАТ и отказывает только алиасу
ПОДПИСАННОГО терма (`aliasOwner` пропускает `status != approved`, `decisions.go:644-659`);
decline терма и его алиаса одним документом — оба принимаются. → канон (условная формулировка
«once this document is applied, would remain only an ALIAS an approved correction still fires
for») + компаньон.
3. Алиасы на проводе НЕ публикуются (`BankTerm` их не несёт, D39.136 п.4б), а вывод-таблица
читалась как будто публикация есть; клиент не может увидеть, что поверхность — алиас. → канон
(скобка «the alias set itself is not published on this surface») + компаньон (названная щель).
4. Класс 12 → `run_in_flight` точен только при сериализации платформой СВОИХ вызовов двери (флок
держит любой глагол, включая превью). → компаньон: обязательство монтажа (в), «точное слово»
переквалифицировано.
5. `400` `errors[/book_id]` не имеет движкового производителя (движковая сверка — класс 10):
производит платформа до спавна. → компаньон: обязательство монтажа (б).
6. Оба потолка у движка — класс 14, канон раскладывает в `400`/`413`: платформа мерит их на
проводной форме до спавна; остаток (движковый документ после рендера > 1 МиБ) падает в `409`.
→ компаньон: обязательство монтажа (а); канон: описание `bank_corrections_refused` несёт
«a set larger than the service applies in one act».
7. `preexisting_faults` «may stop at the bank» читался как благой signing-stop, и объект был сужен
до файлов правок (движок считает и фолты сид-строк). → канон: «bank inputs», «would FAIL at
the bank — a fault, not the signing stop».
8. «`mode` раскладывается без остатка» — ложь: исход `stopped` не был разложен. → компаньон:
назван (`5xx` рестарта, ничего не записано, ретрай сходится).
9. Усечение отказа: на `409` движок печатает полный отчёт, провод несёт только `refusals[]`
не было названо. → компаньон: названо ценой v1.
10. `kind` на проводе сужен до `TermKind`, движок принимает любую строку; источник пятёрки —
классификатор чтения, не `decisions.go`. → компаньон: сужение задекларировано ◆ с доводом.
**Ось «холодный потребитель»:**
11. **MAJOR. Счёт `signature` недостижим, пока нечего послать** (у `corrections` `minItems: 1`,
пустой документ и движок отказывает классом 10). Решено НЕ ослаблением `minItems` (движок
пустой документ не принимает — ослабление дало бы дверь, врущую о глаголе), а НАЗВАННОЙ
узостью: канон (`signature` квитанции + `listBankTerms`) говорит прямо «only there; a client
with nothing to correct or preview cannot ask for it yet»; носитель возврата счёта в чтение —
§2.19-бис.
12. **MAJOR. Семантика `sense`/окна при `decline` в кортежной форме не была определена.** → канон:
«say which row the receipt reports and do NOT narrow the effect», для поверхности вне банка —
`sense: ""` и `null`-окна (сверено: движковый decline ключуется нормализованным `src`,
`addReject`).
13. **MAJOR. Неизвестный `id` — двоякое прочтение (400 c `errors[] unknown` против 409).** → канон
называет код явно: `409` `bank_corrections_refused`, вход в `refusals[]` (движок: класс 14).
14. **MAJOR. Отказ за decline алиаса опирался на данные, невидимые клиенту** — слился с №2/№3.
15. «Would change nothing» как причина 409 против гарантии `already_applied` — граница не была
проведена. → канон: «a `decline` nothing recorded answers to; re-sending an already-recorded
correction is NOT this — that answers `200` with `already_applied`».
16. `AcceptedCorrection.dst` — третье незаявленное исключение из правила шапки
«required+nullable». → канон: `dst` теперь required, `[string, 'null']`, «null on decline».
17. Переиспользованные `TooLarge`/`ServiceUnavailable` несли чужую конкретику (интейк/прогоны) —
докстроки врали бы на этой операции. → канон: описания компонентов обобщены, `503` отсылает к
диспетчеризации по `code`.
18. `BankTerm.id` «its surfaces» (мн.ч.) читалось как «id зависит от dst». → канон: «its SOURCE
surface … not from `dst`, so a corrected rendering keeps the id».
19. Протухший пример `EventHello.contract` `0.4.0`. → канон: `0.5.0`.
20. `Problem.code` обязателен схемой, а вне `/v0` может отсутствовать — правило жило только в
шапке, генератор его не видит. → канон: оговорка в описании `Problem`.
**Ось «чего нет»:**
21. **MAJOR. «Канон и деплой при лендинге СОВПАДАЮТ» — переклейм.** Сверено кодом: `wireBankPage`
(`platform/internal/httpapi/reading.go`) продолжает слать `pending_decisions`/`complete`, и
они НЕ нули — `bankCountsTx` (`pgstore/readmodel.go`) считает «все proposed-строки» с
комментарием «the wire fields are the canon's» (теперь ложным). Совпадение точно по ПУТЯМ, не
по полям. → компаньон: шапка переписана честно, §2.19-бис несёт сверенную правду вместо
«навсегда нули», строка `GET /bank` в §3 пере-снята, носитель снятия — пункт (2в). ⚠ Это
расхождение с БУКВОЙ промта пака (§4 п.2 «СОВПАДУТ точно», §3.2-бис «поля навсегда нули») —
**пинг оркестратору, см. §6.**
22. Композиция «до монтажа 2в счёт недоступен НИГДЕ» была только выводимой. → компаньон
§2.19-бис, названо тремя половинами цены.
23. А-2: «гейт полноты ДЕМОНТИРУЕТСЯ в P7» — будущее время при снятом гейте (сверено:
`reconcile.go`, ветка `awaiting_bank`). → компаньон: «ДЕМОНТИРОВАН, сверено с кодом».
24. §2.9: баннер 0.5.0 стоял ПОСЛЕ перекрытой клаузы — против «баннер прежде содержимого». →
компаньон: переставлен перед ней.
25. Позитив: все семь обязательных «явных фактов» оси 3 подтверждены с картой line-номеров
(вердикт панели, находка-нота) — в канон изменений не требует.
### Отвергнуты, с причиной
1. **`Capabilities.intake_max_bytes` обязателен и не-nullable у read-only деплоя** — реальный
дефект, но 0.3.0/0.4.0-наследие ВНЕ состава этого пака (§3 промта); правка сейчас раздула бы
ломающую поверхность минора без заказа. Оставлено оркестратору как кандидат следующего касания.
2. **`TermKind`/`TermStatus`/`TermOrigin` без пер-схемных нот толерантности** — паттерн 0.3.0, вне
состава пака; общее правило шапки формально покрывает. Кандидат следующего касания (вместе с
№1).
3. **Байт-зеркало фронта (0.2.3) несёт снесённую модель** — отставание РАТИФИЦИРОВАНО (D39.142
п.5, D39.147, фриз фронта); шапка компаньона его уже несёт. Не дефект этого минора.
4. **`unreadable`: «the call was refused before it could count» не имеет носителя на проводе** —
принято НАПОЛОВИНУ: формулировка-триггер из описания снята (правда), но само поле оставлено —
первый триггер («не читается состояние») жив и на 200-квитанции (карта/файлы нечитаемы, вызов
при этом принят byte-no-op'ом… нет: нечитаемые файлы — отказ класса 10; нечитаемой может быть
КАРТА подписи — вызов принят, счёт не измерен). Ровно этот случай поле и несёт.
## 6. Obstacle — что НЕ удалось и что НЕ проверено (§10)
1. **Расхождение промта с кодом — пинг оркестратору (§11 промта):** промт утверждал «платформа
свою половину уже снесла 22.08, после твоего сноса они СОВПАДУТ точно» (§4 п.2) и «поля
навсегда нули» (§3.2-бис). Сверка с кодом (`httpapi/reading.go` `wireBankPage`;
`pgstore/readmodel.go` `bankCountsTx`) показала: путь снесён, а ПРОЕКЦИЯ ЧТЕНИЯ продолжает
слать оба поля, и `pending_decisions` — живое число всех proposed-строк, не ноль. Я не
интерпретировал в свою пользу и не обходил: канон 0.5.0 поля снимает (это и есть заказ), а
расхождение записано в шапку компаньона с носителем (пункт 2в) и сюда. **Оркестратору решить:**
достаточно ли носителя «пункт 2в» для снятия двух полей из `wireBankPage`, или нужна отдельная
строка бэклога платформы (я её завести не могу — не моя зона).
2. **Не проверено исполнением:** поведение НАСТОЯЩЕЙ платформы против 0.5.0 (маршрута нет — 404
проверить не на чем до монтажа); рендер экранов фронта (зона заморожена ратифицированно);
`spectral` гонялся ТОЛЬКО с рулсетом зоны фронта (`spectral:oas`) — иных рулсетов в репо нет.
3. **Названные щели v1 (в канон/компаньон внесены, но не закрыты):** машинного словаря причин
отказов нет (едут developer-facing `detail`); алиасы невидимы клиенту; счёт `signature`
недостижим без правки/превью; на `409` не едут `preexisting`/`signature`; счётчики деплой шлёт
до монтажа 2в.
4. **Гипотетический остаток раскладки потолков:** случай «HTTP-тело < 1 МиБ, движковый документ
после рендера платформой > 1 МиБ» назван и покрыт кодом 409, но живьём не воспроизводился
(двери нет); замер — работа монтажа.
5. Дифф генерённых типов пере-ран после вправлений панели (`types-050b.ts`, exit 0), но
пост-фиксовый ПОЛНЫЙ дифф против 0.4.0 построчно не пере-инвентаризировался — сверены только
изменённые правками места (`AcceptedCorrection.dst`, примеры, описания).
## 7. Таблица комплектности против §3 (§7)
| Пункт заказа | Что сделано | Исполнение-подтверждение |
|---|---|---|
| §3.1 снос: канон, 9 позиций | путь+операция (`:469-505` стар.) · проза `:1830` · три схемы `:1939-2008` — снесены; замена — дверь corrections | `grep -n "bank/decisions\|submitBankDecisions\|BankDecision\b\|BankDecisions\|pending_decisions\|term_id" openapi.yaml` → exit 1 (0 хитов) |
| §3.1 снос: компаньон, 5 позиций | `:124` (список К-11-мест) · `:674` (историческая строка сохранена, статус дописан) · `:685` (пере-снята на живые носители) · `:724` (К-11) · `:1173` (⚠-баннер истории §6в K) | `grep -n "submitBankDecisions\|BankDecisionsRequest\|BankDecisionsResult" README.md` → 0 хитов вне ⚠-контекстов; пер-позиционные диффы в дереве |
| §3.1 «свой греп обязателен, ещё и по `dst`/`UnknownTermError`/`BankPage`» | исполнен до правок | команда и хиты — секция 3 отчёта; `UnknownTermError` — 0 хитов в обоих файлах |
| §3.2 дверь объявлена, тело выведено из глагола | `POST /books/{bookId}/bank/corrections` + 6 схем + 2 корневых кода + member `refusals` | вывод-таблица §2.19 компаньона (каждое поле — file:line движка); панель-опровергатель по оси словаря — секция 5 |
| §3.2 машиночитаемый признак «не построено» | `Capabilities.bank_corrections_enabled` (required; `false``404`) | канон; прецеденты `intake_enabled`/`export_formats` — §2.19 |
| §3.2 ⛔ signature только с запретительным контрактом | `BankSignatureCount` несёт ОБЕ половины D39.144 + `unreadable` + `null`-семантика | канон `:2217+`; проверка панелью (ось 3, факт 7 подтверждён с line-номерами) |
| §3.2-бис счётчики: решить обязан | решено — СНОС с трёх носителей (третий найден моим грепом) | секция 3 отчёта: греп-команда; §2.19-бис компаньона |
| §3.3 конверт вне `/v0`, строка 203(а) | предупреждение в `info.description` + оговорка в `Problem`; ссылка на компаньон §2.14 | буква (а) сверена: `docs/PROGRESS.md:146` (греп `строка 203`); канон `:39-43` |
| §3.4 прогресс НЕ трогать | семантика `:1293` не тронута | `git diff openapi.yaml | grep -c "segment\|counter starts again"` → 0 правок в блоке `Progress` |
| §4 версия | `0.5.0`; гейт платформы краснеет предсказуемо | секция 4.4: команда копии и вывод FAIL с ожидаемым текстом |
| §5 п.1 парс | исполнено | секция 4.1 |
| §5 п.2 ссылки + дубли | исполнено, обе стороны | секция 4.2 |
| §5 п.3 гейт на копии | исполнено, `cp -a --parents` | секция 4.4 |
| §5 опровергатель (заказ) | 3 агента, 21 находка, 17 вправлено, 4 отвергнуто с причиной | секция 5; workflow `wf_20c22060-e6e` |
| §7 записка-план до правок | секция 1 написана до первой правки файлов контракта | git-история дерева не создавалась (не коммичу); порядок виден по этому файлу |
| §9 эхо · §10 obstacle · §11 состав | секции 1, 6; состав сдачи — три файла | `git status` перед сдачей — см. финал сессии |

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
| Платформа | активного НЕТ | читающий пак P8-REVIEW ОТРАБОТАН, ПРИНЯТ и ЗАЛЕНДЁН 27.08 (D39.159): четыре оси, 24 строки регистра, единственный vuln `PD-379`. Промт — `platform/docs/archive/`. Следующая работа зоны — КОДОВЫЙ пак по этим строкам, промт пишется по слову владельца |
| Полигон | [POLYGON_EXP2223_REDO_SESSION_PROMPT.md](POLYGON_EXP2223_REDO_SESSION_PROMPT.md) (отложенный — [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md), строка 85) | фаза Д ИДЁТ; ⚠ живой носитель курса — в `eval/dovodka/`, какой именно называет зона (⚠ [POLYGON_PHASE_D_HANDOFF.md](POLYGON_PHASE_D_HANDOFF.md) — перекрытый снимок, читать не как курс) |
| Фронт | активного НЕТ | **ЗОНА ЗАМОРОЖЕНА** (D39.136 п.2 + D39.147: разморозка отдельным словом владельца, не привязана к P7); перечень первого касания — в зонном журнале |
| Контракт | [CONTRACT_MINOR_SESSION_PROMPT.md](CONTRACT_MINOR_SESSION_PROMPT.md) | ВЫДАН 27.08, минор 0.4.0 → 0.5.0, пункт (2б) очереди D39.156. **Состав — §3 промта, здесь он НЕ пересказан** (один носитель на факт). Ратифицированное исключение: прогресс (строка 200) в минор не входит, едет с паком (2в) — D39.160 |
| Контракт | активного НЕТ | минор **0.5.0** ПРИНЯТ и заленджен 27.08 (D39.161): отменённая пер-термная модель снесена, дверь `POST …/bank/corrections` объявлена и выведена из словаря глагола, признак «не построено» машиночитаем. Следующая работа по контракту — ПОСЛЕ монтажа двери паком (2в) |
Отработанные промты — `archive/prompts/`, отчёты с ревью-шапками — `archive/reports/`. Зонные журналы фронта и платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (прогресс зон только там, D39.100).
- Чужие зоны — фронт и платформа (читать при касании стыка; каждая ведёт СВОЙ зонный бэклог — единый бэклог их строк не принимает, D39.84): [../frontend/](../frontend/) — веб-интерфейс: промт фронт-сессий + [STACK_DECISIONS.md](../frontend/docs/STACK_DECISIONS.md) (пины версий точными числами и ловушки) + [BACKLOG.md](../frontend/docs/BACKLOG.md) · [../platform/](../platform/) — SaaS control plane: README + [BACKLOG.md](../platform/BACKLOG.md) + `docs/` (зонный журнал `platform-PROGRESS.md` · регистр дефектов · `STACK_DECISIONS.md` с рецептом стенда и инвентарём каналов шва · **[ENGINEERING_STANDARDS.md](../platform/docs/ENGINEERING_STANDARDS.md) — ратифицирован; КАЖДЫЙ промт платформенной сессии обязан на него ссылаться, отступление = пинг** · **[PLATFORM_DIRECTION.md](../platform/docs/PLATFORM_DIRECTION.md) — ратифицированное направление зоны: аутентификация, деньги, стандарты, скорость** · архив промтов).

View file

@ -1,4 +1,4 @@
# Реестр D-нот — карта актуальности v2 (D1D39.160;
# Реестр D-нот — карта актуальности v2 (D1D39.161;
> ⚠ **СЛАБОЕ МЕСТО, КОТОРОЕ БЫЛО ЗДЕСЬ (вписано 22.08, ЗАКРЫТО 24.08 — D39.157 п.6).** Колонка ТЕЛА
> у нот D39.107…D39.123 говорила «жив», хотя тела уехали в слайс подрезкой D39.139; семнадцать строк
@ -221,4 +221,5 @@
| D39.158 | 27.08 | Входная дверь шва закончена: стоп банка — ФЛАЖОК в движке (память предъявленного, схема v16); полоса — два яруса + гардрейл «разрушительное на классе»; класс `write_incomplete`/exit 15; отчёт двери v2; пути решений только конвенцией; эрраты 191(б)/203(з) | жив | живой файл | шов · банк · полоса отказов |
| D39.159 | 27.08 | Пак P8-REVIEW принят: числа воспроизведены приёмкой на дереве С лендингом бэкенда (18 пакетов, EXIT=0, скипов 0; регистр 398/96). Ратифицированы норма копии-с-каноном (`ENGINEERING_STANDARDS` §3 п.3), эррата `STACK_DECISIONS` §13 и токен `ОСПОРЕНО(PD-N)` при двусторонней ссылке; статусы трёх спорных строк НЕ пере-открыты. `PD-379` подтверждён чтением, `PD-376` — своей посадкой. Правка гейта формы регистра отклонена замером (краснит 7 законных строк). | ✅ |
| D39.160 | 27.08 | Контрактный минор 0.4.0 → 0.5.0 выдан промтом; **сквозная полоса прогресса (строка 200) ИЗЪЯТА из него словом владельца** и едет с платформенным паком (2в) — гейт версии требует совпадения канона и деплоя в момент лендинга, а сервер прогресс по-новому не считает. Цена «одним куском» обнулена заморозкой фронта. Промт прошёл оба рубежа: 9 находок опровергателя, все применены. | ✅ |
| D39.161 | 27.08 | Контрактный минор **0.5.0** принят и заленджен: отменённая пер-термная модель снесена из канона и компаньона, дверь `POST …/bank/corrections` выведена из словаря `bank-apply` поле в поле, признак «не построено» машиночитаем, счётчики упразднённой модели сняты. Константа платформы поднята тем же коммитом — батарея зоны 18/EXIT=0. Ошибка промта про «поля навсегда нули» найдена исполнителем и вынесена `PD-399`. | ✅ |

View file

@ -1,4 +1,4 @@
# Журнал решений оркестратора — контракт D1D39.160 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
# Журнал решений оркестратора — контракт D1D39.161 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
> **КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Работая с контрактом (греп номера: живой файл → слайсы, целиком НЕ читать — D39.125), держи под рукой, что чем перекрыто:
> ⚠ **Эррата 09.08 (D39.125):** D39.111 п.1 предписывал промту S3 «максимум = баланс МИНУС открытые холды» — формула ОШИБОЧНА (вычитание дважды), исправлена D39.115 п.2(а): максимум = Balance КАК ЕСТЬ; тело D39.111 живёт ниже в этом файле (голова D39.106+).
@ -16,6 +16,7 @@
> ⚠ **Эррата 27.08-г (D39.159 п.8):** отказ от правки `n != shape` СТОИТ, но обоснование было шире истины — «краснит семь ЗАКОННЫХ строк» неверно для двух из них. `PD-99` несла корректно markdown-экранированные черты (виноват был парсер, не строка), а `PD-197` — сырую черту в регексе, которая молча сдвигала ВЕС, потому что он читался с конца, а от конца далеко. Названный там остаточный риск ПОСТРОЕН 27.08 на эмпирике зоны (три молчаливых случая за двое суток): парсер уважает экранирование · вес регистра читается с НАЧАЛА · гейт `tail_vocab` судит словарь хвоста. Проверено подсадкой; числа не сдвинулись. Строка `PD-398` остаётся открытой на отсутствие автоматического пина у самого гейта.
> ⚠ **Эррата 27.08-д (D39.159 п.7) — норма §3 п.8 применена к СОБСТВЕННОМУ бэклогу, три строки переsуждены.** Свип грепом по путям лендинга `d1eb8a9` нашёл, что лекарство трёх строк уже в дереве. **212 ЗАКРЫТА и снята с таблицы:** голый `yaml.Unmarshal` заменён строгим `seed.DecodeFile` (`internal/membank/memseed.go:51`, код называет строку по номеру), пин двойной — `internal/seed/decode_test.go:16` и `internal/membank/seedlint_test.go:39`. **218 ЗАКРЫТА и снята:** три тест-хелпера снесены, `miner_parity_test.go:35` говорит «the old $HOME/books address is dead». **199 СУЖЕНА:** движковая половина цепи доставки правок банка ПОСТРОЕНА, открыт только платформенный конец (пункт 2в очереди) — прежняя формулировка «канал НЕ построен» отправляла бы следующий пак строить построенное. Бэклог 177 → 175. ⚠ Норма окупилась на своём авторе в тот же день, что и на чужом.
> ⚠ **Эррата 27.08-е (D39.159 п.8, финал): `PD-398` ЗАКРЫТА — у построенного гейта появился пин.** `selftest_tail_vocab()` гоняется на каждом `--check`, четыре утверждения, проверен ПОСАДКОЙ трёх мутаций самого гейта (все три пойманы, базовая линия молчит). ⚠ Первая редакция пина молчала на одной из трёх: утверждение про вес проверяло `cells()`, а мутация меняет то, чем пользуется `register()`. Пин, проверенный одним прогоном вместо посадки, — ровно тот класс, который эта строка описывает; поймано только потому, что посадку сделал. Регистр 96 → 95 открытых.
> ⚠ **Эррата 27.08-ж (D39.160 п.2) — ошибка ОРКЕСТРАТОРА, найденная исполнителем пака.** Нота утверждала, что после сноса отменённой двери «канон и деплой СОВПАДУТ точно», а промт минора (§3.2-бис) — что счётчики `pending_decisions`/`complete` «навсегда нули». **Верно по ПУТЯМ, неверно по ПОЛЯМ:** проекция `GET /bank` продолжает их слать, и это не нули — `bankCountsTx` (`platform/internal/pgstore/readmodel.go:330`) считает `proposed`-строки, о чём говорит её собственный комментарий. Клиент 0.5.0 лишние поля игнорирует, но аллоулист-норма нарушена до монтажа (2в). Носитель — `PD-399`. ⚠ Контрактная сессия принесла это ПИНГОМ по §11 промта, вместо того чтобы тихо подогнать работу под неверную посылку; это и есть поведение, которого норма требует.
> ⚠ **Навигация (актуализация 07.08, эра D39.1xx):** append-only-дисциплина (D23.3) означает, что
> НЕВЕРНЫЙ ФАКТ внутри старой ноты не переписывается, а получает эрратау — и тогда он опасен ровно
@ -770,3 +771,38 @@
воспитала бы у экрана подписи отменённую пер-термную модель ТЕМ ЖЕ минором, который её сносит.
6. **Ошибка буквы, исправленная тем же рубежом:** пункт строки 203 про `/auth`**(а)**, не (е);
у строки открыт ещё (к), в пак не входящий.
## D39.161 — КОНТРАКТНЫЙ МИНОР 0.5.0 ПРИНЯТ И ЗАЛЕНДЖЕН: отменённая модель снесена, дверь правок объявлена и выведена из глагола, счётчики сняты (27.08, оркестратор №19). ✅
Пак исполнен по промту `CONTRACT_MINOR_SESSION_PROMPT.md` (D39.160) и принят. Механика — канон,
компаньон и отчёт `docs/CONTRACT_MINOR_REPORT.md`; здесь решённое и то, что добавила приёмка.
1. **Канон 0.5.0 заленджен; константа `platform/internal/httpapi/capabilities.go:13` поднята мной ТЕМ
ЖЕ коммитом**, как обязывала D39.160. Проверено исполнением на копии-с-каноном: до подъёма гейт
краснеет предсказанным текстом («announces contract 0.4.0 and the ratified canon is 0.5.0»), после
подъёма ПОЛНАЯ батарея зоны платформы — **18 пакетов, EXIT=0, линтер 0 issues**. Лендинг чужую
зону красной не оставляет.
2. **Снос полон, пере-проверен приёмкой:** в каноне ноль вхождений пути, операции и трёх схем
отменённой модели; счётчики сняты. Канон парсится, **дублей ключей 0**, висячих `$ref` 0, сирот 0
(две «сироты» — `securitySchemes`, подключаемые блоком `security`, а не ссылкой). В компаньоне три
упоминания — все ИСТОРИЧЕСКИЕ (баннер ломающего минора · провенанс · зачёркнутая строка таблицы,
которую промт велел сохранить как историю).
3. **Дверь `POST /books/{bookId}/bank/corrections` выведена из словаря, а не сочинена** — сверено
приёмкой поле за полем: `BankCorrection` совпадает с `membank.Decision` один в один. Раскладка
отказов сделана по существу: **409 `bank_corrections_refused`** = класс 14 (всё-или-ничего, отказ
всего документа), **503 `bank_corrections_incomplete`** = класс 15 с «слать ТОТ ЖЕ документ» и
названной сходимостью, **409 `run_in_flight`** = класс 12, **413** = потолок 1 МиБ, 5000 — в схеме.
`preview` = `--dry-run`. Отказанные решения едут `refusals[]` в конверте `Problem`, а не списком в
квитанции 200, — следствие всё-или-ничего.
4. **Машиночитаемый признак «не построено» — `Capabilities.bank_corrections_enabled`, false ⇒ 404.**
Требование промта исполнено: дверь объявлена честно, а не голой.
5. **Третий носитель счётчиков нашла САМА сессия**`EventBank`, которого в списке промта не было.
6. **Ошибка ОРКЕСТРАТОРА, найденная исполнителем** (эррата 27.08-ж): «после сноса канон и деплой
совпадут точно» и «поля навсегда нули» верны по ПУТЯМ и неверны по ПОЛЯМ — проекция `GET /bank`
продолжает слать `pending_decisions`/`complete`, и это живой счёт `proposed`-строк. Носитель —
`PD-399`, лечение пунктом (2в). ⚠ Сессия принесла это ПИНГОМ, а не подогнала работу под неверную
посылку промта; это ровно то поведение, которого требует CLAUDE.md.
7. **Строка бэклога 206 ЗАКРЫТА и снята с таблицы** — она и была заказом этого минора («канон объявляет ручку, которой зона больше не отдаёт»); лекарство в дереве, пере-проверено приёмкой грепом: ноль вхождений. Её якорь `openapi.yaml:469` умер именно оттого, что путь снесён. Строка **203** пункт (а) исполнен, открытым по ней остаётся (к). Бэклог 176 → 175.
8. **Щели v1 названы, не спрятаны** (в каноне и компаньоне): машинного словаря причин отказа нет ·
алиасы клиенту невидимы · счёт `signature` недостижим без правки или превью · на 409 не едут
`preexisting`/`signature`, хотя движок их печатает — названная цена, не забывчивость.

View file

@ -51,11 +51,31 @@ cmp-сверка обязательна (D39.138 п.3).
> сверку со стандартами, разобранные альтернативы). При расхождении по ФОРМЕ побеждает YAML;
> при вопросе «почему так» — этот файл.
>
> **Статус: РАТИФИЦИРОВАН.** 0.2.0 (D39.115, 08.08) · 0.2.1 (D39.123, 09.08) · 0.2.2 (D39.129,
> 10.08) · 0.2.3 (D39.135, 15.08) · **0.3.0 (D39.138, 16.08) — ломающий минор по целостному
> ревью research/28** · **0.4.0 (D39.152, 20.08) — синк с платформой, §6в**. Дом канона — этот каталог;
> **Статус: РАТИФИЦИРОВАН по 0.4.0 включительно.** 0.2.0 (D39.115, 08.08) · 0.2.1 (D39.123, 09.08) ·
> 0.2.2 (D39.129, 10.08) · 0.2.3 (D39.135, 15.08) · **0.3.0 (D39.138, 16.08) — ломающий минор по
> целостному ревью research/28** · **0.4.0 (D39.152, 20.08) — синк с платформой, §6в** ·
> **0.5.0 (сдано контрактной сессией 27.08, ратификация — акт оркестратора при лендинге) — снос
> отменённой пер-термной модели подписи (PD-370) + дверь правок банка, выведенная из построенного
> глагола движка (D39.158), + предупреждение о конверте вне `/v0`; вывод и провенанс — §2.19,
> отчёт сессии — `docs/CONTRACT_MINOR_REPORT.md`**. Дом канона — этот каталог;
> `frontend/docs/api-contract/openapi.yaml` — байт-зеркало.
>
> ⚠ **0.5.0 ломающий по построению (мажор `0`): снесены путь `POST …/bank/decisions` и три его
> схемы, из `BankPage` и `EventBank` сняты `pending_decisions`/`complete` (§2.19-бис), в
> `Capabilities.required` добавлен `bank_corrections_enabled`.** Дверь
> `POST …/bank/corrections` объявлена И НЕ ОБСЛУЖИВАЕТСЯ до платформенного пака (2в очереди
> D39.156): деплой честно говорит это флагом `bank_corrections_enabled: false` и отвечает `404`.
>
> ⚠ **Совпадение канона и деплоя при лендинге — точное по ПУТЯМ, не по полям** (находка
> опровергателя этой сессии, сверена с кодом): свою половину ПУТИ платформа снесла 22.08, а
> проекция `GET /bank` ещё кладёт на провод снятые 0.5.0 счётчики (`wireBankPage`,
> `platform/internal/httpapi/reading.go``PendingDecisions`/`Complete`; комментарий «the wire
> fields are the canon's» с этого минора ложен). Клиент 0.5.0 лишние поля игнорирует по общему
> правилу, но аллоулист-норма нарушена, пока пак (2в) не снимет их одной строкой проекции —
> носитель: пункт (2в) очереди D39.156, §2.19-бис. Гейт версии
> (`TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`) этого класса не ловит — он сверяет
> только номер.
>
> ⚠ **0.4.0 ломающий ровно по одному месту, и это ЗАМЕРЕНО, а не объявлено.** Дифф генерённых
> типов 0.3.0 → 0.4.0 (`openapi-typescript@7`, комментарии отброшены) — **одна строка:**
> `Run.stop_requested: boolean`. Всё остальное в этой редакции — проза канона, одна запись в
@ -120,8 +140,9 @@ cmp-сверка обязательна (D39.138 п.3).
получило «No results» — то есть **все 29 находок ревью были смысловыми, ни одной синтаксической**.
А К-11 замерил, что `if`/`then` OpenAPI 3.1 генератор типов ИГНОРИРУЕТ. Отсюда правило,
действующее с 0.3.0: **любое межполевое или условное правило обязано быть записано И схемой, И
словами в описании поля** — схема защищает сервер, слова доезжают до клиента. Мест таких три:
`BankDecision.dst`, `Unit.target`, агрегаты `BankPage`.
словами в описании поля** — схема защищает сервер, слова доезжают до клиента. Мест таких три
(0.5.0 сменил первое: схема `BankDecision` снесена вместе с моделью): `BankCorrection` (`dst` при
`approve`/`decline` и взаимоисключение `id`/кортежа), `Unit.target`, агрегаты `BankPage`.
Инструменты и пины — `STACK_DECISIONS.md` §3 и бэклог зоны.
@ -349,18 +370,23 @@ every proposed term is promoted or rejected» ОПРОКИНУТА D39.144 —
квитанции POST. Экран, перезагруженный посреди стопа, мог прочитать ВЕСЬ банк и не узнать, сколько
решений осталось; единственная реализация выводила признак как `left === 0`
(`frontend/src/mock/handlers.ts:203-204`) — инвариант, которого контракт не объявлял. Теперь
`pending_decisions` и `complete` отвечает и `GET /bank`, и квитанция, и кадр `bank`.
**(0.5.0: клауза счётчиков этого пункта ПЕРЕКРЫТА — прежде чем читать дальше.)** Сняты со
всех трёх носителей; честный счёт едет квитанцией двери правок (`signature`), разбор —
§2.19-бис. Исторический текст 0.3.0: `pending_decisions` и `complete` отвечает и `GET /bank`,
и квитанция, и кадр `bank`.
`POST /runs/{id}/resume` — нормативная операция, а не резерв.
**Открытый остаток, НЕ закрытый батчем и вынесенный вопросом (см. отчёт батча §8).** Чтение банка
отвечает СКОЛЬКО решений осталось (`pending_decisions`, `complete`), но не КАКИЕ строки уже решены:
`TermStatus` — состояние строки банка, а решение живёт отдельной таблицей (`bank_decisions`,
`00002_readmodel.sql`), и на провод оно не проецируется ни одним полем. Экран подписи, перезагруженный
посреди стопа, поэтому знает «осталось 17 из 300» и не знает, какие семнадцать. Лечение — одно
поле `BankTerm.decision` (`approve`/`decline`/`null`), и оно НЕ добавлено: слово владельца 15.08
«добавочные поля `BankTerm` НЕ заводить» (D39.136 п.4б) прямо это запрещает, а промт батча повторяет
запрет. Найдено холодным потребителем; решение — за владельцем.
**Открытый остаток (заведён батчем 0.3.0, пере-привязан 0.5.0 к живым носителям).** Чтение банка
не отвечает, КАКИЕ строки уже решены: `TermStatus` — состояние строки, решения живут файлами
движка (дельта и список отказов — `18-bank-ontology.md`, роли ИСТОЧНИКОВ), и на провод пер-строчная
решённость не проецируется ни одним полем; счётчик «сколько осталось» с 0.5.0 отвечает только
квитанция двери правок (`signature`), не чтение. Экран подписи, перезагруженный посреди стопа,
знает суммарный счёт лишь после первого своего вызова двери и не знает, какие строки решены.
Лечение — одно поле `BankTerm.decision`, и оно НЕ добавлено: слово владельца 15.08 «добавочные поля
`BankTerm` НЕ заводить» (D39.136 п.4б) прямо это запрещает; публикация решённости движком — заказ
на день, когда экран подписи закажут (`18-bank-ontology.md`, «Чего эта форма НЕ несёт»). Решение —
за владельцем.
### 2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
@ -590,7 +616,7 @@ CORS-слоя в платформе нет вовсе: preflight `OPTIONS` с ч
**Форма, выбранная батчем.**
- **`code` — корневой, закрытый, 16 значений; `cause.code` — второй уровень, НЕ закрытый.** Это и
- **`code` — корневой, закрытый, 16 значений (0.3.0; 0.5.0 добавил два банковских — §2.19); `cause.code` — второй уровень, НЕ закрытый.** Это и
есть механизм расширяемости: новый частный случай добавляется в `cause`, не ломая клиентов, — то,
чем Microsoft закрывает «новый код = ломающее изменение». Второй уровень сделан ВЛОЖЕННЫМ объектом,
а не соседним полем: так граница «стабильное / расширяемое» видна структурно, и всё, что внутри
@ -648,6 +674,153 @@ the original request was never applied». Каждый `POST /books` созда
считается по объявленным частям», дофикс ФБ-8 16.08) этой правкой ПЕРЕКРЫТА и оставлена только как
история — читать по канону.
### 2.19. Дверь правок банка (0.5.0) — ✓ выведено из `tmctl bank-apply`; форма HTTP — ◆ этой сессии
Движковая половина ПОСТРОЕНА и заленджена (D39.158, коммит `d1eb8a9`): `tmctl bank-apply` — $0-глагол
со словарём (`backend/internal/membank/decisions.go`), отчётом (`pipeline/bankdecisions.go`,
`BankDecisionsReport`) и полосой отказов 1019 (`cmd/tmctl/main.go:69-75`). Канон 0.5.0 не сочиняет
тело двери — он ПРОЕЦИРУЕТ этот словарь на провод; платформа при монтаже (пак 2в очереди D39.156)
реализует объявленное: контракт-JSON → документ движка → спавн глагола → отчёт → ответ. Перевод
словаря на шве — закон (17-seam-inbound-law п.6), поэтому ниже каждая форма провода названа с её
движковым источником.
**Выведено (✓), с грунтом:**
| Провод | Источник в движке |
|---|---|
| `approve`/`decline`, третьего нет; «un-decide» не существует — решение ЗАМЕНЯЕТСЯ | `decisions.go:33-36,59-63` |
| идентичность: `id` XOR полный кортеж; оба сразу — отказ («назвать два разных терма одним решением») | `decisions.go:88-95,469-472` |
| неизвестный кортеж = ДОБАВЛЕНИЕ терма, не ошибка; неизвестный `id` — отказ с причиной «другая книга или пере-резка» | `decisions.go:94-96` («the form in which a term the bank does not have yet is added»), `:473-477` |
| `dst` обязателен и непуст при `approve`, запрещён при `decline` (вместе с `kind`) | `decisions.go:100-103,459-468` |
| `kind`: отсутствие = «не решено», сбросить в `null` нельзя. ⚠ Провод типизирует его `TermKind` (5 значений — словарь ЧТЕНИЯ, `terminology/classify.go`), движок в этой двери принимает любую непустую строку: сужение провода — ◆, см. список ниже | `decisions.go:104-106,632-634` |
| `note`: принимается, НЕ публикуется; отсутствие = «не решено»; опустошить нельзя, только заменить | `decisions.go:107-115` |
| `aliases` НЕ принимаются; promotion несёт кластер минера дальше; decline алиаса ПОДПИСАННОГО терма — инертен и отказан, судится по РЕЗУЛЬТАТУ документа (алиас неподписанной строки деклайнится законно — `aliasOwner` пропускает `status != approved`; decline терма и его алиаса одним документом — оба принимаются). ⚠ На ПРОВОДЕ `BankTerm` алиасов НЕ публикует (снос — слово владельца, D39.136 п.4б): клиент не видит, что поверхность — алиас; отказ приходит `refusals[].detail`. Названная щель | `decisions.go:78-80`, `termFromBank`, `refuseInertDeclines` (`:399-423`), `aliasOwner` (`:641-659`) |
| `gender`/`speech`/`decl` — полей НЕТ: сид-онли, производителя нет | `decisions.go:81-83` (строка бэклога 210) |
| узость окна v1, ОБЕ половины: approve нового кортежа добавляет строку и оставляет старую; decline снимает ВСЕ окна поверхности | `decisions.go:85-87` |
| сид-терм не правится этой дверью: approve — клэш с подписанной базой, БЕЗУСЛОВНО; decline — отказ ТОЛЬКО ИНЕРТНОМУ (условие `!deltaHoldsSurface`: когда правки книги держат свою строку той же поверхности, decline принимается и снимает её — ремонт ливлока «сид-алиас и строка правок делят firing key») | `decisions.go:357-377` (условие — `:374`; перенос базы — строка 192) |
| `book_id` в теле обязателен И отдельно сверяется с книгой — две разные проверки | `decisions.go:121-124,141-143` и `bankdecisions.go:267-269` |
| неизвестное поле — громкий отказ | `DecodeDecisions`, `DisallowUnknownFields` (`decisions.go:133`) — зеркало 17-seam-inbound-law п.4 |
| всё-или-ничего; один отказ отклоняет набор | `ApplyDecisions` (`decisions.go:282-307`) |
| дубли: один вызов решает терм один раз; decline поверхности против второго решения той же поверхности — противоречие | `duplicateDecisions` (`decisions.go:520-554`) |
| окно, кончающееся раньше начала, — отказ («терм записан и нигде не сработает») | `decisions.go:486-492` |
| идемпотентность: `already_applied` + байтовый no-op (`changed: false`), ретрай безопасен | `decisions.go:221-224`, `bankdecisions.go:209-221` |
| превью прежде мутации | `--dry-run`, `bankdecisions.go:206-208`; закон шва п.7 |
| `depth` — поле отчёта: решение доезжает до редакторской волны и НЕ пере-формирует черновик | `DecisionDepth` (`decisions.go:51-57`), `bankdecisions.go:49-51` |
| `signature{surfaces, undecided, unreadable}` и его запретительный контракт | `SignatureState` (`bankdecisions.go:88-118`), D39.144 |
| потолки: 5000 решений на акт («split it») и 1 МиБ документа | `bankdecisions.go:640-647` (`maxDecisions`), `:637-639` (`maxDecisionsBytes`) |
| `write_incomplete`: документ принят, запись не довершена — слать ТОТ ЖЕ документ | класс 15 (`main.go:74`), ретрай сходится через байтовый no-op |
| занятый арбитр (живой прогон держит флок) — «подожди», не «сломано» | класс 12 (`main.go:71`), `bankdecisions.go:159-163` |
**Решения этой сессии (◆), каждое с доводом; подробный разбор и отвергнутые альтернативы — отчёт
`docs/CONTRACT_MINOR_REPORT.md`:**
- **Один `POST /books/{bookId}/bank/corrections` с документом целиком.** Глагол принимает документ и
отвечает отчётом атомарно; ресурсная модель (PATCH строк) врала бы про атомарность и про то, что
строки банка этим вызовом не меняются.
- **Имя `corrections`, не `decisions` и не `edits`.** Модель владельца: пер-термно существует
ПРАВКА. `edit` — имя волны движка, запрещённое на проводе гейтом §5 (утечка была бы неотличима от
волны грепом); `decisions` вернуло бы имена снесённых схем в сгенерированные типы с другой
семантикой — генерённый диф читался бы как правка старой двери, а не как новая.
- **`preview` — обязательное поле тела** (не query, не второй путь): закон шва п.7 требует проекцию
прежде мутации; обязательность делает выбор акта явным в каждом вызове (прецедент тотальной
обязательности — `stop_requested`, §6в J; в запросах — `RunRequest.stop_for_signing`).
- **Кортеж в проводной форме требует ВСЕ четыре члена** (движок дефолтит опущенные): опущенный
`sense` молча называет ДРУГОЙ ключ — а неизвестный ключ здесь легально ДОБАВЛЯЕТ терм, то есть
цена умолчания — не отказ, а тихая параллельная строка. Проводная форма строже движковой ровно на
ширину этой ловушки; `null`-семантика окна — как у `BankTerm` (проекция `null``0` — платформа,
закон шва п.6).
- **`kind` на проводе сужен до `TermKind`** (движок в этой двери принимает любую непустую строку,
словарь из пяти значений живёт в классификаторе чтения — `terminology/classify.go`): дверь
переиспользует ОПУБЛИКОВАННЫЙ словарь чтения, а не движковую свободу — kind, которого чтение не
знает, нельзя и установить через провод. Цена: сид-авторский kind вне пятёрки через дверь не
повторить (named); рост словаря = минор, тем же правилом, что у самого `TermKind`.
- **Раскладка полосы отказов на провод:** 14 → `409` `bank_corrections_refused` (+`refusals[]`),
15 → `503` `bank_corrections_incomplete` («слать тот же документ»), 12 → `409` `run_in_flight`,
10/11/13 → не пер-операционные (деплой сломан — `5xx` общего вида); движковый исход `stopped`
(SIGTERM до первого байта: ничего не записано) пер-операционного кода тоже не имеет — снаружи
это рестарт деплоя, `5xx`, ретрай сходится; потолок 1 МиБ → `413`, потолок 5000 и вся валидация
формы → `400` `invalid_request`. 14 и 15 получили СВОИ корневые коды: разные адресаты ремеди
(пере-решить человек / повторить машина), совет промта принят.
⚠ **Три обязательства монтажа (2в), которые раскладка создаёт — названы, чтобы не потерялись
(находки опровергателя этой сессии, сверены с кодом):** (а) оба потолка движок классифицирует
КЛАССОМ 14 («the document is well-formed and the deployment is fine», `bankdecisions.go:656-673`),
а канон раскладывает их в `400`/`413` — платформа обязана мерить оба на ПРОВОДНОЙ форме до
спавна; остаточный случай (HTTP-тело < 1 МиБ, отрендеренный документ шва больше) падает в
`409` `bank_corrections_refused`, чьё описание несёт «a set larger than the service applies in
one act»; (б) канонный `400` с `errors[/book_id]` на несовпадение тела с путём производит
ПЛАТФОРМА до спавна — движковая сверка того же факта (`bankdecisions.go:267-269`) есть класс 10,
«конфиг вызывающего», и в `400` сама не раскладывается; (в) свои вызовы двери по одной книге
монтаж СЕРИАЛИЗУЕТ сам: флок движка держит любой глагол, включая второй `bank-apply` и превью
(класс 12 = «another tmctl owns this project», не «книга переводится»), и без сериализации
транзиентный держатель отвечал бы `run_in_flight` — словом про прогон, которого нет. С
сериализацией снаружи остаётся ровно живой прогон, и слово точное.
- **Причины пер-решенческих отказов едут `refusals[].detail` developer-facing и НЕ показываются**
(как `Problem.detail`): у движка причины — свободный текст (`RejectedDecision.Reason`), машинного
словаря причин нет, а замораживать в каноне пересказ — вторая копия растущего словаря. Названная
щель: продуктовые фразы отказов появятся, когда движок даст причинам машинные имена.
- **Что НЕ пошло на провод из отчёта:** пути файлов (`files`) и пофайловая правда записи
(`written_delta`/`written_rejects`) — серверная топология, ремеди клиента от неё не зависит ·
`canonical_rewrite*` — предупреждение оператору файлов, а не пользователю продукта ·
тексты `preexisting_problems` — свободный текст движка; на провод идёт СЧЁТ
(`preexisting_faults`), потому что «книга уже больна, следующий прогон умрёт у банка»
пользователю нужен, а формулировки — нет · `replaced[]` — свободный текст; на провод идёт булев
`displaced` (перекрыл ли ты чьё-то раннее слово — свойство, ради видимости которого поле и
существует) · `mode` — раскладывается на `preview`/`changed`/HTTP-коды; остаток `stopped`
разобран в пункте раскладки выше · `decisions_version` / `report_version` — версии ДОКУМЕНТОВ
ШВА, на проводе их место занимает `contract_version`. ⚠ Отдельно названное УСЕЧЕНИЕ: на отказе
(`409`) движок печатает ПОЛНЫЙ отчёт (с `preexisting` и `signature` — «its report of reasons IS
its product»), а провод несёт только `refusals[]`: конверт `Problem` расширяется членами про
отказ, не квитанцией. Книга, каждый документ которой отказан, своё «уже больна» через дверь не
покажет — названная цена v1, не забывчивость.
- **`depth: edit_wave → refinement`:** движковая константа несёт имя волны — на провод идёт
продуктовое слово с тем же смыслом, открытым словарём; обе половины смысла (следующий прогон;
черновик не пере-формируется) продублированы словами в описании — это первое, о чём экран соврал
бы.
- **`signature` опубликован ВМЕСТЕ с запретительным контрактом** (обе половины D39.144): без запрета
экран подписи при разморозке выучил бы из канона «доведи число до нуля» — отменённую модель через
чёрный ход; без положительной половины поле выглядело бы бесполезным и его бы не строили. `map`
(путь карты) на провод не идёт; «карты ещё нет» выражено `signature: null`.
- **Признак «не построено» — `Capabilities.bank_corrections_enabled`** (прецеденты:
`intake_enabled` — булев с объявленным `404`, `export_formats` — «пусто = не построено»).
`Capabilities` годится: один плоский документ деплоя, одинаковый для всех аккаунтов, читается до
предложения UI. Без признака минор воспроизвёл бы PD-370 тем же коммитом, которым закрывает.
- **Квитанция — НЕ чтение:** ни `revision`, ни `structure_version` не едут — вызов не двигает
read-модель (вид банка пересобирается на границе прогона, `18-bank-ontology.md`), кадр `bank` не
испускается, и канон говорит это прямо, чтобы «исправил, а банк не изменился» читалось как
корректность, а не как баг.
### 2.19-бис. Счётчики `pending_decisions`/`complete` — СНЕСЕНЫ (0.5.0)
Оба поля кормились платформенной таблицей `bank_decisions`, чей write-путь снесён 22.08 вместе с
пер-термной моделью: новая дверь пишет файлы движка, не эту таблицу. ⚠ **«Поля навсегда нули»
(буква промта пака) опровергнута кодом при вычитке опровергателем — на деле ХУЖЕ нулей:**
`pending_decisions` считается как «proposed-строки, которых не коснулось ни одно решение», а
касаться нечем — то есть это число ВСЕХ proposed-строк банка, живое на каждой пересборке, и
`complete` вырождается в «предложений нет вовсе» (`platform/internal/pgstore/readmodel.go`,
`bankCountsTx` — комментарий признаёт это прямо). Замороженно-правдоподобное число учит
отменённой модели живым счётчиком. Из трёх исходов (снести · пере-определить на новую дверь ·
пометить) выбран СНОС со всех трёх носителей (`BankPage`, `EventBank`, квитанция — вместе со
схемой):
- **пере-определить нельзя**: честный счёт нерешённости — движковый (`SignatureState`: карта
последнего стопа против файлов решений); read-модель платформы его НЕ вычислит, не пере-реализовав
движковый закон у себя, что запрещено (17-seam-inbound-law п.6; `18-bank-ontology.md`, «Чего эта
форма НЕ несёт»);
- **пометить («пока нули») нельзя**: поле, обязательное в схеме и вечно лгущее нулём, — это ровно
класс PD-370 («канон объявляет — деплой не обслуживает»), только в поле вместо пути;
- ратифицированная проза «Signing the bank is ONE act over the whole of it» при сносе СОХРАНЕНА в
описании `listBankTerms`; фраза «marked unverified inside the service» из того же абзаца снята —
это обещание без носителя, снятое ещё ФБ-8 (§6б) и уцелевшее в одном месте.
Цена, названная честно — ТРЕМЯ половинами: (1) чтение банка больше не отвечает «сколько
осталось» — экран узнаёт счёт из квитанции двери правок (`signature`), то есть только имея что
послать или что превьюировать; (2) **до монтажа пака (2в) счёт недоступен НИГДЕ** — дверь не
обслуживается, чтение не отвечает: это названная цена окна между минорами, а не пробел; (3) до
того же монтажа проекция `GET /bank` деплоя ещё шлёт снятые поля (см. ⚠ шапки — лишние поля,
клиент их игнорирует; снимает монтаж). Возврат счёта в чтение — день, когда экран подписи закажут
и движок опубликует нерешённость проекцией (`18-bank-ontology.md`, «Чего эта форма НЕ несёт»);
сегодняшние носители лгать не будут.
---
## 3. Зависимости: чтение → источник → строка бэклога
@ -670,8 +843,9 @@ the original request was never applied». Каждый `POST /books` созда
| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
| `GET /books/{id}/chapters`, `/units` | материализация манифеста | **ПОСТРОЕНО P7**`httpapi/reading.go`, материализатор `internal/readmodel` | закрыто D39.153 |
| `GET /books/{id}/notes` | `unit_done` несёт флаг и причину (`runevents.go:126-135`), платформа хранит (`sink.go:227-233`), колонка `notes.reason` заведена под это | канал ЕСТЬ; не хватает карты «причина → код → фраза» (приложение А) и проекции | приложение А + вход P7 |
| `GET /books/{id}/bank` | движок пишет сайдкар всего банка (`pipeline/bankexport.go:16-33,72`, D39.122) | движковая половина ПОСТРОЕНА; не хватает проекции платформы | строка 169 · вход P7 |
| ~~`POST /bank/decisions`~~ | стоп-механика майнера | ⚠ **НЕ «не построено», а ОТМЕНЕНО**: было построено P7 и СНЯТО 22.08 вместе с пер-термной моделью подписи (D39.144, слово владельца). Канон путь ещё держит — это `PD-370` и строка бэклога **206**, ломающий минор 0.5.0 | отменено D39.144 |
| `GET /books/{id}/bank` | движок пишет сайдкар всего банка (`pipeline/bankexport.go:16-33,72`, D39.122) | ⚠ пере-снято 0.5.0: проекция платформы ПОСТРОЕНА (P7 — маршрут в `contractSurface`, `wireBankPage` в `httpapi/reading.go`, `SaveBank` в `pgstore`; прежняя запись «не хватает проекции» устарела при израсходованном носителе «вход P7», D39.153); живой дефект другой — проекция несёт форму 0.4.0 со снесёнными счётчиками, см. ⚠ шапки | пункт (2в) очереди D39.156 — снять счётчики при монтаже |
| ~~`POST /bank/decisions`~~ | стоп-механика майнера | ⚠ **НЕ «не построено», а ОТМЕНЕНО**: было построено P7 и СНЯТО 22.08 вместе с пер-термной моделью подписи (D39.144, слово владельца). **0.5.0 снёс и канон-половину — `PD-370` закрыт этим минором**; преемник — строка `POST …/bank/corrections` ниже | отменено D39.144; снесено 0.5.0 |
| `POST /books/{bookId}/bank/corrections` | `tmctl bank-apply` — движковая половина ПОСТРОЕНА (D39.158, лендинг `d1eb8a9`) | **объявлено 0.5.0, платформой НЕ обслуживается**: деплой говорит это `Capabilities.bank_corrections_enabled: false` и отвечает `404`; монтаж (перевод словаря, спавн глагола, раскладка отказов) — платформенный пак | **пункт (2в) очереди D39.156** |
| `GET /books/{id}/events` (SSE) | эмиттер шва построен (D39.131) | **ПОСТРОЕНО P7**`httpapi/stream.go`, поток регистрируется ВНЕ слоя сжатия (сжатие буферизует поток — единственное, что канон запрещает этому маршруту) | закрыто D39.153 |
| `POST`/`GET /exports` | у движка только stdout-JSON и `--plaintext` (`cmd/tmctl/invocation.go:107`) | НЕ ПОСТРОЕНО с обеих сторон | строка 49 / D29.1 «tmctl export-контракт» |
| Условные чтения (`ETag`/304), сжатие | — | **ПОСТРОЕНО P7**`httpapi/conditional.go`; валидатор считается от БАЙТ ответа. Остаток строки 186 — шаги 35 (скоуп кадра, дельта-чтение, `staleTime`) | шаги 12 закрыты D39.153 |
@ -682,7 +856,7 @@ the original request was never applied». Каждый `POST /books` созда
| Настоящие названия глав (`Chapter.heading` ≠ null) | парсер структуры | НЕ ПОСТРОЕНО | строка 160 (Этап 0) |
| `title_raw` / `kind` (глава ↔ фрагмент) | дизайн-пак структуры глав | передано паку, аддитивно | строка 161 |
| `ErrorCode.content_refused` (400) **и** `RejectReason.content_refused` | прескрин злоупотреблений | НЕ ПОСТРОЕН ни на одной стороне: в платформе только объявление константы (`httpapi/problem.go:42,94`), `ContractRejectReason` (`ingest/vocabulary.go:59-69`) его не отображает; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ (`disposition.go:60-63``Note.code: content_withheld`) и в exit-контракт не выходит — мостá между двумя словарями нет | **строка 94 (ПТ-16)**; там же ограничение числа попыток аккаунта — обязанность падает ВМЕСТЕ с производителем, не раньше |
| `decline` в подписи банка доезжает до работы | движок читает файл `mined_rejects` (`pipeline/mining.go`), платформа его не пишет | решение ЗАПИСЫВАЕТСЯ (таблица `bank_decisions`) и на следующем прогоне НЕ применяется — отклонённый термин уезжает авто-строкой | **строка 192** (отложена владельцем); канон предупреждает на `BankDecision.action` |
| `decline` в подписи банка доезжает до работы | ⚠ пере-снято 0.5.0: старый носитель («таблица `bank_decisions`, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на `BankDecision.action` снесено вместе со схемой | новая дверь пишет ФАЙЛЫ ДВИЖКА (`tmctl bank-apply`), decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ — как только платформа смонтирует дверь; до монтажа двери нет вовсе (флаг `bank_corrections_enabled`) | **пункт (2в) очереди D39.156** |
| Снятие замечания (переход «флаг снят») | движок | **НЕДОСТИЖИМО сегодня, проверено чтением движка** — п. H §6в | **PD-298** регистра платформы (`platform/docs/DEFECT_REGISTER.md`) — там строка и живёт; механизма не строим |
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | **вход P7** (PD-202) |
@ -721,7 +895,7 @@ the original request was never applied». Каждый `POST /books` созда
| К-8 | Стоп по потолку — каким статусом | **✅ ЗАКРЫТ D39.100** (`paused` + оповещение) |
| К-9 | Отказ прескрина не выразим статусами | **✅ ПРИНЯТО ВЛАДЕЛЬЦЕМ 16.08:** прескрин — ещё одна ПРИЧИНА, а не двенадцатый статус: `rejected` + ОДИН грубый код (`content_refused`), максимально абстрактно, без вариации между попытками (§8а). Заведено 0.3.0 |
| К-10 | Пофазность у главы | **✅ ЗАКРЫТ — вердикт «НЕ строить»** (D39.138, поправка приёмки research/28 №1). Пофазных счётчиков на главу не будет: фаз на проводе нет. Исходная жалоба («дерево читает ноль всю первую волну») лечится СЕГМЕНТНОЙ логикой `Chapter.units_done` — §2.5 |
| К-11 | Условная обязательность полей | **ОСТАТОК ИНСТРУМЕНТАЛЬНЫЙ.** 0.3.0 применил правило «схема + слова» к трём местам (`BankDecision.dst`, `Unit.target`, агрегаты `BankPage`) и сделал адресацию `Note` обязательной. Остаток — генератор игнорирует `if`/`then`; лечится сужением на шве клиента (S5) |
| К-11 | Условная обязательность полей | **ОСТАТОК ИНСТРУМЕНТАЛЬНЫЙ.** 0.3.0 применил правило «схема + слова» к трём местам (`BankDecision.dst`, `Unit.target`, агрегаты `BankPage`; 0.5.0: место `BankDecision.dst` унаследовала `BankCorrection` — там теперь `if`/`then`/`else` и `oneOf` идентичности, все продублированы словами) и сделал адресацию `Note` обязательной. Остаток — генератор игнорирует `if`/`then`; лечится сужением на шве клиента (S5) |
| К-12 | Завершение выгрузки: опрос или событие | **✅ ОТВЕЧЕН P0 (опрос)**, подтверждён AIP-151 («The response must not be a streaming response»). 0.3.0 добавил то, без чего опрос не завершался: `state` вместо булева `ready`, `failure_code`, `expires_at` |
| К-13 | `paused_reason` не различает две беды | **✅ ЗАКРЫТ D39.132 п.2а** (`null` на проводе ратифицирован). 0.3.0 добил остаток: описание требовало «`null` in every other state», что противоречило ратифицированному «`paused` + `null`» |
@ -819,7 +993,8 @@ the original request was never applied». Каждый `POST /books` созда
— то есть дефект «замечание, не адресующее ничего» закрыт.
- **`Bank.signed`** («доезжает до клиента и не рисуется»). Тот же довод «нет экрана» — экран подписи
это S5. `total`, `signed` и `pending_decisions` — три НЕЗАВИСИМЫХ факта: строку можно решить и не
подписать (отклонить), поэтому `signed` не выводится из двух других.
подписать (отклонить), поэтому `signed` не выводится из двух других. *(0.5.0: `pending_decisions`
снесён — §2.19-бис; довод о независимости `signed` стоит и без него.)*
- **нагрузка кадров `note` и `bank`** («передаётся и игнорируется»). Игнорировалась она по причине,
которую батч устранил: у замечания не было id, поэтому кадр нельзя было сопоставить со списком.
С `Note.id` кадр `note` несёт ПРИМЕНИМУЮ ДЕЛЬТУ — это ровно первая ветка правила Б-11а, и снятие
@ -1173,6 +1348,10 @@ announce-once-леджер: ключ `unit:<book>:<wave>:<chapter>:<unit>` не
предупреждением на `BankDecision.action` + строкой §3 с носителем — строка 192, отложенная
владельцем. Форму не меняю: обещание верное, не выполнена реализация.
⚠ *(0.5.0: предупреждение умерло вместе со своим носителем — схема `BankDecision` снесена, а дверь-
преемник пишет файлы движка сама, так что «не доезжает» перестало быть свойством формы; остаток —
монтаж, пункт (2в) очереди D39.156. Абзац сохранён как история синка 20.08.)*
### L. Мелкое
- **Ссылка приложения А протухла** — ИСПРАВЛЕНО, и не новым номером, а именем функции
@ -1399,9 +1578,11 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ
| `run_in_flight` | 409 | `pgstore.ErrRunInFlight` (`v0.go:548`) |
| `book_not_ready` | 409 | `runs.ErrBookNotReady` (`v0.go:550`) |
| `run_not_stoppable` | 409 | `runs.ErrNotStoppable` (`v0.go:554`) |
| `run_not_resumable` | 409 | `runs.ErrNotResumable`; `cause`: `ceiling_reached`**с 0.4.0 это же ответ на ИСЧЕРПАННЫЙ прогон в `stopped`/`awaiting_bank`, где платформа сегодня молча отвечает `202`** (A, PD-282: `runs/reconcile.go`, ветка `exhausted` функции `reopen` и её чтение в `Resume`). ⚠ `bank_decisions_incomplete` удалён D39.144 — гейта полноты нет; построенный гейт полноты (`reconcile.go`, ветка `case "awaiting_bank"`) ДЕМОНТИРУЕТСЯ в P7 |
| `run_not_resumable` | 409 | `runs.ErrNotResumable`; `cause`: `ceiling_reached`**с 0.4.0 это же ответ на ИСЧЕРПАННЫЙ прогон в `stopped`/`awaiting_bank`, где платформа сегодня молча отвечает `202`** (A, PD-282: `runs/reconcile.go`, ветка `exhausted` функции `reopen` и её чтение в `Resume`). ⚠ `bank_decisions_incomplete` удалён D39.144 — гейта полноты нет; построенный гейт полноты ДЕМОНТИРОВАН (0.5.0, сверено с кодом: `reconcile.go`, ветка `case "awaiting_bank"` снимает стоп при ЛЮБОМ состоянии решений, комментарий «this is the built half being dismantled with it») |
| `ceiling_unavailable` | 409 | `runs.ErrCeilingOutOfBounds` + `pgstore.ErrInsufficientCredit` (`v0.go:558`); `cause`: `bounds_moved` · `credit_held`; несёт `blocked` |
| `idempotency_conflict` | 409 | форма заведена батчем; реализация — P7 |
| `bank_corrections_refused` | 409 | объявлен 0.5.0 ДО производителя; производитель — монтаж двери правок (пак (2в) очереди D39.156): раскладка отказа класса 14 движка (`tmctl bank-apply`, документ прочитан и отклонён целиком — пользователь пере-решает) |
| `bank_corrections_incomplete` | 503 | объявлен 0.5.0 ДО производителя; тот же монтаж: раскладка класса 15 (документ принят, запись не довершена — слать ТОТ ЖЕ документ, ретрай сходится) |
| `content_refused` | 400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием `rejected` + `reject_reason` |
| `service_unavailable` | 503 | `runner.ErrCeilingNotWired` + `runs.ErrRunnerIncomplete` (`v0.go:562`) |
| `internal_error` | 500 | `v0.go:518,572` · `middleware.go:68` |

View file

@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: TextMachine API
version: 0.4.0
version: 0.5.0
summary: Ratified contract between the frontend and the TextMachine platform.
description: |
**RATIFIED contract.** Canonical copy: `docs/architecture/14-api-contract/`;
@ -36,6 +36,12 @@ info:
**Signing in is not part of this surface**: session mechanics live outside the version prefix and
the flow starts at `GET /auth/login` (companion). A client that meets `401` sends the user there.
⚠ **Outside the version prefix the error envelope is thinner.** A refusal from `/auth/*` (or any
path not under `/v0`) is `problem+json` of the same family but MAY arrive without the mandatory
`code` — there a client dispatches on the HTTP status and shows one neutral phrase, and a client
generated from this document MUST tolerate the absence rather than fail parsing. The surface and
the reasoning live in the companion, §2.14.
## `Location`
Every `Location` here is a URI reference resolved against the request's URL (RFC 9110 §10.2.2);
@ -124,7 +130,7 @@ tags:
- name: reading
description: Chapters, source/translation pairs, notes.
- name: bank
description: Memory bank and term signing.
description: Memory bank — reading it, correcting terms, signing it as one act.
- name: runs
description: Translation runs, live events, control.
- name: export
@ -438,14 +444,17 @@ paths:
The bank ordered by source surface then by the term's window, so the several rows of one
surface stand together.
**This read is also the STATE of a signing stop** — informationally, never as a gate:
`pending_decisions` and `complete` say how much of the bank a person has touched, and a
screen reloaded mid-stop can show it. **Signing the bank is ONE act over the whole of it**,
not a march through every row: the stop is lifted by `resumeRun` with the decisions as they
stand, and a term nobody touched rides on as the service proposed it, marked unverified
inside the service. Per-term decisions are the OPTIONAL correction path, not the unit of
**Signing the bank is ONE act over the whole of it**, not a march through every row: the
stop is lifted by `resumeRun` with the corrections as they stand, and a term nobody touched
rides on as the service proposed it. Per-term corrections
(`POST /books/{bookId}/bank/corrections`) are the OPTIONAL correction path, not the unit of
signing.
**How many surfaces still await a word is NOT this read's answer.** This deployment cannot
count it honestly from the rows it serves; the honest count rides the correction receipt
(`signature` on `BankCorrectionsReceipt`), with the warning attached there — and only
there: a client with nothing to correct or preview cannot ask for it yet.
**Delta read** with `after_version` — a full book's bank is too large to re-read on every
change. Page size default: `GET /capabilities`.
parameters:
@ -466,43 +475,69 @@ paths:
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/books/{bookId}/bank/decisions:
/books/{bookId}/bank/corrections:
parameters:
- $ref: '#/components/parameters/BookId'
post:
tags: [bank]
operationId: submitBankDecisions
summary: Submit term decisions.
operationId: applyBankCorrections
summary: Apply term corrections to the memory bank.
description: |
**Signing is not a row edit**: the bank is rebuilt from its inputs on every run, so a direct
write would be erased. A decision is `approve` (with a translation) or `decline`, keyed by
term, so re-sending one is harmless.
The per-term half of the bank model: signing is ONE act over the whole bank (`resumeRun`),
and what exists per term is a CORRECTION — `approve` a rendering, or `decline` a surface so
it stops being proposed. The whole document is applied as one act, **all or nothing**: one
refused correction refuses the set (`409`, `code: bank_corrections_refused`), because the
refused one is the one the user has to see, and a partial save would hide it behind work
that appears saved.
Submission is PARTIAL and accumulates on the server — a closed tab must not cost an hour of
work.
**Preview first.** Every request says whether it is a preview: `"preview": true` answers
the same receipt without changing anything. A client offers the preview before the save —
this surface says what it will do before doing it.
**A `term_id` this book's bank does not hold** — a stale row from before a rebuild, or one
belonging to another book — refuses the WHOLE call: `400`, `code: invalid_request`, with an
`errors[]` entry pointing at the item (`/decisions/2/term_id`, item code `unknown`). Refusing
the batch rather than skipping the row is deliberate: a silently dropped decision reads on
the screen as a decision that was saved.
**The receipt is not a read.** A correction takes effect on the NEXT run; the bank rows
this surface serves (`GET /books/{bookId}/bank`) do not change until a run next rebuilds
the bank, no `bank` frame fires, and a client re-reading the bank right after a correction
sees it unchanged — correct behaviour, not staleness. What was recorded is in the receipt.
**While the book is being translated the bank cannot be corrected**: `409`,
`code: run_in_flight`. The service reads its own inputs mid-run, and a correction landing
under a live run would enter it unpredictably. Wait for the stop or the end.
**Two ceilings, both hard:** at most **5000** corrections in one act (bound in the schema;
a larger set is refused whole — split the document), and at most **1 MiB** of request
document (`413`, `code: payload_too_large`).
**A failed write answers `503`, `code: bank_corrections_incomplete`: the document was
ACCEPTED and did not land whole. The remedy is to re-send the SAME document** — the retry
converges: what already landed is recognised, not duplicated, and a fully-landed document
answers `200` with `changed: false`.
⚠ **Declared ahead of its serving half.** A deployment that has not mounted this door says
so — `Capabilities.bank_corrections_enabled: false` — and answers `404` here. A client
checks the flag before offering the correction UI rather than discovering the absence by
failing a user's save.
parameters:
- $ref: '#/components/parameters/ClientHeader'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BankDecisionsRequest' }
schema: { $ref: '#/components/schemas/BankCorrectionsRequest' }
responses:
'200':
description: Decisions accepted; the response carries what is left.
description: |
The receipt — for a preview, what WOULD happen; otherwise what happened. `200` means
the whole document was accepted and, unless a preview, landed durably.
content:
application/json:
schema: { $ref: '#/components/schemas/BankDecisionsResult' }
schema: { $ref: '#/components/schemas/BankCorrectionsReceipt' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/Conflict' }
'413': { $ref: '#/components/responses/TooLarge' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/books/{bookId}/run-options:
parameters:
@ -1097,7 +1132,8 @@ components:
schema: { $ref: '#/components/schemas/Problem' }
TooLarge:
description: |
File over the intake cap (`intake_max_bytes` of `GET /capabilities`).
Body over the bound the operation declares: the intake cap (`intake_max_bytes` of
`GET /capabilities`) on the intake, 1 MiB on bank corrections.
headers:
X-Request-Id: { $ref: '#/components/headers/RequestId' }
content:
@ -1115,7 +1151,9 @@ components:
ServiceUnavailable:
description: |
The deployment cannot do this right now: starting or continuing a run needs its machinery
fully configured. Temporary — retry later; no `Retry-After` is promised.
fully configured. Temporary — retry later; no `Retry-After` is promised. On bank
corrections it can instead carry `bank_corrections_incomplete`, whose remedy is specific:
re-send the SAME document. Dispatch on `code`, as everywhere.
headers:
X-Request-Id: { $ref: '#/components/headers/RequestId' }
content:
@ -1220,6 +1258,7 @@ components:
- intake_enabled
- intake_max_bytes
- export_formats
- bank_corrections_enabled
- page_size_default
properties:
contract_version:
@ -1228,7 +1267,7 @@ components:
The version this deployment serves — the only place a non-streaming client learns it. A
client generated against a different one REFUSES to work and says so: while the major is
`0` a differing minor carries breaking changes by design.
examples: ['0.4.0']
examples: ['0.5.0']
language_pairs:
type: array
description: |
@ -1252,6 +1291,13 @@ components:
description: |
Formats `POST /books/{bookId}/exports` accepts. Empty means none are built here.
items: { type: string, minLength: 1 }
bank_corrections_enabled:
type: boolean
description: |
Whether this deployment serves `POST /books/{bookId}/bank/corrections`. `false` means
the door is declared by this contract and not mounted here: the path answers `404`,
and a client does not offer the correction UI. Machine-readable so a client learns it
here — not by failing a user's save against a promised door.
page_size_default:
type: integer
minimum: 1
@ -1827,8 +1873,8 @@ components:
boolean would merge "proposed, nobody has looked" with "a person started and did not finish"
— on a screen of hundreds of rows that is the main filter of work.
This is the state of the ROW. `POST /books/{bookId}/bank/decisions` does not set it: a
decision is recorded against the row and its status follows on the next rebuild.
This is the state of the ROW. `POST /books/{bookId}/bank/corrections` does not set it
directly: a correction is recorded and the status follows on the next rebuild.
enum: [proposed, in_progress, approved]
TermOrigin:
@ -1852,8 +1898,9 @@ components:
id:
$ref: '#/components/schemas/Id'
description: |
Identity of the row, derived from the term itself — its surfaces, sense and window — so
a decision against it survives the bank being rebuilt.
Identity of the row, derived from the term itself — its SOURCE surface, sense and
window; not from `dst`, so a corrected rendering keeps the id — and a decision
against it survives the bank being rebuilt.
⚠ It does NOT survive the book being cut differently: the window is in chapter numbers,
those move with a re-cut, and an identity derived from them moves too. A client that sees
@ -1870,8 +1917,9 @@ components:
- $ref: '#/components/schemas/TermKind'
- type: 'null'
description: |
`null` when the kind could not be decided — legal, and the row still needs signing. A
client MUST show it as "kind not decided" and MUST NOT drop it or invent a kind.
`null` when the kind could not be decided — legal; the row stays and stays
correctable. A client MUST show it as "kind not decided" and MUST NOT drop it or
invent a kind.
status: { $ref: '#/components/schemas/TermStatus' }
origin: { $ref: '#/components/schemas/TermOrigin' }
sense:
@ -1919,29 +1967,70 @@ components:
type: integer
minimum: 0
description: |
Rows in status `approved` in the whole bank. Distinct from the counters below: a row
can be decided and NOT signed, because declining is also a decision.
pending_decisions:
type: integer
minimum: 0
description: |
How many proposed terms still have no decision.
complete:
type: boolean
description: |
The set is complete. INFORMATIONAL, never a gate: signing is one act over the whole
bank, and `resume` lifts the stop with the decisions as they stand. A client may
show "N of M decided"; it never disables the continue action on this.
Rows in status `approved` in the whole bank. Not a count of decisions: a row can be
corrected and NOT signed, because declining is also a correction. How many surfaces
still await a word is not derivable from these rows at all — see the note on the
operation.
terms:
type: array
items: { $ref: '#/components/schemas/BankTerm' }
BankDecision:
BankCorrection:
type: object
additionalProperties: false
description: |
A decision on one proposed term. `dst` is mandatory and non-empty for `approve`: a signed
term with an empty translation matches nothing yet reads as an intended rendering.
required: [term_id, action]
One correction of one term: `approve` a rendering, or `decline` a surface. It names its
term EITHER by `id` — a row of the bank read as it stands — OR by the full
(`src`, `sense`, `since_chapter`, `until_chapter`) tuple that id is derived from; never by
both, because both could name two different terms and the caller would never learn which
one was taken. Stated in the schema AND in words, because a generator ignores the
constraint forms.
**A tuple the bank does not hold is NOT an error — it is how a term is ADDED.** Approving
an unknown tuple writes a new term. Only an unknown `id` refuses — the whole set, `409`
`code: bank_corrections_refused`, the entry named in `refusals[]`: an id comes from a
read, so an unknown one means the read was of another book or predates a re-cut of this
one — the id is derived from the chapter window, and windows move with a re-cut.
**A member this schema does not declare refuses the request** (`400`,
`code: invalid_request`): an unknown member is a caller believing it set something, and the
quiet version of that is a correction half-applied.
**What is deliberately NOT correctable here:**
- a term that came WITH the book (`origin: given`): APPROVING its key is refused —
changing what the book was given re-buys the translation from the start, a different
operation this contract does not offer. DECLINING such a surface is refused only when
it would change nothing; when a recorded approval of this book holds that same surface,
the decline is accepted and removes it — that is the repair for exactly that clash;
- a term's chapter WINDOW in one act: approving a new tuple ADDS a second row and leaves
the old one standing, and declining a surface removes EVERY window of that surface.
Moving or widening a window is a two-call operation — decline, then approve;
- a surface that, once this document is applied, would remain only an ALIAS an approved
correction still fires for: declining it alone is refused as changing nothing — decline
the owning term instead. (The alias set itself is not published on this surface; the
refusal names the owner in its developer-facing `detail`.);
- a `note` cannot be emptied, and a `kind` cannot be cleared back to `null` (see the two
fields).
One call decides a term once. A `decline` names a whole SURFACE: the `sense` and window
of its tuple say which row the receipt reports and do NOT narrow the effect — every
window of the surface goes (for a surface the bank does not list, send `sense: ""` and
`null` windows). So a decline and any second correction of the same surface in one
document contradict each other and are refused; several `approve`s of one surface in
different senses or windows are legitimately several terms.
required: [action]
oneOf:
- title: an existing row, by id
required: [id]
not:
anyOf:
- required: [src]
- required: [sense]
- required: [since_chapter]
- required: [until_chapter]
- title: a term by its full key — the form that can also add one
required: [src, sense, since_chapter, until_chapter]
not: { required: [id] }
if:
properties:
action: { const: approve }
@ -1950,62 +2039,241 @@ components:
required: [dst]
properties:
dst: { minLength: 1 }
else:
not:
anyOf:
- required: [dst]
- required: [kind]
properties:
term_id: { $ref: '#/components/schemas/Id' }
action:
type: string
enum: [approve, decline]
description: |
`approve` — take the term into the book (with a translation in `dst`); `decline` — leave
it out.
⚠ **NOTHING recorded here reaches the work itself yet** — not `decline`, not `approve`,
not the translation typed into `dst`. A deployment stores the decisions and answers the
receipt; the producer of the bank reads its own files and this call does not write them.
So a term left out here can come back proposed on the next run, and a corrected
translation does not change the text. Both halves are read from `BankPage` and neither
is guessed; the companion carries who owes the missing half.
This is a TEMPORARY property of the deployment and not a promise of the contract: the
call exists so the correction has somewhere to go the day the delivery is built, and a
client may show what it recorded. What DOES take effect today is the signature itself —
one `resume` over the whole bank, whatever state the decisions are in.
`approve` — take the term into the book with the rendering in `dst`; it also lifts a
previous decline of the surface. `decline` — leave the surface out and stop it being
proposed again; it also removes every previously approved row of that surface. There is
no "un-decide": a correction is REPLACED by the opposite one, never withdrawn, and the
receipt says when one displaced an earlier word.
id:
$ref: '#/components/schemas/Id'
description: |
The bank row's `id`, exactly as the bank read published it. Mutually exclusive with
the tuple.
src:
type: string
minLength: 1
description: Source surface of the term, as `BankTerm.src`.
sense:
type: string
description: |
Polysemy disambiguator, as `BankTerm.sense`: required in the tuple form, and the EMPTY
STRING means "no disambiguator". Required precisely so a caller cannot omit it and
silently name a DIFFERENT term than it meant: the tuple is the term's whole key, and a
partial key is another key.
since_chapter:
type: [integer, 'null']
minimum: 1
description: |
First chapter of the window, `null` for "from the beginning" — the same coordinates
and the same `null` as `BankTerm.since_chapter`; required in the tuple form for the
same reason as `sense`. A window that ends before it begins is refused: such a term
would be recorded and then apply nowhere.
until_chapter:
type: [integer, 'null']
minimum: 1
description: Last chapter of the window, `null` for "to the end". As `since_chapter`.
dst:
type: string
description: |
Translation. **Required and non-empty when `action` is `approve`** — in words as well as
in the constraint above, because a generator ignores `if`/`then`. ⚠ See the caveat on
`action`: today this text is recorded and not applied.
BankDecisionsRequest:
type: object
required: [decisions]
properties:
decisions:
type: array
minItems: 1
maxItems: 1000
The rendering. **Required and non-empty when `action` is `approve`; forbidden on
`decline`** — in words as well as in the constraints above, because a generator
ignores them. An approved term with no rendering is not a weak approval — it would
fail the next run; and a rendering on a decline says the caller meant to approve, and
half of that is not something to guess at.
kind:
$ref: '#/components/schemas/TermKind'
description: |
Decisions to record, bounded like every other collection here.
items: { $ref: '#/components/schemas/BankDecision' }
Reclassify the term. ABSENT means "not decided" and keeps whatever the term already
carries — including a `kind` of `null`: there is no way to clear a kind back to `null`
through this surface. Forbidden on `decline`, like `dst`.
note:
type: string
description: |
The user's reason, recorded with the correction and NOT published: it never appears in
`BankTerm` or anywhere else on this surface. ABSENT means "not decided" and keeps the
note recorded earlier — so a note cannot be EMPTIED here, only replaced with other
words. Named rather than hidden, because dropping the earlier reasoning by omission is
the mistake a caller makes by accident. Legal on both actions.
BankDecisionsResult:
BankCorrectionsRequest:
type: object
additionalProperties: false
description: |
Receipt of a submission: the same facts the bank read answers, so a client updates its
screen without a second call.
required: [revision, pending_decisions, complete]
The correction document, applied as ONE act, all or nothing. An undeclared member refuses
the request — see `BankCorrection`.
required: [book_id, preview, corrections]
properties:
revision: { $ref: '#/components/schemas/Revision' }
pending_decisions:
type: integer
minimum: 0
description: How many proposed terms still have no decision.
complete:
book_id:
$ref: '#/components/schemas/Id'
description: |
The book these corrections were computed FOR, checked against the book in the path; a
mismatch is `400` `invalid_request` with an `errors[]` entry at `/book_id`.
Deliberately a second carrier of the same fact: corrections carry a user's own words
into a book's canon, and a set computed for one book landing in another is not a
mistake anything downstream could notice.
preview:
type: boolean
description: |
The set is complete. Informational, never a gate — `resume` lifts the stop with the
decisions as they stand.
`true` — answer the receipt and change NOTHING; `false` — apply. Required, not
defaulted: which of the two acts this call is must be said, not implied.
corrections:
type: array
minItems: 1
maxItems: 5000
description: |
At most 5000 in one act — a hard ceiling, not advice: a larger set is refused whole.
Split a larger document and send the parts in turn.
items: { $ref: '#/components/schemas/BankCorrection' }
BankCorrectionsReceipt:
type: object
description: |
The answer of a correction call — the whole of it, for a preview and for an apply alike;
`preview` says which of the two this receipt is. It is NOT a read of the bank: the rows
`GET /books/{bookId}/bank` serves change only when a run next rebuilds the bank.
required: [preview, changed, depth, accepted, preexisting_faults, signature]
properties:
preview:
type: boolean
description: Echo of the request. A preview's receipt promises; an apply's reports.
changed:
type: boolean
description: |
Whether this call changed the recorded corrections (for a preview: whether applying
would). `false` on an apply means the document was ALREADY fully applied — the normal
answer to a retry, and nothing was written again.
depth:
type: string
description: |
How far an accepted correction reaches. `refinement` — it is applied when a run next
refines the text: the translation already produced is corrected in later passes rather
than re-translated from scratch, and nothing changes until a run happens. A client
renders its own words from this value, MUST NOT promise a fresh re-translation, and
treats an unknown value neutrally — this vocabulary can grow.
examples: ['refinement']
accepted:
type: array
description: One entry per correction of the document, in the document's order.
items: { $ref: '#/components/schemas/AcceptedCorrection' }
preexisting_faults:
type: integer
minimum: 0
description: |
Faults the book's bank inputs ALREADY carried — not caused by this call, and not
refusing it: this door must stay usable exactly when the book needs repair. Non-zero
warns that the next run would FAIL at the bank — a fault, not the signing stop —
regardless of this call. The detail is a server-side matter, correlated by
`X-Request-Id`; it is not on this surface.
signature:
oneOf:
- $ref: '#/components/schemas/BankSignatureCount'
- type: 'null'
description: |
The count against the surfaces the LAST signing stop offered — `null` when no run has
reached a signing stop yet, so there is nothing to count against. Carries its own
warning: see the schema. ⚠ A named narrowness of this version: the count lives ONLY
on this receipt, so a client with no correction to send — or preview — has no way to
ask for it yet; the companion carries when a read would take it over.
AcceptedCorrection:
type: object
description: What one correction did — for a preview, would do.
required: [index, action, id, src, dst, state, displaced]
properties:
index:
type: integer
minimum: 0
description: Position of the correction in the request document.
action:
type: string
enum: [approve, decline]
id:
$ref: '#/components/schemas/Id'
description: |
Id of the term the correction named — for a term this call ADDS, the id its tuple
derives to: the same id the bank read will publish for it.
src:
type: string
description: Source surface of the term.
dst:
type: [string, 'null']
description: The rendering; `null` on `decline`.
state:
type: string
enum: [applied, already_applied]
description: |
`already_applied` — the recorded corrections already say this; nothing was (or would
be) written for it. That is the whole of idempotency as a caller sees it: re-sending a
document is safe.
displaced:
type: boolean
description: |
Whether this correction displaced an earlier word — a previous rendering, a previous
decline of the surface, previously approved rows removed by a decline. `true` is not
an error: a correction REPLACES. It is surfaced because overwriting without seeing
that you overwrote is the mistake this door must not enable. The itemization of WHAT
was displaced is not on this surface: its vocabulary is the service's own free text,
which does not cross this boundary.
BankSignatureCount:
type: object
description: |
Of the surfaces the LAST signing stop offered, how many the user has not yet spoken about —
counted by the same rule a run uses to fold corrections into the bank, so the count agrees
with what the next run treats as decided.
⚠ **INFORMATIONAL, never a gate — it counts, it does not decide.** It does NOT answer
whether the stop will lift, in either direction: `undecided: 0` promises NOTHING about the
next run's behaviour, and an undecided remainder forbids nothing — deciding surfaces is
the user's RIGHT, not the stop's demand. A client may show "N of M still open"; it MUST
NOT disable or gate the continue action on these numbers or imply they must reach zero:
`resumeRun` lifts the stop with the corrections as they stand.
required: [surfaces, undecided, unreadable]
properties:
surfaces:
type: integer
minimum: 0
description: How many surfaces the last stop asked about.
undecided:
type: integer
minimum: 0
description: How many of them are still undecided AFTER this call.
unreadable:
type: boolean
description: |
`true` — the two numbers mean NOTHING for this call: the state they are counted from
could not be read. Without this flag, "could not count" would be byte-identical to
"nothing left undecided" — the one thing this schema must never say by accident.
CorrectionRefusal:
type: object
description: One refused correction, or one refusal about the would-be result as a whole.
required: [pointer, detail]
properties:
pointer:
type: string
description: |
JSON Pointer to the refused correction (`/corrections/3`), or the EMPTY STRING for a
refusal about the result as a whole — a contradiction the SET introduces rather than
any one entry.
detail:
type: string
minLength: 1
description: |
Developer-facing sentence naming the reason, like `Problem.detail`: a client MUST NOT
show it to a user — it marks the row and draws its own neutral phrase. The reasons
carry no machine vocabulary on this surface yet: that vocabulary is the service's own
and still growing, and freezing a copy here would be a second source of truth.
RunRequest:
type: object
@ -2304,8 +2572,8 @@ components:
properties:
contract:
type: string
description: Contract version this deployment serves, e.g. `0.4.0`.
examples: ['0.4.0']
description: Contract version this deployment serves, e.g. `0.5.0`.
examples: ['0.5.0']
EventStatus:
allOf:
@ -2361,15 +2629,14 @@ components:
- $ref: '#/components/schemas/EventBase'
- type: object
description: |
The bank changed, or a signing stop happened. The counters are the delta a screen header
needs; the ROWS are read with `after_version` set to the revision the client last
applied.
required: [total, signed, pending_decisions, complete]
The bank changed, or a signing stop happened. The counters are the delta a screen
header needs; the ROWS are read with `after_version` set to the revision the client
last applied. A correction (`POST …/bank/corrections`) does not produce this frame:
it changes no row until the next run.
required: [total, signed]
properties:
total: { type: integer, minimum: 0 }
signed: { type: integer, minimum: 0 }
pending_decisions: { type: integer, minimum: 0 }
complete: { type: boolean }
EventResyncRequired:
description: |
@ -2398,7 +2665,9 @@ components:
Error, per RFC 9457 with the extension members below.
**The machine identifier is `code`.** `type` is `about:blank` on every response and carries
no information: this deployment serves no problem-type documents.
no information: this deployment serves no problem-type documents. On THIS surface `code`
is always present; outside the version prefix it may be absent (header, `/auth/*`) — a
parser applied there must tolerate that rather than fail.
⚠ **`title` and `detail` are written for a DEVELOPER and a log, and a client MUST NOT show
either to a user.** They are English and will not be translated. The sentence the user reads
@ -2413,6 +2682,7 @@ components:
| `errors` | `invalid_request` |
| `cause` | any code with a narrower cause to give |
| `blocked` | `ceiling_unavailable` |
| `refusals` | `bank_corrections_refused` |
| `localized` | codes whose cause cannot be enumerated |
required: [type, title, status, code, request_id]
properties:
@ -2452,6 +2722,13 @@ components:
description: |
Carried by `ceiling_unavailable` when another book of the account holds the credit — the
same shape `RunOptions` answers.
refusals:
type: array
minItems: 1
description: |
Which corrections were refused and why. Carried by `bank_corrections_refused`; the
whole set was refused and NOTHING was applied.
items: { $ref: '#/components/schemas/CorrectionRefusal' }
localized:
$ref: '#/components/schemas/LocalizedMessage'
description: |
@ -2484,7 +2761,8 @@ components:
rather than check the address. The operation says where it is also the answer to an
identifier that never existed;
- `request_timeout` (408) — the body did not arrive whole in time. Retrying is the remedy;
- `payload_too_large` (413) — over `intake_max_bytes`;
- `payload_too_large` (413) — the body is over the bound the operation declares:
`intake_max_bytes` on the intake, 1 MiB on bank corrections;
- `run_in_flight` (409) — this book is already being translated;
- `book_not_ready` (409) — the book cannot be translated yet: it is still arriving, still
being cut, or was rejected;
@ -2503,6 +2781,18 @@ components:
- `idempotency_conflict` (409) — an `Idempotency-Key` was re-used. `cause.code`:
`key_reused` for a different request under the same key, `key_in_flight` for one that is
still running, and then `Retry-After` says how long to wait;
- `bank_corrections_refused` (409) — the correction document was read, understood and
declined WHOLE; nothing was applied. A legal refusal, not a fault: what the document
asks contradicts the book as it stands — a term that came with the book, a
contradiction within the set, an `id` no current row carries, a `decline` nothing
recorded answers to, or a set larger than the service applies in one act. Re-sending an
already-recorded correction is NOT this — that answers `200` with `already_applied`.
The USER re-decides and sends a new document; `refusals` names every refused entry.
Distinct from `invalid_request`, which is about the request's FORM;
- `bank_corrections_incomplete` (503) — the correction document was ACCEPTED and the write
did not land whole. The remedy is to re-send the SAME document: the retry converges —
what landed is recognised, not duplicated. Distinct from `service_unavailable` because
the remedy is this specific;
- `content_refused` (400) — the service will not do this work. **One coarse code for a whole
class** and deliberately so: it does not say which check refused, does not vary between
attempts, and carries neither `cause` nor `errors`. A client shows one neutral phrase and
@ -2535,6 +2825,8 @@ components:
- run_not_resumable
- ceiling_unavailable
- idempotency_conflict
- bank_corrections_refused
- bank_corrections_incomplete
- content_refused
- service_unavailable
- internal_error

View file

@ -1,5 +1,22 @@
# Промт: контрактная сессия, минор 0.4.0 → 0.5.0 — снос отменённой модели и дверь, выведенная из глагола
> ⚠⚠ **ОТРАБОТАН И ЗАКРЫТ. Инструкции отсюда НЕ исполнять** — файл сохранён как заказ, по которому
> судить исполнение.
>
> **Исход:** пак исполнен и ПРИНЯТ 27.08, ратификация **D39.161**. Канон 0.5.0 и компаньон заленджены,
> отчёт — `docs/CONTRACT_MINOR_REPORT.md`.
>
> ⚠ **ДВА МЕСТА, ГДЕ ЭТОТ ПРОМТ БЫЛ НЕВЕРЕН, и это нашёл исполнитель, а не автор.** §4 п.2 обещал, что
> после сноса «канон и деплой СОВПАДУТ точно», а §3.2-бис — что счётчики `pending_decisions`/`complete`
> «навсегда нули». Верно по ПУТЯМ, неверно по ПОЛЯМ: проекция `GET /bank` продолжает их слать, и это
> живой счёт `proposed`-строк (`platform/internal/pgstore/readmodel.go:330`). Носитель — `PD-399`,
> эррата — 27.08-ж в шапке журнала решений. Сессия принесла расхождение ПИНГОМ, а не подогнала работу
> под неверную посылку.
>
> ⚠ Список носителей §3.1 был НЕПОЛОН: третий носитель счётчиков (`EventBank`) сессия нашла своим
> грепом. Промт предупреждал, что его шаблон мог что-то не поймать, — предупреждение оправдалось.
> **Выдан оркестратором №19, 27.08.2026.** Порядок работ по шву ратифицирован **D39.156**; этот пак —
> пункт (2б) очереди, и он идёт ПОСЛЕ движкового пака, который закончен и заленджен 27.08 (**D39.158**,
> коммит `d1eb8a9`). Это существенно: тело новой двери больше не сочиняется — оно ВЫВОДИТСЯ из словаря

File diff suppressed because one or more lines are too long

View file

@ -10,7 +10,7 @@ import "net/http"
// client generated against another one refuses to work and says so — which is why this must be
// raised in the same commit as the code that implements a new minor, and never as a courtesy
// afterwards.
const ContractVersion = "0.4.0"
const ContractVersion = "0.5.0"
// Capabilities is what this deployment can do: one flat document, the same for every account.
type Capabilities struct {