diff --git a/docs/CONTRACT_MINOR_REPORT.md b/docs/CONTRACT_MINOR_REPORT.md new file mode 100644 index 00000000..e83b7afa --- /dev/null +++ b/docs/CONTRACT_MINOR_REPORT.md @@ -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 промта, заказ): 2–4 агента, мандат «дверь против словаря +глагола» + оси §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. Раскладка полосы отказов.** Ратифицированная полоса 10–19 (`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 агента (в пределах веера 2–4, §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` перед сдачей — см. финал сессии | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d6bbd66c..f5f3b495 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,7 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-27, голова D39.160 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №19** (единственный носитель — здесь; роль передана 22.08, №18 закрыт D39.155): (а) **P7 платформы ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН, сессия закрыта владельцем 20.08** (D39.153; зонный фикс-лист из тринадцати пунктов плюс врезанный первым блокер свипа — пинг №18 в `platform/docs/platform-PROGRESS.md`; ⚠ прежний врезанный пример «первым `ContractVersion` 0.3.0 при формах 0.4.0» СНЯТ — константа поднята и запинена гейтом против канона лендингом `31f1f82`) · (а1) **контракт API v0 0.4.0 РАТИФИЦИРОВАН** (D39.152; PD-327 закрыт; зеркало фронта отстаёт ратифицированно, при разморозке `cmp`) · (б) **лендинг петель полигона (фаза Д) — ОТКРЫТ**: приёмка ФАЗЫ у параллельного оркестратора, лендинг за этим; ⚠ **предупреждение в силе: работа полигона в дереве по-прежнему НЕЗАКОММИЧЕНА** (20 позиций: `START_PROMT.MD` + 14 правленых и 5 новых файлов `eval/`). Владелец перевозил её на новую машину транспортным коммитом `temp`, но 24.08 **снял его словом владельца** (`reset --mixed`, дерево не тронуто — сверено побайтно, бэкап `~/tm-backup-2026-08-24/textmachine-HEAD.bundle` + reflog): транспортный коммит вдобавок уносил `START_PROMT.MD`, который канон коммитить запрещает. Состав читать командой `git status --short -- eval/`, снимок здесь протухает. Не трогать и не «прибирать»; приёмка ФАЗЫ — за параллельным оркестратором, пинг №20 — в секции «Полигон». Форма коммита строго pathspec · (в) **дофикс-пак P8-FIX ПРИНЯТ И ЗАЛЕНДЕН 22.08** (D39.154, лендинг `31f1f82`): блокер свипа закрыт четырьмя механизмами и пере-проверен живой пробой оркестратора, деньги сошлись двумя путями из сырого леджера; мои четыре строки регистра — `PD-371`…`PD-374`. ⚠ **Порядок вышел ОБРАТНЫМ этой очереди:** промт дофикса требовал запуска ПОСЛЕ читающего пака, запущен был раньше. Цена перестановки, названная мной вслух 21.08 («блокер живёт всё время читающего пака»), тем самым НЕ заплачена — это лучший исход, чем планировалось; в минус — §4.7 (аддендум релеем) пуст и читающий пак пойдёт по только что переписанному коду. **ЧТО ДАЛЬШЕ С ПЛАТФОРМОЙ — решено 22.08 при актуализации доков, порядок такой.** **(1) Читающий пак P8-REVIEW — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 27.08** (D39.159): четыре оси, 24 новые строки регистра (`PD-375`…`PD-398`), 17 дописок, каталог воспроизведения `platform/docs/p8-review/`. Единственный vuln — `PD-379` (major): открытый поток событий переживает отзыв сессии. Числа сдачи пере-мерены приёмкой на дереве С лендингом бэкенда. Промт архивируется. **Следующая работа зоны — кодовый пак по этим строкам, запуск по слову владельца.** Промт ЖИВ и переписан 22.08 в трёх местах, которые дофикс сделал ложными: «два денежных дефекта не заведены» (заведены и закрыты — `PD-333`/`PD-334`, вместо них дан их КЛАСС и живой образец `PD-372`) · приор оси 4 «наблюдаемость без ручки» (ручка построена ⇒ вопрос стал «достаточна ли она») · абсолют «оси ни в один дифф не входили» (сужен: системного разбора не было, кусками входили). **Довод ЗА пак — базовая ставка, а не вера:** первый же серьёзный взгляд на ОДНУ из четырёх осей (очередь, 21.08) дал пред-продовый блокер, проживший два пака под статусом `fixed`. Осей осталось три, и на двух из них лежат деньги и вход. **(2) ПОРЯДОК РАБОТ ПО ШВУ — ратифицирован D39.156 (23.08), исполняется строго так.** **(2а) Движковый пак — АКТИВНЫЙ ПРОМТ ЗОНЫ** (`BACKEND_SEAM_PACK_SESSION_PROMPT.md`, выдан 23.08, запуск по слову владельца): конвенционные дефолты `mined_delta`/`mined_rejects` · $0-глагол приёма правок банка · путь к файлу ключей аргументом (снимает блокер **211**) · строгость сид-загрузчика · путь артефакта и версия документа в `status --json` (снимает движковую половину **213**). **(2б) Контрактный минор — ПОСЛЕ него и ОДНИМ куском:** снос отменённой пер-термной модели + новая дверь, тело которой выводится из словаря глагола, а не сочиняется до него; сюда же строки **200**, **203** и хвосты компаньона. ⚠ Гигиена держится сознательно: снести дверь сейчас и завести в следующем миноре — две перегенерации фронта вместо одной; фолбэк объявлен — если пак застрянет надолго, гигиену шипить отдельно. **(2в) Платформенный пак — последним:** воркер решений → глагол перед возобновлением · путь ключей из конфига · снятие дубля вывода пути · дев-супервизор на тот же механизм ключей. **(3) `sqlc` отдельной сессией** (решение владельца 22.08, `BACKLOG.md` П-19, граница 41 запрос в пяти файлах) — механическая работа, не мешать её с содержательной. **(4) Живой прогон книги насквозь** — строка 202, гейт прежний: ПОСЛЕ холодного прогона движка. ⚠ **На владельце и блокирует пункт своей темы:** развилка владения `book.yaml` (строка 199а, зона сдала четырьмя вариантами с ценой, рекомендация зоны C или D+B) — без неё труба доставки правок банка не строится, а верхнего края у неё теперь нет вовсе · (г) **контракт:** сквозная полоса прогресса (строка 200, слово владельца 20.08) и хвосты релеев (203) едут тем же минором, что и дверь приёма правок банка — состав собирается ПОСЛЕ ратификации нормы шва, см. пункт (2) · (д) свободные бэкенд: 160 Этап 0, фикс-лист ФЧ строкой 197 (носителя-сессии нет по слову владельца); ⚠ строки 176/181/187 СНЯТЫ с таблицы при лендинге 20.08 — они закрыты ещё D39.149, а очередь всё это время звала 181 свободной работой (поймано ревью доков; остатки живут строками 194/196/197) · (е) фронтовый фрагмент pre-commit хука получает норму «жалуйся владельцу» — **решение владельца 21.08: ДА**, исполняется первым касанием зоны (пинг в её журнале). · (ж) **ПЕРЕЕЗД НА НОВУЮ МАШИНУ — стенд ПЕРЕСОБРАН 24.08 (№19), как поднимать — карта `README.md`, строки «Тулчейн» и `counts.py`.** Ubuntu 26.04 WSL2, пользователь `ubuntu-26`; тулчейн стоял НУЛЕВОЙ. Поставлено: Go 1.26.7 · golangci-lint 2.12.2 · Node 22.23.2 — тарболами вендоров со сверкой sha256 в `~/.local/opt` (симлинки в `~/.local/bin`); PostgreSQL 18.4 — рецептом самой зоны (`platform/docs/STACK_DECISIONS.md` §«Postgres на стенде без root»); `build-essential` + `python3-venv` — руками владельца, они единственные требовали root. **Обе батареи зелены под `-race`:** бэкенд `make battery` EXIT=0, линтер 0 issues, скипы только корпус-гейченные и хелперы; платформа `make check` EXIT=0, 18 пакетов, линтер 0 issues, **скипов 0**. Фронт: `npm ci` + `npm run check` проходит целиком (335 тестов), `vite build` собирается. Сквозняк проверен ЖИВЬЁМ: демон платформы → сид через настоящий интейк → движок распарсил книгу (3 главы) → дев-прокси фронта достучался до платформы. ⚠ **Что НЕ проверено:** ни одного платного вызова · браузеров Playwright нет ⇒ `check:full` не гонялся · локальной модели и GPU на машине нет вовсе (слово владельца 24.08: мерить нечем, ветка не закрыта) · OIDC не поднимался, вход только дев-логином. ⚠ Правка `.gitignore` владельца несла дефект — правило `books/` без якоря глушило ЛЮБОЙ каталог `books`, включая живой пакет `platform/internal/books/` (новый файл там уходил бы в игнор молча); заякорено в `/books/`, проверено пробой в обе стороны. Новые строки бэклога — **217** (пути полигона), **218** (дефолты корпуса в движке — ЗАКРЫТА и снята 27.08, D39.159 эррата-д), **219** (line-якоря D-лога съехали — ⚠ ОБЪЯВЛЕНА 24.08, а В ТАБЛИЦЕ ЗАВЕДЕНА только 27.08: три дня очередь ссылалась на несуществующий носитель) **Открытые обязательства, которых нет больше нигде:** веса `pro` — строка 172-г НЕ закрыта, прогон мерил классификатор на flash; дешёвый вход — спросить полигон, говорят ли что-то данные фазы Д (там pro гоняется редактором), отдельный замер не покупать без этого · `docs/experiments/00-provider-quirks.md` несёт протухшие цены DeepSeek («не изменились, flash $0.14/$0.28») — зона полигона, чинить пингом · санкция на платный прогон в архивном промте пака честности была РАЗОВОЙ · **норма плотности комментариев — ОТВЕЧЕНА владельцем 21.08:** режем ВОДУ, а не длину — счёт строк «пиши одну-две» негодный гейт и снят; уходит пересказ решений, провенанс и изложение исследования вместо ссылки, остаётся всё, что из одной функции не выводится, сколько бы строк ни заняло (норма переписана — `12-go-style-notes` §1; PD-255 платформы этим закрывается). ⚠ Метод-урок: её хендофф объявил «вопросов на владельце нет», а строка регистра просила именно решения — вопрос был закрыт ОБЪЯВЛЕНИЕМ, поймано ревьюером полноты выгрузки. Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей — D39.113, гардрейлы в CLAUDE.md.) +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-27, голова D39.161 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №19** (единственный носитель — здесь; роль передана 22.08, №18 закрыт D39.155): (а) **P7 платформы ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН, сессия закрыта владельцем 20.08** (D39.153; зонный фикс-лист из тринадцати пунктов плюс врезанный первым блокер свипа — пинг №18 в `platform/docs/platform-PROGRESS.md`; ⚠ прежний врезанный пример «первым `ContractVersion` 0.3.0 при формах 0.4.0» СНЯТ — константа поднята и запинена гейтом против канона лендингом `31f1f82`) · (а1) **контракт API v0 0.4.0 РАТИФИЦИРОВАН** (D39.152; PD-327 закрыт; зеркало фронта отстаёт ратифицированно, при разморозке `cmp`) · (б) **лендинг петель полигона (фаза Д) — ОТКРЫТ**: приёмка ФАЗЫ у параллельного оркестратора, лендинг за этим; ⚠ **предупреждение в силе: работа полигона в дереве по-прежнему НЕЗАКОММИЧЕНА** (20 позиций: `START_PROMT.MD` + 14 правленых и 5 новых файлов `eval/`). Владелец перевозил её на новую машину транспортным коммитом `temp`, но 24.08 **снял его словом владельца** (`reset --mixed`, дерево не тронуто — сверено побайтно, бэкап `~/tm-backup-2026-08-24/textmachine-HEAD.bundle` + reflog): транспортный коммит вдобавок уносил `START_PROMT.MD`, который канон коммитить запрещает. Состав читать командой `git status --short -- eval/`, снимок здесь протухает. Не трогать и не «прибирать»; приёмка ФАЗЫ — за параллельным оркестратором, пинг №20 — в секции «Полигон». Форма коммита строго pathspec · (в) **дофикс-пак P8-FIX ПРИНЯТ И ЗАЛЕНДЕН 22.08** (D39.154, лендинг `31f1f82`): блокер свипа закрыт четырьмя механизмами и пере-проверен живой пробой оркестратора, деньги сошлись двумя путями из сырого леджера; мои четыре строки регистра — `PD-371`…`PD-374`. ⚠ **Порядок вышел ОБРАТНЫМ этой очереди:** промт дофикса требовал запуска ПОСЛЕ читающего пака, запущен был раньше. Цена перестановки, названная мной вслух 21.08 («блокер живёт всё время читающего пака»), тем самым НЕ заплачена — это лучший исход, чем планировалось; в минус — §4.7 (аддендум релеем) пуст и читающий пак пойдёт по только что переписанному коду. **ЧТО ДАЛЬШЕ С ПЛАТФОРМОЙ — решено 22.08 при актуализации доков, порядок такой.** **(1) Читающий пак P8-REVIEW — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 27.08** (D39.159): четыре оси, 24 новые строки регистра (`PD-375`…`PD-398`), 17 дописок, каталог воспроизведения `platform/docs/p8-review/`. Единственный vuln — `PD-379` (major): открытый поток событий переживает отзыв сессии. Числа сдачи пере-мерены приёмкой на дереве С лендингом бэкенда. Промт архивируется. **Следующая работа зоны — кодовый пак по этим строкам, запуск по слову владельца.** Промт ЖИВ и переписан 22.08 в трёх местах, которые дофикс сделал ложными: «два денежных дефекта не заведены» (заведены и закрыты — `PD-333`/`PD-334`, вместо них дан их КЛАСС и живой образец `PD-372`) · приор оси 4 «наблюдаемость без ручки» (ручка построена ⇒ вопрос стал «достаточна ли она») · абсолют «оси ни в один дифф не входили» (сужен: системного разбора не было, кусками входили). **Довод ЗА пак — базовая ставка, а не вера:** первый же серьёзный взгляд на ОДНУ из четырёх осей (очередь, 21.08) дал пред-продовый блокер, проживший два пака под статусом `fixed`. Осей осталось три, и на двух из них лежат деньги и вход. **(2) ПОРЯДОК РАБОТ ПО ШВУ — ратифицирован D39.156 (23.08), исполняется строго так.** **(2а) Движковый пак — АКТИВНЫЙ ПРОМТ ЗОНЫ** (`BACKEND_SEAM_PACK_SESSION_PROMPT.md`, выдан 23.08, запуск по слову владельца): конвенционные дефолты `mined_delta`/`mined_rejects` · $0-глагол приёма правок банка · путь к файлу ключей аргументом (снимает блокер **211**) · строгость сид-загрузчика · путь артефакта и версия документа в `status --json` (снимает движковую половину **213**). **(2б) Контрактный минор — ПОСЛЕ него и ОДНИМ куском:** снос отменённой пер-термной модели + новая дверь, тело которой выводится из словаря глагола, а не сочиняется до него; сюда же строки **200**, **203** и хвосты компаньона. ⚠ Гигиена держится сознательно: снести дверь сейчас и завести в следующем миноре — две перегенерации фронта вместо одной; фолбэк объявлен — если пак застрянет надолго, гигиену шипить отдельно. **(2в) Платформенный пак — последним:** воркер решений → глагол перед возобновлением · путь ключей из конфига · снятие дубля вывода пути · дев-супервизор на тот же механизм ключей. **(3) `sqlc` отдельной сессией** (решение владельца 22.08, `BACKLOG.md` П-19, граница 41 запрос в пяти файлах) — механическая работа, не мешать её с содержательной. **(4) Живой прогон книги насквозь** — строка 202, гейт прежний: ПОСЛЕ холодного прогона движка. ⚠ **На владельце и блокирует пункт своей темы:** развилка владения `book.yaml` (строка 199а, зона сдала четырьмя вариантами с ценой, рекомендация зоны C или D+B) — без неё труба доставки правок банка не строится, а верхнего края у неё теперь нет вовсе · (г) **контракт:** сквозная полоса прогресса (строка 200, слово владельца 20.08) и хвосты релеев (203) едут тем же минором, что и дверь приёма правок банка — состав собирается ПОСЛЕ ратификации нормы шва, см. пункт (2) · (д) свободные бэкенд: 160 Этап 0, фикс-лист ФЧ строкой 197 (носителя-сессии нет по слову владельца); ⚠ строки 176/181/187 СНЯТЫ с таблицы при лендинге 20.08 — они закрыты ещё D39.149, а очередь всё это время звала 181 свободной работой (поймано ревью доков; остатки живут строками 194/196/197) · (е) фронтовый фрагмент pre-commit хука получает норму «жалуйся владельцу» — **решение владельца 21.08: ДА**, исполняется первым касанием зоны (пинг в её журнале). · (ж) **ПЕРЕЕЗД НА НОВУЮ МАШИНУ — стенд ПЕРЕСОБРАН 24.08 (№19), как поднимать — карта `README.md`, строки «Тулчейн» и `counts.py`.** Ubuntu 26.04 WSL2, пользователь `ubuntu-26`; тулчейн стоял НУЛЕВОЙ. Поставлено: Go 1.26.7 · golangci-lint 2.12.2 · Node 22.23.2 — тарболами вендоров со сверкой sha256 в `~/.local/opt` (симлинки в `~/.local/bin`); PostgreSQL 18.4 — рецептом самой зоны (`platform/docs/STACK_DECISIONS.md` §«Postgres на стенде без root»); `build-essential` + `python3-venv` — руками владельца, они единственные требовали root. **Обе батареи зелены под `-race`:** бэкенд `make battery` EXIT=0, линтер 0 issues, скипы только корпус-гейченные и хелперы; платформа `make check` EXIT=0, 18 пакетов, линтер 0 issues, **скипов 0**. Фронт: `npm ci` + `npm run check` проходит целиком (335 тестов), `vite build` собирается. Сквозняк проверен ЖИВЬЁМ: демон платформы → сид через настоящий интейк → движок распарсил книгу (3 главы) → дев-прокси фронта достучался до платформы. ⚠ **Что НЕ проверено:** ни одного платного вызова · браузеров Playwright нет ⇒ `check:full` не гонялся · локальной модели и GPU на машине нет вовсе (слово владельца 24.08: мерить нечем, ветка не закрыта) · OIDC не поднимался, вход только дев-логином. ⚠ Правка `.gitignore` владельца несла дефект — правило `books/` без якоря глушило ЛЮБОЙ каталог `books`, включая живой пакет `platform/internal/books/` (новый файл там уходил бы в игнор молча); заякорено в `/books/`, проверено пробой в обе стороны. Новые строки бэклога — **217** (пути полигона), **218** (дефолты корпуса в движке — ЗАКРЫТА и снята 27.08, D39.159 эррата-д), **219** (line-якоря D-лога съехали — ⚠ ОБЪЯВЛЕНА 24.08, а В ТАБЛИЦЕ ЗАВЕДЕНА только 27.08: три дня очередь ссылалась на несуществующий носитель) **Открытые обязательства, которых нет больше нигде:** веса `pro` — строка 172-г НЕ закрыта, прогон мерил классификатор на flash; дешёвый вход — спросить полигон, говорят ли что-то данные фазы Д (там pro гоняется редактором), отдельный замер не покупать без этого · `docs/experiments/00-provider-quirks.md` несёт протухшие цены DeepSeek («не изменились, flash $0.14/$0.28») — зона полигона, чинить пингом · санкция на платный прогон в архивном промте пака честности была РАЗОВОЙ · **норма плотности комментариев — ОТВЕЧЕНА владельцем 21.08:** режем ВОДУ, а не длину — счёт строк «пиши одну-две» негодный гейт и снят; уходит пересказ решений, провенанс и изложение исследования вместо ссылки, остаётся всё, что из одной функции не выводится, сколько бы строк ни заняло (норма переписана — `12-go-style-notes` §1; PD-255 платформы этим закрывается). ⚠ Метод-урок: её хендофф объявил «вопросов на владельце нет», а строка регистра просила именно решения — вопрос был закрыт ОБЪЯВЛЕНИЕМ, поймано ревьюером полноты выгрузки. Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей — D39.113, гардрейлы в CLAUDE.md.) +> - ⚠ **27.08: КОНТРАКТНЫЙ МИНОР 0.5.0 ПРИНЯТ И ЗАЛЕНДЖЕН (D39.161).** Отменённая пер-термная модель снесена из канона и компаньона; дверь `POST /books/{bookId}/bank/corrections` ОБЪЯВЛЕНА и выведена из словаря `bank-apply` поле в поле; отказы разложены (409 = класс 14, 503 = класс 15 «слать тот же документ», 413 = 1 МиБ); признак «не построено» машиночитаем (`bank_corrections_enabled`, false ⇒ 404). **Константа платформы поднята тем же коммитом** — батарея зоны 18 пакетов, EXIT=0, линтер 0. Дверь ждёт МОНТАЖА паком (2в), туда же прогресс (200) и снятие двух полей с провода (`PD-399`). ⚠ Промт содержал ошибку оркестратора («поля навсегда нули»), исполнитель принёс её пингом — эррата 27.08-ж. > - ⚠ **27.08: ВЫДАН ПРОМТ КОНТРАКТНОГО МИНОРА** (`CONTRACT_MINOR_SESSION_PROMPT.md`, пункт 2б очереди D39.156). **ВОПРОС ЗАКРЫТ СЛОВОМ ВЛАДЕЛЬЦА 27.08 (D39.160):** сквозная полоса прогресса (строка **200**) из минора ИСКЛЮЧЕНА и едет с паком (2в). Причина механическая: гейт платформы требует, чтобы объявленная деплоем версия равнялась канону, поэтому канон и деплой обязаны совпасть В МОМЕНТ ЛЕНДИНГА; снос двери этому не мешает (платформа свою половину снесла 22.08), а смена семантики прогресса мешает — сервер её не считает и не будет до пака (2в), и `0.5.0` у старого прогресса был бы ровно той ложью, против которой гейт стоит. Прежнее «одним куском» покупалось тем, что иначе фронт перегенерируется дважды, — но фронт ЗАМОРОЖЕН и перегенерируется один раз независимо от числа миноров. Промт ВЫДАН: прошёл оба рубежа роли, девять находок опровергателя применены. > - ⚠ **27.08: ПАК P8-REVIEW ПЛАТФОРМЫ ПРИНЯТ (D39.159).** Четыре оси прочитаны исполнением, кода не тронуто; регистр 374→398 строк, открытых 72→96 (major 1→3). Ратифицированы норма копии-с-каноном, эррата §13 и токен `ОСПОРЕНО(PD-N)`; статусы трёх спорных строк НЕ пере-открыты. Приёмка пере-мерила батарею на движке из HEAD — **шов пережил лендинг бэкенда `d1eb8a9`** — и подтвердила `PD-376` своей посадкой. Правка гейта формы регистра ОТКЛОНЕНА замером: краснила бы семь законных строк. Новая строка бэклога **225**. > - ⚠ **27.08: ВХОДНАЯ ДВЕРЬ ШВА ЗАКОНЧЕНА И ЗАЛЕНДЖЕНА (D39.158).** Четыре пака зоны бэкенда приняты одним деревом: $0-глагол приёма правок банка · стоп банка стал ФЛАЖКОМ в самом движке (память предъявленного, схема хранилища v16) · единая точка фиксации отчёта (шесть исходов, v2) · полоса отказов в два яруса с классом `write_incomplete`/exit 15 · пути решений только конвенцией. **Порядок деплоя обязателен: движок первым → `tmctl migrate` по каждой книге до прогонов → и только потом платформа снимает свой обход** (пинг в её журнале). Строки **211/212/213/218** закрыты, **191(б)/203(з)** пере-диспозиционированы (устранено, не обойдено), заведены **221–224**. Онтология банка записана — `architecture/18-bank-ontology.md`. @@ -11,7 +12,7 @@ > - **Курс:** ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ → ПАКЕТ-ЧЕКЕРОВ ✅ (D39.59–78) → **ФРОНТ-ЭРА** (D39.81–100: зоны живые, контракт API ратифицирован) → **шов/платформа/движковые блокеры построены** (D39.106–123). Хвосты курса живут строками: 16 (полная цена холодного старта не измерена: у `coldrun-a`/`coldrun-b` нет НИ ОДНОГО редакторского вызова, хотя сама edit-волна отработала и оплачена — 58 вызовов волновым драйвером 23–25.07 на малых прогонах, плюс 648 до драйвера; ⚠ прежняя редакция писала «не гонялась НИ РАЗУ» — абсолют ложен, испр. оркестратором №19 по леджерам `~/books/gu-zhenren/**/*.db`; держит и оси голоса 13б/24) · 46 (дизайн заморожен D39.92/93, промт ждёт выдачи) · coldrun-b фаза C заморожена чекпойнтом легитимно (D39.86; эталон денег/поведения — coldrun-a, read-only); развилка 0731 решена и исполнена (D39.87/91, код `553f1a3`). > - **Горизонт (D39.62/67, освежён D39.95):** **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · цена 16; жильцы ролей решаются ДО прогона эксп-22 — строка 149 · веса K1–K12 13а · вне-претрейн чекпоинт 55; остаток арбитража банка = рецензент спорных кластеров при ре-пробе 74 — D39.102) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 62–68, 85) → Ф3 ридер-IDE (69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 (⚠ + x/text Unicode 17 тем же тулчейном — строка 119, реестр §Б-108 справочника якорей) · платные прогоны разблокированы (проба провода — D39.97, конфиги 112 залендены). > - **Стек (полная карта роль→модель→конфиг→квирки — [STACK.md](STACK.md), D39.126):** draft deepseek-v4-flash thinking-ON `low` **⚠0731** → терминолог (та же модель) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (топология ПОДТВЕРЖДЕНА при неразличимости жильцов — D39.117; закон-блок ОБЯЗАТЕЛЕН — строка 134; glm-5 резерв; вахта маппинга эффорта pro — §Б-108) → судья gemini (Ф2, в движке НЕ построен — строка 33); канал B Mistral+grok; ~$0.85/ранобэ (D30.4, пере-калибровка при следующем платном прогоне). ⚠ **Вендор-факты 13–15.08 (пере-пин ИСПОЛНЕН и ПРИНЯТ, D39.137):** таблица цен запинена ПИКОМ (flash 0.44/1.32 · pro 1.32/3.96/кэш-хит 0.044 за 1M; счёт шиппинг-c1 = 100% DeepSeek ⇒ ×4.2–4.4 в пике / ×2.1–2.2 в долине — замер по трём прогонам); ⚠ про эффорт pro НОСИТЕЛИ ПРОТИВОРЕЧАТ и это НЕ РЕШЕНО (испр. 23.08 №19: прежняя редакция утверждала здесь одну сторону — «эффорт стал настраиваемым low/high/max, квирк 3а устарел»): квирк-канон и `STACK.md` говорят «ручки у pro НЕТ», замера поведением после 13.08 нет ни у одной стороны, и по гардрейлу владельца такое закрывается ВЕНДОР-СВЕРКОЙ, а не выбором стороны; до неё в силе канон, разбор — `STACK.md` строка редактора; ⚠ ВЕСА pro сменились под тем же слагом (V4-Pro-0813, класс D39.61) — вахта-риг готов (остаток 172); ⚠ посылка интерим-редактора «dspro дешевле glm» в пике ПЕРЕВЁРНУТА (×1.26 дороже — вход ратификации фазы Д, D39.137 п.4); покупки фазы Д на deepseek до 16.08 16:00 UTC — по старым ценам. ⚠ **ВЕСЬ банковый контур (банкнота+терминолог+классификатор) в shipping-c1 НЕ включён** — жив ран-локальным конфигом книги (строка 140; сверка STACK.md 09.08 — факт шире прежней декларации «одна банкнота»); эскалация в shipping за `budget_usd: 0` (STACK.md §примечания). -> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 27.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **176** строк · зона бэкенд **80** строго / **121** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178 — D39.136; 179 ЗАКРЫТА D39.138; 180/181 — D39.137; 182 — wire-батч, аудит 15.08; 183–187 — контракт-ревью 28, D39.138; 188–190 — аудит бэклога, D39.140; 191/192 — модель подписи банка и пост-ридинговый цикл, D39.144; 193 — молчащие дыры выдачи, D39.147; 194–196 — приёмка пака честности, D39.149; 197 — фикс-лист ФЧ, D39.150; **198–204 — приёмка P7 и слова владельца 20.08, D39.153; 205 — слепота гейта якорей, аудит 21.08; 206–207 — приёмка P8-FIX, D39.154; 208–209 — аудит доков 22.08 (потеря глоссария на Gemini, риг живых проб); 210 — трассировка цепи банка 22.08 (род не производится авто-путём); **211–213 — консилиум шва 22–23.08: ключи провайдеров не доезжают до движка на SaaS (блокер 202), нестрогий загрузчик сида, дубль конвенции пути у платформы; 214–216 — разбор журнала трассировки банка: подпись не оставляет следа, $0-репин недостижим с платформы, три несущих сценария не проверены живым движком; 217–218, 220 — аудит переезда машины 24.08: абсолютные пути полигона, дефолты корпуса в движке, бутстрап гейта доков чужой замороженной зоной; 219 (съехавшие line-якоря D-лога) ЗАКРЫТА тем же заходом — D39.157 п.6**) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **50** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). +> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 27.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **175** строк · зона бэкенд **80** строго / **121** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178 — D39.136; 179 ЗАКРЫТА D39.138; 180/181 — D39.137; 182 — wire-батч, аудит 15.08; 183–187 — контракт-ревью 28, D39.138; 188–190 — аудит бэклога, D39.140; 191/192 — модель подписи банка и пост-ридинговый цикл, D39.144; 193 — молчащие дыры выдачи, D39.147; 194–196 — приёмка пака честности, D39.149; 197 — фикс-лист ФЧ, D39.150; **198–204 — приёмка P7 и слова владельца 20.08, D39.153; 205 — слепота гейта якорей, аудит 21.08; 206–207 — приёмка P8-FIX, D39.154; 208–209 — аудит доков 22.08 (потеря глоссария на Gemini, риг живых проб); 210 — трассировка цепи банка 22.08 (род не производится авто-путём); **211–213 — консилиум шва 22–23.08: ключи провайдеров не доезжают до движка на SaaS (блокер 202), нестрогий загрузчик сида, дубль конвенции пути у платформы; 214–216 — разбор журнала трассировки банка: подпись не оставляет следа, $0-репин недостижим с платформы, три несущих сценария не проверены живым движком; 217–218, 220 — аудит переезда машины 24.08: абсолютные пути полигона, дефолты корпуса в движке, бутстрап гейта доков чужой замороженной зоной; 219 (съехавшие line-якоря D-лога) ЗАКРЫТА тем же заходом — D39.157 п.6**) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **49** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 11–16, rerun2) · **`-25-31` (паки 17–20 · мини-прогон · полигон-пакеты 5–8 · ToS · холодный прогон; D39.26–58)** · **`-08-01-02` (сессии №9/№10: общность · качество банка · coldrun-b · открытие фронта/платформы; D39.59–90, срез D39.105)** · **`-08-02-04` (сессии №11–№13: ручки эффорта · стандарты · контракт API · платформа P0 · банк-арбитраж; D39.91–105 + снимок шапки эры №15)** · `-08-04-09` (пинги закрытых паков эры №15) · **`-08-14-15` (закрытые бэкенд-записи №16–17: эмиттер шва · migrate · пере-пин DeepSeek; вынесено D39.139)**. Записи ниже — живой хвост (№16+, эра D39.124+; подрезка D39.139). ## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора) @@ -143,10 +144,9 @@ | 200 | **Сквозная полоса прогресса вместо пофазной — требование владельца 20.08. ⚠ ДИСПОЗИЦИЯ 27.08 (D39.160, слово владельца): едет с ПЛАТФОРМЕННЫМ паком (2в), а НЕ с контрактным минором.** Причина механическая: гейт версии требует совпадения канона и деплоя в момент лендинга, а сервер прогресс по-новому не считает и не будет до (2в) — канон, объявляющий новую семантику при старом сервере, лжёт ровно так, как гейт запрещает. Требование не ослаблено, перенесён только носитель. Сегодня прогресс считается ПО ОТРЕЗКАМ и при снятии стопа начинается с нуля (канон §Progress: «When a stop is cleared the counter starts again from zero») ⇒ пользователь видит 100% после черновика и обнуление — владелец назвал это дефектом подписанного, не вкусовщиной. Форма решения: ОДНА доля на всю работу прогона, считает СЕРВЕР (иначе клиент снова начнёт знать про фазы), плюс подпись «что делается сейчас» и неопределённый индикатор там, где доли в главах нет. ⚠ Веса «условно 10/40/40» НЕ нужны для переводческой части: движок объявляет знаменатель каждого прохода и ноль для прохода, которого на деплое нет (`backend/internal/pipeline/events.go:297-323`) ⇒ доля = сделано по всем проходам ÷ запланировано, без подобранных констант, и на деплое без редактора честно доходит до 100%. Догадка остаётся только у банка: поток прогресса несёт лишь два переводческих прохода, майнинг знаменателя не объявляет — **вопрос движку: может ли майнинг объявлять знаменатель** (он идёт по главам). Цена: правит канон §Progress + `Book.chapters_done`, то есть новый минор; пока фронт заморожен, минор бесплатен | контракт → платформа + бэкенд | скоро (следующий минор контракта) | контрактная сессия + платформенная половина; вопрос движку о знаменателе майнинга | слово владельца 20.08, D39.153 | | 201 | **Движковое «Глава N» доезжает читателю ВНУТРИ текста, обходя дисциплину `heading: null`** (линза шва P7, пере-прочитано оркестратором): `backend/internal/pipeline/export.go:230` приклеивает детерминированный порядковый к экспортному тексту первой юнит-главы (`ce.FinalText = chunk.ApplyHeading(...)`), колонка `Source` при этом остаётся heading-stripped. Платформа переносит обе как есть и честно отдаёт `heading: null` — то есть клиент нарисует СВОЙ порядковый на языке своего интерфейса над абзацем, который уже начинается с русского «Глава N», а исходная колонка соответствующего маркера не несёт: пара визуально рассинхронизирована на каждой первой главе. Конкретное следствие открытого К-2 контракта; родня движковой строки **160** (глава без заголовка) | бэкенд | когда-нибудь (с 160) | отдельное решение | приёмка P7 (D39.153) | | 202 | **Живой перевод книги НАСКВОЗЬ через API платформы не гонялся НИ РАЗУ** (obstacle сессии P7, подтверждён приёмкой): всё после «прогон стартовал и остановился» — подпись банка, halt на потолке, резюм после пополнения — покрыто батареей против живого Postgres, но с движком-заглушкой; настоящий манифест, сайдкар банка и причины флагов будут ПЕРВЫМИ данными, форму которых платформа не сочинила сама. Форма проверки (решена): только через API, как ходил бы клиент — интейк → старт → живой движок с ключами провайдера → поток событий → подпись и продолжение → чтение глав/юнитов/замечаний/банка и сверка с тем, что движок реально произвёл; качество перевода НЕ предмет (полигон). Цена ≈ $0.05 на десятиглавую книгу. **Решение владельца 20.08: ПОСЛЕ холодного прогона движка** — гонять одну неготовую систему против другой смысла нет | платформа | скоро (гейт: холодный прогон движка) | короткая сессия по промту оркестратора | слово владельца 20.08, D39.153 | -| 203 | **Хвосты контракта после синка 0.4.0 — ОТКРЫТА ЧЕТВЕРТЬ, остальное исполнено** (тела релеев — `platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; сюда переписаны НЕ тела, а статус — один носитель на факт). ⚠ Сверено грепом при лендинге, потому что первая редакция этой строки уже разошлась с деревом: **(б) ИСПОЛНЕН** — срезанная фраза компаньона §2.14 стоит на месте (`14-api-contract/README.md:510`=`различать причины отказа клиент не может по замыслу`) · **(д) ИСПОЛНЕН** — клетка приложения А заполнена `unspecified` (`README.md:1376` — ⚠ первая редакция писала `:1391`, это строка ДРУГОГО приложения; испр. 20.08) · **(г) наполовину** — `unspecified` ратифицирован в каноне (`openapi.yaml:1784-1787`), открыта только ГРАНИЦА ступеней замечаний (две ступени при девяти рангах движка) → это строка **148** · **(и)** закрыт полем `stop_requested` (D39.152) · (л)(м)(н)(о) закрыты контрактной сессией. **ОТКРЫТО РЕАЛЬНО:** (а) канон-половина про `/auth` — компаньон поверхность описывает (§2.14), а канон говорит только «Signing in is not part of this surface» (`openapi.yaml:35-36`) и НЕ предупреждает, что конверт вне `/v0` может прийти БЕЗ обязательного `code`; сгенерированный по канону клиент об это спотыкается · (з) ⚠ **СНЯТО КАК РАТИФИКАЦИЯ 22.08 — конфликта моделей НЕТ, я нёс причину и следствие наоборот.** Прежняя редакция писала: «D39.144 ратифицировал подпись = ОДИН акт, движок держит пер-термный гейт полноты, одно из двух должно уступить». Сквозная трассировка это опровергла КОДОМ: гейт проверяет полноту не ПОДПИСЕЙ, а ФАЙЛА решений, и ОДИН акт его снимает — `loadMinedDelta` штампует только `Source`, статус не трогает (`mining.go:855-867`) → опущенный статус дефолтится в `approved` (`membank/memseed.go:96`=`status = "approved"`) → `unsignedEngineSurfaces` выбрасывает лишь `Source=="mined" && Status!="approved"` (`mining.go:710-719`) ⇒ промоутнутые строки остаются сид-поверхностями, дельта пустеет, стоп снимается. Шапка файла говорит это прямым текстом. **Один файл со всем банком = один ОК.** Обход в платформе (`--verify-bank` не передаётся на resume) — костыль под ОТСУТСТВУЮЩУЮ ДОСТАВКУ, а не под расхождение моделей: как только доставка появится, флаг можно просто оставить включённым. Настоящий и единственный блокер темы — владение `book.yaml`, строка **199(а)**. Ратификации здесь НЕ нужно; остаётся снять обход одной боевой строкой (`platform/internal/runs/spawn.go`) и семью строками ставшего ложным обоснования — работа платформенного пака, не решение владельца · (к) ключ `project_db` в `book.yaml`: договорить, кто им владеет — шаблон оператора его не содержит, движок делает необязательным. ⚠ **Следствие «на штатном деплое банк не читается» ОПРОВЕРГНУТО живым кодом** (проверено при лендинге): акт 5 внёс фолбэк `.db` и назвал прежний отказ багом (`platform/internal/runner/artifacts.go:82-90`=`The key is OPTIONAL to the engine`); первая редакция этой строки повторила клейм хендоффа зоны, не сверив с деревом — остаток чисто договорной · **(л) остаток закрытой строки 148 — Приложение А компаньона.** ⚠ Сама 148 СНЯТА 21.08 по слову владельца: она держала мир, которого больше нет — «карта потеряна инцидентом 08.08, реконструкция у оркестратора №15, решить, брать ли черновиком». Реконструкция брать нечего: прежняя карта вела «причина движка → ФРАЗА», то есть предполагала, что фразу рисует СЕРВЕР, а батч 0.3.0 эту политику ОТМЕНИЛ — `Note` несёт машинный `code`, фразу рисует клиент, `Note.message` с провода снят (`14-api-contract/README.md:1332`=`Карта «причина → КОД контракта`). Реально пуста другая клетка: соответствие причин движка кодам замечаний, и правила её заполнения в компаньоне уже написаны (фраза по ДОККОММЕНТУ `disposition.go`, а не по имени константы — инверсия уже стоила двух фраз; класс 2 схлопывается в ОДИН код). Исполнитель — контрактная сессия, не оркестратор ⚠ **ПЕРЕ-ДИСПОЗИЦИЯ 27.08 (D39.158 п.7):** хвост (з) закрыт — «ратификации не нужно» отвечало на вопрос про один файл решений, не про флажок; движковый гейт полноты снят, не обойдён.| контракт/доки | скоро (следующее касание контракта, вместе с 200) | контрактная сессия | приёмка P7 (D39.153) | +| 203 | **Хвосты контракта после синка 0.4.0 — ОТКРЫТА ЧЕТВЕРТЬ, остальное исполнено** (тела релеев — `platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; сюда переписаны НЕ тела, а статус — один носитель на факт). ⚠ Сверено грепом при лендинге, потому что первая редакция этой строки уже разошлась с деревом: **(б) ИСПОЛНЕН** — срезанная фраза компаньона §2.14 стоит на месте (`14-api-contract/README.md:536`=`различать причины отказа клиент не может по замыслу`) · **(д) ИСПОЛНЕН** — клетка приложения А заполнена `unspecified` (`README.md:1376` — ⚠ первая редакция писала `:1391`, это строка ДРУГОГО приложения; испр. 20.08) · **(г) наполовину** — `unspecified` ратифицирован в каноне (`openapi.yaml:1784-1787`), открыта только ГРАНИЦА ступеней замечаний (две ступени при девяти рангах движка) → это строка **148** · **(и)** закрыт полем `stop_requested` (D39.152) · (л)(м)(н)(о) закрыты контрактной сессией. **ОТКРЫТО РЕАЛЬНО:** ⚠ **(а) ИСПОЛНЕН 27.08 минором 0.5.0 (D39.161)** — канон получил предупреждение о конверте вне `/v0`; открытым по строке остаётся (к). Прежний текст пункта: (а) канон-половина про `/auth` — компаньон поверхность описывает (§2.14), а канон говорит только «Signing in is not part of this surface» (`openapi.yaml:35-36`) и НЕ предупреждает, что конверт вне `/v0` может прийти БЕЗ обязательного `code`; сгенерированный по канону клиент об это спотыкается · (з) ⚠ **СНЯТО КАК РАТИФИКАЦИЯ 22.08 — конфликта моделей НЕТ, я нёс причину и следствие наоборот.** Прежняя редакция писала: «D39.144 ратифицировал подпись = ОДИН акт, движок держит пер-термный гейт полноты, одно из двух должно уступить». Сквозная трассировка это опровергла КОДОМ: гейт проверяет полноту не ПОДПИСЕЙ, а ФАЙЛА решений, и ОДИН акт его снимает — `loadMinedDelta` штампует только `Source`, статус не трогает (`mining.go:855-867`) → опущенный статус дефолтится в `approved` (`membank/memseed.go:96`=`status = "approved"`) → `unsignedEngineSurfaces` выбрасывает лишь `Source=="mined" && Status!="approved"` (`mining.go:710-719`) ⇒ промоутнутые строки остаются сид-поверхностями, дельта пустеет, стоп снимается. Шапка файла говорит это прямым текстом. **Один файл со всем банком = один ОК.** Обход в платформе (`--verify-bank` не передаётся на resume) — костыль под ОТСУТСТВУЮЩУЮ ДОСТАВКУ, а не под расхождение моделей: как только доставка появится, флаг можно просто оставить включённым. Настоящий и единственный блокер темы — владение `book.yaml`, строка **199(а)**. Ратификации здесь НЕ нужно; остаётся снять обход одной боевой строкой (`platform/internal/runs/spawn.go`) и семью строками ставшего ложным обоснования — работа платформенного пака, не решение владельца · (к) ключ `project_db` в `book.yaml`: договорить, кто им владеет — шаблон оператора его не содержит, движок делает необязательным. ⚠ **Следствие «на штатном деплое банк не читается» ОПРОВЕРГНУТО живым кодом** (проверено при лендинге): акт 5 внёс фолбэк `.db` и назвал прежний отказ багом (`platform/internal/runner/artifacts.go:82-90`=`The key is OPTIONAL to the engine`); первая редакция этой строки повторила клейм хендоффа зоны, не сверив с деревом — остаток чисто договорной · **(л) остаток закрытой строки 148 — Приложение А компаньона.** ⚠ Сама 148 СНЯТА 21.08 по слову владельца: она держала мир, которого больше нет — «карта потеряна инцидентом 08.08, реконструкция у оркестратора №15, решить, брать ли черновиком». Реконструкция брать нечего: прежняя карта вела «причина движка → ФРАЗА», то есть предполагала, что фразу рисует СЕРВЕР, а батч 0.3.0 эту политику ОТМЕНИЛ — `Note` несёт машинный `code`, фразу рисует клиент, `Note.message` с провода снят (`14-api-contract/README.md:1097`=`Карта «причина → КОД контракта`). Реально пуста другая клетка: соответствие причин движка кодам замечаний, и правила её заполнения в компаньоне уже написаны (фраза по ДОККОММЕНТУ `disposition.go`, а не по имени константы — инверсия уже стоила двух фраз; класс 2 схлопывается в ОДИН код). Исполнитель — контрактная сессия, не оркестратор ⚠ **ПЕРЕ-ДИСПОЗИЦИЯ 27.08 (D39.158 п.7):** хвост (з) закрыт — «ратификации не нужно» отвечало на вопрос про один файл решений, не про флажок; движковый гейт полноты снят, не обойдён.| контракт/доки | скоро (следующее касание контракта, вместе с 200) | контрактная сессия | приёмка P7 (D39.153) | | 204 | **Движок не публикует причины флагов ДАННЫМИ — карта причин у платформы рукописная и расходится молча** (релей §7(в) хендоффа P7; зона платформы в чужой бэклог не пишет и просила строку сюда — `platform/docs/archive/platform-PROGRESS-P7.md:206`). Сегодня `platform/internal/ingest/notes.go` держит рукописную копию закрытого словаря ЧУЖОЙ зоны: 15 причин движка → коды замечаний контракта, сверено побайтно на 20.08 и верно СЕЙЧАС. Импортировать движок платформе нельзя (D39.85 — разные модули), поэтому расхождение появится молча в тот день, когда движок добавит причину: платформа отдаст `unspecified` и напишет ERROR в лог, то есть деградация честная, но карта протухнет незаметно. Лечение: движок публикует свой словарь причин артефактом-данными (тем же классом, что манифест и сайдкары банка), платформа читает его вместо копии. ⚠ Носителем был ТОЛЬКО регистр платформы (PD-246) — то есть обязательство к зоне движка жило внутри зоны автора; поймано ревьюером полноты выгрузки при лендинге | бэкенд | когда-нибудь (со следующим касанием эмиссии флагов) | отдельное решение | приёмка P7 (D39.153), релей §7(в) | | 205 | **Гейт якорей: ИСПОЛНЕН 21.08 — сверка по содержимому, двусторонняя дельта, объявленная слепота.** Был: проверял только существование файла и переполнение номера, поэтому дал ЗЕЛЁНЫЙ на четыре якоря, сломанных архивацией — а оркестратор на эту зелень опёрся вместо ручной сверки. Стало (`docs/scripts/counts.py`): якорь может нести ОПТ-ИН токен ожидания `` `путь:12-14`=`подстрока` ``, и такой якорь сверяется по содержимому; область расширена с `docs/` на `platform/docs` и `frontend/docs` (98 доков); из хука убран быстрый выход по «коммит не трогает docs/» — он ампутировал ЦЕЛЕВУЮ сторону, а цели у нас в основном КОД, то есть главный двигатель протухания приезжал ровно тем коммитом, на котором хук молчал (проверено ТЕСТ-КОММИТОМ, не ручным прогоном — норма D39.148). ⚠ **Разбор с внешним ревьюером снял три моих ошибки дизайна:** (1) у дельты ДВЕ роли, и путать их нельзя — ТРЕБОВАТЬ токен можно только на затронутых строках (иначе долг блокирует всех), а ПРОВЕРЯТЬ уже написанные токены дельта ограничивать не должна вовсе: токен был истинным при записи, значит упавший — гниль по построению; corpus-wide проверка схлопывает «пропущенное окно» (коммит без хука, `--no-verify`, свежий клон, проигнорированный warn) из «навсегда» в «до следующего прогона»; (2) окно диапазона `80-82,243` считалось как `min..max` — 164 строки, и токен, случайно живущий внутри, красил зелёным; теперь ОБЪЕДИНЕНИЕ отрезков; (3) литеральная сверка токена была машиной ложных тревог — фраза, разорванная переносом строки в цели, непроверяема в принципе, а будущая пере-вёрстка абзаца кричала бы «цель уехала» на невиновного; теперь ПРОБЕЛЬНАЯ нормализация (детерминированная и симметричная), разметка остаётся литеральной. ⚠ Мой довод «неудобство и есть механизм» был неверен: мой собственный случай был FALSE POSITIVE самого гейта — токен был истинным, цель не двигалась. **ОБЪЯВЛЕННАЯ СЛЕПОТА (печатается гейтом каждый прогон):** якорь БЕЗ токена — только существование и длина · подмена цели файлом с тем же путём ловится только токеном · цели читаются из ДЕРЕВА, поэтому чужой незакоммиченный WIP может транзиентно и уронить токен, и спасти · токен доказывает ссылочную свежесть, НЕ истинность фразы дока о цели · в ручном прогоне «дельта» = всё незакоммиченное кем угодно, поэтому ТРЕБОВАНИЕ токена живёт только под `--from-index`. **ОСТАТОК:** миграция корпуса на токен-форму органическая — токен требуется на ЗАТРОНУТЫХ строках, поэтому счёт растёт сам с каждым касанием. Числа здесь НЕ держим (первая редакция держала «4 из ~580» и протухла за сутки): текущий счёт токенов, якорей и доков гейт печатает КАЖДЫЙ прогон — `python3 docs/scripts/counts.py --lint` | оркестратор/доки | сделано 21.08, миграция органическая | исполнено; остаток гасится касаниями | аудит работы №18 + разбор с внешним ревьюером, 21.08 | -| 206 | **Канон 0.4.0 объявляет ручку, которой зона больше не отдаёт — ломающий минор 0.4.0 → 0.5.0** (контрактная половина `PD-370`, лендинг P8-FIX D39.154). `D39.144` отменил пер-термную подпись 16.08, зона сняла write-путь 22.08, а словарь канона его держит: путь `POST /books/{bookId}/bank/decisions`, глагол и три схемы — **~17 мест в `openapi.yaml` + ~8 в компаньоне**, поимённый разбор (6 мест прямой лжи, 3 самопротиворечия) — `platform/docs/archive/platform-PROGRESS-P8.md`, раздел «Насколько страдает контракт». ⚠ **ОБЪЁМ ШИРЕ ОДНОЙ РУЧКИ — сверено аудитом доков 22.08 против кода платформы, и это меняет форму работы:** (1) пер-термный механизм решений живёт ещё в ПЯТИ местах ЖИВЫХ, обслуживаемых схем — после удаления пути канон продолжит учить механизму, которого нет; (2) §2.9 компаньона («Подпись — набор решений») стоит на отменённой модели ЦЕЛИКОМ и просит поле под таблицу, в которую больше ничего не пишется; (3) таблица §3 компаньона — единственное место, где ведётся статус построенного, — объявляет НЕ ПОСТРОЕННЫМИ шесть поверхностей, которые P7 построил, а P8 сохранил, и держит снятую ручку строкой «НЕ ПОСТРОЕНО, вход P7», то есть как отложенную работу; (4) провенанс-якоря компаньона на `httpapi/v0.go` сдвинулись примерно на +200 строк и указывают в посторонний код. **И дыра, которую спил вскрыл:** `approve`+`dst` был ЕДИНСТВЕННЫМ живым носителем того, что D39.144 оставила пер-термным — «поправить термин», — а «добавить свой термин» модель называет, и в контракте его нет вовсе. То есть у собственной фразы канона «correct what they care to, then release it with ONE resume» ручки не осталось. До правки деплой обслуживает на одну ручку меньше, чем объявляет — цена, названная заранее, а не найденная приёмкой. ⚠ **БАТЧ РАЗДЕЛЁН 22.08 по итогам сквозной трассировки — и довод, которым я его склеивал, снят.** Прежняя редакция строки везла 206 ОДНИМ минором со строкой 200 и хвостами 203 «ради одной перегенерации фронта вместо трёх» (D39.152 п.7). Довод сегодня почти ничего не весит: зеркало фронта ратифицированно стоит на 0.2.3 и при разморозке платит переход ОДНИМ куском в любом случае. А блокеры у этих работ РАЗНЫЕ, и склейка подчиняла незаблокированное заблокированному: **206 не заблокирована ничем**; ручка правки термина заблокирована ратификацией 199(а); **строка 200 заблокирована ответом ДВИЖКА** на вопрос «может ли майнинг объявлять знаменатель»; из хвостов 203 в минор едет одна текстовая правка (а), а (з) и (к) — ратификации, а не правки. **Порядок:** 0.5.0 СЕЙЧАС и только гигиена — она снимает живую ложь (деплой обслуживает на одну ручку меньше, чем объявляет) и не ждёт ничьих решений; новая ручка, сквозной прогресс и хвосты — 0.6.0 после ратификаций. Объём гигиены назван: ~14 мест в `openapi.yaml` (4 блока / ~105 строк на снос плюс проза в 10 живых схемах) + ~7 живых мест в компаньоне + таблица §3 (уже пере-снята 22.08) + одна строка в платформе (`ContractVersion`, иначе краснеет гейт `gates/contract_test.go`). ⚠ **ДОБОР СОСТАВА аудитом 22–23.08 — с ЧЕСТНОЙ атрибуцией, потому что это находки оркестратора №19, а не ратифицированные факты.** **Пере-проверено мной лично:** (а) строки :672/:673 таблицы §3 компаньона объявляют отсутствующей платформенную проекцию банка и замечаний, а она ПОСТРОЕНА — маршруты `platform/internal/httpapi/v0.go:79-80`=`GET", path: "/books/{bookId}/bank` и хендлеры `platform/internal/httpapi/reading.go:193`=`func (h *v0) listBank`; (б) зеркало фронта стоит на 0.2.3 против канона 0.4.0, и сверка канон↔зеркало существует ТОЛЬКО прозой. **СО СЛОВ панели, мной НЕ пере-проверено — исполняющая сессия проверяет сама:** баннер таблицы §3 объявляет её пере-снятой при трёх строках против кода · ≈19 голых провенанс-якорей компаньона на код платформы протухли. ⚠ **ВОЗВРАЩЕНО В СОСТАВ 23.08 — моё прежнее опровержение стояло на ЛОЖНОМ основании.** Я написал «слова „пять“ в строке нет вовсе» — оно есть, тремя предложениями выше: «пер-термный механизм решений живёт ещё в ПЯТИ местах ЖИВЫХ, обслуживаемых схем», и рядом стоит «проза в 10 живых схемах». Две цифры об одном множестве сосуществуют и противоречат друг другу; клейм панели «ярлыки перепутаны» я НЕ разрешил и снимать его было нельзя. **Машинный ПОЛ для исполняющей сессии, с командой и с явной границей пола:** `grep -cE 'BankDecision|bank/decisions' docs/architecture/14-api-contract/openapi.yaml` → **9**, и раскладка их такая — **восемь внутри четырёх сносимых тел** (путь `docs/architecture/14-api-contract/openapi.yaml:469`=`/books/{bookId}/bank/decisions:` плюс три схемы на строках 1939, 1980, 1992) **и одно прозаическое** в живой схеме `BankTerm` (строка 1830). ⚠ **Граница пола названа честно: СЕМАНТИЧЕСКАЯ проза о пер-термной модели этим токеном НЕ ловится** — пример на строках 442–446, и её свип, как и разъятие «пять мест» против «десяти схем», остаётся работой минора: пол даёт нижнюю границу, а не ответ | оркестратор/контракт | скоро (следующим минором) | ратифицировано D39.154 п.8 | D39.154, `PD-370` | | 207 | **Пол самосогласованности манифеста стоит только у материализатора, интейк тот же документ ПРИНИМАЕТ — и по такой книге можно ОПЛАТИТЬ прогон** (`PD-367`, вторая волна ревью P8-FIX). Манифест `{ChaptersTotal: 120, UnitsTotal: 400}` с пустым списком глав `Whole()` отвергает, а `books.Parse` заводит книгу `not_started` с `chapter_count=120` и пустым деревом; потолок считается ОТ `chapter_count`. **Очевидное лекарство опровергнуто исполнением самой зоной:** применить `Whole()` на интейке нельзя — вся батарея интейка ездит на манифестах без списка глав, контракт интейка это только счётчики. Значит решение не зонное: либо контракт интейка меняется, либо пол ставится в другом месте цепи | оркестратор → платформа | скоро | нужно решение (лекарство ломает запиненный контракт интейка) | D39.154 п.10, `PD-367` | | 208 | **Эскалационный хоп на Gemini ТИХО ТЕРЯЕТ инъекцию банка памяти — книга едет без канона имён** (живая проба полигона 22.08, подтверждена вендор-докой и пере-проверена оркестратором по коду). OpenAI-совместимый слой Gemini принимает РОВНО ОДНО системное сообщение, лишние выбрасывает МОЛЧА — без ошибки, при `finish=stop` и внешне правильном переводе. Замер полигона на одном и том же системном тексте 7970 знаков: одним сообщением → `prompt_tokens` 2994 и маркерная инструкция ИСПОЛНЕНА, двумя → 535 токенов и инструкция НЕ исполнена. Механизм вендорский, поэтому уход на нативный API НЕ лечит: `systemInstruction` там — один объект `Content`. Наша сторона: `backend/internal/pipeline/render.go` штатно строит ВТОРОЕ системное сообщение (инъекция глоссария/STM), `backend/internal/llm/httpllm.go` `toOpenAIMessages` копирует сообщения дословно, склейки под Gemini нет нигде — а `gemini-3.1-pro-preview` стоит в ратифицированной цепочке эскалации ВО ВСЕХ четырёх арм-конфигах. ⇒ любой хоп на Gemini при непустой инъекции теряет глоссарий, и **это невидимо гейтам годности**. ⚠ Класс в проекте УЖЕ решён рядом и не обобщён: `provider_anthropic.go` сворачивает ведущий system-префикс в одно поле и падает громко на system посреди диалога. Оговорка D22.3 «экспозиция Ф1 нулевая» держится на МАРШРУТЕ (до Gemini в дефолте не доходят), а не на коде — то есть на одном включённом `budget_usd` от дефекта в проде. Полигон у себя починил склейкой по вендору (`eval/dovodka/rol.py`), в движок не лез; квирк записан в `00-provider-quirks.md` | бэкенд | **скоро (до первого платного прогона с ненулевым `budget_usd` эскалации)** | склейка системных сообщений для Gemini в транспорте + пин | пинг полигона №19 (22.08), вендор-дока `ai.google.dev/api/generate-content` | | 209 | **Риг живых проб не может залогировать тела запросов ни при каких env** (находка бэкенд-пака честности 17.08, оставшаяся без носителя до аудита доков 22.08): `LOG_LLM_BODIES=1`+`LOG_LEVEL=debug` объявлены носителем тел, но ключ читается из `obs.ReqInfo` контекста, а `live_reprobe_test.go` строит ctx БЕЗ `WithReqInfo` — гейт `obs/logging.go` не пропускает. Обход, которым пользовались: ручной дамп сообщений в файл. Цена — каждая живая проба, которой нужно прочитать провод глазами, платит за обход заново | бэкенд | когда-нибудь (следующее касание рига живых проб) | малое касание (`WithReqInfo` в риге) + пин | пак честности 17.08, вынесено аудитом доков 22.08 | diff --git a/docs/README.md b/docs/README.md index bd1f0693..736c548c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) — ратифицированное направление зоны: аутентификация, деньги, стандарты, скорость** · архив промтов). diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index 8b6eb02c..8aac0dba 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.160; +# Реестр D-нот — карта актуальности v2 (D1–D39.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`. | ✅ | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index d406ed25..3ca33c50 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.160 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.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`, хотя движок их печатает — названная цена, не забывчивость. diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md index 46563ba3..6cc1cbe5 100644 --- a/docs/architecture/14-api-contract/README.md +++ b/docs/architecture/14-api-contract/README.md @@ -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`) и полосой отказов 10–19 (`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 — шаги 3–5 (скоуп кадра, дельта-чтение, `staleTime`) | шаги 1–2 закрыты 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::::` не предупреждением на `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` | diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml index 452641a8..3f653a45 100644 --- a/docs/architecture/14-api-contract/openapi.yaml +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -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 diff --git a/docs/CONTRACT_MINOR_SESSION_PROMPT.md b/docs/archive/prompts/CONTRACT_MINOR_SESSION_PROMPT_2026-08-27.md similarity index 95% rename from docs/CONTRACT_MINOR_SESSION_PROMPT.md rename to docs/archive/prompts/CONTRACT_MINOR_SESSION_PROMPT_2026-08-27.md index 8246b5e8..5204471b 100644 --- a/docs/CONTRACT_MINOR_SESSION_PROMPT.md +++ b/docs/archive/prompts/CONTRACT_MINOR_SESSION_PROMPT_2026-08-27.md @@ -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`). Это существенно: тело новой двери больше не сочиняется — оно ВЫВОДИТСЯ из словаря diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 0a2dc6c0..2140b811 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -64,6 +64,7 @@ | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | |---|---|---|---|---|---|---| | PD-398 | standards | info | `docs/scripts/counts.py` `malformed`, строки `PD-99` и `PD-197` этого файла | **Гейт формы регистра ловит только НЕДОСТАЧУ ячеек, а не избыток — и в файле уже есть две строки, которые он пропускает.** `malformed()` сравнивает `if n < shape`, поэтому строка с лишним символом вертикальной черты внутри инлайн-кода проходит молча: счёт ячеек через `awk` с разделителем-чертой даёт `PD-99` (11 ячеек) и `PD-197` (10), а `counts.py --check` печатает `битая форма: []`. Обе строки допаковые, обе сломаны греп-альтернацией и регекспом в тексте. ⚠ Что СМЯГЧАЕТ и почему это `info`, а не выше: `col()` считает колонки С КОНЦА, поэтому статус и вес таких строк читаются ВЕРНО, и сегодняшние числа зоны не врут — пере-проверено, `PD-99` и `PD-197` попадают в свои корзины правильно. Дыра латентная: лишняя черта в одной из ТРЁХ последних колонок сдвинет уже их, и гейт снова промолчит. Пак наткнулся на это ДВАЖДЫ собственной рукой, и второй раз — этой же строкой: первая её редакция цитировала команду счёта ВМЕСТЕ с разделителем-чертой и сломала себя ровно тем, что описывает. моя же правка `PD-396` внесла конвейер в ячейку, счётчик весов выдал мусорный ключ вместо `info`, а `битая форма` осталась пустой — то есть симптом виден в СЧЁТЕ, а не в проверке формы, которая для этого и написана. Лечение однострочное и не моё: `if n != shape` вместо `if n < shape` (зона `docs/`, решение оркестратора). Родня по классу — весь этот пак: гейт, доказывающий меньше, чем читается ⚠ **ПЕРЕ-ФОРМУЛИРОВАНА приёмкой (оркестратор №19, 27.08, D39.159 п.8): предложенная правка ОТКЛОНЕНА замером.** `n != shape` краснит СЕМЬ законных строк (пять бэклога, две регистра), потому что колонки читаются С КОНЦА НАМЕРЕННО — `counts.py:132` называет это прямо и приводит ровно этот случай (строка бэклога 127 несёт три слова через вертикальную черту внутри инлайн-кода). Избыток в ПЕРВОЙ содержательной ячейке легален, и числа из-за него не врут: проверено, статус и вес читаются из хвоста, `PD-99` и `PD-197` разбираются верно. **Настоящий остаточный риск — избыток в ХВОСТОВОЙ ячейке:** там лишняя вертикальная черта сдвинула бы именно статус или вес, и тихо. Ловится не счётом ячеек, а сверкой хвостового словаря (статус ∈ {open, fixed, accepted-risk}); сегодня у такой сверки два известных доброкачественных исключения — `PD-59` и `PD-273` несут статус прозой. Строка остаётся ОТКРЫТОЙ под эту формулировку ⚠⚠ **ВТОРАЯ ПОЛОВИНА ПОСТРОЕНА 27.08 (оркестратор №19), смягчение строки ОПРОВЕРГНУТО.** Довод «читаем с конца, значит лишняя черта безвредна» неверен: он защищает СТАТУС (третья с конца), но не ВЕС — тот читался `col(l,6)`, то есть тоже с конца, а от конца он далеко, и любая лишняя черта в «сути» сдвигала его молча. Живой пример — сама `PD-197`: вес читался из ячейки «где». Эмпирика, принесённая зоной: три случая за двое суток, все у тех, кто в этот момент про этот класс ПИСАЛ (правка `PD-396` · первая редакция этой строки · пере-формулировка её приёмкой), и во всех трёх «битая форма» оставалась пустой. **Построено три вещи, каждая проверена исполнением:** (1) парсер уважает markdown-экранирование — `PD-99` несла корректно экранированные черты, и ломался парсер, а не строка; (2) ВЕС регистра читается с НАЧАЛА (`cells(l)[3]`), потому что ведущие ячейки коротки и черт не несут, хвостовые тоже, а свободный текст живёт в СЕРЕДИНЕ — оба конца безопасны, середина нет; (3) новый гейт `tail_vocab` судит СЛОВАРЬ хвоста (статус и вес), а не счёт ячеек, с одним грандфазерным исключением `PD-59`. Проверено подсадкой: черта в статусную ячейку краснит гейт немедленно; на чистом дереве EXIT=0, числа не сдвинулись (398/96, 3/27/66). ⚠ Правка `n != shape` по-прежнему ОТКЛОНЕНА и теперь на точном основании: после уважения экранирования в бэклоге остаются пять строк с законной сырой чертой внутри инлайн-кода, и их хвост чист. **Строка остаётся ОТКРЫТОЙ ровно на одно: у построенного гейта нет автоматического пина — он проверен подсадкой руками. Закрывать её без пина значило бы нарушить правило `PD-1` в строке, которая про это правило и есть** ⚠ **ЗАКРЫТА 27.08: пин появился.** `selftest_tail_vocab()` в `docs/scripts/counts.py` гоняется на КАЖДОМ `--check` и несёт четыре утверждения — чистая строка молчит · сырая черта в СТАТУСНОЙ ячейке краснит · экранирование законно и лишней ячейкой не считается · черта в СЕРЕДИНЕ не сдвигает вес. Проверен ПОСАДКОЙ, а не заявлением: три мутации гейта (снять уважение к экранированию · вернуть вес на чтение с конца · обезвредить словарь статуса) — пин ловит все три, базовая линия молчит. ⚠ Первая редакция пина МОЛЧАЛА на второй мутации: утверждение про вес проверяло `cells()`, а мутация меняет то, чем пользуется `register()`, — пин смотрел не туда. Переписано на настоящий путь; называю, потому что пин, проверенный одним прогоном вместо посадки, — это ровно тот дефект, который эта строка и описывает | fixed(приёмка 27.08, дерево сессии) | ревью-пак P8-REVIEW (наткнулся при правке собственной строки, подтверждено редакторским аудитом) | +| PD-399 | standards | minor | `internal/pgstore/readmodel.go:330` `bankCountsTx`, `internal/httpapi/reading_test.go:113` | **`GET /books/{bookId}/bank` шлёт два поля, которых канон 0.5.0 больше НЕ объявляет.** Минор снёс `pending_decisions` и `complete` из `BankPage` и `EventBank` вместе с пер-термной моделью, которую они обслуживали, а проекция платформы их по-прежнему кладёт на провод, и это НЕ нули: `bankCountsTx` считает `proposed`-строки, и её собственный комментарий это говорит. Клиент 0.5.0 лишние поля игнорирует по общему правилу канона, поэтому вес minor, но аллоулист-норма нарушена, а комментарий «the wire fields are the canon's» с этого минора ЛОЖЕН. ⚠ Присутствие полей ЗАПИНЕНО (`reading_test.go:113`), значит снятие — правка с пином, а не вычёркивание. Лечение — пункт **(2в)** очереди D39.156, тем же паком, что монтирует дверь `bank/corrections`. ⚠ Найдено САМОЙ контрактной сессией и принесено пингом: промт (§3.2-бис) утверждал «поля навсегда нули», а нота D39.160 п.2 — «после сноса канон и деплой совпадут точно»; верно по ПУТЯМ, неверно по ПОЛЯМ. Ошибка оркестратора, исправлена эрратой | open | пинг контрактной сессии при сдаче минора 27.08, пере-проверен приёмкой | | PD-397 | hardening | info | `internal/pgstore/credits.go:52-53`=`A ledger row is never edited: the correction is another row`, `internal/pgstore/credits.go:354-356`=`The two are never written apart`, миграция `internal/pgstore/migrations/00007_credits.sql` | **Два самых сильных денежных инварианта объявлены ПРОЗОЙ и держатся ТОЛЬКО кодом — схема их не навязывает.** `Adjust` обещает «леджер не правится, коррекция это ещё одна строка, и именно это делает сумму воспроизводимой»; `appendLedger` обещает «кэш и леджер никогда не пишутся врозь, потому что отстающий кэш — это второй ответ про деньги». Проба прямым SQL по стенду показывает, что DDL допускает нарушение обоих: `UPDATE` и `DELETE` строки леджера ПРИНЯТЫ, кэш баланса выставляется ЛОЖЬЮ и ОТРИЦАТЕЛЬНЫМ тоже. Пере-проверено координатором пака независимо от агента — все четыре приняты, и откат пробы сам же оставил расхождение кэша с леджером в 1 микро-доллар, которое поймало только сведение двумя путями, а не база. ⚠ Что схема при этом ДЕРЖИТ и что находкой НЕ является (иначе строка читается как «денежных констрейнтов нет»): знак по каждому виду строки, обязательная нота у коррекции, закрытый словарь видов, непустые `source`/`source_id`, уникальность ключа идемпотентности в пределах аккаунта, положительность сумм резервации, согласованность состояния и времени закрытия, владение книгой через композитный внешний ключ, и переполнение bigint в кэше. То есть DDL закрывает ФОРМУ строки и не закрывает ИСТОРИЮ. Цена названа и она не про сегодняшний код: пути правки леджера в Go нет, поэтому эксплуатации нет — опасны миграция данных, операторский `psql` и будущий инструмент, каждый из которых по построению идёт мимо кода, а прозу в доккомментарии не читает. Лечится либо триггером на `update`/`delete` по `credit_ledger`, либо явной записью «append-only — дисциплина кода, не схемы» рядом с обещанием. ⚠ Заведено ЗАПОЗДАЛО и это отдельный факт: работа была сделана агентом оси 1 по прямому требованию промта («попробуй нарушить каждый прямым SQL; констрейнт, которого нет, это находка»), артефакт `docs/p8-review/axis1-money/constraint-probe.out` лежал в сдаче, а строки не имел — нашёл редакторский аудит полноты. Воспроизведение: `docs/p8-review/axis1-money/constraint-probe.sh` | open | ревью-пак P8-REVIEW, ось 1 (проба агента, пере-проверена координатором; строка заведена по аудиту полноты) | | PD-396 | standards | info | `internal/pgstore/books.go:306`=`chunker_version = $4, parse_started_at = null,`, `internal/pgstore/sink.go:127`=`update books set chunker_version = $2 where id = $1`, `internal/ingest/resync.go:30`=`UnsignedBankTerms int `json:"unsigned_bank_terms"`` | **Мёртвые поля шва: у `books.chunker_version` ДВА писателя и НОЛЬ читателей, и это второй экземпляр класса, первый назвал оркестратор.** Колонку пишет интейк из манифеста движка (`FinishParse`) и пишет тейлер из хендшейка потока (`effect`); ни одного `select` по ней в зоне нет — грепом ноль. Сегодня это безвредно, но следствие названо ЗАРАНЕЕ, потому что оно семантическое, а не техническое: в день, когда читатель появится, РАСХОЖДЕНИЕ двух писателей (чанкер прогона против чанкера разбора) станет значением, и решать, какой из них правда, придётся задним числом — по колонке, у которой уже накоплена история из обоих источников. Родня — п.4 пинга оркестратора №19: `ingest.StatusReport.UnsignedBankTerms` разбирается из ответа движка и не используется НИ ОДНОЙ строкой продакшн-кода (грепом — только объявление и его доккомментарий, где поле описано как опора экрана подписи). Формулировка оркестратора применима дословно к обоим: мёртвое поле в структуре шва читается как контракт. ⚠ Заведено ОТДЕЛЬНОЙ строкой по прямому указанию закрывающего ревью старшей модели и с его же доводом: этой фразе НЕ место в `PD-166` — та строка про потерю значения между двумя автокоммитами, и её свойство построено; держать дефект-строку открытой как плейсхолдер несуществующей фичи есть ровно та патология «open, а лекарство построено», против которой пак завёл шестнадцать дописок. Диспозиция — вопрос владельца, а не зоны: либо назначить владельца колонки (один писатель), либо записать расхождение как ожидаемое до появления читателя Воспроизведение — две команды без конвейера (символ вертикальной черты в ячейку регистра не влезает): `grep -rn chunker_version platform/internal --include=*.go` даёт два `update` и ни одного `select`, `grep -rn UnsignedBankTerms platform/internal platform/cmd --include=*.go` даёт только объявление и его доккомментарий | open | ревью-пак P8-REVIEW (побочная находка сверки реестра, оформлена по указанию закрывающего ревью) | | PD-378 | bug | info | `internal/pgstore/books.go:1053`=`u.RemainingPercent = int(balance * 100 / granted)`, `internal/httpapi/v0.go` `usageState`, канон `14-api-contract` `remaining_percent` | **`/v0/usage` отдаёт `remaining_percent` вне контрактных 0..100 и зажигает предупреждение «low» на полном счёте: `balance * 100` переполняет int64.** Порог измерен точно: баланс 92 233 720 368 547 758 микро ещё даёт 99%, следующий микро-доллар даёт минус 99. Ответ нарушает схему (`minimum: 0`, `maximum: 100`), и хуже того `usageState` видит отрицательное значение ниже порога `lowCredit` и отдаёт `state: "low"` — «денег почти нет» счёту на сто миллиардов. Замерено на проводе: до гранта `{"state":"ok","remaining_percent":96}`, после `grant --usd 100000000000` → `{"state":"low","remaining_percent":-84}`; соседняя арифметика (`pricing.Scale`, `balance`) при том же балансе отвечает верно, то есть переполнение локально именно в этой строке. ⚠ Рефутер сузил minor → info: чтобы туда попасть, оператор должен добавить на счёт не меньше 92.23 млрд долларов, ни одна пользовательская ручка кредит не пишет; прецедент веса — `PD-39`. ⚠ Оговорка рефутера в другую сторону: более правдоподобный носитель — не разовая команда, а конфиг `TM_PLATFORM_SIGNUP_GRANT_USD`, у которого верхней границы нет и значение НАМЕРЕННО не печатается в стартовый лог, так что промах в нём сломал бы `/usage` каждому новому аккаунту невидимо. Воспроизведение: `docs/p8-review/axis1-money/a1-usage-overflow.sh` (сам откатывает грант) | open | ревью-пак P8-REVIEW, ось 1 (живой провод, сужено рефутером с измеренным порогом) | diff --git a/platform/internal/httpapi/capabilities.go b/platform/internal/httpapi/capabilities.go index fb5ebe9d..b47abc17 100644 --- a/platform/internal/httpapi/capabilities.go +++ b/platform/internal/httpapi/capabilities.go @@ -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 {