Ratify contract 0.4.0 and record the P7 acceptance with the second-rubezh corrections, seven new backlog rows and the fix list carried to the zone

This commit is contained in:
Claude (backend session) 2026-08-20 23:56:21 +03:00
parent 9b23e8c024
commit 6e845744e7
7 changed files with 710 additions and 79 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
# Реестр D-нот — карта актуальности v2 (D1D39.151; строка 167; титул — носитель головы, бампать при каждом аппенде — испр. оркестратором №17 15.08 по аудиту: отставал на четыре ноты)
# Реестр D-нот — карта актуальности v2 (D1D39.153; строка 167; титул — носитель головы, бампать при каждом аппенде — испр. оркестратором №17 15.08 по аудиту: отставал на четыре ноты)
> Одна строка на КАЖДУЮ ноту журнала решений: № · дата · суть · статус · где тело · темы. Ведёт оркестратор при лендингах: новая нота = новая строка ТЕМ ЖЕ коммитом (полноту сторожит `docs/scripts/counts.py --check`). Суть и статус здесь — НАВИГАЦИЯ, не контракт: при конфликте побеждает ТЕЛО ноты (живой [`05-decisions-log.md`](05-decisions-log.md) → слайсы `../archive/architecture/05-decisions-*.md`). Несущие эрраты к фактам старых нот живут в шапке живого D-лога — их читать ОБЯЗАТЕЛЬНО, реестр их не дублирует. Темы (для грепа оси «какой закон по X»): деньги · шов · банк · промпт · судья · гейты · контракт · платформа · фронт · полигон · ToS · общность · процесс · нарезка · голос · инфра · модели.
> Родословная: прозаическая карта D1D39.28 (ревизии D31D38.2) пересобрана в реестр докс-паком 167 (09.08.2026, №16): извлечение 6+6 агентов по worksheet `../archive/reports/DOC_AUDIT_INVENTORY_2026-08-09.md`, полнота сверена скриптом против заголовков всех шести файлов. «тело: жив» = живой D-лог; «слайс N» = `../archive/architecture/05-decisions-N.md`.
@ -204,3 +204,5 @@
| D39.149 | 17.08 | Приёмка пака «честные числа и статусы» (панель 5 линз + пере-раны оркестратора): ПРИНЯТ С ФИКС-ЛИСТОМ ФЧ-1…ФЧ-8, регрессов нет; ратифицированы паника→exit 1 (PD-212 со стороны движка закрыт, полосы не двинуты), PanicError без Unwrap, пере-прайс фактическими токенами через шов сеттла с починкой обеих половин гейта, выборочный порог в риге (4/5 PASS); деньги пробы $0.005582424 сошлись двумя независимыми путями; строки 176/181/187 закрыты, 172-г запрещено закрывать этим прогоном, новые 194 (деньги банковых ролей вне согласия) · 195 (маркер поколения — схема, на владельце) · 196 (fatal error residual); владельцу — ось МОДЕЛИ в числе согласия | ЖИВОЕ: фикс-лист ФЧ и два решения владельца | жив | деньги приёмка шов бэкенд |
| D39.150 | 17.08 | Ось МОДЕЛИ в числе согласия — решение владельца: прайсить по модели, которую стадия резолвит СЕЙЧАС, + округление вверх + оговорка в тексте согласия; обещание механизма переформулировано с точности на НАПРАВЛЕНИЕ ошибки («никогда не занизить молча»), обещание «status не может назвать другую сумму» снимается при исполнении; фикс-лист ФЧ-1…ФЧ-8 записан строкой 197 без заказа сессии; маркер поколения (195) отложен и вынесен на внешнее мнение — найдено, что это четвёртый жилец семьи 49/124/195 с общим корнем «денежная таблица не помнит конфигурацию»; ПТ-14 понижен ✅→🔶, промт пака в архив с баннером-исходом | ЖИВОЕ: строка 197; вопрос 195 у внешнего ревьюера | жив | деньги владелец бэкенд процесс |
| D39.151 | 17.08 | Владелец выбрал вариант А: маркер поколения отдельной колонкой на `checkpoints` НЕ строится, строка 195 закрыта как самостоятельная стройка и переселена осью (г) «идентичность оплаченной работы» в дизайн-пак 124 (гейт — его триггеры: 185/МАСШТАБ/внешний потребитель числа); внешний разбор опроверг две посылки оркестратора — колонка НЕ даёт тождества ($0-пере-пин законно несёт старую цену на новом снапшоте, бэкфилла нет ⇒ вывод остаётся навсегда) и «семья 49+124+195» слишком широка (49 = легальность переиспользования, 195 = честность учёта); настоящий корень — отсутствие связи «строка ↔ оплатившие её вызовы»; срочности нет под контрактом D39.150, остаток чинится без схемы строками 197/194 (194 миграции НЕ требует — RoleSpentUSD ключуется по роли) | ЖИВОЕ: ось (г) пака 124; строки 197/194 | жив | деньги владелец бэкенд |
| D39.152 | 20.08 | Контракт API v0 **0.4.0 РАТИФИЦИРОВАН** (синк с платформой по её записке, 14 пунктов AN; author≠ratifier соблюдён контрактной сессией; условие закрытия PD-327 наступило, САМА строка регистра ждёт правки зоной — пункт 10 фикс-листа); рубеж исполнен ревьюером другой модели (чтение обеих версий целиком, «ратифицировать с правками», корневой enum 16 значений не тронут); четыре текстовые правки внесены оркестратором при ратификации (сеттер `stop_requested` в `stopRun` · стухшая фраза компаньона §6в A против его же аддендума · «cleared by УСПЕШНЫЙ resume» · приписка третьего случая к таблице резюма); пункт G («сделано» = последний проход этого деплоя) ратифицирован как шаг-независимый — рамка снята владельцем 20.08 требованием, чтобы форма пайплайна не была видна клиенту; ⚠ клейм зоны «ломающая правка одна» верен для диффа ТИПОВ и неточен поведенчески — против 0.3.0 расходятся ЧЕТЫРЕ места; оговорка про пер-термные решения расширена с `decline` на всю ручку (временная, снимается строкой 199а); следующий минор (сквозная полоса, 200) не втиснут — пока фронт заморожен, миноры бесплатны | ЖИВОЕ: канон 0.4.0 | жив | контракт платформа фронт владелец |
| D39.153 | 20.08 | Приёмка и лендинг P7 платформы (пять актов, читающая поверхность): **ПРИНЯТ С ФИКС-ЛИСТОМ**; шесть линз в изоляции + пере-раны и 8 собственных посадок оркестратора (поймано 7); батарея 18 пакетов EXIT=0 скипов 0, реестр 327/65 открытых без major, тестов 532 (не 533), семь удалённых имён имеют преемников; замер разворота 00016→00022 пере-выведен независимо (джойн 14.6 · счётчик 5.3 · без счётчика 2.9 мс) — разворот законен, но процент назван тремя носителями по-разному; НЕ поймана дыра: снятие `structure_version`/`revision` из `emitFrame` проходит всю батарею; ратифицированы долг-с-арендой на материализацию (§33/§34), `edit_wave` как монотонное свойство КНИГИ от объявления движка (§35), владелец клейма идемпотентности + тождество дайджестом, halt с аккаунта, счётчик замечаний на главе, демонтаж гейта полноты банка; новые строки 198203; решения владельца — sqlc берём (+ разбор собранных запросов Postgres'ом, которого в предложении зоны не было), живой прогон ПОСЛЕ холодного, сквозная полоса прогресса; ⚠ эррата к D39.144 п.2 (пер-термная формула рождена в ДВИЖКЕ 19.07 как лечение ливлока, контрактная линия изобрела из неё ГЕЙТ 04.08); `ContractVersion` оркестратором НЕ правился — первый пункт зонного фикс-листа | ЖИВОЕ: фикс-лист в пинге №18 зонного журнала; строки 198203 | жив | платформа приёмка контракт шов деньги |

View file

@ -1,4 +1,4 @@
# Журнал решений оркестратора — контракт D1D39.151 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
# Журнал решений оркестратора — контракт D1D39.153 (живой файл: карта · эрраты · живые тела · голова 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+).
@ -7,6 +7,7 @@
> ⚠ **Эррата 15.08-в (D39.136 п.5):** форма контракт-ревью «фазный воркфлоу оркестратора» ОТМЕНЕНА словом владельца тем же днём — исполняет ОТДЕЛЬНАЯ СЕССИЯ со своим онбордингом (норма D39.120 п.2; оркестратор — автор части ратификаций 0.2.3, author≠reviewer); запущенная воркфлоу-фаза 1 остановлена, её результаты выброшены; промт — `docs/CONTRACT_REVIEW_SESSION_PROMPT.md`.
> ⚠ **Эррата 16.08-д (D39.144 п.3):** клейм «`bank_decisions_incomplete` в файле 0» проверял ТОКЕН, а не ПРАВИЛО — четыре места канона/компаньона продолжали нести снятый гейт полноты прозой (`BankDecisionsResult.complete` «stop clears ONLY on a complete set» · `BankPage.complete` «can be cleared» · компаньон §2.14 и А-2); дочищены 16.08 при пересборке промта P7 (нашёл опровергатель промта). Плюс факт, который D39.144 не назвала: гейт полноты УЖЕ ОТЛИТ в коде платформы (`internal/runs/reconcile.go:934-938`) — его ДЕМОНТАЖ заказан P7 явным пунктом.
> ⚠ **Эррата 16.08-г (D39.138 п.2а, часть «TermStatus снять с провода»):** ОПРОКИНУТА пост-ревью АВТОРА отчёта research/28 (по заказу владельца, речек оркестратора уликами): обоснование Б-0 «ни один экран их не рисует» подменяло «экрана подписи ещё НЕТ» — ось статуса ПРОДУКТОВАЯ (спека `:1070-1072`: «главный фильтр работы» S5; шов клиента уже потребляет — `frontend/src/api/vocabulary.ts:159-163`). Читать так: ось `TermStatus` ОСТАВИТЬ, движковые слова значений переименовать в продуктовые; «`TermOrigin` снять/переименовать» (`ruby` — паро-специфика) — В СИЛЕ. Контекст метода: Б-0/Б-11а/Б-23 добавлены в отчёт ПОСЛЕ раунда адверсариального судейства сессии — их основания проверять строже (помечено в ревью-шапке отчёта).
> ⚠ **Эррата 20.08 (D39.153 п.7б):** D39.144 п.2 атрибутирует пер-термный гейт полноты банка «изобретению КОНТРАКТНОЙ линии (Ф-14 → спека → 0.3.0)» — верно про ГЕЙТ и **неверно про ФОРМУЛУ**. Формула «стоп очищается, когда каждый терм промоутнут ИЛИ отклонён» родилась 19.07 В ДВИЖКЕ как лечение ливлока R1-FL-B (отклонённый терм переспрашивался каждый прогон — тело D39.19, слайс `../archive/architecture/05-decisions-D39-arch-reset.md:22`) и живёт там до сих пор (`backend/internal/pipeline/mining.go:80-82,243`); контрактная линия 04.08 переписала её дословно и вывела из неё ЭКРАННЫЙ гейт — счётчик «решено N из M» и запрет продолжить (коммит `16e20e5`, `frontend/docs/API_CONTRACT_INPUT.md:258-264`). **Следствие, из-за которого эррата несущая:** снимая гейт, D39.144 адресовала только контракт, поэтому движковая половина осталась БЕЗ явной отмены — она сегодня ОБОЙДЕНА платформой (флаг не передаётся на resume), а не устранена; носитель — строка 191(б), развилка — строка 203(з).
> ⚠ **Навигация (актуализация 07.08, эра D39.1xx):** append-only-дисциплина (D23.3) означает, что
> НЕВЕРНЫЙ ФАКТ внутри старой ноты не переписывается, а получает эрратау — и тогда он опасен ровно
> тем, что грепающий его найдёт ПЕРВЫМ, а эрратау ниже может не дочитать. Поэтому поправки к фактам
@ -517,3 +518,47 @@
**3. Настоящий корень назван (для будущего пака 124).** Не «денежная таблица не помнит конфигурацию», а «в сторе нет первоклассной связи *строка диспозиции ↔ вызовы, которые её оплатили*»: `checkpoints` append-only на жизнь книги, `chunk_status.cost_usd` перезаписывается каждым поколением, и единственный пер-поколенный авторитет — деньги самой строки. Снапшот на чекпоинте — прокси этой связи, а не примитив; класть в схему надо провенанс, а не флаг конфигурации.
**4. Почему срочности нет.** Под контрактом D39.150 («гарантируется НАПРАВЛЕНИЕ ошибки, не точность») ни один исход нынешнего механизма не занижает молча: нормальный случай точен, недостача несётся историческими деньгами С РАСКРЫТИЕМ, пойманный откат откатывается к устаревшей цене ВСЛУХ. Остаточные нарушения контракта чинятся БЕЗ схемы и уже имеют носителей — **197** (прибить гарантии тестами и довести раскрытие до `status`) и **194** (деньги банковых ролей в число согласия). ⚠ Уточнение цены 194, сверенное оркестратором: миграция ей НЕ нужна — `store.RoleSpentUSD` уже ключуется по РОЛИ чекпоинта, а не по колонке класса (`internal/store/ledger.go:105-118`, докстринг прямо: «a new call class needs no migration to become countable»). (17.08.2026, оркестратор №18) ✅
## D39.152 — КОНТРАКТ API v0 **0.4.0 РАТИФИЦИРОВАН** (синк с платформой, 14 пунктов; author≠ratifier соблюдён сессией); четыре текстовые правки внесены при ратификации; клейм «ломающая правка одна» верен для типов и неточен поведенчески (20.08). ✅
**1. Что ратифицируется.** `docs/architecture/14-api-contract/openapi.yaml` + компаньон, `info.version: 0.4.0`. Автор — ОТДЕЛЬНАЯ контрактная сессия (20.08) в ответ на записку зоны платформы `platform/docs/CONTRACT_SYNC_FROM_PLATFORM.md` (14 пунктов AN); разбор каждого — §6в компаньона. Сессия ратификацию за собой не записала и сдала текст оркестратору — норма author≠ratifier исполнена ею самой, отмечаю как правильное поведение. **0.3.0 действовал до этой ноты**; зависимость зоны (PD-327) закрывается ею.
**2. Рубеж перед ратификацией исполнен** (норма: тяжёлая ратификация — контракт — смотрится ревьюером другой модели, мнение владельцу ДО). Fable-5, чтение обеих версий ЦЕЛИКОМ: вердикт «ратифицировать с правками». Пере-проверено ИМ независимо дважды: структурный дифф схем и дифф генерённых типов дают ровно одну строку `stop_requested`. Корневой `ErrorCode` — 16 значений, байт-в-байт не тронут; все открытые уровни (`ErrorCause`, `ErrorItem.code`, `Note.code`, три reason-словаря) объявлены открытыми явно. Все 14 пунктов записки имеют диспозицию, остатков прежних формулировок ноль (грепы по снятым фразам = 0).
**3. Четыре правки внесены оркестратором при ратификации** (микро-дефекты доков своей зоны; в yaml — без маркера, описания компилируются в типы фронта): (а) `stopRun` не называл СЕТТЕРА нового поля и отправлял клиента хранить состояние у себя — прямо против описания самого поля («survives a reload, which a client's own pending state does not»); теперь названо, что стоп-вызов и есть сеттер и `202` уже несёт `true`. (б) Компаньон §6в A нёс «пустой СЧЁТ — не ответ этого вызова», что опровергается его же аддендумом и каноном (`credit_unavailable` — ровно ответ этого вызова); исправлено с пометкой. (в) «cleared by `resume`» уточнено до УСПЕШНОГО resume — отвергнутый буквально тоже resume. (г) Третий случай `resumeRun` приписан к таблице явно, как это сделала вторая ось: судится ПОСЛЕ таблицы и только там, где та сказала `202`.
**4. Пункт G («сделано» = последний проход, который книга на ЭТОМ деплое реально получает) — ратифицирован, и вот на каком основании.** У G, в отличие от J и A, записанного слова владельца не было, и зона сама пометила его продуктовым. Развилка снята владельцем 20.08 иначе, чем спрашивалось: он отверг саму рамку «какая фаза считается» и потребовал, чтобы форма пайплайна вообще не была видна клиенту. Формулировка канона этому УЖЕ удовлетворяет дословно — «the last pass the book actually gets, **whatever that is**» — и потому ратифицируется как есть: она не связывает пайплайн, а альтернатива (счёт по редакторской волне) — живой денежный дефект, на деплое без редактора ни одна глава не сделана никогда и шкала покупки не подрезается. Признак монотонен ⇒ книга, прошедшая редактирующий пайплайн, назад не откатывается.
**5. Клейм зоны «ломающая правка ровно одна» — верен для ТИПОВ, неточен поведенчески; записано, чтобы следующая приёмка на него не опёрлась.** Дифф генерённых типов слеп ко всему, что не двигает тип. Против 0.3.0 расходятся ПЯТЬ мест (⚠ первая редакция этой ноты называла четыре — пятое принесла проверка записей при лендинге): `Run.stop_requested` (типовое) · тождество интейка по дайджесту СОДЕРЖИМОГО против дословного 0.3.0 «compared over the DECLARED parts… and **never over the bytes themselves**» · `409 ceiling_reached` на исчерпанном прогоне там, где таблица §resumeRun 0.3.0 говорит «continues it — `202`» · валидатор/`304` на `getRunOptions` и `getUsage` сверх объявленных · **`410 Gone`**: 0.3.0 отвечает им «главе, которая БЫЛА и больше нет» и прямо противопоставляет `404` («would mean a typo»), 0.4.0 — «whether or not the id was ever real» (носитель расхождения назван зоной сама: `platform/docs/DEFECT_REGISTER.md` PD-253 — «расхождение снято НЕ кодом, канон изменён в нашу сторону»). Все пять 0.4.0 благословляет ⇒ цена уплачена этой ратификацией, дефекта пака здесь нет.
**6. Оговорка про пер-термные решения расширена с `decline` на ВСЮ ручку — внесена в канон при ратификации.** Прежний текст предупреждал только про `decline`, а инертны одинаково `approve`, `decline` и впечатанный `dst`; у `dst` не было ни слова, и грепающий канон расширения не увидел бы (поймано проверкой записей при лендинге — клейм ноты опережал текст). Теперь `BankDecision.action` несёт «NOTHING recorded here reaches the work itself yet», `dst` — отсылку к ней, и там же сказано, что́ работает СЕГОДНЯ: сама подпись, один `resume` над всем банком при любом состоянии решений. Оговорка ВРЕМЕННАЯ и объявлена свойством деплоя, а не обещанием контракта — снимается исполнением строки 199(а).
**7. Почему следующий минор не втиснут сюда.** Требование сквозной полосы прогресса (строка 200, слово владельца 20.08) — новое обязательное поле, то есть ещё один минор и ещё одна перегенерация клиента. Пока фронт заморожен и не потребляет ничего, миноры бесплатны: оба уедут в ОДНУ перегенерацию при разморозке. Значит нет причины вскрывать уже принятый пак платформы и заставлять зону доделывать на ходу. Зеркало фронта отстаёт ратифицированно (D39.142 п.5), при разморозке сверяется `cmp`. (20.08.2026, оркестратор №18) ✅
## D39.153 — ПРИЁМКА И ЛЕНДИНГ P7 ПЛАТФОРМЫ (пять актов, читающая поверхность): **ПРИНЯТ С ФИКС-ЛИСТОМ**; шесть линз + собственные посадки, одна дыра пинов найдена оркестратором; новые строки 198203; сессия закрыта владельцем (20.08). ✅
**1. Приёмка исполнением.** Панель шести линз в изолированных копиях, каждой свой каталог, бэкап чужого незакоммиченного ДО запуска (D39.113): слепая (заказ+дифф ДО отчёта) · контракт-конформность (пофайловый дифф wire-структур против ОБЕИХ версий канона) · деньги (два независимых пути из сырого леджера + живой стенд) · шов (инвентарь ВСЕХ каналов движка чтением чужого кода) · вне карты · ревью канона 0.4.0 другой моделью. Ни одной REJECT, регрессов против HEAD нет. **Пере-проверено рукой оркестратора:** `make check` с обоими гейтами — 18 пакетов, EXIT=0, **скипов 0**, линтер 0 issues · реестр 327 строк / 65 открытых (13 minor, 52 info), major и BLOCKER нет · оба док-гейта зелёные · миграционный манифест сходится, released-миграции не тронуты · `tmplatformctl books --migratable` живой. ⚠ Тестовых функций **532**, не 533 (команда — `grep -rh '^func Test' platform --include='*_test.go' | wc -l`; норма «дифф `^func Test` исполнением, не памятью» — `docs/ORCHESTRATOR_SESSION_PROMPT.md` §кодовый промт, со ссылкой на D39.121; в теле самой D39.121 команды нет — испр. при проверке записей); удалённых имён семь — каждое проверено, у каждого есть преемник, и преемники СИЛЬНЕЕ либо перевёрнуты по ратифицированному решению (гейт подписи банка → `TestResumeLiftsABankStopWithTheDecisionsAsTheyStand` плюс НОВЫЙ встречный `TestTheReconcilerDoesNotLiftABankStopNobodySigned`).
**2. Замер, обосновавший разворот схемного решения ВНУТРИ пака (00016 → 00022), пере-выведен независимо.** Свой бенчмарк на трёх вариантах в копии дерева: джойн **14.6 мс** · счётчик **5.3 мс** · без счётчика **2.9 мс**. Порядок и вывод подтверждаются, разворот законен: 00016 снёс колонку с доводом «её никто не пишет», 00022 вернул её ВМЕСТЕ с писателем. ⚠ Процент назван ЧЕТЫРЬМЯ носителями (журнал зоны 83% · `platform/internal/pgstore/perf_test.go:13` 96% · миграция 00022 «16.8 против 3.4, джойн сам 9 мс» · регистр PD-306 повторяет 83% и 16.8/3.4) — распад нормы «один носитель на факт» (D39.112) внутри одного пака. ⚠ **Первая редакция этой ноты писала «96% не выводится ни из чего» — ЭТО НЕВЕРНО и снято проверкой записей при лендинге:** число выводится точно, просто из ДРУГОГО замера — `platform/docs/P7_ACT5_FIX_PLAN.md:439-441`, «636 мс против 24 мс» на холодном корпусе акта 4, (63624)/636 = 96.2%. То есть носители меряли разное (холодный корпус против вакуумированного) и оба честны; сводить их надо указанием УСЛОВИЙ замера, а не удалением одного как фантома — иначе следующая сессия снесёт верное число.
**3. Собственные посадки ВНЕ списка автора — 8, поймано 7.** Долг не поставлен третьей концовкой · `edit_wave` не монотонен · снят `Vary` · HEAD лишён валидатора · `reopen` снимает стоп банка кому угодно · погашение долга без сверки метки · вынос метки долга из закрывающей транзакции во второй оператор (техника `xmin` из их §36 работает — проверено исполнением, а не принято на слово). **НЕ поймано:** снятие `structure_version` И `revision` из `emitFrame` проходит ВСЮ батарею — два поля, которые канон требует на КАЖДОМ кадре `EventBase`. По норме самой зоны (PD-1) свойство без пинящего теста считается незакрытым.
**4. Что ратифицируется этой нотой.** (а) **Долг на материализацию — колонка с арендой** (`books.read_model_owed_at`), ставится в ТОЙ ЖЕ транзакции, что закрывает границу работы, гасится сверкой метки, берётся в аренду тем, кто платит; очередь в памяти процесса и второй бэкстоп удалены — один механизм вместо двух. Правило «каждая транзакция, ЗАКАНЧИВАЮЩАЯ прогон, оставляет книгу должной» (их §33) принято вместе с формой его записи одним фрагментом на все три концовки. (б) **«Есть ли у пайплайна редактор» — свойство КНИГИ, монотонное, от объявления движка** (§35): читать с последнего прогона нельзя — счётчик ходил назад в момент допуска нового прогона, что канон запрещает. Половина «форму объявляет движок» проверена чтением ЧУЖОГО кода (`beginWaves`: знаменатель ноль у волны, которой нет). (в) **Владелец у клейма идемпотентности + тождество интейка дайджестом содержимого** — закрывает класс «застрявшая попытка удаляет строку преемника / пишет квитанцию поверх чужой». (г) **Остановка АККАУНТА читается с аккаунта**, а не с потолка одного прогона; аномалия «97% остатка рядом с `credit_exhausted`» закрыта СТРУКТУРНО (единственная точка присваивания под `!Spendable`), пере-проверено денежной линзой живьём. (д) **Счётчик замечаний вернулся на главу** — по замеру п.2. (е) **Демонтаж гейта полноты банка** в платформе — исполнение D39.144; согласованность с движком проверена чтением его кода: движок уходит по авто-ветке при снятом флаге, зависания нет.
**5. Фикс-лист и долг — где живут.** Зонный фикс-лист (десять пунктов, первым — `ContractVersion` 0.3.0 при формах 0.4.0) — **пинг оркестратора №18 в `platform/docs/platform-PROGRESS.md`**; зона заводит из него строки СВОЕГО бэклога и регистра. ⚠ Константу версии оркестратор НЕ правил: зонное правило «код не правишь, находки в фикс-лист» держится и при закрытой сессии; практического вреда сегодня нет (фронт заморожен на 0.2.3, 0.3.0-клиент лишнее поле игнорирует), но деплой объявляет версию, которую не отдаёт. Новые строки ЕДИНОГО бэклога: **198** (апгрейд движка стирает замечания и счётчики безвозвратно — композиция зачистки при ре-кате и announce-once движка; гейт холодного прогона) · **199** (канал доставки правок банка) · **200** (сквозная полоса прогресса) · **201** («Глава N» внутри текста экспорта) · **202** (живой прогон насквозь) · **203** (хвосты контракта, релеи §7 зоны).
**6. Решения владельца 20.08, записанные этой нотой.** (а) **sqlc БЕРЁМ** («я вообще за») — отступление P7 от D39.132 п.2б закрыто решением, а не оправданием; ⚠ оркестратор добавил к заказу то, чего в предложении зоны не было: все рантайм-ошибки «нет такой колонки» случились в СКЛЕЕННОМ SQL read-модели, куда sqlc по построению не дойдёт (⚠ счёт: РЕПОЗИТОРИЙ несёт две — `platform/docs/DEFECT_REGISTER.md` PD-44 и журнал зоны, «дважды укусил этот пак»; ещё две — `column c.note_count does not exist` и `missing FROM-clause entry for table "b"` — зона назвала в ответе владельцу 20.08, пересланном оркестратору, и в доки зоны они не попали: проверяемого носителя у них нет, поэтому опираться следует на две), поэтому тем же паком — разбор каждого СОБРАННОГО запроса Postgres'ом против мигрированной схемы и ответ на вопрос «есть ли запрос, которого не касается ни один тест». (б) **Живой прогон книги насквозь — ПОСЛЕ холодного прогона движка** («гонять одну неготовую систему против другой смысла нет»), строка 202. (в) **Сквозная полоса прогресса вместо пофазной** — строка 200; заодно снята рамка вопроса о «сделано» (см. D39.152 п.4).
**7. Метод-заметки, купленные этой приёмкой.** (а) **Композиция двух известных фактов — класс, который карта отчёта не содержит по построению.** Строка 198 сложена линзой «вне карты» из двух вещей, каждая известна автору ПО ОТДЕЛЬНОСТИ (зачистка ратифицирована PD-264, «движок не переанонсирует» установлено PD-298) — никто не сложил. (б) **Ошибка оркестратора, названная вслух:** пер-термную ручку банка я подал владельцу как продуктовую дыру; гейт был снят четырьмя днями раньше D39.144 в том самом разговоре, на который владелец ссылался, а контракт с 16.08 сам называет пер-термные решения ОПЦИОНАЛЬНЫМ путём правки. Расследование (5 читающих линз + сводка + два опровергателя) установило провенанс: формула «стоп очищается, когда каждый терм промоутнут ИЛИ отклонён» родилась 19.07 В ДВИЖКЕ как лечение ливлока R1-FL-B (отклонённый терм переспрашивался каждый прогон), то есть как условие выхода из паузы ОПЕРАТОРА CLI; 04.08 контрактная линия переписала её в спеку дословно и вывела продуктовое правило, которого никто не заказывал — счётчик «решено N из M» и запрет продолжить (коммит `16e20e5`, `frontend/docs/API_CONTRACT_INPUT.md:258-264`; журнал фронта сам пишет, что этого «в Ф-14 не было»). ⚠ **Эррата к D39.144 п.2:** атрибуция «гейт был изобретением контрактной линии» верна про ГЕЙТ и неверна про ФОРМУЛУ — она старше контракта на три недели и рождена в движке, поэтому движковая половина осталась без явной отмены (строка 191б, живёт в `mining.go:80-82,243` и сегодня ОБОЙДЕНА платформой, а не устранена). (в) **Опровергатели поймали ошибку собственной сводки:** она описывала РАБОЧЕЕ ДЕРЕВО, выдавая его за заленденный код — то есть нарушила ровно то правило «объявленное ≠ построенное», на котором ловила контрактную линию. Формулировка исправлена: до этого лендинга «один ОК» владельца не «доезжал пустым», а был ОТКАЗАН на первом звене (`HEAD:reconcile.go:934-938`).
**8. ВТОРОЙ РУБЕЖ — приёмка приёмки (слово владельца 20.08: «сначала приёмка твоей же работы, потом коммит»).** Четыре линзы прочитали не пак, а РАБОТУ ОРКЕСТРАТОРА: широкий инвентарь доков (другая модель, чтение всей документации четырёх зон целиком) · claim-fidelity обеих нот и всех сегодняшних записей · поиск того, что панель пропустила · полнота выгрузки долга и зонная дисциплина. Результат оправдал остановку коммита.
**(а) Пять овер-атрибуций в первой редакции ЭТИХ нот — снято, каждая проверена автором лично.** «96% не выводится ни из чего» — неверно: число выводится точно из ДРУГОГО замера зоны (`P7_ACT5_FIX_PLAN.md:439-441`, 636 мс против 24 мс на холодном корпусе), и удаление его как фантома снесло бы верное измерение · расхождений с 0.3.0 не четыре, а ПЯТЬ (пятое — семантика `410 Gone`, носитель PD-253) · «четыре рантайм-ошибки» проверяемого носителя не имеют, репозиторий несёт две · клейм «оговорка расширена на всю ручку» опережал текст канона — оговорка внесена только сейчас (D39.152 п.6) · «фикс-лист из восьми пунктов» устарел на собственной правке. **Класс один и тот же — верное утверждение приписано не тому источнику**, и это ровно тот провал, против которого написана дисциплина «эрудиция ≠ улика».
**(б) Долг, живший только внутри зоны автора, вынесен наружу.** Строка в бэклог ДВИЖКА про публикацию причин флагов данными (релей §7в) жила только в регистре платформы, который движок не читает — заведена строка **204** · вопрос платформы К ВЛАДЕЛЬЦУ про норму плотности комментариев (PD-255) был объявлен несуществующим её же хендоффом («вопросов на владельце нет») — вынесен в обязательства CURRENT-STATE · норма «жалуйся владельцу, не обходи молча» для фронтового фрагмента хука жила только в моей прозе — написан пинг в зонный журнал фронта. Плюс сняты три строки бэклога (**176/181/187**), закрытые ещё D39.149: очередь всё это время звала 181 свободной работой, то есть направляла сессию чинить построенное.
**(в) Найден жёсткий блокер ВНЕ пака, которого диффовая линза увидеть не могла по построению** (проверен мной построчно): `Sweep` платформы имеет бюджет прохода 2 минуты (`cmd/tmplatformd/runner.go:189`) при бюджете одного прогона 60 с (`internal/runs/reconcile.go:70`) — **двух медленных прогонов достаточно, чтобы проход кончился до `UnsettledRuns`**, единственного ретрая отложенного расчёта, а живые прогоны ниже по списку не реконсилировались и не спавнились; `ListLiveRuns` сортирует по `started_at`, поэтому заклиненный прогон стоит в голове и голодит остальных детерминированно, а ручки нет — `runs.Config.RunBudget` объявлен и в `startRunner` не присваивается. Цена: холд не возвращается, `runs_one_live_per_book` не даёт запустить новый прогон книги. Код приехал в P6, дифф `Sweep` против HEAD идентичен ⇒ ни одна линза приёмки P7 его видеть не могла. ⚠ **`PD-169` стоит `fixed(P5)` и этим ЛЖЁТ приёмке** — его пин гоняет `Sweep` без дедлайна прохода, то есть доказывает пер-прогонный бюджет, а не выживание прохода; пере-открыть. Носитель — первый пункт фикс-листа в пинге №18. **Тот же класс, что строка 198: композиция двух известных по отдельности фактов.**
**(г) Побочно получен ответ на вопрос, который я поставил открытым в фикс-листе:** собранный SQL read-модели извлечён через `go/types` и прогнан `EXPLAIN (GENERIC_PLAN)` против мигрированной схемы — 141 запрос, все планируются чисто, тестом недостижимы десять, и ни один из десяти не трогает схему 0001600024. Дыры сегодня нет; гейт нужен как ПОСТОЯННЫЙ, потому что именно он делает этот ответ воспроизводимым.
**9. Что НЕ проверено (obstacle приёмки).** Живого перевода через платформу не было НИ РАЗУ — всё после «прогон стартовал и остановился» покрыто батареей против живого Postgres, но с движком-заглушкой (строка 202). Четыре оси не смотрел никто и никогда: деньги и леджер целиком · вход/сессии/CSRF · очередь и джобы · метрики — пак их не менял, а все приёмки смотрели дифф; это отдельная работа, не довесок. PD-297 зона не взяла осознанно (мерить на корпусной книге). Сессия P7 ЗАКРЫТА владельцем 20.08; промт отработан → `platform/docs/archive/` с баннером-исходом. (20.08.2026, оркестратор №18) ✅

View file

@ -1,13 +1,24 @@
<!-- ======================================================================
РЕВЬЮ-ШАПКА (ратификация D39.99, 04.08.2026, оркестратор №12;
батч 0.3.0 — D39.138, 16.08.2026, оркестратор №17)
батч 0.3.0 — D39.138, 16.08.2026, оркестратор №17;
синк с платформой 0.4.0 — 20.08.2026, контрактная сессия; РАТИФИЦИРОВАН D39.152, 20.08.2026, оркестратор №18)
СТАТУС: РАТИФИЦИРОВАН как контракт API v0. Нормативная поверхность —
СТАТУС: РАТИФИЦИРОВАН как контракт API v0 по 0.4.0 включительно (D39.152). Нормативная поверхность —
openapi.yaml РЯДОМ. ⚠ Зонная копия frontend/docs/api-contract/ ВРЕМЕННО ОТСТАЁТ (0.2.3 при каноне
0.3.0) — ратифицировано D39.142 п.5 на время фриза фронта; синк байт-в-байт + перегенерация типов =
0.4.0) — ратифицировано D39.142 п.5 на время фриза фронта; синк байт-в-байт + перегенерация типов =
первое касание зоны при разморозке; после него правило прежнее: расхождение = дефект лендинга.
Генерация типов фронта после этой ратификации идёт из ЭТОЙ копии.
20.08.2026: СИНК С ПЛАТФОРМОЙ (запрос platform/docs/CONTRACT_SYNC_FROM_PLATFORM.md, 14 пунктов) —
канон 0.3.0 → 0.4.0, разбор и диспозиция КАЖДОГО пункта в §6в, отдельным пунктом §6в п.0 — сверка
0.2.3 → 0.3.0 на молча срезанные при резке прозы правила (найдена одна, восстановлена).
Ратификацию этой редакции контрактная сессия за собой НЕ записывает — это акт оркестратора:
автор и ратификатор одного и того же текста совпадать не должны. Файлы уже несли 0.4.0 к моменту сдачи; в силу
редакция вступила лендингом 20.08 — РАТИФИЦИРОВАНА D39.152 (там же: четыре текстовые правки,
внесённые оркестратором при ратификации, и разбор того, что против 0.3.0 расходится ЧЕТЫРЕ места,
а не одно). Строку в 05-decisions-log сессия тоже не писала по той же причине — готовый текст ноты
был сдан оркестратору вместе с этой редакцией.
Приёмка 0.2.0: три адверсариальных воркфлоу-пасса (черновик · дофикс round-1 · round-2),
19 подтверждённых находок, 0 опровергнутых, все вправлены; батарея фронта с
контракт-шагом (spectral + дрифт-тест генерённых типов) зелёная исполнением.
@ -42,8 +53,23 @@ cmp-сверка обязательна (D39.138 п.3).
>
> **Статус: РАТИФИЦИРОВАН.** 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**. Дом канона — этот каталог; `frontend/docs/api-contract/openapi.yaml`
> байт-зеркало.
> ревью research/28** · **0.4.0 (D39.152, 20.08) — синк с платформой, §6в**. Дом канона — этот каталог;
> `frontend/docs/api-contract/openapi.yaml` — байт-зеркало.
>
> ⚠ **0.4.0 ломающий ровно по одному месту, и это ЗАМЕРЕНО, а не объявлено.** Дифф генерённых
> типов 0.3.0 → 0.4.0 (`openapi-typescript@7`, комментарии отброшены) — **одна строка:**
> `Run.stop_requested: boolean`. Всё остальное в этой редакции — проза канона, одна запись в
> таблицу `resumeRun` и одно значение ВТОРОГО уровня (`cause.code: credit_unavailable`, а он по
> замыслу не закрыт и типов не двигает): корневой enum не сужен и не расширен, обязательных полей
> больше не появилось. Линтер зоны (`spectral --ruleset frontend/.spectral.yaml`) — 0 ошибок.
>
> ⚠ **Две границы этого замера, названные ревью диффа и обе честные.** (1) База — 0.3.0, а зонное
> зеркало фронта стоит на 0.2.3: при разморозке фронт платит переход 0.2.3 → 0.4.0 целиком, и это
> совсем другой объём (шапка ниже про отставание зеркала). Замер отвечает «что добавил ЭТОТ синк»,
> не «что стоит фронту разморозка». (2) Гейт §5 на утечку конвейерного словаря пере-ран и чист по
> сути, но «ноль вхождений» сказать нельзя: слово `edit` встречается дважды как обычный английский
> глагол («not an edit», «not a row edit»), `stage` — один раз в самой формулировке запрета. Ни
> одного нового вхождения этот синк не внёс.
>
> **0.x ломает миноры и будет ломать весь бета-период** (решение владельца 16.08, §8 п.17
> research/28): 0.3.0 — обычный ломающий минор, а НЕ последний перед 1.0. Окно 1.0 — после беты.
@ -280,6 +306,13 @@ drop юнит несёт причину ПЕРВОГО выпавшего чле
`given` (пришло с книгой), `ruby``annotated` (сам текст книги сказал, как читать), `mined`
`found` (сервис нашёл в тексте). Проекция трёх пар — работа платформы.
**Правило пустого `kind` — и его проекция** (было в 0.2.3, при резке прозы уцелело в каноне и
пропало здесь; возвращено синком 20.08). `kind` присутствует всегда и допускает `null`; `null`
значит «сервис не решил», строка при этом остаётся подписываемой, и клиенту запрещено и выбрасывать
её, и додумывать тип за движок (канон, схема `BankTerm.kind`). У движка та же строка несёт `Type:
""` (`membank/memseed.go:323-326`), поэтому **проекция `""` → `null` — работа платформы**; это
единственное место, где пустая строка и `null` означают одно, и потому оно записано.
**Фантом `auto` в провенансе убран ещё в 0.2.0:** комментарий схемы движка (`migrate.go:193`:
`seed|ruby|auto`) устарел — `"auto"` пишет СТАТУС, не провенанс (`membank/memseed.go:328`:
`Status:"auto", Source:"ruby"`), майнинг ставит `mined` (`pipeline/mining.go:424`).
@ -473,7 +506,32 @@ column» (`00002_readmodel.sql:188-199`).
`return_to` принимает ТОЛЬКО путь этого сайта, и чужой путь сервер молча заменяет дефолтом
(`login.go:safeReturnTo`). Обе `POST`-ручки лежат на cookie-пути, то есть требуют `X-TM-Client`.
Отказ входа — `problem+json`.
**Отказ входа — `problem+json`, как везде; различать причины отказа клиент не может по замыслу.**
⚠ Эта фраза стояла здесь в 0.2.3, была молча срезана коммитом лендинга батча `8d82096` (ловится
`git log -S`) и **восстановлена синком 20.08** — не как переоткрытый спор, а как правило, которое
никто не отменял. Разбор потери — §6в п.0.
**Что это значит для клиента, дословно (синк 20.08, п. C записки платформы).** `/auth/*` отвечает
тем же конвертом, что и `/v0`, но БЕЗ машинного `code` — и это решение, а не пробел: клиент
диспетчеризует по статусу и показывает одну нейтральную фразу. Особый случай ровно один — `429`
с `Retry-After`, который вход действительно отдаёт (`login.go:173,233`, `dev.go:138`); ветвление по
нему это механизм HTTP по назначению, а не запашок.
Арифметика остатка, из-за которой словарь здесь не нужен: из шести статусов входа четыре уже имеют
точные соответствия в `ErrorCode` (401 → `unauthenticated`, 400 → `invalid_request`,
404 → `not_found`, 503 → `service_unavailable`), а из двух оставшихся `405`баг клиента, не
пользовательский случай. Обсуждался по существу ОДИН код.
**Проводного словаря в этом файле не объявляется, и это принципиально.** Компаньон объявлен не
нормативным для формы; словарь, записанный здесь, сделал бы его нормативным с чёрного хода — форму
стало бы нечем ни сгенерировать, ни отвалидировать, ни удержать линтером. Механическая половина
разбора уже исполнена платформой без разрешения и правильно: `codeForStatus` снят
(`grep -rn "codeForStatus" --include=*.go platform/` → 0).
**Триггер пересмотра, записанный явно:** в день, когда на входе появится ВТОРОЙ пользовательски
осмысленный случай — «аккаунт отключён» против «провайдер лёг», — эта поверхность получает
собственный нормативный документ (маленький OpenAPI на `/auth/*`), а не таблицу в компаньоне.
### 2.15. Потолок прогона — ◆ форма, РАТИФИЦИРОВАНА оркестратором №15 (08.08)
@ -584,6 +642,12 @@ the original request was never applied». Каждый `POST /books` созда
Окно 24 часа — не замер, а достаточная граница для клиентского ретрая; сокращать его дешевле, чем
удлинять, поэтому взята нижняя обещаемая граница («не менее»).
**0.4.0 сменил ОДНУ половину — что такое «тот же запрос» на multipart** (§6в E): не объявленные
части плюс длина, а метаданные + имя файла + СОДЕРЖИМОЕ, и **сервер, который тождества установить не
может, не реплеит, а отвечает `409`**. Остальное выше в силе. ⚠ Формулировка §6б ниже («тождество
считается по объявленным частям», дофикс ФБ-8 16.08) этой правкой ПЕРЕКРЫТА и оставлена только как
история — читать по канону.
---
## 3. Зависимости: чтение → источник → строка бэклога
@ -598,7 +662,7 @@ the original request was never applied». Каждый `POST /books` созда
| `GET /books`, `GET /books/{id}` | read-модель платформы | ПОСТРОЕНО | — |
| `GET /books/{id}/run-options`, `POST /runs` | шкала + холды | ПОСТРОЕНО | — |
| `POST /runs/{id}/stop` | реконсилятор | ПОСТРОЕНО (кнопки на экране нет) | зона фронта |
| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **поведение на паузе по потолку 0.3.0 МЕНЯЕТ**: сегодня платформа отвечает `202` и возвращает прогон в то же состояние (`runs/reconcile.go:906` — сама `Resume`; ветка `case "paused"` `:919` доходит до `reopen`, 409 только на дневном потолке движка `:931`), а контракт теперь требует `409` `run_not_resumable` · `cause.code: ceiling_reached`. Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) |
| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **0.3.0 сменил поведение на паузе по потолку, а 0.4.0 — на ИСЧЕРПАННОМ прогоне**. Пауза: платформа уже отвечает `409` `ceiling_reached` (`runs/reconcile.go`, ветка `case "paused"` в `Resume`) — совпало. Исчерпанность: платформа отвечает `202` и возвращает прогон в то же состояние (`reconcile.go`, оба выхода `exhausted` из `reopen` → ветка `exhausted` в `Resume``httpapi/v0.go`, хендлер `resumeRun`), а контракт теперь требует `409` `run_not_resumable` · `cause.code: ceiling_reached`. Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op, а на `awaiting_bank` это ещё и не вызывает `ReleaseBankStop`. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) |
| `GET /usage` | кредиты | ПОСТРОЕНО, **не читается ни одним экраном** | зона фронта |
| `GET /capabilities` | конфигурация деплоя | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
@ -615,6 +679,10 @@ the original request was never applied». Каждый `POST /books` созда
| `Note.code` как enum спеки | карта приложения А | не enum, пока не написаны фразы | строка 148 (фразы владельца) |
| Настоящие названия глав (`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` |
| Снятие замечания (переход «флаг снят») | движок | **НЕДОСТИЖИМО сегодня, проверено чтением движка** — п. H §6в | **PD-298** регистра платформы (`platform/docs/DEFECT_REGISTER.md`) — там строка и живёт; механизма не строим |
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | **вход P7** (PD-202) |
**Три прежних предупреждения СНЯТЫ как устаревшие** (Б-7а), и это причина, по которой заведена
таблица выше:
@ -808,7 +876,7 @@ the original request was never applied». Каждый `POST /books` созда
`required: true` стоит при живом исключении для bearer, потому что условной обязательности по схеме
безопасности OpenAPI не выражает, и валидатор не должен читать законное отсутствие как нарушение ·
список отказов интейка помечен неисчерпывающим (авторитет — `code` ответа) · правило резолюции
`Location` · тождество `Idempotency-Key` на multipart считается по объявленным частям, а `408` не
`Location` · ⚠ (ПЕРЕКРЫТО 0.4.0, см. §2.18) тождество `Idempotency-Key` на multipart считается по объявленным частям, а `408` не
считается состоявшейся попыткой · `min_chapters` при `max_chapters: 0` — не диапазон, клиент проверяет
максимум первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» — снято
как обещание без носителя, заменено на наблюдаемое (`BankPage.signed` против `total`) · `409` у
@ -823,6 +891,405 @@ the original request was never applied». Каждый `POST /books` созда
«companion К-6»): описания компилируются в исходники клиента, и ссылка на ревью в чужом репозитории
там — мусор; носители остались здесь.
## 6в. Синк с платформой 20.08 → канон 0.4.0: что решено и почему
Вход — `platform/docs/CONTRACT_SYNC_FROM_PLATFORM.md`, четырнадцать мест, где провод сообщает
состояние и не даёт клиенту действия. Записка честно разделяет ФАКТ / ⚠ МНЕНИЕ ПЛАТФОРМЫ / ГРАНИЦА,
и разбор шёл по этой границе: ФАКТы перепроверены исполнением, МНЕНИЯ проверялись на ПОСЫЛКУ, а не
только на предложенную форму. Два мнения посылку не выдержали (B, часть A), одно оказалось
слабее собственного факта (L о метках глав).
**База ссылок этого раздела — РАБОЧЕЕ ДЕРЕВО 20.08, а не HEAD.** В `platform/` на этот момент 81
незакоммиченный файл (P7 в работе), поэтому одна и та же строка в HEAD и в дереве разная, и ссылка
без базы проверяема только случайно. `backend/` чист, его ссылки годятся в обоих. Там, где
конструкция переживёт любую перенумерацию, адрес дан ИМЕНЕМ функции или ветки — это дешевле и
надёжнее номера. Поймано ревью диффа: первая редакция §6в смешивала обе базы в одной строке таблицы.
### п.0. Сверка на потери прозы 0.2.3 → 0.3.0 — исполнена; найдена ОДНА потеря, она и была заявлена
Метод: полный дифф лендинга `8d82096` по обоим файлам (300 удалённых строк компаньона, 597 канона),
чтение ВСЕХ удалённых строк, выделение из них утверждений-правил и проверка каждого на наличие
смыслового двойника в живых файлах.
**Отдельно про метод, потому что он чуть не дал ложный результат.** Первый проход греп-проверки
шёл построчно и дал шесть «пропаж», которых нет: правило переносится через перенос строки, и
`grep "fewer rows than asked"` не находит текст, где `fewer` заканчивает строку. Сверка пере-ранена
на файлах, склеенных в одну строку (`tr '\n' ' '`), — и все шесть нашлись на месте. Тот же класс
ошибки в этой сессии уже ловился однажды; ставлю его сюда как метод, а не как случай.
Итог: **единственная потерянная НОРМА — фраза §2.14** («различать причины отказа клиент не может по
замыслу»), восстановлена. Остальное удалённое — три класса, и ни один не является потерей правила:
- **правило уцелело в КАНОНЕ, а канон нормативен**`kind`/`null` («MUST NOT drop it or invent a
kind»), «MUST NOT clamp it again», «stale read is the CLIENT's duty», «catch-up reads `>=`»,
«assembling a book's text on the client is forbidden», «pairs are read PER CHAPTER», уникальность
термина по пятёрке, «a phrase MUST exist for a reason we do not know yet», `no-store`, CSRF,
запрет реплея истории, «`max_chapters: 0` = экран исчерпанности». Один из них — правило пустого
`kind` — уцелел в каноне, но потерял здесь ПРОЕКЦИЮ (`""``null` — работа платформы); клауза
возвращена в §2.8;
- **утверждение снято ОСОЗНАННО и с записанным основанием** — «carried forward marked as
unverified» (ФБ-8, обещание без носителя), «замена в `failed` запрещена» (ужато до «не `failed`»),
«Required: HTTP/2 at the edge» (Б-16, RFC 9205), пофазные счётчики, `EventCeiling`, `genre`,
`finalizing`, пример `«CJK leak … 第一节»` (он и был утечкой, §2.12);
- **пропала МОТИВИРОВКА, а не норма** — «1.9 юнита на главу», «иначе строки выглядят дубликатами и
удаляются», «portable to the desktop client», `Intl.DisplayNames`. Это цена резки прозы, принятая
осознанно; ни одно не меняет того, что обязана делать сторона.
**Вывод, который стоит записать отдельно.** Приёмка батча сверяла мультимножество модальных глаголов
и этот класс структурно не ловила — но пере-сверка показала, что улов класса равен одному. Отсюда
дешёвый гейт вместо дорогого: при следующей резке прозы сверять не модальность, а **список
собственных имён формы** (полей, значений, кодов, заголовков) — норма почти всегда стоит рядом с
именем, а фраза §2.14 пропала именно там, где имени не было (`/auth/*` — не поле). Носитель: эта
запись.
### A. Прогон с исчерпанным бюджетом (PD-282) — ПРАВЛЮ КАНОН; половину мнения отклоняю
**Факт подтверждён и оказался шире записки.** `reopen` даёт `exhausted` в ДВУХ местах, а не одном
(`runs/reconcile.go`): `remaining <= 0` и `ErrInsufficientCredit` от `RestartRun` — то есть
«кончился потолок прогона» и «нечем взять холд на счёте» уже сегодня схлопнуты в один вердикт, и
`Resume` отвечает на оба одинаково: `202` + неизменённый `Run` (`httpapi/v0.go`, хендлер
`resumeRun`). `paused` до этой ветки не доходит — он отказан выше, поэтому молчаливый `202` живёт
ровно на `stopped` и `awaiting_bank`. Худший из двух — `awaiting_bank`: человек подписал банк,
нажал «продолжить», получил «принято», а `ReleaseBankStop` не вызывался (зона это записала сама,
`P7_ACCEPTANCE_HANDOFF.md` §7(и)).
**Номера строк в чужой зоне в этом разделе сняты намеренно** (находка ревью): дерево платформы
на 20.08 держит незакоммиченные правки, поэтому одна и та же строка в HEAD и в рабочем дереве
разная, и половина ссылок первой редакции указывала в одно дерево, половина — в другое. То же
лечение, что у приложения А: адресуемся именем функции и ветки.
**Правка канона (§resumeRun).** Записана ВТОРАЯ ОСЬ: исчерпанность лимита прогона перебивает обе
строки `202`; ответ — `409` `run_not_resumable`, `cause.code: ceiling_reached`, лечение — новый
прогон. Новых значений enum не заводится: `ceiling_reached` уже есть и его собственный текст этот
случай описывает дословно. Тем же ходом канон получил обратную гарантию, которой не имел:
**`202` теперь означает, что работа действительно переоткрыта**, и клиенту не нужно второе чтение,
чтобы отличить успех от no-op. И записано разделение, о котором предупреждает сам канон у
`AccountHaltReason`: пустота СЧЁТА не сворачивается в `ceiling_reached`у неё своя причина
`credit_unavailable` (аддендум ниже), а состояние аккаунта ЦЕЛИКОМ по-прежнему говорит `Usage`, не
этот вызов. *(Первая редакция раздела писала здесь «пустой счёт — не ответ этого вызова», что
противоречит и аддендуму, и канону; испр. оркестратором при ратификации.)*
**Признак «этот прогон ещё можно продолжить» на `Run` — ОТКЛОНЁН, с оговоркой.** Посылка «платформа
знает его точно» верна, посылка «дёшево» — нет: `ReadRun` — один запрос по `books join runs`
(`pgstore`, функция `ReadRun`), а флагу нужны ТРИ чтения, которых в нём нет — `RunSpent`,
`AttemptReservationOpen` и баланс счёта. `Run` сериализуется в карточке книги, а карточку канон
велит перечитывать НА КАЖДЫЙ кадр потока («it is still read on a frame, on navigation and on
focus»), то есть цена платится на горячем пути ради факта, который
(а) устаревает к моменту клика и (б) уже отвечен ратифицированным порядком чтения пяти мест
(§RunOptions): `max_chapters == 0` + `blocked` — это ровно «стоит ли предлагать старт сейчас и что
мешает», и рисует его тот же экран. Авторитет по-прежнему у мутации, как канон и объявляет.
⚠ Поправка ревью к предыдущему абзацу: сам `Run` в КАДРЕ не едет — ни один payload его не несёт;
горячий путь создаёт не кадр, а предписанная им перечитка карточки. Вывод не меняется, носитель
цены — другой; первая редакция называла его неверно.
**Триггер пересмотра:** замер, показывающий, что мёртвый клик частый, ЛИБО сворачивание трёх чтений
в коррелированные подзапросы того же запроса (агент проверил — возможно). Тогда форма — `Run`,
поле, минор.
### B. Суточный потолок движка — ОТКЛОНЯЮ ОБЕ ФОРМЫ; посылка не выдержала проверки
Записка просит слово или признак для паузы, которую деньги не лечат. Проверка посылки:
1. **Владелец уже решил этот вопрос — 15.08, D39.132 п.2а.** `day_usd` УБРАН из платформенного
шаблона книги; трата прогона ограничена купленным объёмом (холд + `--ceiling-usd`), дневная ось
на платформе объявлена дублирующей, в движке остаётся ОПЕРАТОРСКОЙ опцией, а обработка
`daily_ceiling`/409 — предохранитель. Там же дословно: «**PD-199 закрыт этим же решением: `null`
на проводе подтверждён, слово в контракт не заводится**». Заводить его сейчас значило бы
опрокинуть ратификацию пятидневной давности ради случая, который зона сама пометила ГРАНИЦЕЙ
(«живьём не воспроизводилось»). Проверено: платформа это поле **не заводит** — её рендер
`book.yaml` проставляет ровно пять строковых ключей, «the whole of what this platform claims
about the engine's schema» (`books/render.go`, функция `render`), и потолки в их число не входят.
⚠ Уточнение ревью, и оно важнее исходной формулировки: «не заводит» ≠ «не пишет». Шаблон деплоя
пере-маршалится целиком, поэтому оператор, положивший `ceilings.day_usd` в СВОЙ шаблон, получит
это поле в каждой книге, которую платформа создаст (запинено `render_test.go` — «the operator's
ceilings did not survive»). Именно поэтому триггер ниже сформулирован через шаблон, а не через
код платформы: настройка живёт в артефакте деплоя, и вернуться она может без единой правки Go.
2. **Предложенная форма неверна по СМЫСЛУ, даже если бы случай был.** «Лечится покупкой / не
лечится» — булево, а суточный потолок это ОКНО: он снимается сам в полночь UTC
(`store/ledger.go:60-72`, `todayUTC()`). Клиент, прочитавший «не лечится», спрячет кнопку
навсегда там, где честный ответ — «не сейчас». Булево, у которого один из двух исходов врёт,
хуже, чем отсутствие поля.
3. **Второе значение enum — та же проблема плюс своя:** оно называет настройку чужого файла на
проводе, что запрещает правило §5 («на проводе нет движковых словарей»), и зона сама этого не
хочет.
**Что записано вместо этого — одна фраза в каноне (§resumeRun).** «Новый прогон легален при любом
`paused`» осталось нормой, но перестало читаться как обещание прогресса: деплой может держать
СВОИ лимиты, которых этот контракт словом не называет, и прогон под таким лимитом останавливается
так же; клиент всё равно предлагает новый прогон — лучшего действия у него нет, — но не подаёт его
как гарантию, а повторный `paused` это законный исход, а не сбой. Цена ошибки при этом наблюдаемая
и самокорректирующаяся: холд возвращается, провайдерских денег не теряется (ФАКТ записки).
**Триггер пересмотра, записанный явно:** день, когда `day_usd` вернётся в платформенный шаблон
книги. Тогда это настоящая дыра и контракт открывается заново.
**Попутно найдено и стоит знать зоне:** суточный потолок — НЕ единственный стоп, который деньги
не лечат. Их четыре класса (`cmd/tmctl/main.go:52-79`): подпись банка (exit 3), graceful stop
(exit 5), полоса отказов 1019 и суточный потолок (exit 4, scope `day`). Просто три из четырёх уже
имеют свою проекцию (`awaiting_bank`, `stopped`, `failed` + `RunFailureReason`), и «денег не
хватает» из них не следует ни для одного. То есть класс закрыт, а дыра — только в четвёртом.
### C. Вход `/auth/*` — ИСПОЛНЕНО (§2.14)
Фраза восстановлена, вариант A уточнённый записан целиком, триггер пересмотра зафиксирован, словарь
в компаньоне НЕ объявлен. Разбор потери — п.0 выше. Возражение зоны про «нормативность с чёрного
хода» принято дословно и стало основанием.
### D. `Note.code` против пустой клетки — ПРАВЛЮ ОБА ФАЙЛА
Дыра реальна и была острее, чем в записке: канон не просто требует поле, он называет `Note.code`
«a closed vocabulary of this version», а карта в приложении А оставляла клетку пустой — то есть
сервер обязан прислать значение, которого словарь не содержит. `unspecified` ратифицирован как
ОБЯЗАННОСТЬ сервера (не строка словаря причин), записан и в канон, и в приложение А, п.5. Довод
записки про независимый выпуск принят. Прецедент D39.144 опорой не служит — согласен, его довод
(«спека ещё никем не потреблена») больше не верен.
**Родственный вопрос про ступени — НЕ решаю:** К-6 открыт, слово владельца 16.08 — «решим потом».
Но провизорность построенного зафиксирована в приложении А, п.6, чтобы `ingest/notes.go` не
читался как ратифицированная карта.
### E. Отпечаток интейка (PD-262) — ПРАВЛЮ КАНОН, форму НЕ трогаю
ФАКТ подтверждён дословно: `intakeFingerprint` берёт `r.ContentLength` (`httpapi/v0.go:511-517`), и
комментарий рядом (`:503-510`) сам называет оба остатка. Из двух исходов один безопасный (ложный
`key_reused` на другой границе multipart — честный ретрай отвергнут), второй нет: chunked-клиент
даёт `"unknown"` обоим запросам, и **две разные книги под одним ключом реплеят первую** — то есть
ключ возвращает ответ, принадлежащий чужому запросу. Это нарушение самой гарантии, ради которой
ключ существует.
**Выбрано: привести фразу канона к тому, что сервер может сравнить, БЕЗ поля размера в форме.**
Записано: «тот же запрос» = те же метаданные + то же ИМЯ файла + то же СОДЕРЖИМОЕ; чем сервер
устанавливает последнее — его дело (дайджест на лету стоит ноль, книгу в памяти держать не надо);
обрамление multipart и длина тела запросом НЕ являются; **сервер, который не может установить
тождество, не реплеит, а отвечает `409` `idempotency_conflict`**. Почему не поле размера: оно
дало бы клиенту ещё одно обязательное поле, не закрыв случай «две разные книги одной длины», и
всё равно потребовало бы сверки с фактически прочитанным. Механизм отката для этого уже построен
и назван зоной верно (`trailingParts` `v0.go:536-563``books.go:190-204`); регистр зоны сам
называет полное лечение «сверка принятого дайджеста на завершении с откатом» — контракт теперь это
разрешает вместо того, чтобы описывать деталь, которой в форме нет.
### F. `ETag`/`304` на любом безопасном чтении — ПРИНЯТО, канон правится одной фразой
Зона права дважды: RFC 9110 валидатор на любом GET разрешает, и список исключений в слое записи
протух бы за пак. Записано в шапке канона: валидатор на любом безопасном чтении легален и
объявления не требует; операции, которые его объявляют, — те, где клиенту есть смысл им
пользоваться, а не исчерпывающий список. Плюс две границы, чтобы фраза не стала лицензией:
клиент НИКОГДА не обязан слать `If-None-Match`, а `304` бывает ответом только на присланный.
`getUsage`/`getRunOptions` в объявленные НЕ добавлены сознательно — оба читаются ровно перед
действием, и условное чтение там не даёт ничего, кроме риска показать устаревшую шкалу.
### G. Draft-only конвейер (PD-202) — ОТВЕЧАЮ: ДА, начерновленная глава «сделана»; правлю канон
Проверено чтением обеих зон, и цена выше заявленной. У движка деплой без второго прохода — не
экзотика: разбиение выводится из РОЛЕЙ стадий (`pipeline/waverun.go:43-53`), конфиг требует лишь
«хотя бы одна стадия» (`config/pipeline.go:841`), ветка живая и покрыта тестом
(`contractblockers_test.go:115-136`); в репозитории такого конфига пока нет, но выбирает его
оператор, и платформа шаблон не читает. У платформы `units_done` — жёстко второй проход
(`pgstore/sink.go:252-263`, `readmodel.go:200-208`).
**Следствие, которого в записке нет:** ноль навсегда — это не только короткая полоса.
`Book.chapters_done` и `ChaptersLeft` (`pgstore`, функции `chaptersDone` и `BookRunContext`) читают
ту же колонку, а `ChaptersLeft` подрезает ШКАЛУ покупки. То есть на таком деплое сервис бесконечно
предлагает купить главы, которые уже переведены. Это выводит вопрос за рамки «косметика полосы» и
делает ответ обязательным.
⚠ **Первая редакция этого абзаца дописывала «и берёт за них деньги» — снято ревью как неверенное,
и снято ИЗ КАНОНА тоже** (там оно уже стало нормативным текстом, компилирующимся в JSDoc клиента).
Что проверено: платформа держит ПОТОЛОК, а не цену, и рассчитывается по ЗАМЕРЕННОЙ трате, а
неизменённая уже начерновленная книга переигрывается с чекпойнтов за $0 — то есть в обычном случае
холд возвращается целиком и не берётся ничего. Плата возникает только если между прогонами сдвинулся
снапшот (слаг модели, промпт, langpack, флип гейта), и тогда пере-покупка действительно оплачивает
переведённое заново. Довод «предлагают уже сделанное» стоит сам по себе и в поправке не нуждается;
денежный довод был сильнее фактов, и это ровно тот класс ошибки, который у меня уже ловился.
**Ответ контракта.** «Сделано» = последний проход, который эта книга на ЭТОМ деплое реально
получает. Записано в `Progress` и в `Book.chapters_done` — вместе с доводом: контракт не говорит,
сколько проходов бывает, значит и назвать один из них для счёта не может, а обещание «дробь
доходит до единицы» — это обещание, а не проходы. Имён волн в тексте нет (гейт §5 пере-ран, чисто).
Форма не меняется, генерённые типы не двигаются; работа — проекция платформы, вход P7.
### H. Снятие замечания (PD-298) — СВЕРЕНО С ДВИЖКОМ ПЕРВЫМ, как просила зона; механизма НЕ строим
Порядок соблюдён: сначала чтение движка. Результат — **переход достижим у драйвера и НЕ достижим на
проводе**. `flagged` вычисляется на каждой эмиссии, а не хранится (`pipeline/events.go:351,377-379`,
`waverun.go:139-140,205-206`), и `redrive`/`--resnapshot`/правка исходника пере-атакуют юнит, так
что второе разрешение законно может выйти `flagged=false`. Но эмиссия идёт через
announce-once-леджер: ключ `unit:<book>:<wave>:<chapter>:<unit>` не несёт ни снапшота, ни прогона
(`events.go:399-401`), `EnqueueOnce` вторую строку не пишет (`store/outbox.go:74-106`), леджер
переживает прогоны (`outbox.go:205-212`) и запинен тестом
(`runevents_test.go:242-246`). Единственное окно — падение между записью строки в журнал и
пометкой (`events.go:154-164,200-202`); плюс `redrive` платформа не вызывает вовсе
(`runner/engine.go:83-124`).
**Поэтому: строка «недостижимо» в регистр, а не механизм** — ровно тот исход, который зона назвала
правильным. Но в каноне закрыт РАЗРЫВ ПРАВИЛА, который эта проверка обнажила: §AfterVersion говорил
«удаление так не выразить» только про ОПТОВУЮ замену, а про исчезновение ОДНОЙ строки не говорил
ничего. Записано: строка, once delivered, поодиночке не отзывается; коллекция, которая обязана
потерять строку, теряет её единственным выразимым способом — заменой целиком с `resync_required`.
Это обязанность сервера, а не пожелание, и она делает поведение платформы выводимым: сегодня при
`flagged=false` строка обновляется и ревизия двигается (`pgstore/sink.go:237-244`), дельта её
прячет (`readmodel.go:725-735`), а `note_count` в кадре главы уже УМЕНЬШАЕТСЯ (`sink.go:310-328`) —
то есть клиент увидел бы счётчик, противоречащий списку. Теперь ясно, что должно произойти вместо
этого.
### I. `content_refused` — forward-looking, НО с носителем; правлю канон и §3
Ответ на вопрос зоны: **forward-looking, долг зона не заводит.** Значение ратифицировано К-9
(владелец 16.08) до того, как появился производитель, и это сознательно: форму отказа лучше решить
не под давлением. Проверено — производителя нет ни на одной стороне (в платформе только константа и
строка таблицы; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ и уезжает как
`Note.code: content_withheld`, `ingest/notes.go:45-48`, — другой словарь, моста нет).
Правки: (а) оба значения помечены в каноне как объявленные ДО производителя, с причиной;
(б) **обязанность ограничивать число попыток аккаунта пере-привязана**: она падает ВМЕСТЕ с
производителем, а не висит на сервере, который отказа не выдаёт, — иначе это обязательство без
адресата, ровно тот класс, из-за которого заведено правило Б-21; (в) §3 получил строку с носителем
(строка 94, ПТ-16). Клиент обрабатывает код с сегодня — старый клиент, встретивший его впервые, и
есть та беда, ради которой значение объявлено заранее.
### J. «Остановлено по вашей просьбе» — ПОЛЕ ЗАВЕДЕНО; форма отличается от предложенной зоной
Решение владельца 17.08 принято, форму делегировали контрактной сессии
(`P7_ACCEPTANCE_HANDOFF.md:452`). Заведено `Run.stop_requested` — **обязательное булево, а не
необязательное.** Довод: функция тотальна — прогон, который никто не просил остановить, это `false`,
а не «поля нет»; необязательность дала бы два написания одного факта, и это ровно тот дефект, который
контракт уже чинил у `sense` (0.2.2), у `reject_reason` и у `kind`. Ценой стал ломающий минор —
`Run` получил обязательное поле, — и он объявлен честно в шапке компаньона, вместо того чтобы
называть правку «аддитивной» и оставить генератор доказывать обратное. В описании записано и то,
чего в заказе не было: флаг — запись ПРОСЬБЫ, а не исхода, и остаётся `true` на прогоне, который
успел доработать, потому что это и есть случай, который экран обязан объяснить.
### K. `--verify-bank`НЕ КОНТРАКТ, релей в зоны; но контрактно видимая половина закрыта
Расхождение «D39.144 (подпись = один акт) против пер-термного гейта движка» — ратификация и
поведение движка, обе вне этого файла: контрактная половина уже исполнена (D39.144/145 сняли гейт
полноты из канона и компаньона, `409` `bank_decisions_incomplete` удалён). Обход зоны (не передавать
флаг на `resume`) — её решение и её же декларация.
**Контрактно видимая половина — вторая, и она молчала.** Канон обещал `decline` — «leave it out»,
а решение до движка не доезжает (`mined_rejects` платформа не пишет), то есть отклонённый термин
возвращается предложенным. Это обещание пользователю, которого система не держит. Записано
предупреждением на `BankDecision.action` + строкой §3 с носителем — строка 192, отложенная
владельцем. Форму не меняю: обещание верное, не выполнена реализация.
### L. Мелкое
- **Ссылка приложения А протухла** — ИСПРАВЛЕНО, и не новым номером, а именем функции
(`flagReasonSeverity`): номер строки в чужой зоне протухает за пак, что здесь и произошло.
- **`heading` всегда `null`** — посылка «у читателя НЕТ меток глав вообще» **неверна**, и это тот
случай, где сказалась объявленная зоной граница (фронт не смотрели). 0.3.0 снял запрет клиентской
служебной метки: «Глава N» рисует КЛИЕНТ, в локали интерфейса, за $0 (§2.3, решение владельца
09.08). Без меток остаётся только книга, у которой их нет в данных, — а это легальная книга.
Продуктовый пробел «настоящих названий нет» держится видимым строкой §3 (строка 160) и никуда не
делся; менять нечего.
- **`410` на опечатку в id главы (PD-253)** — **канон изменён в сторону зоны, а не наоборот.**
Требование различать «была и исчезла» от «такой не было» невыполнимо: id непрозрачны, после
пере-разбора не хранятся, и доказать, что id никогда не минтился, можно только кладбищем всех
выданных. Записано: `410` — ответ на id, которого нет в ТЕКУЩЕЙ структуре, был он когда-то или
нет; `404` остаётся за книгой. Лечение у клиента в обоих случаях одно — перечитать дерево.
Расхождение перестаёт быть расхождением.
### M. Что зона сделала сама — сверено, возражений нет
Все восемь пунктов сверены с каноном: область `Idempotency-Key` `(принципал, метод, путь)`,
подрезание `limit`, `resync_required` вместо тихого старта, `blocked` только когда холд реально
укорачивает шкалу, `heading: null`, `finalizing` снят, полоса и её база на одном проходе, снятие
`codeForStatus`. Совпадает.
### N. Границы этой сессии
Закрыто из того, что зона объявила своей границей: пункт H сверен с движком (чтением), пункт G
сверен с обеими зонами, совместимость с генерённым фронтом проверена исполнением
(`openapi-typescript@7` — чисто; единственная ломающая правка названа). Пункт B живьём тоже не
воспроизводился — и не требовался: он закрыт ратификацией, а не замером. Не проверялось: как
экраны фронта рисуют новые ответы (зона заморожена), и поведение построенного пути `resume` после
правки таблицы — это работа P7.
**Что забирает зона (P7):** `409` `ceiling_reached` на исчерпанном прогоне вместо `202` (A) ·
`unspecified` как обязанность, а не самодеятельность (D) · тождество интейка по содержимому с
откатом вместо `Content-Length` (E) · «сделано» = последний проход этого деплоя (G) ·
`Run.stop_requested` в `projectRun` (J) · при снятии флага — `resync_required`, а не тихое
исчезновение (H). **Что закрыто одной фразой в тексте и работы зоны не требует:** C, F, I, L.
### Ревью диффа этой зоны (исполнено этой же сессией, 20.08) — что оно изменило
Четыре независимые линзы по диффу: ХОЛОДНЫЙ ПОТРЕБИТЕЛЬ (модель другого семейства — fable; читал
ТОЛЬКО финальный канон, без диффа, без компаньона, без записки платформы, и пытался реализовать
шесть изменённых мест) · опровергатель ПОСЫЛОК диспозиций · регрессионная линза (умерло ли правило
вместе с удалённой строкой + независимая пере-сверка 0.2.3 → 0.3.0 ДРУГИМ срезом: не по удалённым
строкам, а по перечислению всех схем/полей/операций 0.2.3) · линза стандартов (RFC по
первоисточникам). Каждая находка потом адверсариально верифицировалась отдельным агентом с
установкой «по умолчанию опровергнуто». **31 находка, 5 подтверждено, 26 опровергнуто; 35 агентов,
0 упавших** (важно: упавший агент — это НЕ «опровергнуто»).
Ни одна из пяти не опрокинула диспозицию — все пять о точности МОЕГО текста, и это правильный
результат для ревью, которое ищет не «согласен ли я», а «проверяемо ли написанное»:
1. **Денежный довод в пункте G был сильнее фактов — и уже стоял НОРМОЙ в каноне.** «Предлагает
уже сделанное и берёт за них деньги»: вторая половина не проверена и для обычного случая неверна
(потолок, а не цена; расчёт по замеренной трате; неизменённая книга переигрывается за $0). Снято
и из канона, и отсюда; довод «предлагает уже сделанное» стоит сам.
2. **«Платформа физически не пишет `day_usd`»** — неверный глагол: не ЗАВОДИТ, но шаблон деплоя
пере-маршалится целиком, и оператор может внести поле без единой правки Go. Поправка усиливает
триггер пересмотра, а не отменяет отказ.
3. **«`Run` едет в кадре потока»** — ложный факт: ни один payload `Run` не несёт. Цена реальна, но
её носитель другой — предписанная кадром перечитка карточки.
4. **Носитель строки H («регистр движка») — документ, которого нет.** Живой носитель — PD-298
регистра платформы. Ровно то нарушение правила Б-21, которое этот же файл и вводит.
5. **Ссылки в чужую зону мешали ДВЕ базы** (HEAD и рабочее дерево с 81 незакоммиченным файлом
платформы) в одной строке таблицы. Лечение — база названа явно, адресация именами функций.
**Три опровержения я отклонил и правку внёс.** Три линзы независимо споткнулись об один шов —
что отвечает `resume`, когда у прогона лимит ещё есть, а СЧЁТ не тянет холд. Каждую формулировку
верификатор опроверг по отдельности («тексты можно прочесть согласованно»), но холодный потребитель
прямо написал, что решить не может, а платформа этот случай уже схлопывает в тот же вердикт.
Сходимость трёх независимых линз на одном месте — сигнал сильнее трёх поштучных опровержений.
Заведено `cause.code: credit_unavailable` (второй уровень не закрыт по замыслу — типы не двигаются),
`ceiling_reached` пере-сформулирован как «ЭТОТ прогон закончен», и явно сказано, что подменять друг
друга они не могут. Тем же ходом закрыт вопрос холодного потребителя про `stop_requested` после
`resume` (снимается) и возвращена клауза «предел попыток не на проводе», умершая с моей же правкой.
### Второе ревью: велосипеды и объём прозы (заказ владельца 20.08)
Первое ревью проверяло текст на соответствие RFC. Владелец задал ДРУГОЙ вопрос — «а так вообще делают,
или ты изобрёл своё», плюс «не раздул ли ты прозу». Отдельная проверка: линза охоты за велосипедами
(модель другого семейства, с обязательным чтением первоисточников через WebFetch) + линза размещения
прозы, которой прямо сказали, что автор диффа склонен переобъяснять. **20 находок, подтверждена 1.**
**Велосипедов не подтверждено ни одного.** Проверялись против настоящих спек: тождество интейка
против `draft-ietf-httpapi-idempotency-key-header` и **RFC 9530 (Digest Fields)** — формулировка
«чем сервер устанавливает тождество, его дело» черновику СООТВЕТСТВУЕТ, а `Repr-Digest` предписать
клиенту мы не можем и не должны · `credit_unavailable` под `409` — 402 зарезервирован, двухуровневый
код конвенционален (Microsoft `innererror`, `google.rpc.ErrorInfo`) · `stop_requested` булевым против
конвенции таймстемпа (k8s `deletionTimestamp`, AIP-165) — отклонено · `410` на никогда не
существовавший id против RFC 9110 §15.5.11 — отклонено · «валидатор легален на любом безопасном
чтении» против OAS 3.1 — отклонено · пер-строчные tombstone по образцу JMAP (RFC 8620) — не наш
случай.
**По объёму — замер стоит, классификация не устояла.** Верификатор пере-ран замер пином зоны
(`openapi-typescript@7.13.0`) и воспроизвёл его до байта: канон дал **+10 546 байт и +144 строк
комментариев** в исходники фронта. Но гипотеза «62 % этого — обоснование, которому место здесь»
пере-проверку не прошла: из тринадцати заявленных блоков выжил ОДИН — довод в `Progress` («контракт
не говорит, сколько проходов бывает…»), который (а) дословно уже записан здесь, в §6в G, и (б)
локально пере-выводит глобальное правило шапки канона. Срезано; правило осталось одной фразой.
Своим решением, а не по находке, срезан ещё денежный хвост у `Book.chapters_done` — он ничего не
меняет в поведении клиента. Итог трима: 152 758 → 152 002 байта. **Урок для следующей правки канона:
норма — в канон, довод — сюда; проверять не глазом, а диффом генерённого клиента.**
### Ратификация этой редакции — D39.152 (20.08, оркестратор №18)
> Текст ноты, сданный контрактной сессией, **исполнен и заменён живым**: ратификация записана в
> `docs/architecture/05-decisions-log.md`, нота **D39.152**, строка реестра — `05-decisions-index.md`.
> Черновик ноты, живший здесь, снят при лендинге (испр. оркестратором №18): он расходился с
> итоговым текстом в двух местах, и две копии одной ноты — ровно тот распад «один носитель на
> факт», против которого написана норма D39.112. **Что в живой ноте отличается от черновика:**
> (1) внесены ЧЕТЫРЕ текстовые правки при ратификации (сеттер `stop_requested` в `stopRun` ·
> стухшая фраза §6в A против собственного аддендума · «cleared by a resume that SUCCEEDS» ·
> приписка третьего случая `resumeRun` к таблице); (2) клейм «ломающая правка ровно одна» уточнён:
> он верен для диффа генерённых ТИПОВ и неточен поведенчески — против 0.3.0 расходятся ЧЕТЫРЕ
> места (см. D39.152 п.5).
---
## 7. Эксплуатационные примечания — НЕ норма контракта
Вынесено из спеки в 0.3.0 (Б-16). RFC 9205 §4.1 прямо про наш случай: «Requiring a particular version
@ -881,6 +1348,16 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ
карты.
4. Последняя строка — не формальность: контракт обязан иметь фразу для причины, которой ещё не
существует, и она обязана читаться нейтрально, а не как «ошибка».
5. **Клетка кода последней строки БОЛЬШЕ НЕ ПУСТА — `unspecified`** (синк 20.08, п. D записки).
Пустая клетка была невыполнима: `Note.code` в каноне обязателен и `minLength: 1`, движок и
платформа выпускаются независимо, поэтому окно «пришла причина, которой этот билд не знает» —
штатное. Платформа уже отдавала стабильный плейсхолдер `unspecified` (`ingest/notes.go`), не
имея на него ратификации; синк её ратифицировал и записал в канон обязанностью сервера, а не
строкой словаря. Бампа схемы не требует — `Note.code` объявлен `type: string`.
6. **Столбец «Ступень» пуст, а платформа уже провела границу.** `ingest/notes.go` раскладывает
девять движковых рангов на два проводных значения по правилу «потерял ли читатель текст». Это
ПРОВИЗОРНО и ратификацией не является: К-6 открыт, ответ владельца 16.08 — «решим потом». Строка
записана здесь, чтобы построенное не читалось как решённое.
| Причина движка | Ранг | Код контракта ◆ | Продуктовая фраза | Ступень |
|---|---|---|---|---|
@ -896,10 +1373,12 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ
| `empty` | 6 | `empty_answer` | ⬜ | ⬜ |
| `sanitizer_stripped` | 7 | `markup_cleaned` | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
| `upstream_not_ok` | 8 (по умолчанию) | `unavailable` | ⬜ | ⬜ |
| незнакомая причина | 8 (по умолчанию) | — (клиент рисует нейтральную фразу по правилу схемы) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
| незнакомая причина | 8 (по умолчанию) | **`unspecified`** (обязанность сервера, п.5; клиент рисует ту же нейтральную фразу, что и на незнакомый код) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
Причин пятнадцать; `upstream_not_ok` не имеет своей ветки в `flagReasonSeverity` и падает в ранг по
умолчанию (`pipeline/status.go:174`), как и любая будущая причина.
Причин пятнадцать; `upstream_not_ok` не имеет своей ветки в `flagReasonSeverity`
(`backend/internal/pipeline/status.go`, функция `flagReasonSeverity`) и падает в ранг по умолчанию,
как и любая будущая причина. ⚠ Номер строки здесь намеренно не ставится: прежняя ссылка `:174`
протухла за один пак (там теперь `GlossaryMissFlagged`) — поймано синком 20.08, п. L записки.
### Приложение А-2. Карта кодов ОШИБОК — заполнена (0.3.0)
@ -912,12 +1391,13 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ
| `unauthenticated` | 401 | гард сессии (`server.go:109-110`) |
| `forbidden` | 403 | `server.go:99` + `auth/csrf.go` (отсутствие `X-TM-Client` / чужой origin) |
| `not_found` | 404 | `pgstore.ErrNoBook`/`ErrNoAccount`/`ErrNoRun` · охраняемый catch-all `server.go:134` |
| `gone` | 410 | `pgstore.ErrNoChapter``v0.go:761` — глава, которой нет в ТЕКУЩЕЙ структуре книги (была она когда-то или нет — не различается по замыслу, `openapi.yaml` §`gone`; лечение клиента одно: перечитать дерево). ⚠ Строка дописана оркестратором №18 при ратификации 0.4.0: карта объявляла себя «заполненной», а несла 15 значений из 16 корневых — производитель у `gone` живой с P7 |
| `request_timeout` | 408 | `os.ErrDeadlineExceeded``v0.go:417` |
| `payload_too_large` | 413 | `*http.MaxBytesError``v0.go:409` |
| `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` (`v0.go:556`); `cause`: `ceiling_reached` (`bank_decisions_incomplete` удалён D39.144 — гейта полноты нет; построенный гейт `reconcile.go:934-938` ДЕМОНТИРУЕТСЯ в 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 — гейта полноты нет; построенный гейт полноты (`reconcile.go`, ветка `case "awaiting_bank"`) ДЕМОНТИРУЕТСЯ в P7 |
| `ceiling_unavailable` | 409 | `runs.ErrCeilingOutOfBounds` + `pgstore.ErrInsufficientCredit` (`v0.go:558`); `cause`: `bounds_moved` · `credit_held`; несёт `blocked` |
| `idempotency_conflict` | 409 | форма заведена батчем; реализация — P7 |
| `content_refused` | 400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием `rejected` + `reject_reason` |

View file

@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: TextMachine API
version: 0.3.0
version: 0.4.0
summary: Ratified contract between the frontend and the TextMachine platform.
description: |
**RATIFIED contract.** Canonical copy: `docs/architecture/14-api-contract/`;
@ -47,6 +47,12 @@ info:
Every collection read and the book card answer an `ETag` and honour `If-None-Match` with `304`. A
client is expected to use them: a frame says only THAT something changed.
A validator on any OTHER safe read of this surface is legal too and needs no declaration here —
the operations that declare one are those where a client gains by using it, not an exhaustive
list of where a server may send one. Two rules bound that: a client is never REQUIRED to send
`If-None-Match`, and `304` is only ever the answer to one it did send. Stated so that a server
can put a validator in one place instead of keeping a list of exceptions that rots.
An `ETag` is bound to the principal and to the representation; a response negotiated on
`Accept-Encoding` MUST carry `Vary: Accept-Encoding`.
@ -198,12 +204,19 @@ paths:
all (`intake_enabled`) · `408` the body did not finish in time, retry · `413` over
`intake_max_bytes`.
**`Idempotency-Key` on a multipart body.** "The same request" is compared over the DECLARED
parts — the metadata and the file's name and size — and never over the bytes themselves: a
server does not hold a book in memory to compare it, and a retry of an interrupted upload
re-sends the same file. Consequence, and it is the case that matters: **a `408` is not a
completed attempt**, so the same key may be presented again; the retry is a repeat of the
first call, not a second book.
**`Idempotency-Key` on a multipart body.** "The same request" means the same metadata parts,
the same file NAME, and the same file CONTENT. How a server establishes the last of those is
its own affair — it need not hold a book in memory, and a digest taken while the bytes stream
past costs it nothing — but the guarantee is the one that matters: **a key never answers with
a response that belongs to a different request.** A server that cannot establish sameness
MUST NOT replay: it answers `409` `idempotency_conflict` rather than the first response.
The framing is NOT the request. Two uploads of one file differ in their multipart boundaries
and in the length of the body carrying them, and a server that compared either would refuse
the honest retry this key exists to serve.
Consequence, and it is the case that matters: **a `408` is not a completed attempt**, so the
same key may be presented again; the retry is a repeat of the first call, not a second book.
parameters:
- $ref: '#/components/parameters/ClientHeader'
- $ref: '#/components/parameters/IdempotencyKey'
@ -350,8 +363,13 @@ paths:
introduced, and the client's whole memory model stands on that. Page size default:
`GET /capabilities`.
**`410 Gone`** answers a chapter that existed and no longer does: a book cut again leaves the
old ids gone rather than absent, and the client re-reads the tree. `404` would mean a typo.
**`410 Gone`** answers a chapter id this book's current structure does not hold: a book cut
again leaves the old ids gone rather than absent, and the client re-reads the tree.
It is the answer whether or not the id was ever real. Ids here are opaque and are not kept
after a re-cut, so "gone" and "never existed" are not two states a server can tell apart
without keeping a graveyard of every id it ever minted — and a client's remedy is the same in
both cases anyway. `404` stays for the BOOK, which a server does know about.
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
@ -656,8 +674,10 @@ paths:
**The `202` does not mean the run has stopped** — the `Run` it returns still carries a live
status. Stopping is asynchronous and there is no `stopping` value in `RunStatus`: the run
reaches `stopped` when the work winds down, and the `status` frame says so. Between the two
the client shows its own pending state. `409` answers a run that is not running at all.
reaches `stopped` when the work winds down, and the `status` frame says so. This call is what
SETS `stop_requested`, and the `Run` in this `202` already carries it `true` — so the state
between the two survives a reload instead of living only in the client. `409` answers a run
that is not running at all.
parameters:
- $ref: '#/components/parameters/ClientHeader'
responses:
@ -690,6 +710,28 @@ paths:
| `failed`, `ready` | `409` `run_not_resumable`, no `cause` — it is not a stopped run | a NEW run |
| `translating` | `409` `run_not_resumable`, no `cause` | nothing; it is already running |
⚠ **The limit of the run is a SECOND axis, and it overrides the two `202` rows above.** A run
that has spent everything its `ceiling_chapters` bought has nothing left to continue WITH —
there is no work this call could pay for — and that is true whether it stands in
`awaiting_bank` or in `stopped`, and however much credit the account holds. Such a call is
`409` `run_not_resumable`, `cause.code: ceiling_reached`, exactly as `paused` is, and the
remedy is the same: a NEW run. **A `202` therefore means work was actually reopened**, and a
client may render it as "continuing" without a second read.
**The account is a THIRD case, and it is answered separately.** Continuing costs money, so a
run with room left in its limit still cannot be reopened while the account cannot cover the
rest of it: that is `409` `run_not_resumable`, `cause.code: credit_unavailable`, and the
remedy is to top up rather than to start anything. It is deliberately not folded into
`ceiling_reached` — that one says "this run is finished with", and saying it to a user whose
run has chapters left would send them to buy a new run they do not need. The reverse mistake
is the one the canon already warns about at `AccountHaltReason`: an account-wide state lit
from a fact about one run tells a user with money that they have none.
Like the second axis, this one is judged AFTER the table and only where the table said
`202`: a row that already answers `409` keeps the `cause` the table gives it, so a `paused`
run on an empty account is still `ceiling_reached` — the remedy there is a new run either
way, and the account's own emptiness is what `Usage` is for.
⚠ **A NEW run is legal from ANY paused book, whatever `paused_reason` says — `null`
included.** The limit travels with the START of a run and nothing changes it afterwards, so a
run stopped at one is never continued by this call; the reason is a hint about WHY the work
@ -698,6 +740,12 @@ paths:
bought again. A client that waits for a particular reason value before offering that strands
the user on the commonest stop there is.
That legality is not a promise of progress: a deployment may hold limits of its OWN that no
purchase lifts and this contract has no word for, and a new run under one stops the same way
it did before. The remedy a client offers is still the new run — it has no better one — but
it does not present it as a guarantee, and a `paused` run that returns immediately to
`paused` is a legal outcome rather than a fault to report.
**503 answers a deployment that cannot run at all** — continuing a run is starting a process.
parameters:
- $ref: '#/components/parameters/ClientHeader'
@ -945,6 +993,13 @@ components:
A DELETION cannot be expressed this way. Two answers close that: `resync_required` on the
stream when a collection is replaced wholesale, and `400` with
`cause.code: version_too_old` for a watermark that predates such a replacement.
⚠ **A row already delivered is therefore never withdrawn ONE AT A TIME.** A delta read has no
shape for "this one is gone", so a row that stops belonging to its collection would sit on the
client for as long as the tab is open — and a count beside it would already disagree, which is
the visible half of the same fault. A collection that must lose a row loses it the only way
this contract can express: replaced wholesale, announced with `resync_required`. A server that
finds itself retracting a single row owes that frame; it does not simply stop sending it.
schema: { $ref: '#/components/schemas/Revision' }
Limit:
name: limit
@ -1031,8 +1086,10 @@ components:
schema: { $ref: '#/components/schemas/Problem' }
Gone:
description: |
The object existed and does not any more; its identifier will not be reissued. A client
holding a reference re-reads the collection it came from.
The identifier does not belong to the current structure of this book and will not be
reissued. A client holding a reference re-reads the collection it came from. It is also the
answer to an identifier that never existed, on a collection whose identifiers a server cannot
prove it never minted — the operation says which.
headers:
X-Request-Id: { $ref: '#/components/headers/RequestId' }
content:
@ -1171,7 +1228,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.3.0']
examples: ['0.4.0']
language_pairs:
type: array
description: |
@ -1240,6 +1297,9 @@ components:
reaches one. The book-wide figure is `Book.chapters_done` against `Book.chapter_count` and
answers a different question.
⚠ **"Finished" means the work THIS deployment does on a chapter is finished** — the last pass
the book actually gets, whatever that is.
No ready-made percentage is shipped: how a fraction is drawn is a product decision.
required: [done, total, eta_seconds]
properties:
@ -1314,6 +1374,8 @@ components:
- `content_refused` — the service will not translate this book. **One coarse reason for a
whole class**: it never says which check refused, never varies between attempts, and gives
nothing to search against. A client shows one neutral phrase and does not invite a retry.
⚠ **Declared ahead of any producer**, on the same footing and for the same reason as
`ErrorCode.content_refused`; the companion carries who owes the check.
A client MUST tolerate an unknown value under a minor bump and MUST render a rejected book
that carries no reason at all: `null` is legal.
@ -1402,10 +1464,14 @@ components:
type: integer
minimum: 0
description: |
Chapters fully translated. Against `chapter_count` this is the book's own progress —
Chapters fully translated — "finished" as `Progress` defines it, the last pass this
deployment gives a chapter. Against `chapter_count` this is the book's own progress —
what a library row shows — and it never moves backwards WITHIN one `structure_version`;
cutting the book again recomputes both numbers. The bar of a RUNNING run is
`Run.progress`, which measures what that run bought.
⚠ It is also the number a service reads to decide **how much of the book is left to
buy**, so a count that cannot reach `chapter_count` keeps offering work already finished.
character_count:
type: [integer, 'null']
minimum: 0
@ -1460,6 +1526,7 @@ components:
- revision
- status
- stop_for_signing
- stop_requested
- ceiling_chapters
- progress
- paused_reason
@ -1479,6 +1546,25 @@ components:
type: boolean
description: |
The run was started with a stop for the book's terms to be signed.
stop_requested:
type: boolean
description: |
The user asked this run to stop. **It answers "was this what I asked for", which no
`status` can answer**: stopping is not instant, so a stop asked for during translation can
meet the run reaching a stop of its own — the bank signature — and the run then ends in
`awaiting_bank`, a status that offers to continue on a click that meant "stop". A client
that reads this flag says "stopped — the work is parked at the signature, continuing is
cheap" instead of offering the wrong action, and it survives a reload, which a client's
own pending state does not.
Required and never absent, `false` for a run nobody asked to stop: absence would be a
second way of writing `false` and the reader would have to handle two.
It is a record of the REQUEST, not of the outcome — it stays `true` on a run that went on
to finish anyway, because that is exactly the case the screen has to explain. It is
cleared by a `resume` that SUCCEEDS: continuing IS the withdrawal of the request, and a
run that was asked to continue is no longer a run someone asked to stop. A refused
`resume` clears nothing — nothing was withdrawn.
ceiling_chapters:
type: integer
minimum: 1
@ -1695,9 +1781,16 @@ components:
in the companion's appendix A. A client MUST show a neutral phrase — never the word
"error" — for a code it does not know.
Not enumerated here: the map is a table the contract's owner fills, and freezing a list
in the schema before the words exist would make it a second copy. It becomes an enum when
appendix A is written.
⚠ **`unspecified` is reserved and is the server's answer when it has no code for what it
was told.** The service that produces notes and the one that serves them ship apart, so a
reason newer than this build is an ordinary event, not a fault — and this field is
required, so a server has to write something. Writing `unspecified` is the obligation;
inventing a word for the reason is forbidden, because an invented word is a vocabulary
nobody agreed to. A client treats it exactly as it treats a code it does not know.
Not otherwise enumerated here: the map is a table the contract's owner fills, and freezing
a list in the schema before the words exist would make it a second copy. It becomes an
enum when appendix A is written.
chapter_id:
$ref: '#/components/schemas/Id'
description: |
@ -1862,12 +1955,27 @@ components:
action:
type: string
enum: [approve, decline]
description: '`approve` — take the term into the book (with a translation in `dst`); `decline` — leave it out.'
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.
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`.
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
@ -2196,8 +2304,8 @@ components:
properties:
contract:
type: string
description: Contract version this deployment serves, e.g. `0.3.0`.
examples: ['0.3.0']
description: Contract version this deployment serves, e.g. `0.4.0`.
examples: ['0.4.0']
EventStatus:
allOf:
@ -2371,9 +2479,10 @@ components:
presented by cookie, or the request came from an origin this deployment does not accept;
- `not_found` (404) — no such object, or one this account may not see, or a path this
deployment does not serve;
- `gone` (410) — the object existed and does not any more, and its identifier will not be
- `gone` (410) — the identifier does not belong to the current structure and will not be
reissued. Told apart from `not_found` because the remedy differs: re-read the collection,
rather than check the address;
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`;
- `run_in_flight` (409) — this book is already being translated;
@ -2381,11 +2490,13 @@ components:
being cut, or was rejected;
- `run_not_stoppable` (409) — this run is not running;
- `run_not_resumable` (409) — this run cannot be continued. `cause.code` says why:
`ceiling_reached` — the run stopped at a limit, the chapters it bought or the credit
behind them, and the remedy is a NEW run rather than this call. A run that is not a
stopped run at all carries no `cause`. An incomplete set of bank decisions is NOT a
reason: signing is one act over the whole bank, and `resume` lifts that stop with the
decisions as they stand;
`ceiling_reached` — THIS RUN is finished with: it stopped at the limit it was given, or has
spent all of it. The remedy is a NEW run rather than this call, and it is the answer in
every status, not only `paused` (see `resumeRun`). `credit_unavailable` — the run has room
left but the ACCOUNT cannot cover the rest of it; the remedy is to top up, and no new run
would help either. The two never stand in for one another. A run that is not a stopped run
at all carries no `cause`. An incomplete set of bank decisions is NOT a reason: signing is
one act over the whole bank, and `resume` lifts that stop with the decisions as they stand;
- `ceiling_unavailable` (409) — the limit asked for cannot be started. `cause.code`: `bounds_moved`
— the bounds changed between the read and this call; `credit_held` — the account's credit
is held elsewhere, and `blocked` names the book holding it;
@ -2395,9 +2506,18 @@ components:
- `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
does not invite a retry, and the server bounds how many times one account may try — that
bound is the server's and is not on the wire. A refusal of a whole BOOK is not reported
here at all: it is a state of the book, `rejected` with `reject_reason: content_refused`;
does not invite a retry. A refusal of a whole BOOK is not reported here at all: it is a
state of the book, `rejected` with `reject_reason: content_refused`.
⚠ **Declared ahead of any producer** — the check that would raise it is not built, and
nothing on either side of this boundary emits it today. It is here because the shape of the
answer was settled before the check was, and settling it later would have meant settling it
under pressure. The obligation that comes with it — that whatever raises it also bounds how
often one account may try, since an unbounded refusal is an oracle for guessing past it —
falls due WITH the producer, not before; the companion carries who owes both. That bound is
the server's and is never on the wire in any form: a number a client can read is a number a
client can plan around, which is the whole of what the bound exists to stop. A client
handles the code from today: an old client meeting it for the first time is the failure this
note exists to prevent;
- `service_unavailable` (503) — the deployment cannot do this right now;
- `internal_error` (500) — a defect on our side. Nothing about it is actionable by a client
beyond quoting `request_id`.

View file

@ -57,7 +57,7 @@
| ПТ-22 | Настройки от человека: 18+, жанр и т.п. (V0-п.22) | 🔶 | `book.yaml` (labels) данными; UI = Ф3. ⚠ ЖАНР из продукта ВЫКИНУТ словом владельца 16.08 (D39.138 п.2в, строка 184: ни на чём не ветвится, влияние не измерено; снимается из промптов/брифа/контракта/формы) |
| ПТ-23 | Веб-ПЕРВЫЙ фронт; SEO/ранжирование Google — куки, страницы-сироты, актуальные алгоритмы (V5-п.2); продукт = **SaaS**, не локальное приложение (решение владельца 02.08, D39.81 — натив-прицел V0-п.26 СНЯТ этим решением, вернуть только новым словом владельца); SEO только под Google, Яндекс снят; рынок глобальный; десктоп = PWA первым шагом, Tauri по явным триггерам (решения владельца 02.08, D39.84) | ⭕ | строка 71 (Ф3-бриф); SEO вошёл в стек-дизайн ДО выбора (D39.84, требование исполнено): двухконтурный деплой — SPA `app.<домен>` целиком под noindex + статический публичный контур ≤3 кликов от главной (лечение страниц-сирот), `frontend/docs/STACK_DECISIONS.md` §4; SSR-вопрос закрыт этим решением |
| ПТ-24 | Цикл согласования глоссария с профпереводчиком/редактором (V0-п.23) | 🔶 | подписной цикл терминолога в бэкенде есть; UI = Ф3. Модель цикла записана словом владельца 16.08 (D39.144 + строка 192): черновой перевод и черновой банк появляются вместе → ОДИН ОК всему банку (не пер-термная масс-подпись) → доработка → пост-ридинговые точечные правки с пере-генерацией по вхождениям (механика — после полигона) |
| ПТ-33 | Интерфейс НЕ раскрывает конвейер: ни моделей, ни стадий, ни внутренней терминологии (решение владельца 02.08; граница пере-читана 15.08 — охранять АЛГОРИТМЫ, не минорную механику, D39.136 п.4а). ⚠ «Лестница» фаз «загрузка → … → финал → готово» — устная фраза владельца, НЕ норма продукта (его слово 16.08, D39.138: `finalizing` сворачивается) | 🔶 | инвариант D39.81; словарь статусов принят (D39.100/К-1); карта «вердикт → фраза» — приложение А компаньона (В-3), после D39.138 пишется по словарю машинных кодов Б-1. ⚠ Ревью 28 (Б-0) ОПРОВЕРГЛО исполнением ответ §5 компаньона «сменится стадия — фронт не правится»: волны draft/edit утекали в `Progress` до React-компонента; снято ИСПОЛНЕННЫМ батчем 0.3.0 (D39.142/143: Progress — один счётчик в каноне, анти-утечка-греп чист); машинный ГЕЙТ на утечку — фронту при разморозке (пинг 16.08) |
| ПТ-33 | Интерфейс НЕ раскрывает конвейер: ни моделей, ни стадий, ни внутренней терминологии (решение владельца 02.08; граница пере-читана 15.08 — охранять АЛГОРИТМЫ, не минорную механику, D39.136 п.4а). ⚠ «Лестница» фаз «загрузка → … → финал → готово» — устная фраза владельца, НЕ норма продукта (его слово 16.08, D39.138: `finalizing` сворачивается) | 🔶 | инвариант D39.81; словарь статусов принят (D39.100/К-1); карта «вердикт → фраза» — приложение А компаньона (В-3), после D39.138 пишется по словарю машинных кодов Б-1. ⚠ Ревью 28 (Б-0) ОПРОВЕРГЛО исполнением ответ §5 компаньона «сменится стадия — фронт не правится»: волны draft/edit утекали в `Progress` до React-компонента; снято ИСПОЛНЕННЫМ батчем 0.3.0 (D39.142/143: Progress — один счётчик в каноне, анти-утечка-греп чист); машинный ГЕЙТ на утечку — фронту при разморозке (пинг 16.08). ⚠ **ОСТАТОК, названный владельцем 20.08 (D39.153, строка 200):** конвейер не утекает СЛОВАМИ, но утекает ФОРМОЙ прогресса — доля считается по отрезкам и обнуляется при снятии стопа, то есть пользователь видит 100% после черновика и старт заново; и фаза, которая глав не заканчивает (сбор банка), сегодня полосой невыразима — знаменателя она не объявляет; может ли объявлять, это открытый вопрос движку в той же строке 200. Требование: ОДНА сквозная доля на всю работу прогона, считает СЕРВЕР (иначе клиент снова узнаёт про фазы), плюс подпись «что делается сейчас» и неопределённый индикатор там, где доли в главах нет; веса не подбираются — знаменатель берётся из объявленного движком плана проходов |
| ПТ-34 | Пользовательский перевод НИКОГДА не попадает на публично индексируемый URL — ни витрин примеров, ни «поделиться главой», ни публичных ссылок чтения (жёсткий продуктовый запрет, решение владельца 02.08; авторское право на исходники + политика Google по масштабируемому машин-контенту действуют на весь домен) | ⭕ | инвариант D39.84; носители: двухконтурный деплой `frontend/docs/STACK_DECISIONS.md` §4 (SPA под `X-Robots-Tag: noindex` на каждом HTML-ответе; публичный контур без контента переводов) + контракт 14 (`architecture/14-api-contract/`, D39.99) + платформенный бэклог П-1; правило доступа к экспорт-ссылке (срок/подпись) — батч 183 (research/28 Б-4) |
| ПТ-35 | Лимиты «как в Claude Code» (решение владельца 04.08, D39.100): страница лимитов/использования в настройках (статус, НЕ суммы — запрет денег на экране в силе); стоп по потолку = статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны» | 🔶 | контракт К-8 закрыт, спека несёт `paused`/`paused_reason` (0.2.0+, D39.115); ручки /v0 — платформа-раннер P4 (D39.123); статус `paused` по потолку ЧЕСТЕН с P6 (D39.132: exit 4/событие → `paused`, не `failed`; дневной потолок оператора различим и из платформенного шаблона убран — решение владельца 15.08); остаток: страница лимитов и оповещение на фронте (после P7) · точное продуктовое слово — В-3 на владельце · ~~К-13~~ ЗАКРЫТ (D39.132 п.2а: `null` на проводе; пере-суждён ревью 28 §4 №9 и НЕ опрокинут — с уходом дневной оси различие перестало быть продуктовым) |
| ПТ-36 | Интерфейс МУЛЬТИЯЗЫЧНЫЙ, русский не хардкодится в разметке (слово владельца 10.08; язык интерфейса — отдельная ось от языков перевода). ⚠ До 10.08 требование в реестре отсутствовало — пропуск поймал владелец (D39.127 п.2г) | 🔶 | ПОСТРОЕНО S3.7 (принят D39.129): каталог `frontend/src/i18n/ru.ts` (157 ключей) · механизм `@internationalized/string` · ESLint-гейт «текст местом» · стор языка с персистом · `<html lang>` следует выбору; содержимое — только ru; продуктовый список языков — 🔒 владелец (В-9 зоны фронта) |