From 008e1659538baa960e0beb18d0ac4f2e46bdc496 Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 16 Aug 2026 02:40:59 +0300 Subject: [PATCH] Ratify D39.138: accept the contract review report, order the 0.3.0 batch, close row 179 and PD-104, open rows 183-187, archive the review prompt --- docs/PROGRESS.md | 14 +- docs/README.md | 4 +- docs/architecture/05-decisions-index.md | 3 +- docs/architecture/05-decisions-log.md | 26 +- docs/architecture/14-api-contract/README.md | 9 + ...TRACT_REVIEW_SESSION_PROMPT_2026-08-15.md} | 2 + docs/research/28-contract-review.md | 1657 +++++++++++++++++ 7 files changed, 1706 insertions(+), 9 deletions(-) rename docs/{CONTRACT_REVIEW_SESSION_PROMPT.md => archive/prompts/CONTRACT_REVIEW_SESSION_PROMPT_2026-08-15.md} (97%) create mode 100644 docs/research/28-contract-review.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index c95effed..dac5305d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,14 +1,14 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-15, голова D39.137 — пере-пин DeepSeek принят, потолки снова защищают; пакет слов владельца — D39.136; приёмка S4 + контракт 0.2.3 в каноне — D39.135; приёмка бэкенд-migrate — D39.134; передача №16→№17 — D39.133; приёмки S3.6/S3.7 — D39.127/129, P5 — D39.130, эмиттера — D39.131, P6+дофикс — D39.132). Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей после инцидента отката чужих файлов — D39.113, гардрейлы в CLAUDE.md. +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-16, голова D39.138 — контракт-ревью ПРИНЯТО (research/28: 4 опровергателя, 42 улики, 0 опровергнуто), решения владельца 16.08 ратифицированы, батч 0.3.0 заказан отдельной спек-сессией (строка 183, промт — очередь №17); пере-пин DeepSeek — D39.137; пакет слов владельца — D39.136; приёмка S4 + контракт 0.2.3 в каноне — D39.135; приёмка бэкенд-migrate — D39.134; передача №16→№17 — D39.133; приёмки S3.6/S3.7 — D39.127/129, P5 — D39.130, эмиттера — D39.131, P6+дофикс — D39.132). Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей после инцидента отката чужих файлов — D39.113, гардрейлы в CLAUDE.md. > - **Эра №15 закрыта — семь приёмок, все ПРИНЯТЫ и залендены**; лента, коммиты и разборы — D39.109–123 (D-лог) и реестр нот `architecture/05-decisions-index.md`; снимок прежних бюллетеней этой шапки — архив-слайс `-08-02-04`. -> - **ЖИВОЕ:** полигон эксп-22/23 — дерево ЗАЛЕНДЕНО 10.08 с ревью-шапками приёмки №16 (числа подтверждены независимым пересчётом; **выводы НЕ ратифицированы** — владелец 10.08 признал постановку неполной), **фаза Д ИДЁТ (заказ 10.08)**: промт `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md` (staged полигоном); деньги фазы: расход $6.0024, цепь потолков $4.50→$5.40→$6.74 — **САНКЦИОНИРОВАНО словом владельца 15.08 напрямую оркестратору №17** («потолок поднимал я, мы с полигоном в синке»); замена ja-книги на `enkan_no_hate_ja` — им же, СТОП ja-оси снят (ход — секция «Полигон» ниже); при ратификации фазы Д в D-ноту: декой-правило + обязательный кросс-семейный опровергатель приёмки (одобрены владельцем 10.08) · фронт: **ЗОНА ЗАМОРОЖЕНА словом владельца (D39.136 п.2)** после принятого S4 (D39.135, `267aa35`: путь «файл → загрузка → разбор → запуск с потолком → прогресс» в мире моков, контракт **0.2.3 В КАНОНЕ**); разморозка = лендинг P7 + ответы гейта S5 (В-10 спойлер · пере-заказ фраз В-11); фикс-лист ФС-1..12 + Ф-63/Ф-28 (одобрены) — вход первого касания зоны; живой smoke против дев-стенда — легитимен и во фризе; i18n-механизм построен (ПТ-36 🔶) · **платформа P5 ИСПОЛНЕНА и ПРИНЯТА в три раунда (D39.130, `69d485a`)**: интейк книг `POST /books` с сентинелом хранилища · стоп/резюм с намерением в Postgres · Prometheus · Go-floor 1.26.6; развилка `book.yaml`: бета = форма Б (стройка следующим касанием зоны), движковая форма = строка 170 · **эмиттер ИСПОЛНЕН и ПРИНЯТ (D39.131, `9cfe080`, дофикс до лендинга)**: `events.jsonl` (StreamVersion 1.1) + exit 4 потолок / 5 graceful / полоса отказов 10–19 + пер-вызовный репэйр-гейт; строки 103/135/165/102 закрыты; движковая половина PD-113/PD-196 построена D39.131 · **платформа P6 + дофикс ИСПОЛНЕНЫ и ПРИНЯТЫ (D39.132)**: потребительская половина шва (exit-коды одним словарём, `unit_done` присваиванием, scope потолка, TM_TRACE_ID против усыновления) + интейк формы Б + дев-стенд с сидом — **PD-113/PD-196 ЗАКРЫТЫ, шов закрыт с обеих сторон, major в регистре платформы 0**; деплой-порядок против деадлока v15 записан и исполним (`tmplatformctl books --migratable`) · **`tmctl migrate` ИСПОЛНЕН и ПРИНЯТ (D39.134, `d55edd4`; панель 6 линз, 6/6 мутаций приёмки пойманы)**: exit 13 ФИНАЛИЗИРОВАН, строка 174 закрыта, 175 пере-скоуплена на дальний остаток, новая 177 — гигиена restore point; деплой эмиттер-бинаря РАЗБЛОКИРОВАН схемно (⚠ рантбук деплоя живым `migrate` end-to-end ещё НЕ гонялся — предусловие первого выката, пинг платформе 15.08) · эскроу (136) — следующим денежным промтом (П-18). -> - **Открыто на владельце:** **развязка git с origin** (темп-коммит `535581d` откачен локально и пересобран чистыми коммитами — D39.134 п.0; локальная линия ИСТИННА, 15 коммитов поверх `b751ac6`, origin держит старую линию ⇒ force-push вашей рукой; не пуллить) · Приложение А контракта (148: реконструкция с баннером или заново) · продуктовое слово «остановлена: лимиты» (В-3) · К-13/Ф-31 (paused_reason один — «свой потолок» и «нет кредита» неразличимы) · В-4/Ф-30 (глава без заголовка) · фри-тир PD-104 (ноль на бете против автогранта $5 с суточным потолком) · ~~порог 116~~ ЗАКРЫТ D39.136 п.6б (порог выборкой, по делегации) · мини-проба флора 44 (одобрена, промт не выдан) · подпись денежного шага 46 · лист В-3+К-6 · авто-резюм paused (вопрос платформы) · ~~В-11~~ ПЕРЕ-ЗАКАЗ 15.08: фразы НЕ утверждены — конкретнее причину, граница ПТ-33 пере-читана «охранять алгоритмы, не минорку» (D39.136 п.4а; новые фразы — вход S5; мультиязычность фраз всех зон — ревью 179) · **В-10: спойлер — вежливость или защита?** (факт: банк приезжает клиенту страницами по 1000, sense лежит в DOM, размытие чисто визуальное — потому находится Ctrl+F и печатью; «защита» = правка контракта «sense по раскрытию», вход ревью 179) · ~~Ф-62~~ РЕШЕНО направлением 15.08 (переименование книги — в контракт; форма — из ревью 179) · ~~Ф-63~~ ОДОБРЕНА перекраска 15.08 (первым касанием зоны) · ~~строка 172: вендор-сессия DeepSeek~~ ОТРАБОТАЛА и ПРИНЯТА (D39.137, `76049bb`: пик запинен, потолки защищают) — **на владельце остались: схема time-based** (доклад §5 отчёта; рекомендация — пик оставить + операционное правило «прогоны не стартуют в 01–04/06–10 UTC», scheduler не начинать до ответа вендора об отметке времени тарификации) **и вход ратификации фазы Д: посылка «dspro дешевле glm» в пике перевёрнута** (×1.26 дороже, D39.137 п.4) · ~~СТОП ja-оси фазы Д~~ СНЯТ 15.08 (замена книги на `enkan_no_hate_ja` — рука владельца) · **возражение Sol по проходу `tier` эксп-23** (пере-судить починенным ригом или оставить с записанным возражением — PROGRESS «Полигон»; вес вырос: Д14.10 показал нули прибора claude на реальной инверсии смысла) · ~~поле `title` в `BookIntake`~~ РЕШЕНО 14.08: ратифицировано в 0.2.3 (замечание 12 S3.5, владелец подтвердил; исполняет S4) · фронт-вопросы зонного журнала: В-7 (плотность интерфейса; держит и Ф-54 — обрезку центра на зуме) · В-8 (слово состояния в дереве) · **В-9 (какие языки интерфейса обещает продукт — механизм готов, ПТ-36)** · Ф-38 (вкладка «Замечания») · ~~граница ПТ-33 для полей `BankTerm`~~ ЗАКРЫТ 15.08: полей НЕ заводить, смысла хватает (D39.136 п.4б; sense коротковат — строка 178) · ~~санкция расхода фазы Д~~ ДАНА 15.08 · ~~дизайн-ратификация 160/161~~ ДАНА 15.08 (D39.136 п.3: Этап 0 паком, 161 — дизайн-паком с мандатом качества) · ~~PD-203~~ РАТИФИЦИРОВАН (D39.136 п.6а: флаг аккаунта только по аккаунт-причинам) · ~~Ф-28~~ ОДОБРЕН бамп пинов 15.08 (первым касанием зоны) · Ф-56/Ф-57/Ф-61 — входы контракт-ревью 179 (решение владельца 15.08). +> - **ЖИВОЕ:** полигон эксп-22/23 — дерево ЗАЛЕНДЕНО 10.08 с ревью-шапками приёмки №16 (числа подтверждены независимым пересчётом; **выводы НЕ ратифицированы** — владелец 10.08 признал постановку неполной), **фаза Д ИДЁТ (заказ 10.08)**: промт `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md` (staged полигоном); деньги фазы: расход $6.0024, цепь потолков $4.50→$5.40→$6.74 — **САНКЦИОНИРОВАНО словом владельца 15.08 напрямую оркестратору №17** («потолок поднимал я, мы с полигоном в синке»); замена ja-книги на `enkan_no_hate_ja` — им же, СТОП ja-оси снят (ход — секция «Полигон» ниже); при ратификации фазы Д в D-ноту: декой-правило + обязательный кросс-семейный опровергатель приёмки (одобрены владельцем 10.08) · фронт: **ЗОНА ЗАМОРОЖЕНА словом владельца (D39.136 п.2)** после принятого S4 (D39.135, `267aa35`: путь «файл → загрузка → разбор → запуск с потолком → прогресс» в мире моков, контракт **0.2.3 В КАНОНЕ**); разморозка = лендинг P7 + ответы гейта S5 (~~В-10~~ ЗАКРЫТ 16.08 «вежливость», D39.138 п.2к · пере-заказ фраз В-11 — форма дана ревью 28: клиент рисует фразу по машинному `code`, Б-1/§8а, зонная половина — после батча 183); фикс-лист ФС-1..12 + Ф-63/Ф-28 (одобрены) — вход первого касания зоны; живой smoke против дев-стенда — легитимен и во фризе; i18n-механизм построен (ПТ-36 🔶) · **платформа P5 ИСПОЛНЕНА и ПРИНЯТА в три раунда (D39.130, `69d485a`)**: интейк книг `POST /books` с сентинелом хранилища · стоп/резюм с намерением в Postgres · Prometheus · Go-floor 1.26.6; развилка `book.yaml`: бета = форма Б (стройка следующим касанием зоны), движковая форма = строка 170 · **эмиттер ИСПОЛНЕН и ПРИНЯТ (D39.131, `9cfe080`, дофикс до лендинга)**: `events.jsonl` (StreamVersion 1.1) + exit 4 потолок / 5 graceful / полоса отказов 10–19 + пер-вызовный репэйр-гейт; строки 103/135/165/102 закрыты; движковая половина PD-113/PD-196 построена D39.131 · **платформа P6 + дофикс ИСПОЛНЕНЫ и ПРИНЯТЫ (D39.132)**: потребительская половина шва (exit-коды одним словарём, `unit_done` присваиванием, scope потолка, TM_TRACE_ID против усыновления) + интейк формы Б + дев-стенд с сидом — **PD-113/PD-196 ЗАКРЫТЫ, шов закрыт с обеих сторон, major в регистре платформы 0**; деплой-порядок против деадлока v15 записан и исполним (`tmplatformctl books --migratable`) · **`tmctl migrate` ИСПОЛНЕН и ПРИНЯТ (D39.134, `d55edd4`; панель 6 линз, 6/6 мутаций приёмки пойманы)**: exit 13 ФИНАЛИЗИРОВАН, строка 174 закрыта, 175 пере-скоуплена на дальний остаток, новая 177 — гигиена restore point; деплой эмиттер-бинаря РАЗБЛОКИРОВАН схемно (⚠ рантбук деплоя живым `migrate` end-to-end ещё НЕ гонялся — предусловие первого выката, пинг платформе 15.08) · эскроу (136) — следующим денежным промтом (П-18). +> - **Открыто на владельце:** **развязка git с origin** (темп-коммит `535581d` откачен локально и пересобран чистыми коммитами — D39.134 п.0; локальная линия ИСТИННА, 15 коммитов поверх `b751ac6`, origin держит старую линию ⇒ force-push вашей рукой; не пуллить) · **запуск спек-сессии батча 0.3.0** (промт готовит №17; состав — research/28 §5, ратификация D39.138) · Приложение А контракта (148: реконструкция с баннером или заново; карта «код → фраза» теперь пишется по словарю Б-1 батча) · продуктовое слово «остановлена: лимиты» (В-3) · ~~К-13/Ф-31~~ пере-суждён ревью 28 и НЕ опрокинут (§4 №9: с уходом дневной оси различие перестало быть продуктовым; остаток-описание `:879` — батч 183) · В-4/Ф-30 (глава без заголовка) · ~~фри-тир PD-104~~ ЗАКРЫТ словом владельца 16.08 (D39.138 п.2л: грант 0 на бете, начисление руками; возврат $5 — с суточным потолком при платежах; исполнение — P7-однострочник) · ~~порог 116~~ ЗАКРЫТ D39.136 п.6б (порог выборкой, по делегации) · мини-проба флора 44 (одобрена, промт не выдан) · подпись денежного шага 46 · лист В-3+К-6 · авто-резюм paused (вопрос платформы) · ~~В-11~~ ПЕРЕ-ЗАКАЗ 15.08: фразы НЕ утверждены — конкретнее причину, граница ПТ-33 пере-читана «охранять алгоритмы, не минорку» (D39.136 п.4а; новые фразы — вход S5; мультиязычность фраз всех зон РЕШЕНА ревью 28: Б-1 вариант B — клиент рисует по машинному `code`, батч 183) · ~~В-10~~ ЗАКРЫТ 16.08: «вежливость» (D39.138 п.2к — контракта не касается, `sense` обязателен, размытие — визуальное дело фронта) · ~~Ф-62~~ РЕШЕНО: `PATCH /books/{id}` title-only + `DELETE` (ревью 28 Б-5, батч 183; правка — отображение, до брифа не доезжает) · ~~Ф-63~~ ОДОБРЕНА перекраска 15.08 (первым касанием зоны) · ~~строка 172: вендор-сессия DeepSeek~~ ОТРАБОТАЛА и ПРИНЯТА (D39.137, `76049bb`: пик запинен, потолки защищают) — **на владельце остались: схема time-based** (доклад §5 отчёта; рекомендация — пик оставить + операционное правило «прогоны не стартуют в 01–04/06–10 UTC», scheduler не начинать до ответа вендора об отметке времени тарификации) **и вход ратификации фазы Д: посылка «dspro дешевле glm» в пике перевёрнута** (×1.26 дороже, D39.137 п.4) · ~~СТОП ja-оси фазы Д~~ СНЯТ 15.08 (замена книги на `enkan_no_hate_ja` — рука владельца) · **возражение Sol по проходу `tier` эксп-23** (пере-судить починенным ригом или оставить с записанным возражением — PROGRESS «Полигон»; вес вырос: Д14.10 показал нули прибора claude на реальной инверсии смысла) · ~~поле `title` в `BookIntake`~~ РЕШЕНО 14.08: ратифицировано в 0.2.3 (замечание 12 S3.5, владелец подтвердил; исполняет S4) · фронт-вопросы зонного журнала: В-7 (плотность интерфейса; держит и Ф-54 — обрезку центра на зуме) · В-8 (слово состояния в дереве) · **В-9 (какие языки интерфейса обещает продукт — механизм готов, ПТ-36)** · Ф-38 (вкладка «Замечания») · ~~граница ПТ-33 для полей `BankTerm`~~ ЗАКРЫТ 15.08: полей НЕ заводить, смысла хватает (D39.136 п.4б; sense коротковат — строка 178) · ~~санкция расхода фазы Д~~ ДАНА 15.08 · ~~дизайн-ратификация 160/161~~ ДАНА 15.08 (D39.136 п.3: Этап 0 паком, 161 — дизайн-паком с мандатом качества) · ~~PD-203~~ РАТИФИЦИРОВАН (D39.136 п.6а: флаг аккаунта только по аккаунт-причинам) · ~~Ф-28~~ ОДОБРЕН бамп пинов 15.08 (первым касанием зоны) · ~~Ф-56/Ф-57/Ф-61~~ ОТВЕЧЕНЫ ревью 28 (§6: поток на книгу Б-6а · `/capabilities` Б-2 · машинный `code` Б-1; исполнение — батч 183). > - **Сделано (сводно)** — снимок ленты закрытых эр в [archive/PROGRESS-2026-08-04-09.md](archive/PROGRESS-2026-08-04-09.md); детали — D-лог и слайсы. > - **Курс:** ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ → ПАКЕТ-ЧЕКЕРОВ ✅ (D39.59–78) → **ФРОНТ-ЭРА** (D39.81–100: зоны живые, контракт API ратифицирован) → **шов/платформа/движковые блокеры построены** (D39.106–123). Хвосты курса живут строками: 16 (edit-волна не гонялась НИ РАЗУ — держит и оси голоса 13б/24) · 46 (дизайн заморожен D39.92/93, промт ждёт выдачи) · coldrun-b фаза C заморожена чекпойнтом легитимно (D39.86; эталон денег/поведения — coldrun-a, read-only); развилка 0731 решена и исполнена (D39.87/91, код `553f1a3`). > - **Горизонт (D39.62/67, освежён D39.95):** **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · цена 16; жильцы ролей решаются ДО прогона эксп-22 — строка 149 · веса K1–K12 13а · вне-претрейн чекпоинт 55; остаток арбитража банка = рецензент спорных кластеров при ре-пробе 74 — D39.102) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 62–68, 85) → Ф3 ридер-IDE (69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 (⚠ + x/text Unicode 17 тем же тулчейном — строка 119, реестр §Б-108 справочника якорей) · платные прогоны разблокированы (проба провода — D39.97, конфиги 112 залендены). > - **Стек (полная карта роль→модель→конфиг→квирки — [STACK.md](STACK.md), D39.126):** draft deepseek-v4-flash thinking-ON `low` **⚠0731** → терминолог (та же модель) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (топология ПОДТВЕРЖДЕНА при неразличимости жильцов — D39.117; закон-блок ОБЯЗАТЕЛЕН — строка 134; glm-5 резерв; вахта маппинга эффорта pro — §Б-108) → судья gemini (Ф2, в движке НЕ построен — строка 33); канал B Mistral+grok; ~$0.85/ранобэ (D30.4, пере-калибровка при следующем платном прогоне). ⚠ **Вендор-факты 13–15.08 (пере-пин ИСПОЛНЕН и ПРИНЯТ, D39.137):** таблица цен запинена ПИКОМ (flash 0.44/1.32 · pro 1.32/3.96/кэш-хит 0.044 за 1M; счёт шиппинг-c1 = 100% DeepSeek ⇒ ×4.2–4.4 в пике / ×2.1–2.2 в долине — замер по трём прогонам); у pro эффорт стал настраиваемым low/high/max (квирк 3а устарел — вахта §Б-108 сработала); ⚠ ВЕСА pro сменились под тем же слагом (V4-Pro-0813, класс D39.61) — вахта-риг готов (остаток 172); ⚠ посылка интерим-редактора «dspro дешевле glm» в пике ПЕРЕВЁРНУТА (×1.26 дороже — вход ратификации фазы Д, D39.137 п.4); покупки фазы Д на deepseek до 16.08 16:00 UTC — по старым ценам. ⚠ **ВЕСЬ банковый контур (банкнота+терминолог+классификатор) в shipping-c1 НЕ включён** — жив ран-локальным конфигом книги (строка 140; сверка STACK.md 09.08 — факт шире прежней декларации «одна банкнота»); эскалация в shipping за `budget_usd: 0` (STACK.md §примечания). -> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 15.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **140** строк · зона бэкенд **71** строго / **96** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178/179 — D39.136; 180/181 — D39.137; 182 — wire-батч, аудит 15.08) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **31** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74 (D39.102 п.2); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). +> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 16.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **144** строки · зона бэкенд **72** строго / **99** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178 — D39.136; 179 ЗАКРЫТА D39.138; 180/181 — D39.137; 182 — wire-батч, аудит 15.08; 183–187 — контракт-ревью 28, D39.138) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **33** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74 (D39.102 п.2); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 11–16, rerun2) · **`-25-31` (паки 17–20 · мини-прогон · полигон-пакеты 5–8 · ToS · холодный прогон; D39.26–58)** · **`-08-01-02` (сессии №9/№10: общность · качество банка · coldrun-b · открытие фронта/платформы; D39.59–90, срез D39.105)** · **`-08-02-04` (сессии №11–№13: ручки эффорта · стандарты · контракт API · платформа P0 · банк-арбитраж; D39.91–105 + снимок шапки эры №15)**. Записи ниже — живой хвост (№14+, эра D39.106+). ## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора) @@ -124,7 +124,11 @@ | 176 | **Паника движка неотличима от «completed with flags»**: непойманная паника Go-рантайма = exit 2, а 2 — «успешный» код контракта; платформа записывает `ready` для прогона, упавшего посреди работы (деньги целы — расчёт из `status --json`; врёт статус). Лечение движковое: `recover` в main tmctl либо другой номер для флагов; найдено адверсариальным ревью P6 (PD-212 регистра платформы) · тем же касанием exit-контракта — usage-строка `invocation.go` не знает migrate/backup/seed-lint (замороженный контракт; чинить разом — D39.134 п.3) | бэкенд | когда-нибудь (следующее касание exit-контракта) | малое касание | D39.132, PD-212, D39.134 | | 177 | **Гигиена restore point `tmctl migrate`** (находки приёмки ВНЕ карты отчёта; фикс-лист ФМ — запись приёмки 15.08, PROGRESS «Бэкенд»): ретрай в ту же секунду после сорванного шага = exit 1 «backup already exists» вне полосы — ровно на цикле самолечения платформы · kill -9 посреди шва оставляет РВАНУЮ копию под легитимным именем (ручной откат по ней = потеря данных; лечение — temp+rename) · копии restore point не подчищаются (повтор после срыва копит их) · SIGINT/SIGTERM в migrate не прерывают (ctx не доезжает) · комментарий migrate.go «no money touched» на no-op неверен (recovery зануляет leftover reserved) | бэкенд | когда-нибудь (следующее касание migrate/бэкапа) | малое касание + тесты | D39.134, приёмка №17 | | 178 | **Калибровка `sense` банка** — слово владельца 15.08 (D39.136 п.4б): смысл должен быть «ёмкий и достаточный на всю книгу, но не слишком подробный» (образец: «спутница главного героя»); сейчас возможно КОРОТКОВАТ; замер длины/достаточности на подписанном банке → правка промптов банкноты/терминолога (двигает провод — цену назвать при внесении) | бэкенд/полигон | когда-нибудь (касание банка) | замер → правка промптов | D39.136 п.4б | -| 179 | **Контракт-ревью 0.3.0 — заказ владельца 15.08** (D39.136 п.5): жёсткий ресёрч всем репо (код трёх зон + планы), greenfield-проект API без нашей спеки + линза индустриальных стандартов + аудит квирков «контракт или протёкшая реализация», КРОСС-МОДЕЛЬНОЕ судейство каждого буллета с миграционной ценой ДО записи в бэклог; мандат анти-инерции «ратифицировано ≠ правильно»; входы: Ф-56 · Ф-57 · Ф-61 · Ф-62 · форма PD-172 · пагинация/производительность чтения банка · i18n сообщений всех зон · спойлер (если В-10 = «защита») | ресёрч-сессия | скоро (**промт `CONTRACT_REVIEW_SESSION_PROMPT.md` ВЫДАН 15.08**; форма «отдельная сессия» — слово владельца, эррата 15.08-в: воркфлоу-заход №17 остановлен, результаты выброшены) | сессия по промту → доклад → ратификация → 0.3.0 батч | D39.136 п.5, эррата 15.08-в | +| 183 | **Батч 0.3.0 контракта — ломающий минор по ПРИНЯТОМУ ревью** (⚠ НОСИТЕЛЬ состава и порядка — **`docs/research/28-contract-review.md` §5**, решений владельца — §8/D39.138 п.2; исполняющим сессиям читать ОРИГИНАЛ, не пересказ; Б-1 модель ошибок — ПЕРВОЙ, от словаря кодов зависят Б-2/Б-3/Б-8/Б-14а/§8а): конвейер с провода (Б-0) · машинный `code`+`request_id` (Б-1, два класса §8а) · безопасность машинно (Б-15) · `GET /capabilities` (Б-2) · правда о движке/resume (Б-7а) · поток на книгу+конец (Б-6) · экспорт умеет провалиться (Б-4) · `Note.id`+снимок стопа (Б-9/14а) · глава без запрета клиентской метки (Б-19) · структурная версия+ETag/304 (Б-7) · Б-12 · Ц1-блок Б-3/Б-8/Б-13/Б-13а/Б-5/Б-10/Б-11/Б-14 · однострочники и резка §5а · сжатие/условные чтения В спеку (§5б) · компаньон ВМЕСТЕ со спекой + дисциплина Б-21; открытый вход: масштаб замечаний не замерен (страница 500 выбрана без числа) | оркестратор → спек-сессия | скоро (промт — очередь №17) | промт → отдельная сессия → приёмка → лендинг канон-первым с cmp-зеркалом (D39.138 п.3) → зонные половины (платформа · фронт S5) | **research/28 §5**, D39.138 | +| 184 | **Жанр книги — ВЫКИНУТЬ (слово владельца 16.08):** снять `{{genre}}` из 6 top-level промптов пары + поле из канона брифа (двигает `BriefHash` ⇒ ТОЛЬКО в общее resnapshot-окно, координировать со 161/182 — дешёвое окно пока платная книга одна) · из `book.yaml`-рендера платформы · из `BookIntake`/`Book` контракта (батч 183) · из формы фронта (разморозка); влияние жанр-строки на качество не измерено и уже не будет | бэкенд (+платформа/контракт/фронт) | скоро (движковая половина — resnapshot-окном) | касание в окне 161/182 + батч 183 | research/28 Б-23/§8 п.6, D39.138 п.2в | +| 185 | **Дописывание глав в ТЕКУЩУЮ книгу («костылей не будет»):** форма `POST /books/{id}/parts` + ответ «сколько глав добавится, что сдвинется и почём» ДО подтверждения; экономика уже позволяет В КОНЕЦ за ~$0.02 (ключ вызова позиционный, исходник вне снапшота), вставка в середину = МОЛЧАЛИВАЯ перекупка хвоста ~$9–11/500 глав — движковый гейт против молчания ОБЯЗАТЕЛЕН (родня дыры Р6 строки 160); платформа второй файл принять не может (re-parse нет); ja-мина: новый ruby-алиас двигает `memory_version` → снапшот (громкий `--resnapshot`, уточнение приёмки); полный $0-резюм хвоста — предусловие D15.2 (строка 49) | бэкенд+платформа+контракт | когда-нибудь (вместе со строками 160–162 — дизайн в паке 161) | дизайн-пак 161 → стройка | research/28 Б-19а/§8 п.7, D39.138 п.2г | +| 186 | **Сеть: перечитывания ничем не удешевлены** (замер 16.08: дерево 2283 глав = 250 КБ на каждый кадр `status`, фокус-рефетч 12 вкладок = 562 КБ — самый частый трафик порождает переключение вкладок, не пайплайн; гипотетический кадр банка на главу = 372 МБ/прогон; сжатия/ETag/304 нет НИГДЕ): план по эффекту — (1) gzip на текстовых ответах, НЕ на SSE (2) `ETag`/`If-None-Match`→304 на списочных GET (3) скоуп в кадре (id+версия, БЕЗ текста) (4) дельта-чтение банка/замечаний `?after_version=` (5) `staleTime` у клиента; шаги 1–2 — платформа (её же `PLATFORM_DIRECTION.md:202-205`), семантика — в спеку батчем 183; транспорт НЕ менять (решение владельца по §5б) | платформа (+контракт/фронт) | скоро (шаги 1–2 — до первой живой книги под фронтом) | мидлварь/edge платформы + батч 183 + разморозка фронта | research/28 §5б/Б-11а, D39.138 п.2д | +| 187 | **Два комментария движка лгут о `memory_version`** (описывают до-pack-20 поведение): `snapshot.go:188-189` («auto/draft исключены per D8») и докстринг `membank/memory.go:360-363` («Approved-only») — а код с пака-20 сворачивает КАЖДУЮ строку любого статуса вместе с алиасами (`membank/memory.go:410-421,441-446`); правка двух комментариев, $0, wire-нейтрально | бэкенд | когда-нибудь (попутно гигиена-паком 177) | правка комментариев | research/28 §9 + приёмка D39.138 | | 49 | **Этапы Б+В спеки D15.2** (content-addressed resume / `guard_hash` — D39.31 сознательно не строил) ; этап В = tmctl export-контракт D29.1 (annot-v1 · цвет-мап+rollup · политика красных fail-closed) + операторский протокол-документ релиза гендер-твиста (D5.1): сам механизм УЖЕ построен — until_ch-правка → точечный re-edit со сметой и $0-пере-пином (`repin.go`/`rebill.go`, D39.42 п.5/D39.45) | бэкенд | скоро (ПОДТЯНУТА D39.81: annot-v1 = источник замечаний для фронта — критический путь подключения) | отдельное решение (annot-половина — по контракту 14, D39.99) | D39.34(4), D33 п.5, D39.81 | | 49а | **ALTER-шаги миграций v8–v14 не идемпотентны вопреки шапке `migrate.go:9-11`** (полу-применённая БД не сходится; счётчик версий скрывает) — находка критика полноты пака-19 | бэкенд | когда-нибудь | отдельное решение | D39.56, PACK19_BUILD §6.9 | | 50 | F3-остаток идемпотентности | бэкенд | когда-нибудь | отдельное решение | D39.34(4) | diff --git a/docs/README.md b/docs/README.md index eb7ad03e..17972005 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,10 +14,10 @@ - Топикальные входы (D39.126; НЕ источники истины, при конфликте побеждает D-лог): [15-money-path.md](architecture/15-money-path.md) — деньги от гранта до settle одним маршрутом · [16-events-emitter.md](architecture/16-events-emitter.md) — сборка-норматив эмиттера шва (обязательное пре-чтение сессии строки 103) · [STACK.md](STACK.md) — карта «роль → модель → конфиг → квирки». - Исторические, читать через ⚠-баннеры: [01-decisions.md](architecture/01-decisions.md) (Р1–Р10) · [02-mvp-plan.md](architecture/02-mvp-plan.md) · [03-implementation-notes.md](architecture/03-implementation-notes.md) · [04-unhappy-paths.md](architecture/04-unhappy-paths.md) · [06-memory-risk-registry.md](architecture/06-memory-risk-registry.md). - `experiments/` — эмпирика полигона: [00-provider-quirks.md](experiments/00-provider-quirks.md) — **читать перед любым вызовом провайдера**; [08-cost-model-v2.md](experiments/08-cost-model-v2.md) — денежная модель; [09-pilot-protocol.md](experiments/09-pilot-protocol.md) — пилот Ф2.5; остальные 01–21 — отчёты ЗАКРЫТЫХ экспериментов (21 принят D39.117); ⚠ 22/23 — залендены с ревью-шапками приёмки №16 (10.08), НЕ ратифицированы: выводы заморожены до фазы Д (шапка первична; 18–20 — с ревью-шапками приёмки D39.108). -- `research/` — фактура ресёрчей 01–27; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (транспорт superseded D39.106) · 25 холодное ревью шва — форма D39.106, отвергнутые альтернативы, требования к эмиттеру (читать перед любым кодом стыка) · 24 арбитраж банка (ПРИНЯТ D39.102: консилиум закрыт классом, вход фикс-пака банка — §G) · 26 официальные практики Anthropic (D39.121: записка-план, сниппеты хендоффов, карта «уже делаем/перенять»). +- `research/` — фактура ресёрчей 01–28; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (транспорт superseded D39.106) · 25 холодное ревью шва — форма D39.106, отвергнутые альтернативы, требования к эмиттеру (читать перед любым кодом стыка) · 24 арбитраж банка (ПРИНЯТ D39.102: консилиум закрыт классом, вход фикс-пака банка — §G) · 26 официальные практики Anthropic (D39.121: записка-план, сниппеты хендоффов, карта «уже делаем/перенять») · **28 контракт-ревью API v0 (ПРИНЯТ D39.138: решения владельца 16.08 — §8, состав батча 0.3.0 — §5; НОСИТЕЛЬ для исполняющих сессий, читать оригинал, не пересказ)**. - [PROGRESS.md](PROGRESS.md) — журнал: CURRENT-STATE + **ЕДИНЫЙ БЭКЛОГ** (единственный трекер) + живой хвост хроники. НЕ источник решений. - `scripts/counts.py` — **производные числа доков считаются им, а не руками** (голова по трём носителям · счёт очереди и зон · вес открытых строк регистра платформы · полнота реестра нот); `--check` даёт ненулевой код на расхождении; `--lint` — линтер file:line-якорей живых доков (D39.126). Его же зовёт зонный хук `scripts/githooks/pre-commit` при коммите, задевающем D-лог или PROGRESS — предупреждает, не блокирует. Заведено по D39.112 п.5б: голова отставала трижды у трёх разных оркестраторов. -- Активные хендофф-промты сессий (состав обновляется при каждом лендинге — норма D39.80): [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояния не дублирует) · **Контракт-ревью: [CONTRACT_REVIEW_SESSION_PROMPT.md](CONTRACT_REVIEW_SESSION_PROMPT.md)** — ревью API v0 на адекватность/индустриальные нормы отдельной сессией (строка 179, D39.136 п.5 + эррата 15.08-в; запуск по слову владельца; отчёт → `docs/research/28-contract-review.md`) · **Полигон: [POLYGON_EXP2223_REDO_SESSION_PROMPT.md](POLYGON_EXP2223_REDO_SESSION_PROMPT.md)** — фаза Д доводки эксп-22/23 ИДЁТ (заказ владельца 10.08, потолок фазы $4.50; выводы 22/23 заморожены до неё) · [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md) (отложен) . **Фронт: ЗОНА ЗАМОРОЖЕНА (D39.136 п.2)** — S4 ИСПОЛНЕН и ПРИНЯТ (D39.135, лендинг `267aa35`; контракт 0.2.3 в каноне; промт в `frontend/docs/archive/`); разморозка = лендинг P7 + гейт-ответы S5 (В-10 · фразы В-11); живой smoke против дев-стенда легитимен и во фризе; фикс-лист ФС — запись приёмки 15.08 в зонном журнале · **Платформа: активного промта НЕТ** — P6 + дофикс ИСПОЛНЕНЫ и ПРИНЯТЫ (D39.132, промты в `platform/docs/archive/`); следующий пак — P7 (читающая поверхность + форвард-половина 0.2.3: чтение `title` и проекция `reject_reason`, D39.135 п.2в + хвосты D39.134 п.4), промт — очередь №17 · **Бэкенд: активного промта НЕТ** — пере-пин цен DeepSeek ИСПОЛНЕН и ПРИНЯТ (D39.137, лендинг `76049bb`; промт в `archive/prompts/`), потолки снова защищают; `tmctl migrate` — D39.134 (`d55edd4`), деплой эмиттер-бинаря разблокирован. Эмиттер шва ИСПОЛНЕН и ПРИНЯТ (D39.131, промт в архиве). Все ОТРАБОТАННЫЕ промты — в `archive/prompts/`, отчёты с ревью-шапками — в `archive/reports/`; статусы паков — CURRENT-STATE и D-лог, здесь не дублируются. Зонные журналы фронта/платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (прогресс зон только там, D39.100). +- Активные хендофф-промты сессий (состав обновляется при каждом лендинге — норма D39.80): [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояния не дублирует) · **Контракт-ревью ОТРАБОТАЛО и ПРИНЯТО** (D39.138; отчёт [research/28-contract-review.md](research/28-contract-review.md) — носитель решений владельца §8 и состава батча 0.3.0 §5; промт в `archive/prompts/`; следующий промт — спек-сессия батча 0.3.0, очередь №17) · **Полигон: [POLYGON_EXP2223_REDO_SESSION_PROMPT.md](POLYGON_EXP2223_REDO_SESSION_PROMPT.md)** — фаза Д доводки эксп-22/23 ИДЁТ (заказ владельца 10.08, потолок фазы $4.50; выводы 22/23 заморожены до неё) · [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md) (отложен) . **Фронт: ЗОНА ЗАМОРОЖЕНА (D39.136 п.2)** — S4 ИСПОЛНЕН и ПРИНЯТ (D39.135, лендинг `267aa35`; контракт 0.2.3 в каноне; промт в `frontend/docs/archive/`); разморозка = лендинг P7 + гейт-ответы S5 (В-10 · фразы В-11); живой smoke против дев-стенда легитимен и во фризе; фикс-лист ФС — запись приёмки 15.08 в зонном журнале · **Платформа: активного промта НЕТ** — P6 + дофикс ИСПОЛНЕНЫ и ПРИНЯТЫ (D39.132, промты в `platform/docs/archive/`); следующий пак — P7 (читающая поверхность + форвард-половина 0.2.3: чтение `title` и проекция `reject_reason`, D39.135 п.2в + хвосты D39.134 п.4), промт — очередь №17 · **Бэкенд: активного промта НЕТ** — пере-пин цен DeepSeek ИСПОЛНЕН и ПРИНЯТ (D39.137, лендинг `76049bb`; промт в `archive/prompts/`), потолки снова защищают; `tmctl migrate` — D39.134 (`d55edd4`), деплой эмиттер-бинаря разблокирован. Эмиттер шва ИСПОЛНЕН и ПРИНЯТ (D39.131, промт в архиве). Все ОТРАБОТАННЫЕ промты — в `archive/prompts/`, отчёты с ревью-шапками — в `archive/reports/`; статусы паков — CURRENT-STATE и D-лог, здесь не дублируются. Зонные журналы фронта/платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (прогресс зон только там, D39.100). - Зоны фронта (чужие, читать при касании стыка; каждая ведёт СВОЙ зонный бэклог — единый бэклог их строк не принимает, D39.84): [../frontend/](../frontend/) — веб-интерфейс: промт фронт-сессий S0–S7 + [STACK_DECISIONS.md](../frontend/docs/STACK_DECISIONS.md) (пины версий точными числами и ловушки, сверены с вебом 02.08) + [BACKLOG.md](../frontend/docs/BACKLOG.md) · [../platform/](../platform/) — SaaS control plane: README + [BACKLOG.md](../platform/BACKLOG.md) (П-1..П-14) + `docs/` (зонный журнал `platform-PROGRESS.md` · регистр дефектов · архив промтов P0–P5). - `archive/` — история ([правила архива](archive/README.md)): закрытые промты (`prompts/`) · отчёты с ревью-шапками (`reports/` — на них ссылаются приёмки) · исполненные арх-доки (`architecture/`) · слайсы хроники `PROGRESS-*.md`. Инструкции оттуда не исполнять. - Диаграммы: [../backend/docs/components.puml](../backend/docs/components.puml) · [../backend/docs/pipeline.puml](../backend/docs/pipeline.puml) — дом рядом с кодом (D39.80), правятся бэкендом одним коммитом с кодом; вручную НЕ рендерить (владелец смотрит PlantUML-расширением VS Code). diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index f58419af..b52178a1 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.137; строка 167; титул — носитель головы, бампать при каждом аппенде — испр. оркестратором №17 15.08 по аудиту: отставал на четыре ноты) +# Реестр D-нот — карта актуальности v2 (D1–D39.138; строка 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 · общность · процесс · нарезка · голос · инфра · модели. > Родословная: прозаическая карта D1–D39.28 (ревизии D31–D38.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`. @@ -190,3 +190,4 @@ | D39.135 | 15.08 | Приёмка фронта S4 (267aa35, панель 5 линз): контракт 0.2.3 в каноне байт-зеркалом; ратификации: PD-172 уточнён по коду платформы (стоп чтения на файле), title/reject_reason — форвард-контракт с носителем в P7, not_configured — словарь версии, verify_bank дефолт OFF до S5; фикс-лист ФС-1..12 → вход S5 (major: интейк-мир стартует прогон с total=0) | ЖИВОЕ: владельцу В-10/В-11 (фразы PD-173, кавеат not_configured)/Ф-62/Ф-63; поправки отчёта — запись приёмки в зонном журнале | жив | фронт контракт приёмка | | D39.136 | 15.08 | Пакет слов владельца напрямую №17: санкции фазы Д и ja-книги; фриз фронта до P7+гейт-ответов; структура глав в очередь (гейт D39.122 п.4 пал: Этап 0 паком, 161 дизайн-паком); контракт-ревью 0.3.0 заказан (строка 179, кросс-модельное судейство буллетов до бэклога); фразы PD-173 пере-заказ + граница ПТ-33 пере-читана (алгоритмы, не минорку); поля BankTerm не заводить, строка 178 (sense коротковат); Ф-62 в контракт, Ф-63/Ф-28 одобрены; PD-203 ратифицирован (флаг аккаунта только по аккаунт-причинам); 116 закрыта (порог выборкой), 126 закрыта; промт DeepSeek-репина выдан | ЖИВОЕ: строки 178/179; вендор-сессия DeepSeek отработала — D39.137; форма ревью — эррата 15.08-в (отдельная сессия) | жив | владелец фронт контракт банк деньги | | D39.137 | 15.08 | Приёмка вендор-сессии пере-пина DeepSeek (76049bb, инлайн исполнением: вендор-страница пере-прочитана независимо, голден бит-в-бит, дифф = 3 строки): пик запинен (flash 0.44/1.32/0.014, pro 1.32/3.96/0.044), множитель счёта c1 ×4.2–4.4 пик; строка 172 пере-скоуплена (схема time-based на владельце, ставка 166, exp08 v3, вахта весов Pro-0813); новые 180 (prices_checked одно поле) и 181 (консент-число ребилла врёт по историческим ценам); ToS-дельты нет (Wayback-дифф 0) | ЖИВОЕ: посылка интерим-редактора D39.22 в пике перевёрнута (dspro дороже glm ×1.26) — вход ратификации фазы Д; coldrun-a пробивает потолок 0.25 новой ценой | жив | деньги вендор бэкенд приёмка | +| D39.138 | 16.08 | Приёмка контракт-ревью research/28 (4 опровергателя, 42 улики исполнением, 0 опровергнуто; находка приёмки: К-10 закрывается «не строить»): решения владельца 16.08 ратифицированы — конвейер с провода (Progress один счётчик до остановки, знаменатель купленный), модель ошибок вариант B (машинный code+request_id, два класса конкретности §8а, К-9 грубым кодом), жанр выкинуть, дописывание глав POST /parts + гейт молчаливой перекупки, транспорт не менять (сжатие→ETag/304→скоуп→дельты), экспорт остаётся с Б-4, история прогонов не нужна, В-10 «вежливость», PD-104 закрыт (грант 0 на бете); батч 0.3.0 заказан отдельной спек-сессией (Б-1 первой; канон первым, компаньон вместе со спекой); 179 закрыта, новые 183–187 | ЖИВОЕ: промт батча — очередь №17; кросс-семейный проход доклада — отдельный заказ | жив | контракт владелец приёмка фронт платформа деньги | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 858780b9..1994ca55 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.137 (живой файл: карта · эрраты · живые тела · голова D39.106+; тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.138 (живой файл: карта · эрраты · живые тела · голова D39.106+; тела закрытых эр — в слайсах `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+). @@ -562,3 +562,27 @@ **3. Строки.** 172 ПЕРЕ-СКОУПЛЕНА: пере-пин ИСПОЛНЕН; остаток — (а) схемное решение владельца по докладу §5 отчёта (рекомендация сессии: пик оставить; scheduler-вариант не начинать до ответа вендора «по какой отметке времени тарифицируется вызов» — не задокументировано — и замера доли пика; дешёвый рычаг = операционное правило «прогоны не стартуют в 01–04/06–10 UTC», почти всё ×2 без строчки Go) · (б) пере-калибровка ставки $0.03/глава (166/П-10; множитель замерен) · (в) exp08 v3 (85) обязателен до budget_usd · (г) вахта весов **V4-Pro-0813** (веса сменились под тем же слагом — класс D39.61; риг готов: §7.4 отчёта, `TestLiveClassifierHarmSet`, после 16:00 UTC 16.08). Новые: **180** — `prices_checked` одно поле на ВСЮ таблицу: бамп по факт-чеку одного вендора сдвинул 120-дневный гейт свежести остальным семи (дедлайн 07.11→13.12); лечение — пер-модельная дата ЛИБО ре-чек остальных вендоров до исходного дедлайна. **181** — консент-число пере-покупки врёт после смены цен: `projectRebill` складывает ИСТОРИЧЕСКИЙ `cost_usd` (`rebill.go:166`), а резерв/сеттл считают НОВОЙ таблицей (`stagerun.go:431/:613`) ⇒ оператор соглашается на сумму до ×4.4 меньше реальной, `projected_book_usd` у `status` — так же; траты защищены потолками, врёт ЧИСЛО СОГЛАСИЯ. **4. Владельцу/фазе Д (не решается этой нотой):** экономическая посылка интерим-редактора D39.22 «dspro ×2 дешевле glm» в ПИКЕ ПЕРЕВЁРНУТА — замер на 129 реальных edit-вызовах: dspro пик ×1.26 ДОРОЖЕ glm-5, офф-пик дешевле лишь ×1.59 (контрфактика по оси цены, оговорка сессии честная). Вход ратификации жильцов при фазе Д. (15.08.2026, оркестратор №17) ✅ + +## D39.138 — ПРИЁМКА КОНТРАКТ-РЕВЬЮ (research/28) ПРИНЯТА; решения владельца 16.08 по докладу РАТИФИЦИРОВАНЫ; заказан ломающий батч 0.3.0; строка 179 закрыта, PD-104 и В-10 закрыты, новые строки 183–187 (16.08). ✅ + +**1. Приёмка (оркестратор №17, $0).** Отчёт — `docs/research/28-contract-review.md` (сессия по промту `CONTRACT_REVIEW_SESSION_PROMPT.md` → архив; зона не нарушена: единственный файл сессии, дерево не коммичено). Верификация: 4 независимых опровергателя пере-открыли ИСПОЛНЕНИЕМ 42 улики несущих блоков (Б-0/Б-13а · Б-1/Б-15 · Б-2/Б-3/Б-23 · Б-7а-п.4/Б-19а) — **0 опровергнуто по существу**; 5 косметических поправок + находка приёмки (вердикт К-10 в §6 противоречит Б-0 — закрывается «НЕ строить») — всё в ревью-шапке отчёта. REJECTED-секция существует (29 отвержений: 9 на ратификациях владельца, 4 — самопризнанные ошибки чтения); покрытие обязательных входов промта — полное (таблица §6); leave-one-out по линзам задекларирован и выборочно сходится. Ограничение метода честно заявлено сессией: все опровергатели — семейство Claude; кросс-СЕМЕЙНЫЙ проход доклада — отдельный заказ (этой нотой не решается, полигон занят фазой Д). + +**2. Ратификация решений владельца 16.08 (первоисточник — §8 отчёта; перенос сюда — по §8б п.1; при конфликте побеждает эта нота).** +(а) **Б-0 «переделываем»:** конвейер уходит с провода — `Progress` → ОДИН счётчик до ближайшей остановки, знаменатель — КУПЛЕННЫЙ объём (после подписи банка полоса заново — В-5, §8 п.8); `finalizing` свернуть (лестница фаз — устная фраза владельца, не норма, §8 п.15); `verify_bank` → `stop_for_signing`; `TermOrigin`/`TermStatus` снять с провода; конвейерные слова вычистить из описаний (компилируются в исходники клиента); завести гейт на утечку. Колонки платформы и словарь шва движок↔платформа НЕ трогаются. +(б) **В-1 модель ошибок — вариант B (§8 п.4):** машинный `code` (двухуровневый: стабильный корневой + расширяемый вложенный) + `request_id`; `title`/`detail` — developer-facing, клиент НЕ показывает, фразу рисует клиент по словарю кодов; серверная локализованная фраза — отдельным полем только для неперечислимых причин; `errors[]` с указателем поля. **Два класса конкретности (В-2, §8а):** класс 1 (детерминированные — вход/счёт) — конкретика максимальная; класс 2 (модельные — прескрин/провайдер/фильтр) — ОДИН грубый код на класс, без вариации между попытками, с лимитом попыток; К-9 тем же решением: `rejected` + грубый код, 12-й статус не заводить. +(в) **Жанр — выкинуть** (Б-23, «Просто выкидываем»): из промптов, канона брифа (двигает `BriefHash` ⇒ только в общее resnapshot-окно), `book.yaml`-рендера платформы, контракта, формы — строка 184. +(г) **Дописывание глав — в ТЕКУЩУЮ книгу, «костылей не будет»** (Б-19а): форма `POST /books/{id}/parts` + ответ «что сдвинется и почём» ДО подтверждения; движковый гейт против МОЛЧАЛИВОЙ перекупки хвоста при вставке не в конец; вместе со строками 160–162 — строка 185. +(д) **Транспорт НЕ менять** (§5б, проверено агентом другого тира с веб-ресёрчем): REST/SSE остаются, паттерн poke/pull легитимен; план — сжатие → ETag/304 → скоуп в кадре → дельта-чтения → staleTime (строка 186); сжатие и условные чтения записываются В КОНТРАКТ, не в зонный док. +(е) **Экспорт БУДЕТ:** операции остаются; Б-4 обязателен — `state` вместо булева `ready`, `failure_code`, `expires_at`, эхо формата, правило доступа к ссылке (ПТ-34); закрывает К-12. +(ж) **История прогонов — в МВП не нужна** (из батча снята; данные уже в Postgres, один read-путь по запросу поддержки). +(з) **В-6:** `run-options`/409 получают `blocked: {code, book_id}`. +(и) **К-6:** замечания показывать ВСЕ; в тексте сворачивать/раскрывать кнопкой; словарь ступеней заранее НЕ проектировать; зависимость `Note.id` закрывает батч; зонная половина — фронту при разморозке. +(к) **В-10 спойлер = «вежливость» — ЗАКРЫТ:** контракта не касается, `sense` обязателен, размытие — визуальное дело фронта, находимость Ctrl+F — приемлемое следствие. +(л) **PD-104 ЗАКРЫТ словом владельца** (§8 п.14): `SignupGrantMicroUSD` → **0** на бете, начисление руками; возврат $5 — вместе с суточным агрегатным потолком при появлении платежей; исполнение — P7-однострочник (грант при неверифицированном email уже 0, `login.go:306-308`). +(м) **0.3.0 — НЕ последний 0.x:** право ломать в 0.x держится весь бета-период; окно 1.0 — после беты. + +**3. Батч 0.3.0 ЗАКАЗАН (строка 183).** Состав и порядок — §5 отчёта (порядок не косметика: Б-1 модель ошибок ПЕРВОЙ — от словаря кодов зависят Б-2/Б-3/Б-8/Б-14а/§8а); резка — §5а; направления вне батча — хвост §5. Исполнение — ОТДЕЛЬНАЯ сессия по промту (норма D39.120 п.2 / эррата 15.08-в), промт — очередь №17. Правила лендинга (§8б): решения перенесены ЭТОЙ нотой до любой правки спеки · компаньон правится ВМЕСТЕ со спекой (его §5-ответ про волны опровергнут исполнением, Б-0; «канала банка нет» — Б-7а) · канон первым, зеркало фронта отдельным зонным коммитом, cmp-сверка обязательна. + +**4. Строки.** 179 ЗАКРЫТА (сессия → доклад → эта ратификация; исполнение — 183). Новые: **183** батч 0.3.0 (носитель состава — research/28 §5) · **184** жанр вон · **185** дописывание глав + гейт молчаливой перекупки · **186** сеть/перф §5б · **187** два лживых комментария движка о `memory_version` (`snapshot.go:188-189` + `membank/memory.go:360-363` — оба описывают до-pack-20 поведение). Владелец-лист: PD-104 и В-10 закрыты; Ф-56/Ф-57/Ф-61/Ф-62 — диспозиции §6 (исполнение батчем); К-13-остаток (описание `:879`) — в батч. + +**5. Пинги розданы** (§8б п.4/§9): платформе — деньги/надёжность §9 + PD-104 + платформенная половина батча + К-10 «не строить»; фронту — диспозиции четырёх вопросов зоны + мок-находки §9 + перф-входы разморозки. Движковые находки §9 — строки 187 и 185 (ja-мина: новый ruby-алиас двигает `memory_version` → снапшот; громкий `--resnapshot`, не тихая перекупка — уточнение приёмки). (16.08.2026, оркестратор №17) ✅ diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md index 6b8b8806..f4692c2c 100644 --- a/docs/architecture/14-api-contract/README.md +++ b/docs/architecture/14-api-contract/README.md @@ -22,6 +22,15 @@ openapi.yaml РЯДОМ (байт-копия зонной frontend/docs/api-cont потолка — ИСПОЛНЕНА S4, D39.135) · 127 (DDL-коммент; испр. №17 15.08 по аудиту). Приложение А — заготовка: фразы и ступени заполняет владелец (В-3). +16.08.2026 (№17): ЦЕЛОСТНОЕ РЕВЬЮ КОНТРАКТА ПРИНЯТО (docs/research/28-contract-review.md, D39.138) — +заказан ЛОМАЮЩИЙ батч 0.3.0 (состав — research/28 §5; строка 183). До его лендинга нормативной остаётся +0.2.3. Вердикты по К-вопросам — research/28 §6 (К-10 — «НЕ строить», следствие Б-0: фазы уходят с +провода). ⚠ Известные опровергнутые утверждения ЭТОГО файла и спеки (ответ §5 «добавлена волна — фронт +не правится» опровергнут исполнением, Б-0; «канала банка нет» §3 против собственной строки 267, Б-7а; +resume-абзац спеки :440-445 запрещает работающее лечение, Б-7а п.4) правятся ВМЕСТЕ со спекой батчем — +до тех пор читать через research/28. Правило лендинга батча: канон первым, зеркало фронта отдельным +зонным коммитом, cmp-сверка обязательна (D39.138 п.3). + Тело ниже — авторский текст фронт-сессии S3 (два раунда дофикса), не переписано. ПОСТ-РАТИФИКАЦИЯ (решения владельца 04.08 вечер, D39.100): К-1 ЗАКРЫТ (десять diff --git a/docs/CONTRACT_REVIEW_SESSION_PROMPT.md b/docs/archive/prompts/CONTRACT_REVIEW_SESSION_PROMPT_2026-08-15.md similarity index 97% rename from docs/CONTRACT_REVIEW_SESSION_PROMPT.md rename to docs/archive/prompts/CONTRACT_REVIEW_SESSION_PROMPT_2026-08-15.md index 1c7d77ba..314b2b4c 100644 --- a/docs/CONTRACT_REVIEW_SESSION_PROMPT.md +++ b/docs/archive/prompts/CONTRACT_REVIEW_SESSION_PROMPT_2026-08-15.md @@ -1,3 +1,5 @@ +> ⚠ **АРХИВ — ОТРАБОТАН.** Исполнен отдельной контракт-ревью-сессией 15–16.08.2026 (разбор с владельцем — 16.08), принят приёмкой оркестратора №17 (D39.138): отчёт — `docs/research/28-contract-review.md` — носитель решений владельца (§8) и состава батча 0.3.0 (§5); строка 179 закрыта, исполнение — строка 183. Инструкции отсюда не исполнять. + # Промт: сессия КОНТРАКТ-РЕВЬЮ — API v0 фронт↔платформа на адекватность и индустриальные нормы (строка 179) > Выдан оркестратором №17 15.08.2026 по заказу владельца (D39.136 п.5; форма «отдельная сессия» diff --git a/docs/research/28-contract-review.md b/docs/research/28-contract-review.md new file mode 100644 index 00000000..00bd6c82 --- /dev/null +++ b/docs/research/28-contract-review.md @@ -0,0 +1,1657 @@ +# 28 — Контракт-ревью API v0 (фронт ↔ платформа): доклад к батчу 0.3.0 + +> **Заказ:** D39.136 п.5 (слово владельца 15.08), промт `docs/CONTRACT_REVIEW_SESSION_PROMPT.md`, отдельная +> сессия (эррата 15.08-в: author≠reviewer — оркестратор соавтор ратификаций 0.2.3). +> **Предмет:** `docs/architecture/14-api-contract/openapi.yaml` 0.2.3 + компаньон. +> **Мандат:** «ратифицировано ≠ правильно». **Сессия ничего не правит:** ни спеку, ни код, ни бэклог, +> ни D-лог. Единственный файл сессии — этот. Дерево не коммичено. +> **Метод:** инвентарь кода трёх зон (639 фактов с `file:line`) · greenfield-панель из 4 проектировщиков, +> НЕ видевших спеку · постатейная сверка с RFC 9110/9111/9205/9457, WHATWG SSE, semver, AIP-151/155/158/193, +> Zalando, Microsoft, Stripe, GitHub (тексты скачаны, цитаты — дословные подстроки) · адверсариальное +> судейство каждого буллета (аудитор улик + адвокат нынешней формы, разными моделями). + +> **⟶ ЖИВАЯ ГОЛОВА: 16.08 владелец разобрал доклад и принял решения — §8.** Там же то, что решено, что +> отложено и что уходит в общий бэклог. Сессия отчёта ничего не правит и не заводит: строки заводит +> оркестратор. +> +> **Куда смотреть, если вы исполнитель, а не читатель:** §8 — что решено владельцем и что это значит · +> §8б — что должен сделать оркестратор при лендинге · §5 — состав батча 0.3.0 по порядку исполнения · +> §5а — что резать · §5б — транспорт и сеть · §9 — находки чужих зон, которые ждут своих сессий. +> Буллеты §2 — обоснования, к ним ходят за «почему». + +> **⟶ РЕВЬЮ-ШАПКА ПРИЁМКИ (оркестратор №17, 16.08.2026; ратификация — D39.138). ПРИНЯТ.** +> Верификация: четыре независимых опровергателя (2×fable на денежно-критичном, 2×opus) пере-открыли +> ИСПОЛНЕНИЕМ 42 улики самых несущих блоков (Б-0/Б-13а · Б-1/Б-15 · Б-2/Б-3/Б-23 · Б-7а-п.4/Б-19а) — +> **ни один клейм не опровергнут по существу**; выборочные проверки оркестратора поверх (X-Request-Id · +> no-store на всех ответах · «epoch» ровно один блок · 16 operationId) — сходятся. Единственная правка +> тела при лендинге — МЕХАНИЧЕСКАЯ: 9 фронтовых якорей получили префикс `frontend/src/` (линтер якорей +> counts.py --lint; смысл не тронут). Поправки приёмки (читать тело через этот список): +> 1. **К-10 в §6 противоречит Б-0/§8 п.1:** «закрывается бесплатно правкой проекции» дало бы пофазность +> у главы, а решение владельца снимает фазы с провода — К-10 закрывается «НЕ строить». +> 2. Б-13а: «величины „глав сделано" на поверхности нет вообще» — смягчить: клампованная производная +> уходит через `run-options.max_chapters = min(affordable, chaptersLeft)` (`pricing.go:83`); клиент +> не отличит её от денежного капа, вывод буллета не двигается. +> 3. Б-5: «путь удаления достижим только из свипа» неверно — `abandon` достижим и из веток отказа самого +> интейка (`books.go:163,180`); суть («нет пользовательской ручки») верна. +> 4. Б-23: жанр — в 6 из 7 top-level ролевых промптов; файлов у пары 11 (repair-хвост без жанра). +> 5. Б-19а/§9: код банка — `backend/internal/membank/memory.go` (НЕ `pipeline/memory.go`), номера строк +> верны; O_EXCL стоит на файле исходника, не на директории; лживых комментариев ДВА — +> `snapshot.go:188-189` и докстринг `membank/memory.go:360-363` (оба до-pack-20; строка 187). +> Усиления от опровергателей: bearer-предъявление — ВТОРОЙ недокументированный CSRF-карваут +> (`csrf.go:55-56`); грант уже 0 при неверифицированном email (`login.go:306-308`); `required:` у +> `BookIntake` тоже ставит `file` первым (`:905`); ja-мина Б-19а — громкий `--resnapshot`, не тихая +> перекупка (mined-строки вне base-версии драфт-волны, `membank/memory.go:349`). +> Ограничение метода (честно заявлено сессией в §7/§10): опровергатели — семейство Claude; кросс-СЕМЕЙНЫЙ +> проход — отдельный заказ. Решения §8 перенесены в D-лог нотой D39.138 (по §8б п.1); состав батча §5 — +> носитель для исполняющих сессий, строка бэклога 183. + +## 0. Резюме + +**Контракт здоров в ядре и дефектен на периферии.** Ядро — то, что построено обеими сторонами и прошло +контакт с кодом (библиотека, карточка книги, старт прогона, шкала потолка, прогресс, деньги вне провода) +— независимая greenfield-панель воспроизвела почти дословно (§3). Дефекты сосредоточены там, где контракт +писался ВПЕРЁД, без потребителя: ошибки, поток, экспорт, замечания, структура глав. + +**Кандидатов было около 75; выжило 29 буллетов (24 CONFIRMED, 4 PLAUSIBLE, 1 решён владельцем), +отвергнуто 29.** Отвержения — тоже результат: девять опираются на ратифицированные решения владельца +(три приняты 15.08, в день заказа ревью), четыре суть мои ошибки чтения стандарта или кода. Часть +отвергнутого переформулирована и вернулась в §2 в узком виде, поэтому суммы не сходятся арифметически. +Три буллета (Б-0, Б-11а, Б-23) добавлены 16.08 по вопросам владельца — то есть разбор доклада дал +находок больше, чем некоторые линзы ревью. + +**Главное — одно.** Причина отказа не существует в машинном виде: `type` — константа `about:blank` на +каждом ответе, `detail` не отправляется никогда, а единственный различитель — английская фраза, которую +контракт предписывает показывать пользователю «as-is». Это одновременно нарушение RFC 9457, корень Ф-61 +и стена для ПТ-36. Всё остальное дешевле. + +**Момент.** Восемь операций из шестнадцати не построены НИ ОДНОЙ зоной сервера, а зона фронта заморожена +(D39.136 п.2) и живёт на моках. Правка их формы стоит сегодня спеки и моков; после P7 — воркера, +миграции и экрана. Второй такой дешёвый момент не наступит. + +--- + +## 1. Карта фактов: что стоит на земле под контрактом + +Построена чтением кода ДО чтения компаньона и D-нот (порядок против якорения). + +### 1.1. Контракт объявляет 16 операций на 15 путях; платформа отвечает на 8 из них + +`contractRoutes` регистрирует восемь путей (`platform/internal/httpapi/v0.go:51-78`): + +| Операция контракта | Платформа | Клиент (код) | Экран | +|---|---|---|---| +| `GET /books` · `POST /books` · `GET /books/{id}` | есть | есть | есть | +| `GET /books/{id}/run-options` · `POST /books/{id}/runs` | есть | есть | есть | +| `POST /runs/{id}/stop` · `/resume` | есть | есть | **кнопок нет** | +| `GET /usage` | есть | есть | **не читается ничем** | +| `/chapters` · `/units` · `/notes` · `/bank` | **нет** | есть | моки | +| `POST /bank/decisions` | **нет** | есть | **экрана нет** | +| `GET /runs/{id}/events` (SSE) | **нет** | есть | моки | +| `POST /exports` · `GET /exports/{id}` | **нет** | **нет** | нет | + +Непостроенное отвечает охраняемым `404 «Object not found»` (`platform/internal/httpapi/server.go:134`). +SSE на платформе нет ни строкой (`grep text/event-stream` по не-тестовому Go — только комментарии в +`serve.go:36`, `middleware.go:105`). + +### 1.2. Ошибочная поверхность: один литерал и английская проза + +`WriteProblem` (`platform/internal/httpapi/problem.go:22-28`) ставит `type: "about:blank"` **всегда**; +`detail` пуст на всех вызовах контрактной поверхности (единственные непустые — два ответа `/readyz`, +`server.go:158,169`); `instance` спекой объявлен (`openapi.yaml:1444`), а структурой `Problem` в коде +(`problem.go:14-19`) не предусмотрен вовсе. Фраз меньше, чем причин: `«Request could not be read»` — +шесть разных условий (`v0.go:238,250,333,339,531,547`), `«The upload is incomplete»` — четыре +(`v0.go:345,356,419,423`). Ни `Accept-Language`, ни локали в платформе нет (грепом ноль). Фронт печатает +серверную строку как есть в трёх местах (`frontend/src/showcase/Loaded.tsx:66`, `RunStart.tsx:188`, +`AddBook.tsx:289`) при русском интерфейсе. Рядом: `X-Request-Id` ставится на каждый ответ +(`platform/internal/reqid/reqid.go:16,27`) и контрактом не упомянут. + +### 1.3. Коды, которых в спеке нет + +Спека перечисляет 200/201/202/400/401/404/408/409/413/503, `default`-ответа нет. На проводе сверх этого: +**403** «Cross-origin request rejected» (`server.go:99`, `auth/csrf.go:38` — он же отвечает на честный +same-origin запрос без `X-TM-Client`); **500** (`v0.go:518,572`, `middleware.go:68`); **431** от net/http +(`serve.go:79`); **429** на `/auth` (`login/login.go:173,234`). `405` на `/v0` не бывает: catch-all +глотает метод (проверено по stdlib go1.26.6 — 405 синтезируется только когда ни один паттерн не совпал). + +### 1.4. Интейк + +Цикл прерывается на части `file` (`v0.go:337-378`): обязательное поле после файла → 400, необязательное +— **теряется молча, ответ 201**. Порог 413 деплойный и не сообщается; дедлайн тела 10 минут, кап 64 МиБ +(`server.go:85`). Идемпотентности нет — каждый POST создаёт новую книгу (`books.go:125`), `Location` не +ставится (`v0.go:384`), а удалить дубликат нечем. Числа зашиты: `maxIntakeField = 1<<10`, +`maxIntakeParts = 16` (`v0.go:299-302`) с комментарием «Neither is the contract's business» — при том что +спека их объявляет (`openapi.yaml:141-142`). + +### 1.5. Поток: словари не совпадают + +Движок пишет `hello · progress · unit_done · bank_stop · ceiling · spend · finished` +(`backend/internal/runevents/runevents.go:45-61`), `ceiling` несёт `scope: book|day` +(`runevents.go:151-153`), `finished` — исход из шести значений (`:95-102`); платформа `finished` +сознательно не считает авторитетным (`platform/internal/pgstore/sink.go:178-181` — закрытие прогона идёт +от exit-маркера). Словарь контракта другой и терминального кадра не имеет (`openapi.yaml:1304-1313`). + +### 1.6. Масштабы и коллекции + +Реальная книга — 2284 раздела, 7,78 млн знаков (`docs/product-requirements.md:13`). Библиотека: +`defaultPage = 100`, `maxPage = 1000`, и `limit` выше максимума **тихо понижается до дефолта** +(`platform/internal/pgstore/books.go:510,586-587`). Курсор — `base64(scope|время|id)` без структурной +эпохи (`books.go:821-831`). Клиент никогда не шлёт `limit` и всегда обходит коллекцию до конца +(`frontend/src/api/client.ts:87-115`), поиск делает в браузере (`showcase/Bank.tsx:89`, `Goto.tsx:25`). + +### 1.7. Данные движка, которые контракт называет иначе + +`heading` манифеста — **не** метка из данных книги, а детерминированный рендер «Глава N», и комментарий +кода прямо запрещает подавать его как метку книги (`backend/internal/pipeline/manifest.go:77-86`). +Id юнита нестабилен при смене чанкера, id главы выживает (`manifest.go:102`). Банк-сайдкар несёт +`aliases` (`bankexport.go:72`), стоп-таблица — 14 полей (`mining.go:317-322`). `BankExport.signed` и +`StatusReport.unsigned_bank_terms` считают разное под похожими именами (`status.go:445`). + +### 1.8. Read-модель платформы местами богаче контракта + +`notes.id` и `notes.created_at` существуют (`platform/internal/pgstore/migrations/00002_readmodel.sql:134-140`), +контракт их не отдаёт. `exports.failed_reason` и `ready_at` существуют (`:188-199`), контракт состояния +отказа не имеет. `chapters.units_draft_done`/`units_edit_done` существуют (`:102-105`), контракт отдаёт +один счётчик. `units` имеют чеки `translated ⇒ target≠''` и `не translated ⇒ target=''` (`:127-128`), +то есть `target` детерминирован, а контракт объявляет его необязательным. + +### 1.9. Сеть: как обновляется текст и сколько это весит (замер 16.08) + +Раздел добавлен по вопросу владельца «за счёт чего работает „текст меняется на лету“ и не тяжело ли +гонять тексты». В первой редакции доклада перформанс числами не мерился — это был пробел ревью. + +**Механика.** Кадр SSE НИКОГДА не несёт текста; он только провоцирует REST-чтение +(`frontend/src/showcase/useRunStream.ts:84-161`): `progress` — чистый патч карточки, ноль чтений; +`chapter` — патч строки + чтение юнитов той главы; `status` — юниты ВСЕХ открытых вкладок + всё дерево +глав + библиотека; `note`/`bank` — перечитывание СПИСКА ЦЕЛИКОМ (дельту применить нечем); +`resync_required` — сброс всего мира книги. + +**Объёмы**, сериализованы поверх настоящей книги (`~/books/gu-zhenren`: 23,1 МБ, 7,99 млн символов; +манифест движка — 2283 главы, 4276 юнитов, 5071 чанк; кириллица считалась 2 Б/символ, фактически 1,81): + +| Чтение | JSON | тот же ответ под gzip | +|---|---|---| +| юниты одной главы (оригинал + перевод) | **26,1 КБ** | 10,4 КБ | +| дерево 2283 глав | **250 КБ** | 39 КБ | +| банк 1000 строк | **167 КБ** | 17 КБ | +| замечания 500 строк | 95 КБ | — | +| вся книга юнитами | **~57 МБ** | — (такой ручки в контракте нет намеренно, `:224-227`) | + +Сходится с независимым фикстурным замером платформы (2284 главы = 289 КБ, +`platform/docs/archive/platform-PROGRESS-P0-P3.md:910-911`). Основание цифры «глава» я пере-проверил +сам: `~/books/gu-zhenren/minirun/export.json` — 14 юнитов на 10 глав, 94 490 символов перевода, +174 217 Б JSON, то есть ~17,4 КБ перевода на главу; плюс исходник (CJK, 3 Б/символ) даёт названные 26 КБ. + +**Фан-аут одного кадра `status`:** 1 вкладка — 282 КБ, 12 — 562 КБ, 20 — 767 КБ (замер зоны: «20 вкладок +это 20 чтений на кадр», `frontend/docs/BACKLOG.md:48`; пиннится тестами `useRunStream.test.tsx:271,289,302`). + +**Гасителей нет ни одного.** `grep -riE "etag|if-none-match|304|gzip|content-encoding|brotli"` по +контракту и по `frontend/src` — ноль. Сжатие и ETag названы требованиями к платформе только в её +собственном доке (`platform/docs/PLATFORM_DIRECTION.md:202-205`), в контракт не внесены; stdlib +`net/http` не сжимает, edge-конфига в репозитории нет. + +--- + +## 2. Буллеты + +Форма: **вердикт · факт с уликами · чья боль · цена · рекомендация · контраргумент** (для «оставить как +есть» контраргумент обязателен). Классы цены по построенному: +**Ц0** операция не построена ни платформой, ни экраном → правка спеки и моков · +**Ц1** правка проекции платформы без миграции · **Ц2** миграция read-модели или новая ручка · +**Ц3** переписывается построенный путь с обеих сторон. + +--- + +### Б-0 (CONFIRMED · HIGH). Контракт знает устройство конвейера: число волн и их имена — обязательные поля провода + +Буллет заведён по прямому вопросу владельца 16.08: «насколько контракт, фронт и платформа знают о +внутренностях пайплайна? Они не должны знать ничего. Единственная архитектурная точка, которую фронт +вправе знать, — общий банк памяти книги: там пайплайн останавливается и ждёт подписи». + +**Что спрятано хорошо (это часть ответа).** Ни одного имени модели, провайдера, температуры, промпта, +langpack — грепом по спеке ноль. Денег нет: только процент и шкала в главах, `EventCeiling` «carries no +figures». Причины флагов движка платформа хранит и не проецирует +(`platform/internal/ingest/events.go:124-127`). Не проецируются `wave` юнита, `engine_run_id`, +`chunker_version`, `spend`, scope потолка. Движок не адресуется ни одним путём. Метка «черновой вариант» +на экране выведена из ПРОДУКТОВОГО статуса (`frontend/src/showcase/format.ts:86-87`, +`isDraft = status !== 'ready'`), а не из волны. + +**Что протекает.** `Progress` с обязательной парой `draft`/`edit` (`openapi.yaml:735-738`; `Counter` +там же описан как «Wave counter, in UNITS») — это не абстракция, а сквозной проброс внутренней +структуры движка до React-компонента, пришпиленный в шести местах: + +`backend/internal/runevents/runevents.go:195-196` (`WaveDraft`/`WaveEdit`) → +`platform/internal/ingest/events.go:121,130-135` («A closed vocabulary on both sides of the seam») → +SQL-констрейнт `check (wave in ('draft','edit'))` +(`platform/internal/pgstore/migrations/00015_seam_ceiling_and_units.sql:56`) и колонки +`draft_done/draft_total/edit_done/edit_total` (`00002_readmodel.sql:55-58`) → +`wireProgress` (`platform/internal/httpapi/v0.go:92-96`) → спека → генерённые типы → +`frontend/src/showcase/format.ts:109-110` и `About.tsx:96`. + +**Компаньон отвечает на собственный ревью-вопрос неверно.** §5 таблицы: «переименована стадия / +добавлена волна → **нет**, фронт не правится». Проверено ИСПОЛНЕНИЕМ (реальный `format.ts` собран через +vite и вызван): +- третья волна приезжает как новое поле — клиент «игнорирует неизвестные поля» — и + `translatedPercent` отдаёт **100 %**, когда треть работы не сделана. Молча; +- переименование фаз или снятие редактуры → **TypeError в рендере** (`Status.tsx:39`, `About.tsx:96`), + а `ErrorBoundary`/`errorElement`/`componentDidCatch` во фронте нет ни одного (грепом пусто); +- незнакомую волну платформа **тихо дропает** (`platform/internal/pgstore/sink.go:206-208`) — прогресс + занижается без единой ошибки; +- «правок ноль» на деле = миграция БД + шов + спека + регенерация типов (её принуждает дрифт-тест + `frontend/src/api/contract.test.ts:30-37`). + +Верна только половина ответа: имён СТАДИЙ в спеке действительно нет. + +**Прочие утечки того же рода.** `finalizing` — имя фазы в продуктовом словаре, и писателя у него нет +нигде (Б-13). `verify_bank` в описании называет следующую фазу: «Stop for bank signing **before the +final pass**» (`:1201`). `Unit` объяснён как «one edit unit», «roughly 1.9 units per chapter» +(`:994, :220-221`). `TermOrigin: [seed, ruby, mined]` — колонка `source` движка, с японской `ruby` в +общем слое (Б-17). `TermStatus: [auto, draft, approved]` — статус-машина банка движка. `promote`/ +`decline` — глаголы оператора майнера. Отдельно: **объяснительная проза спеки компилируется в исходники +фронта** как JSDoc генерённых типов — вместе с «chunk verdict», «flagged», «sanitizer cleanup», «stage +boundaries», операторскими рангами и примером «CJK leak in the ru output: 第一节» +(`openapi.yaml:1430` → `frontend/src/api/schema.ts:1047`), то есть ровно той строкой, которую контракт +объявляет запретной. + +**Гейта нет.** `.spectral.yaml` держит только `spectral:oas`; `generality.test.ts` ловит лишь языковую +специфику. Утечку конвейера не стережёт ничто. + +**Чья боль.** Бэкенд (его архитектура подвижна, а контракт делает её изменение ломающим для фронта), +фронт (падение в рендере вместо деградации), продукт (ПТ-33). + +**Цена.** Ц1-Ц2: фазовый сплит остаётся в платформе — колонки уже есть; на провод уходит один счётчик. +Переименования (`finalizing`, `verify_bank`, `TermOrigin`, `Unit`) — Ц0/Ц1. + +**Рекомендация — менять спеку и проекцию** (владелец 16.08: «переделываем»): +1. `Progress` → **один счётчик до ближайшей остановки** плюс, если нужна честность, серверная доля. + Число волн становится внутренним делом бэкенда. Это же закрывает Б-13а: знаменатель считается по + КУПЛЕННОМУ объёму, поэтому полоса всегда доходит до конца, а после подписи банка начинается заново + (форма владельца, В-5). +2. `finalizing` свернуть в `translating`; `verify_bank` → `stop_for_signing` без упоминания следующей + фазы; `TermOrigin`/`TermStatus` снять с провода (ни один экран их не рисует); `Unit` описать как + фрагмент без «edit unit» и «1.9 на главу». +3. Из описаний вычистить конвейерные слова — иначе они уезжают в исходники клиента. +4. Завести гейт: правило «на проводе нет ни имён стадий/волн, ни движковых словарей» проверяется + тестом, как уже проверяется языковая специфика. + +**Контраргумент.** «Пофазный прогресс — не утечка, а честность: сквозной счётчик читал бы ноль всю +черновую волну» (`:729-731`). Довод верен и сохраняется полностью: честность даёт СЕРВЕР, считая долю +или сегмент, а не клиент, складывающий две названные волны. Именно решение «формула — продуктовое +решение клиента» (`:734`) и вытолкнуло имена фаз наружу. + +--- + +### Б-1 (CONFIRMED · HIGH). Причины отказа не существует в машинном виде, а фразу пользователю пишет сервер + +**Факт.** `Problem.type` обязателен (`openapi.yaml:1436-1438`), платформа ставит туда `"about:blank"` +всегда (`problem.go:25`). RFC 9457 регистрирует это значение как «the problem has no additional semantics +beyond that of the HTTP status code» (§4.2.1) и требует: «Consumers **MUST** use the "type" URI (after +resolution, if necessary) as the problem type's primary identifier» (§3.1.1), а `title` объявляет +вспомогательным: «The "title" string is advisory and is included only for users who are unaware of and +cannot discover the semantics of the type URI (e.g., during offline log analysis)» (§3.1.3, +https://www.rfc-editor.org/rfc/rfc9457.txt). Наша спека делает наоборот: `title` — «Product phrase naming +the class of failure. **Shown to the user as-is**» (`openapi.yaml:1441`). + +Четыре следствия, каждое проверено исполнением: + +1. **Различителя нет там, где он нужнее всего.** `fail()` кладёт шесть разных доменных отказов на 409 и + на 400, отличая их только фразой (`v0.go:544-570`). Клиент не отличит «допиши подпись банка» от «уже + идёт прогон» от «потолок уехал». +2. **`detail` мёртв.** Он специфицирован (`openapi.yaml:1434-1435`: «`title` — CLASS, `detail` — the + specific sentence»), клиентом ПРЕДПОЧИТАЕТСЯ (`Loaded.tsx:66`, `AddBook.tsx:289`, `RunStart.tsx:188` + — `problem?.detail ?? problem?.title ?? …`), и не отправляется НИКОГДА: все вызовы контрактной + поверхности передают `""`. Весь различающий слой не существует на проводе. +3. **Пустая фраза легальна.** `title` обязателен, `minLength` нет, а спека прямо говорит «either may be + empty» (`openapi.yaml:1435`). Клиенты используют `??`, а `""` не nullish — значит конформный сервер + может отрисовать пустое сообщение об ошибке вместо локального запасного. +4. **Даже пара «about:blank + продуктовая фраза» отклоняется от стандарта:** «When "about:blank" is used, + the title SHOULD be the same as the recommended HTTP status phrase for that code … although it MAY be + localized to suit client preferences (expressed with the **Accept-Language** request header)» + (RFC 9457 §4.2.1). Стандарт называет и механизм локализации, которого у нас нет. + +**Внутренняя несогласованность.** Тот же документ для двух причин делает правильно: `RejectReason` и +`PausedReason` — машинные enum'ы, «the phrase the user reads is drawn by the client» +(`openapi.yaml:781-782, 848-849`). Одна спека, две противоположные политики. + +**Почему это не видно тестами.** Моки отвечают по-русски НАМЕРЕННО (`frontend/src/mock/handlers.ts:29-35`, +`frontend/src/mock/intake.ts:271-273` — «A fixture that answered in English would be modelling a platform we do not +have»). Вся батарея фронта зелёная против платформы, которой нет; двуязычие видно только на живом стенде. + +**Независимая проверка.** Все четыре greenfield-дизайна пришли к «код + параметры у клиента», причём +формулировки почти дословно противоположны нашей: «`detail` in every error is developer English that the +client must never display». Один добавил серверную локализацию по `Accept-Language` только для +неперечислимых причин (модерация, отказ провайдера). + +**Чья боль.** Фронт (два языка на экране, Ф-61), продукт (ПТ-36 + слово владельца 15.08: «мультиязычность +фраз ВСЕХ зон … — вход контракт-ревью», D39.136 п.4а), поддержка (нечем коррелировать), индустрия. + +**Цена. Ц1 — и она меньше, чем кажется:** клиент УЖЕ держит таблицу «статус → русская фраза + совет» +(`AddBook.tsx:282-288`) и отбрасывает её всякий раз, когда сервер прислал любую фразу (`:289`). Перевод +интейка на машинные коды на стороне клиента — снятие одной строки. На платформе — словарь причин +(их около двенадцати, все уже перечислены в `fail()`) и расширение структуры `Problem`. + +**Рекомендация — менять спеку и код, спеку первой.** +1. Расширения RFC 9457 (§3.2 «Problem type definitions MAY extend the problem details object with + additional members»): `code` — закрытый словарь причин версии, и `request_id` (значение уже + существует, `reqid.go:27`); либо заполнять `instance`, который уже объявлен в схеме. +2. Записать: `title`/`detail` — для разработчика и лога, клиент их НЕ показывает; фразу рисует клиент по + `code`; незнакомый код → нейтральная фраза (правило уже написано для `RejectReason`, + `openapi.yaml:797-799`). +3. Для валидации — расширение `errors[]` с указателем на поле (RFC 9457 приводит ровно такой пример). + Сегодня клиент не узнаёт даже, какое поле не прошло (`v0.go:242`), а самый болезненный случай — + `v0.go:356`: «The upload is incomplete» в ответ на форму, где прислано всё, но в другом порядке. +4. Записать правило для случая «`problem+json` не пришёл вовсе»: сегодня оба клиента гейтят разбор по + media-type (`client.ts:73`, `upload.ts:137`), и любой ответ промежуточного узла или stdlib даёт + `problem === null` и своё поведение на каждом экране. + +**Контраргумент, который я рассмотрел.** «Клиент не может иметь фразу для причины, которой ещё нет» — +снимается тем же приёмом, что уже записан для `RejectReason`. Второй: «конкретная причина раскрывает +конвейер» — снят словом владельца 15.08: «граница ПТ-33 ПЕРЕ-ЧИТАНА: охранять АЛГОРИТМЫ бэкенда, а не +минорную механику … „не раскрывать внутренности“ не значит „говорить абстракциями“» (D39.136 п.4а). +Третий, самый сильный: «RFC разрешает `about:blank` для самоочевидных ошибок» — верно, и поэтому +рекомендация не «код на каждый ответ», а «код там, где под одним статусом живёт больше одной причины». + +--- + +### Б-2 (CONFIRMED · HIGH). Возможности деплоя нигде не выражены, поэтому клиент их зашивает + +**Факт.** Ни одной ручки «что этот деплой умеет». Проверено по каждому измерению: + +- **Пары языков.** `LangCode` принимает любой код (`openapi.yaml:710-716`), проверяется только ФОРМА + (`platform/internal/books/books.go:118-121`), равенство `source == target` не проверяет никто. Список + `zh·ja·en → ru` зашит в клиент (`frontend/src/showcase/languages.ts:14-16`), и файл сам называет это + «the honest weak point». Отказ по неподдерживаемой паре не выражен ни одним классом ответа. Худший + случай прослежен до конца (и уточнён 16.08 — первая редакция называла механизм неверно): пар-промпты + в репозитории есть ТОЛЬКО для `zh-ru` (`backend/prompts/`), а отсутствие промпта пары — это ЖЁСТКАЯ + ошибка конфигурации, не деградация: «no prompt for pair %q role %q … never silently substitute another + pair's conventions» (`backend/internal/config/pipeline.go:952-956`). Langpack при этом деградирует + тихо (`backend/internal/pipeline/runner.go:326-329`), но до него дело не доходит. Итог: две из трёх + предлагаемых формой пар сегодня выполниться не могут; книга принимается, пишется на диск оператора, + разбирается, пользователь проходит денежный экран и жмёт «перевести» — и прогон умирает на валидации + конфигурации ДО единого платного вызова. **Деньги не сгорают** (холд закрывается по фактическому + расходу, то есть нулю), но пользователь получает `failed` без причины (Б-8) в конце всего пути. +- **Порог размера файла** не сообщается (`openapi.yaml:148-150`) и не сообщается даже в отказе: + `TooLarge` — голый `Problem` (`:639-643`), а `v0.go:409` передаёт пустой `detail`. Клиент не имеет + предпроверки (`AddBook.tsx:286` только отображает 413 постфактум) и узнаёт порог, потратив минуты + аплоада. +- **Форматы экспорта** — свободная строка (`openapi.yaml:1280-1284`). +- **Размеры страниц** объявлены прозой per-operation, и по-разному: для глав/замечаний/банка число зашито + (`:197, 252, 278`), для библиотеки и юнитов — «the platform's choice» (`:99, 226`). +- **Версия контракта** живёт ровно в одном месте на проводе — `EventHello.contract` + (`openapi.yaml:1342-1351`), внутри прогонного SSE, который платформа не реализует. То есть сегодня + версии на проводе нет нигде, а REST-клиент не узнает её и потом. Правило «клиент пинит точную 0.x» + (`:47-50`) не имеет носителя. + +**Независимая проверка.** Все четыре greenfield-дизайна независимо ввели ручку возможностей +(`/config`, `/capabilities`, `/limits`+`/languages`); один связал её ключи с ключами в ошибках, чтобы +отказ ссылался на объявленный предел. + +**Чья боль.** Фронт (данные деплоя в коде клиента), платформа (смена набора пар или порога = релиз +клиента), продукт (ПТ-2 «пары во все стороны» — список соврёт первым же деплоем; сегодня форма предлагает +две пары, которых деплой не умеет). + +**Цена. Ц2** — одна read-ручка без миграции: пары из конфигурации, порог из `UploadLimits` +(`server.go:70-86`), страницы из констант (`books.go:586-587`), версия из сборки. + +**Рекомендация — менять спеку и код.** `GET /capabilities` (или `/config`): `contract_version`, +`language_pairs[]` со статусом, `intake_max_bytes`, `export_formats[]`, `page_size{default,max}`. +Плюс отказ по паре — своим кодом в `Problem` на интейке, а не молчаливым прогоном. +Судья справедливо предупредил о соразмерности: это НЕ повод строить многотенантный документ +возможностей с ETag и пер-аккаунтными оверрайдами — нужен один плоский ответ на один деплой. + +**Контраргумент.** «Числа принадлежат деплою, поэтому в контракте им не место» (`openapi.yaml:148-150`) +— верно ровно наполовину и, по сути, аргумент ЗА эту ручку: обоснование спеки («a number in the contract +would be a second copy of it») исключает константу в СПЕКЕ, а не значение в ОТВЕТЕ. + +--- + +### Б-3 (CONFIRMED · HIGH). Приём книги: молчаливая потеря на 201, а схема указывает НЕВЕРНЫЙ порядок частей + +**Факт.** Сервер читает форму потоково и прерывает цикл на части `file` (`v0.go:352-361`); часть после +файла не читается никогда. Отсюда ратифицированное 0.2.3 правило (`openapi.yaml:119-128`): обязательное +поле после файла → 400, необязательное → **теряется молча, ответ 201**. + +Судейство добавило то, чего в кандидате не было: **схема моделирует обратный порядок.** Свойства +`BookIntake` перечислены как `title` (`:907`), `file` (`:918`), `source_lang` (`:922`), `target_lang` +(`:923`), `genre` (`:924`) — файл ВТОРЫМ, впереди обоих обязательных языков. Генератор, эмитящий части в +порядке свойств (обычный дефолт), произведёт ровно ту форму, которую сервер отвечает 400. То есть +кодоген не просто не ловит правило — он указывает его нарушить. Бьёт это по тому самому второму клиенту, +портируемость к которому декларирована (`openapi.yaml:564-570`). + +Сопутствующее в том же запросе: +- **Идемпотентности нет** (`books.go:125` — свежий id на каждый вызов), а удалить дубликат нечем: `DELETE` + на поверхности отсутствует, и `pgstore/books.go:349` прямо это фиксирует. RFC 9110 §9.2.2: «A client + SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know + that the request semantics are actually idempotent … or some means to detect that the original request + was never applied» (https://www.rfc-editor.org/rfc/rfc9110.txt) — а контракт при этом сам называет + ретрай средством от 408 (`openapi.yaml:146-147`). +- **Числа порога** спека объявляет («more than 16 parts, a text field longer than a kilobyte», + `:141-142`), а код называет их не своим делом (`v0.go:296-298`) — и описывают они РАЗНЫЕ правила: код + считает части ДО файла, спека — все; мок фронта считает все (`frontend/src/mock/intake.ts:212`), то есть две + построенные реализации уже расходятся. «Килобайт» без единицы: код считает байты (`v0.go:390-396`). + У `genre` в схеме нет `maxLength` вовсе, хотя фактический предел есть. + +**Независимая проверка.** Три из четырёх greenfield-дизайнов выбрали ДВУХШАГОВУЮ загрузку (метаданные +JSON → `PUT` байтов; у одного — tus с возобновлением). При ней проблема порядка не существует, размер +проверяется до передачи, повтор безопасен, обрыв докачивается. + +**Чья боль.** Продукт (жанр теряется молча; 10 минут ожидания ради отказа по размеру), платформа +(дубликаты на диске без квоты — `platform/BACKLOG.md:24`), второй клиент (схема указывает неверный +порядок). + +**Цена.** Ц3 для двухшаговой формы. **Дешёвый ход внутри нынешней — Ц1:** после того как `Accept` +дочитал файл, `parts.NextPart()` вызывается снова — трейлинг-части можно применить или ответить 400. +Молчаливая потеря исчезает без смены транспорта. Перестановка свойств в схеме — Ц0. + +**Рекомендация — менять код и спеку.** (а) Любая часть после файла → 400 с машинным кодом, никакой +тихой потери на 201; (б) переставить свойства `BookIntake` так, чтобы `file` был последним (поле `genre` +к этому моменту уходит совсем — Б-23, решение владельца 16.08, так что его `maxLength` не нужен); +(в) `Idempotency-Key` на `POST /books` и `POST /runs`; (г) `Location` на 201; (д) двухшаговую загрузку +записать направлением на после-беты с ценой. + +**Контраргумент.** «Потоковое чтение вынужденное: строка книги нужна до байтов» — верно и сохраняется; +меняется только реакция на нарушение. «Клиент один и он кладёт файл правильно» (`upload.ts:64-69`) — +контракт пишется не для одного клиента, и порядок свойств в схеме уже сегодня противоречит его прозе. + +--- + +### Б-4 (CONFIRMED · HIGH). Экспорт не умеет провалиться — при том что платформа умеет + +**Факт.** `Export` — `required: [id, ready]` плюс необязательный `url` (`openapi.yaml:1286-1298`). +Состояния отказа нет: упавшая сборка навсегда остаётся `ready: false`, а `GET` отвечает 200 с +`Retry-After` (`:530-541`) — клиент опрашивает вечно. Спека сама показывает, что авторы этот тупик +видели («without this read the creating call is a dead end», `:528`) и закрыли только счастливую половину. +В БД платформы поля уже есть: `exports.failed_reason`, `ready_at` +(`platform/internal/pgstore/migrations/00002_readmodel.sql:188-199`). + +AIP-151 формулирует это как MUST: «Operations that fail during their execution phase must return an error +response (AIP-193), placed in the `Operation.error` field» (https://google.aip.dev/151). + +Сверх того: `format` принимается и нигде не отражается (два экспорта одной книги неразличимы), у ссылки +нет срока, а её защита — только проза «Served to the authenticated owner only» (`:1295-1297`), при том +что носителем ПТ-34 назначен в том числе контракт (`docs/product-requirements.md:61`). + +**Цена. Ц0** — обе операции не построены. **Рекомендация — менять спеку:** +`state: pending|ready|failed|expired` вместо булева `ready`; `failure_code`; эхо `format`; `expires_at`; +правило доступа к `url` (подписанная ссылка со сроком либо отдача через API). Размер и TTL — по +AIP-151 «may», не настаиваю. + +**Контраргумент.** «Форматы — работа S7, сейчас фиксируется только форма вызова» (`:492`). Именно поэтому +и дёшево: форма вызова без состояния отказа — не заготовка, а тупик, и стоит она одной правки схемы. + +--- + +### Б-5 (CONFIRMED · HIGH). У книги нет ни одной записи; у прогона нет чтения + +**Факт.** На всей поверхности только `get:` и `post:` — ни `PATCH`, ни `PUT`, ни `DELETE` +(проверено перечислением всех 15 путей). Переименовать книгу нечем (Ф-62, +`frontend/docs/BACKLOG.md:68`: подсказка формы обещала «поправить позже» — обещание сняли, потому что +контракт его не даёт). Удалить книгу нечем: путь удаления в платформе достижим только из своего свипа +интейка (`pgstore/books.go:349`). Название при этом по умолчанию выводится из ИМЕНИ ФАЙЛА с обрезкой до +200 рун (`platform/internal/books/books.go:365`) — то есть заведомо случайно, а исправить нечем; ошибка +лечится второй книгой на диске оператора. + +Прогон: `GET /runs/{runId}` не существует — у прогона три под-ресурса (`/events`, `/stop`, `/resume`) и +ни одного чтения себя; в ответе `stop`/`resume` нет `book_id` (`v0.go:123-132`), хотя в read-модели он +есть (`pgstore/books.go:481`). Клиент, потерявший тело 202, может перечитать прогон только через карточку +книги. + +**Направление уже задано владельцем:** «Переименование книги (Ф-62) — В КОНТРАКТ направлением; форма — +из контракт-ревью» (D39.136 п.4в). + +**Цена. Ц2.** `PATCH` — колонки есть; `DELETE` — мягкое удаление плюс свип; `GET /runs/{runId}` — +проекция существующей строки. + +**Рекомендация — менять спеку и код.** `PATCH /books/{bookId}` **только с `title`** (merge-patch; 409 при +живом прогоне) — жанр из продукта уходит совсем (Б-23), пара языков в правку НЕ входит (её смена это +пере-перевод, а не правка метаданных); `DELETE /books/{bookId}` (409 при живом прогоне, физическая уборка +свипом); `GET /runs/{runId}`; `book_id` в `Run`. +⚠ Записать в спеке явно: правка названия — **отображение**, до движкового брифа она не доезжает. +Основание: `title` входит в `BriefHash` (`backend/internal/config/book.go:279-283`), но платформа пишет +`book.yaml` ровно один раз и никогда не перезаписывает (`platform/internal/books/render.go:71-77`) — +поэтому сегодня переименование бесплатно, а наивный «пере-рендер конфига» перекупил бы книгу целиком. +Список прогонов книги (история) — снят решением владельца 16.08: в МВП не нужен. + +**Контраргумент.** «Метаданные приходят из файла, править их — лишняя поверхность» — не держится, см. +происхождение названия. «История прогонов нужна поддержке» — вероятно, но требования нет; отделяю +чтение прогона (дёшево, нужно клиенту) от истории (продуктовый вопрос). + +--- + +### Б-6 (CONFIRMED · HIGH). Поток: канал привязан к прогону, конца нет, и правила докачки не сходятся + +Четыре дефекта одного канала; все — Ц0, потому что канала нет ни строкой. + +**(а) Конец разбора не объявляет никто.** Поток — `GET /runs/{runId}/events` (`openapi.yaml:378`), а +книга в `uploading`/`parsing` прогона не имеет по построению: строка прогона создаётся только в +`StartRun` (`platform/internal/pgstore/runs.go:66,80`), весь разбор живёт на строке книги +(`books.go:180,237,283`). Клиент лечится опросом раз в 3 с (`frontend/src/api/queries.ts:64-75`) плюс +вторым хуком, перечитывающим дерево на выходе из интейка (`frontend/src/showcase/useIntakeEnd.ts:31`). Разбор +настоящей книги — минуты. Это Ф-56, и зона правильно отдала его владельцу контракта. + +**(б) Конца потока нет.** Терминального кадра в таблице нет (`:1304-1313`), а `204` в ответах операции +не объявлен (`:401-407`) — при том что SSE именно им останавливает переподключение: «Clients will +reconnect if the connection is closed; a client can be told to stop reconnecting using the HTTP 204 No +Content response code» (https://html.spec.whatwg.org/multipage/server-sent-events.html). После +завершённого прогона браузер будет переподключаться вечно. +*Поправка судейства, которую принимаю:* движковый `finished` НЕ является пропавшим сигналом — платформа +не считает его авторитетным намеренно (доставка at-least-once, хвост рвётся при крахе; авторитет — код +выхода и реконсилятор). Дефект — в отсутствии дисциплины конца потока, а не в игнорировании `finished`. + +**(в) Разрешение склейки уничтожает единственный кадр, который склеивать нельзя.** «The server MAY +COALESCE frames» (`:1320-1323`) — без ограничения по типу. Шесть кадров из восьми снимки, а `note` +(`:1381-1388`) — добавление: склейка двух `note` теряет замечание навсегда, на живом соединении, без +переподключения и потому без `resync_required`. Это ровно тот исход, которым спека двумя абзацами выше +обосновывает запрет реплея: «a one-shot event such as `note` would otherwise be lost silently» +(`:396-397`, повтор `:1416-1417`). Два правила одного документа исключают друг друга. + +**(г) Докачка не определена.** Спека требует `Last-Event-ID` и одновременно запрещает реплей +(`:386-397`), не говоря, ЧТО именно можно переслать; два конформных сервера поведут себя противоположно. +*Здесь мой исходный вывод «докачка невозможна в принципе» судейство опровергло:* связное чтение есть — +короткий живой буфер после предъявленного id это не «реплей истории». Остаётся реальный дефект: правило +не определено. Плюс форма `id`: спека просит «a monotonic `id`» и говорит, что он несёт ревизию книги, +одна транзакция даёт НЕСКОЛЬКО кадров с одним id (`:1315-1318`), лексическая форма не зафиксирована — а +клиент обязан сравнивать его численно (`:681-684`). Сервер, сделавший id уникальным на кадр (`1841-2`), +не нарушит ни слова прозы и при этом навсегда отключит клиентский гард: `Number('1841-2')` = `NaN` +(`frontend/src/api/stream.ts:131`, `revision.ts:16-20`). + +**Рекомендация — менять спеку, пока канала нет.** Перевесить поток на книгу (`GET /books/{id}/events`), +прогонный оставить узким видом; добавить кадры конца разбора и конца прогона и объявить `204` при +переподключении к завершённому; развести `id` кадра (позиция потока, форма зафиксирована) и ревизию +книги (поле в `data`); выбрать одно правило докачки; разрешить склейку только снимкам и запретить `note` +(или заменить его счётчиком + перечитыванием — половина этого пути уже построена: `note_count` есть на +`Book`, `Chapter` и `EventChapter`). + +**Контраргумент.** «Прогонный поток честнее: события принадлежат прогону» — не выдерживает того, что две +из одиннадцати книжных фаз прогону не принадлежат, а статус их показывает в том же поле. «Никакой +вменяемый реализатор не станет склеивать `note`» — контракт пишется против невменяемого реализатора и +против себя-через-год. + +--- + +### Б-7 (CONFIRMED · HIGH). Структурная эпоха объявлена, но её никто не наблюдает; условных чтений нет + +**Факт.** Спека привязывает курсор к «STRUCTURAL epoch of the collection — the generation of the +manifest, the chunker version» и вменяет серверу MUST-отказ по мёртвому курсору (`openapi.yaml:702-708`). +Слово «epoch» встречается в документе ТОЛЬКО там: ни один ответ эпохи не несёт, `ChapterList` — это +`{revision, next_cursor, chapters}` (`:967-975`). Реализованный курсор (библиотечный) эпохи не содержит +(`pgstore/books.go:821-831`), у таблицы `chapters` такой колонки нет. + +Структура при этом живая: «число глав может измениться против эвристики — недоразмеченное добавится, +ложное удалится» (`docs/research/27-chapter-detection.md:35`, решение владельца 09.08), а id юнита +нестабилен при смене чанкера (`backend/internal/pipeline/manifest.go:102`). + +Вторая половина — цена перечитывания. Условных запросов нет ни одного: `ETag`, `If-None-Match`, `304` не +встречаются ни в спеке, ни в платформе (грепом). Кадр `status` заставляет клиента перечитать юниты ВСЕХ +открытых глав, весь список глав и библиотеку (`frontend/src/showcase/useRunStream.ts:111-122`); зона +замерила фан-аут «20 вкладок = 20 чтений юнитов на кадр» (`frontend/docs/BACKLOG.md:48`). На книге в +2284 главы это полное дерево на каждой границе стадии. + +**Независимая проверка.** Все четыре greenfield-дизайна ввели версию дерева на каждом ответе главы плюс +ленту изменений (`added|removed|moved|split|merged`) и `410 Gone` с наследниками; три из четырёх — +условные чтения. + +**Цена.** Ц2 для версии дерева (колонка + поле). Ц1 для условных чтений: `ETag: W/""` берётся +из уже существующей ревизии. + +**Рекомендация — менять спеку и код.** (а) Публиковать структурную версию в каждом ответе с главами или +юнитами и в `hello`; (б) `410 Gone` на исчезнувшую главу; (в) `If-None-Match`/`304` на дерево, банк и +юниты; (г) ленту изменений структуры — направлением к этапу 161, не в этот батч. + +**Контраргумент.** «`resync_required` уже закрывает смену эпохи» — только для клиента, ДЕРЖАЩЕГО поток; +вернувшийся на вкладку через час получает 200 и молча рисует старое дерево. «`no-store` делает 304 +бессмысленным» — `no-store` запрещает хранение КЭШУ («the no-store response directive indicates that a +cache MUST NOT store any part of either the immediate request or the response», RFC 9111 §5.2.2.5), а +копия живёт в памяти приложения; но раз это неочевидно, в контракте это надо записать, а не +подразумевать. + +--- + +### Б-7а (CONFIRMED · HIGH). Спека утверждает о движке и платформе то, что перестало быть правдой — и одно из утверждений отнимает у клиента работающее лечение + +Контракт содержит четыре предупреждения о недостроенном; три устарели, а одно ещё и предписывает +клиенту НЕ показывать средство, которое существует. + +1. **`:274-277` («⚠ No backing channel exists for this read today. The engine ships a signing table, not + a bank export»)** — неверно с D39.122: движок пишет сайдкар всего банка + (`backend/internal/pipeline/bankexport.go:16-33,72`), и компаньон это уже исправил четырьмя строками + выше собственного противоречащего абзаца (`README.md:267` против `:271-278`). Зона фронта до сих пор + учится по старой версии (Ф-43). +2. **`:1384-1385` (`EventNote` «the engine does not emit per-unit notes mid-run today»)** — эмиттер + приземлился 14.08: `unit_done` несёт `Flagged` и причину (`backend/internal/runevents/runevents.go:126-135`), + платформа их читает и хранит (`platform/internal/pgstore/sink.go:227-233`). +3. **`:1403` (`EventCeiling` «Depends on the event emitter»)** — то же: событие построено + (`runevents.go:151-154`). +4. **`:440-445` (`resume` после стопа по потолку) — самое дорогое.** Спека пишет: «no handle raises it … + The mechanism is the platform's and does not exist yet … so the client MUST NOT offer resume as the + remedy for `paused`». Первая половина верна (resume действительно не двигает такой прогон), вторая — + нет: стоп по потолку ЗАКРЫВАЕТ прогон, `finished_at` пишется тем же оператором + (`platform/internal/pgstore/runs.go:672-676`), `paused` входит в допустимые для старта состояния + (`platform/internal/runs/runs.go:229`), `HasLiveRun` при этом ложь (`runs.go:187`) — то есть + **новый прогон с бо́льшим потолком запускается и является лечением уже сегодня.** Контракт же + оставляет клиента без единого действия: «resume не предлагать», а про «начать заново с бо́льшим + потолком» не сказано ничего. Пользователь видит тупик там, где его нет. + +**Чья боль.** Продукт (тупик на самом частом остановочном состоянии), фронт (учится по устаревшему +тексту), движок (о нём написана неправда в ратифицированном документе), доверие к контракту как +источнику истины. + +**Цена. Ц0** — правки текста; для п.4 ещё одна строка в описании `startRun` («потолок поднимается новым +прогоном»). + +**Рекомендация — менять спеку.** Снять три устаревших предупреждения, п.4 переписать на действующее +лечение. И системно: предупреждения вида «зависит от того, что ещё не построено» обязаны нести ссылку на +строку бэклога — иначе они переживают своё основание и начинают лгать. В компаньоне такая таблица +предлагается в Б-21. + +**Контраргумент.** «Устаревший комментарий — не дефект формы» — для пп.1-3 почти так (цена — обучение +зоны неправде); для п.4 нет: это НОРМАТИВНОЕ «MUST NOT» на основании факта, который неверен. + +--- + +### Б-8 (CONFIRMED · MEDIUM). Единственное состояние, которое ЕСТЬ ошибка, — единственное без причины + +**Факт.** У контракта две машинные оси причин: `reject_reason` при `rejected` (`openapi.yaml:820-831`) и +`paused_reason` при `paused` (`:875-879`). У `failed` — «run aborted by an error» (`:754`) — нет ни поля, +ни кода: `Run.required` (`:860`) его не содержит, `EventStatus` несёт только статус и `paused_reason` +(`:1359-1365`), а `Problem` тут не работает — прогон умирает асинхронно, через часы после своего 202. +Клиент не может отличить временную беду от окончательной и решить, показывать ли повтор; цена ошибки — +оплаченный прогон. + +**Цена. Ц1** — гадать не нужно: у движка исход уже словарь (`clean|flagged|bank_stop|ceiling|stopped|failed`, +`backend/internal/runevents/runevents.go:95-102`) плюс полоса кодов отказа 10–19 (D39.131), у платформы +есть `exit_result` и карантин (`platform/internal/runs/reconcile.go:149,320`). + +**Рекомендация — менять спеку.** `Run.failure_reason` тем же паттерном, что две существующие оси; даже +двух значений (временная / окончательная) достаточно, чтобы решить судьбу кнопки повтора. + +**Контраргумент.** «Не выдумывать таксономию раньше движка» — именно так ждали `RejectReason` до 0.2.3; +но здесь ждать нечего, словарь уже есть, его надо спроецировать. + +--- + +### Б-9 (CONFIRMED · MEDIUM). Замечание — единственная строка документа без идентификатора + +**Факт.** `Note` требует `[severity, message]` (`openapi.yaml:1038`), `chapter_id` и `unit_id` +необязательны — то есть схема допускает замечание, не адресующее ничего, вопреки собственной фразе +операции «A note addresses a unit or a whole chapter» (`:249`). Идентификатора нет ни в списке, ни в +кадре (`:1381-1388`), при том что у `Book`, `Chapter`, `Unit`, `BankTerm` он обязателен. В read-модели +платформы он ЕСТЬ, вместе с `created_at` (`00002_readmodel.sql:134-140`). Клиент ключует строки позицией +в массиве (`frontend/src/showcase/Notes.tsx:41-43`, комментарий это фиксирует), `unit_id` не читает ни +один экран (`Notes.tsx:63-66`), а кадр `note` невозможно сопоставить с прочитанным списком — значит +невозможны ни дедупликация, ни «скрыть замечание», ни устойчивая ссылка. + +**Цена. Ц0/Ц1.** **Рекомендация — менять спеку:** `id` и `created_at` обязательными; адресацию сделать +обязательной (`chapter_id` обязателен, `unit_id` опционален) либо явно описать смысл безадресного +замечания. **Контраргумент.** «Замечание — не сущность, а строка» — не держится, раз оно приходит двумя +каналами и должно сойтись. + +--- + +### Б-10 (CONFIRMED · MEDIUM). `limit` без максимума, и превышение понижает до дефолта, а не подрезает + +**Факт.** `Limit` — `{ type: integer, minimum: 1 }`, без `maximum` (`openapi.yaml:597-605`); ключевое +слово `maximum` в этом же файле используется (`:1268`), то есть это не стиль, а пропуск. Сервер: +`if limit <= 0 || limit > maxPage { limit = defaultPage }` (`pgstore/books.go:509-511`) — `?limit=1001` +отдаёт 100 строк, меньше, чем `?limit=1000`. Индустрия говорит обратное: «If the user specifies +`page_size` greater than the maximum permitted by the API, the API **should** coerce down to the maximum +permitted page size» (https://google.aip.dev/158); GitHub — то же («the value is automatically reduced to +the maximum»). + +**Цена. Ц1.** **Рекомендация — менять спеку и код:** объявить `maximum`, подрезать до максимума. +**Контраргумент.** «Сервер вправе вернуть меньше, клиент решает только по `next_cursor`» (`:602-604`) — +формально да, поэтому это не поломка; но поведение «попросил больше — получил меньше всех» узнаётся +только экспериментом. + +--- + +### Б-11 (CONFIRMED · MEDIUM). Ни одного параметра запроса, кроме `limit` и `cursor` + +**Факт.** На всей поверхности существуют ровно два query-параметра (`openapi.yaml:572-616`): ни поиска, +ни фильтра, ни сортировки. Порядок объявлен только для глав («Chapters in reading order», `:193`) — для +библиотеки, юнитов, замечаний и банка не объявлен нигде, хотя keyset-курсор предполагает стабильный +полный порядок. Цена уже оплачена клиентом: поиск по банку идёт в браузере по всем строкам +(`frontend/src/showcase/Bank.tsx:89`), палитра перехода ищет в памяти и режет до 50 +(`showcase/Goto.tsx:25-30`), а зона записала «поиска по книге нет ни в одной из пятнадцати ручек» +(`frontend/docs/BACKLOG.md:39`). Отдельно: `BankDecisionsRequest.decisions` — единственный +неограниченный массив в документе, который всё остальное ограничивает (`:1173-1178`). + +*Поправка судейства, которую принимаю:* «нет тоталов» — неверно; `Bank.total`/`signed` есть (`:1134-1141`), +а размеры прочих коллекций живут на родителе (`Book.chapter_count`, `note_count`, +`Chapter.units_total`) — это согласованное решение, а не пропуск. + +**Цена.** Ц1 для порядка и `maxItems`; Ц2 для фильтра и поиска (индексы). +**Рекомендация.** Объявить порядок каждой коллекции и `maxItems` у `decisions` — в 0.3.0; фильтр по +статусу для библиотеки и поиск по банку/дереву — направлением к S5/S6, не в батч. +**Контраргумент.** «Клиент виртуализует, ему годится любой размер» (К-7) — верно для отрисовки и неверно +для сети: сегодня клиент выкачивает коллекцию целиком именно потому, что иначе не может ни искать, ни +считать. + +--- + +### Б-11а (CONFIRMED · MEDIUM). Кадр без данных заставляет перечитать список целиком — и это единственный настоящий узкий проход + +**Факт.** Кадры `note` и `bank` несут полезную нагрузку, которую клиент применить не может (у замечания +нет id — Б-9; у банка кадр несёт три счётчика, а строки нет), поэтому оба ведут к перечитыванию всего +списка (`frontend/src/showcase/useRunStream.ts:143-149`). Банковский экран смонтирован всегда +(`Showcase.tsx:45-59`). Арифметика на настоящей книге: если эмиттер даст кадр банка на главу, это +**2283 × 167 КБ ≈ 372 МБ за прогон**; на термин (1200 строк) — 195 МБ. Гасителя нет ни одного: ни +условных чтений, ни дельт, ни сжатия. Единственная защита — необязательное «сервер МОЖЕТ склеивать» +(`openapi.yaml:1320-1323`), то есть надежда, а не механизм. + +Рядом, по убыванию: +- **дерево 2283 глав целиком на каждом `status`** (250 КБ, под gzip было бы 39) — ради счётчиков, + которые кадр `chapter` уже принёс; это цена открытого К-10; +- **`refetchOnWindowFocus` при `staleTime: 15 c`** (`frontend/src/main.tsx:22-24`): возврат в окно + перечитывает всё активное — 12 вкладок = 768 КБ, и это десятки раз в час, тогда как `status` — 4-6 + раз за прогон. **Самый частый трафик порождает не пайплайн, а переключение вкладок браузера.** + +**Где узкого места НЕТ** (это тоже вывод): экран чтения дёшев (26 КБ на главу), книга целиком по сети +не ходит, дерево виртуализовано (31 строка в DOM на 2284), прогресс — патч без чтений. + +**Чья боль.** Платформа (её нагрузка), пользователь на мобильном канале, деньги на трафик. +**Цена.** Ц0 для правил в контракте; сжатие — конфигурация деплоя; условные чтения — Ц1 (ETag берётся +из уже существующей ревизии). +**Рекомендация.** (а) Обязать сжатие на текстовых ответах и записать это в контракт, а не в зонный док; +(б) условные чтения на дерево, банк, юниты (Б-7); (в) заменить «сервер МОЖЕТ склеивать» на правило +«кадр несёт либо применимую дельту, либо счётчик, но не заставляет перечитывать коллекцию»; +(г) фронту — снять `refetchOnWindowFocus` для тяжёлых коллекций (зона фронта, не контракт). +**Контраргумент.** «Пока эмиттер банка не построен, 372 МБ — арифметика на бумаге». Верно — и потому это +правится сейчас за Ц0, а не после того, как эмиттер даст первый прогон. + +--- + +### Б-12 (CONFIRMED · MEDIUM). Агрегаты всей коллекции лежат внутри страничного конверта + +**Факт.** `Bank` обязывает нести `total` и `signed` «в целом по банку, не по странице» на КАЖДОЙ +странице, при этом у каждой страницы своя `revision` (`openapi.yaml:1128-1141`), и правила, какая +страница главная, нет. Референсный клиент неизбежно смешивает два момента времени: счётчики берутся с +последней страницы (`frontend/src/api/queries.ts:144-148`), а ревизия — минимум по страницам, то есть с +первой (`frontend/src/api/client.ts:99-106`). Обе половины написаны осознанно и обе правильны по +отдельности. + +**Цена. Ц0** (чтение не построено). **Рекомендация — менять спеку:** либо агрегаты только на первой +странице (и правило «клиент берёт их оттуда»), либо ревизия конверта общая для всего обхода. +**Контраргумент.** «Счётчики на каждой странице удобны» — удобны, но без правила согласования они дают +неопределённость, которую клиент вынужден разрешать сам, и он уже разрешил её двумя разными способами. + +--- + +### Б-13 (CONFIRMED · MEDIUM). Один enum на две машины состояний; мёртвое значение в словаре + +**Факт.** `Run.status` типизирован как `BookStatus` (`openapi.yaml:864`) — 11 значений, из которых на +прогоне легальны 7, и спека сама это говорит прозой (`:857-859`). В DDL платформы узкий словарь уже +записан: `check (status in ('translating','awaiting_bank','finalizing','ready','stopped','failed','paused'))` +(`00002_readmodel.sql:47-49`) — то есть контракт отстаёт от собственного сервера, а генерённый union +шире правды. `BookDetail` отдаёт оба статуса без правила старшинства, и фронт уже разошёлся: полоса +состояния решает «paused» по прогону (`showcase/Status.tsx:21`), карточка — по книге. +`finalizing` при этом не пишет никто: у движка такой фазы нет вовсе (`grep -ri finaliz backend/internal` +— пусто), в словаре ingest её нет (`platform/internal/ingest/events.go:53-67`), в платформе значение +живёт только в валидаторах. + +**Цена. Ц0/Ц1.** **Рекомендация — менять спеку:** отдельный `RunStatus` (7 значений); правило +старшинства «книга производна от прогона, кроме `uploading`/`parsing`/`not_started`/`rejected`»; +`finalizing` — **снять** (решение опирается на слово владельца 16.08: лестница «загрузка → разбор → +перевод → подпись банка → финал → готово» была его устной формулировкой, а не нормой продукта, поэтому +статус ею не связан; под 0.x значение возвращается минором так же дёшево, если фаза когда-нибудь +появится у движка). +**Контраргумент.** «Переиспользование словаря экономит клиенту карту статус→вид» — экономия мнимая: +карта всё равно одна, а лишние значения обязывают писать недостижимые ветки. + +--- + +### Б-13а (CONFIRMED · MEDIUM). Пользователь покупает главы, а видит долю от всей книги + +**Факт.** Потолок объявлен в ГЛАВАХ и принадлежит прогону (`openapi.yaml:868,1202`). Прогресс — в +ЮНИТАХ и по всей книге: `Progress.draft/edit` проецируются из книжных счётчиков +(`platform/internal/httpapi/v0.go:473-477`), а движок считает их по книге сознательно — «**The counters +are BOOK state, not process state**: a resumed run re-walks every finished unit, and a counter that +started at zero would walk a reader's progress bar backwards» +(`backend/internal/pipeline/events.go:337-339`), и знаменатель засевается из всей нарезки книги +(`events.go:295-324`). Значит прогон, +купленный на 10 глав из 2284, показывает дробь, которая не может дойти до единицы, а затем книга уходит +в `paused` — и ни одно поле на проводе не переводит потолок в ось прогресса. Величины «глав сделано» и +«глав осталось» на поверхности нет вообще (проверено перечислением всех «главных» полей: `chapter_count`, +`ceiling_chapters`, `min/max/default_chapters`, `since/until_chapter`, `chapter_id` — и всё), при том что +платформа её уже считает в SQL для денежной шкалы (`platform/internal/pgstore/books.go:677-679`) и не +отдаёт. + +**Чья боль.** Продукт (полоса прогресса врёт на каждом прогоне с потолком — а с потолком идут ВСЕ +прогоны, поле обязательное), фронт (ему нечем построить честный индикатор), поддержка. + +**Цена. Ц1** — величина существует в SQL; нужен знаменатель прогона (или пара «глав куплено / глав +сделано») в `Run`. + +**Рекомендация — менять спеку и код:** добавить `Run.chapters_done` (и/или `chapters_left` на книге), +либо явно записать, что `Progress` — по книге, а не по прогону, и клиент обязан рисовать это как +книжную полосу с отдельной отметкой купленного объёма. + +**Контраргумент (сильный, судейство его выдвинуло).** Единицы разошлись не по недосмотру: главы у +потолка — потому что доллары на провод не идут (D39.110), юниты у прогресса — потому что сквозной +счётчик по главам читал бы ноль всю черновую волну (`:729-731`). Оба довода в силе, и я не предлагаю +менять единицы. Дефект в другом: у прогона нет СВОЕГО знаменателя, и потому дробь на экране означает +не то, что человек купил. + +--- + +### Б-14 (CONFIRMED · MEDIUM). «Необязательное» в этом документе значит четыре разных вещи + +Кандидат был шире; судейство сняло три пары как ЗАЩИЩЁННЫЕ спекой (см. §4 №22-24) и оставило те, где +обоснования нет нигде: + +| Место | Что не сходится | +|---|---| +| `Usage.paused_reason` **необязателен** (`:1256` не включает его, поле `:1270-1276`), а ТО ЖЕ имя и тип **обязательны** на `Run` (`:860`) и на `EventStatus` (`:1359`) | Одно поле, две разные обязательности, обоснования нет нигде. Клиент уже платит: `frontend/src/api/contract.ts:97` — одна ветка, `:124-127` — две; платформа при этом всегда шлёт (`v0.go:161,439-444`) | +| Внутри ОДНОЙ схемы: `Run.finished_at` optional+nullable (`:881-883`), `Run.paused_reason` required+nullable (`:860`) | Клиент записал цену в комментарий: «`finished_at` приходит и как `null`, и вовсе отсутствующим … обе формы значат одно и то же» (`frontend/src/showcase/About.tsx:109-111`) | +| `Book.genre` и `character_count` необязательны (`:812-817`), а сервер шлёт их ВСЕГДА (`v0.go:104-110`, без `omitempty`) | Опциональность фиктивна, а смысл СЕНТИНЕЛА не определён: `genre` пуст, когда пользователь его не указал, и экран рисует `book.genre ?? '—'` (`About.tsx:92`) → пустая ячейка вместо прочерка. `character_count: 0` в интейке значит «ещё не знаем», а выглядит как ноль | +| `eta_seconds`: проза предписывает ОДНО кодирование («absent», `:743`), схема разрешает ДВА (`[integer,'null']` + не в required) | Каждый потребитель носит лишнюю ветку: `format.ts:114-115` (`number \| null \| undefined`), `frontend/src/mock/live.ts:8` (`?? null`), генерённый тип — три состояния для одного факта | +| На сегодняшнем проводе «необязательное» значит «всегда есть, бывает `null`» у шести полей и «не приходит никогда» у двух — `Problem.instance` (`:1444`) и `Book.reject_reason` (`:820`) | Клиент не может отличить, какое отсутствие значимо | +| Имя конверта коллекции: `Library` · `ChapterList`/`UnitList`/`NoteList` · `Bank` (`:835,967,1014,1050,1128`) | Один паттерн (revision + next_cursor + строки), три стиля имени; `operationId` при этом единообразны | +| `total` значит «юнитов в волне» в `Counter` (`:724`) и «строк во всём банке» в `Bank`/`EventBank` (`:1134,:1394`) | Одно слово, два референта; у полей `EventBank` описаний нет вовсе | + +**Плюс самопротиворечие документа.** `TermStatus` формулирует принцип: «A boolean `signed` would merge +"proposed by the engine, nobody looked" with "a human started and did not finish"» (`:1070-1072`) — и тот +же документ дважды поставляет ровно такую форму: `Export.ready` (сливает «идёт», «упало», «истекло», +Б-4) и `EventCeiling.halted` (обязательный булев, всегда `true`). + +Для `Unit.target` правка бесплатна и не спорна: в БД уже стоят чеки `translated ⇒ target≠''` и +`не translated ⇒ target=''` (`00002_readmodel.sql:127-128`), то есть поле детерминировано; форма — +не «как у `sense`», а условная обязательность по `state`, как уже сделано у `BankDecision.dst` +(`:1153-1160`), с оговоркой К-11: генератор `if/then` игнорирует, значит условие защищает сервер, а +клиента приходится сужать на шве. + +**Цена. Ц0/Ц1.** **Рекомендация — менять спеку:** записать ОДНО правило кодирования отсутствия и +применить его к `Usage.paused_reason`, `finished_at`, `character_count`, `eta_seconds` (`genre` из +списка выпадает — поле уходит совсем, Б-23); +`Unit.target` — условной обязательностью; имена конвертов и второй смысл `total` — привести. +**Контраргумент.** «Каждое место обосновано отдельно» — для трёх пар это правда (§4 №22-24), и я их +снял; для перечисленных выше обоснования нет ни в спеке, ни в компаньоне, а цена уже уплачена +ветками в клиенте. + +--- + +### Б-14а (CONFIRMED · MEDIUM). У стопа подписи нет снимка: `complete` живёт только в квитанции POST + +**Факт.** `complete` — поле, по которому решается, можно ли предлагать «продолжить»: `resumeRun` +«**Answers 409 while the set of bank decisions is incomplete**» (`openapi.yaml:447`). Существует оно +только в ответе на `POST /bank/decisions` (`:1180-1193`) и, косвенно, в кадре `EventBank` (`:1390-1396`). +Чтение `GET /books/{id}/bank` (`:1128-1144`) не несёт ни `complete`, ни `pending_decisions`. Экран, +перезагруженный посреди стопа `awaiting_bank`, может прочитать ВЕСЬ банк и всё равно не узнать, сколько +решений осталось и можно ли продолжать; единственная реализация выводит признак как `left === 0` +(`frontend/src/mock/handlers.ts:203-204`) — инвариант, которого контракт нигде не объявляет. Кадр как +второй носитель не помогает: он не построен (и по компаньону §3 движок таких событий mid-run не шлёт). + +**Чья боль.** Продукт (экран подписи S5 — центральный сценарий), фронт, платформа. +**Цена. Ц0** (чтение не построено). +**Рекомендация — менять спеку:** перенести `pending_decisions` и `complete` в ответ `GET /bank` (или +завести `GET /bank/signing` — маленький снимок стопа), а квитанцию POST оставить его же формой. +**Контраргумент.** «Клиент только что отправил решения и знает остаток» — верно ровно до перезагрузки +вкладки, а стоп по построению длинный (сотни терминов, «a closed tab must not cost an hour of work», +`:303-305`). + +--- + +### Б-15 (CONFIRMED · MEDIUM). Требование CSRF невидимо для инструментов, а его отказ — не легальный ответ + +**Факт.** Правило «на каждом небезопасном запросе — `X-TM-Client`» живёт в описании схемы безопасности +(`openapi.yaml:555-563`): генератор его не создаёт, spectral не проверяет, контрактный тест не ловит. +Клиент носит его двумя копиями руками (`frontend/src/api/client.ts:13,40` и `frontend/src/api/upload.ts:81`). +Отказ — 403 (`server.go:99`), которого в контракте нет, и он же отвечает на честный same-origin запрос +без заголовка, сообщая «Cross-origin request rejected» (`auth/csrf.go:38`). CSRF-слой стоит ПЕРЕД +аутентификацией (`server.go:109-110`), так что для небезопасных запросов правило «401 раньше 404» не +работает. + +Сама защита держится не на свойстве заголовка (значение фиксировано и публично), а на факте из другого +раздела спеки — «CORS-слоя нет вовсе» (`:30-35`); ссылки между этими местами нет, и ничто не сломается +громко, если инвариант перестанет держаться. + +Ещё три места того же класса — правило, читаемое только человеком: + +- **Множество «небезопасных» методов расходится с кодом.** Спека: «anything other than GET and HEAD» + (`:558`). Код освобождает и `OPTIONS` (`platform/internal/auth/csrf.go:51-54`), что совпадает с + RFC 9110 («the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe») и не совпадает со + спекой. Клиент следует СПЕКЕ, не коду. +- **401 идёт без `WWW-Authenticate`:** «The server generating a 401 response **MUST** send a + WWW-Authenticate header field (Section 11.6.1) containing at least one challenge applicable to the + target resource» (RFC 9110 §15.5.2, https://www.rfc-editor.org/rfc/rfc9110.txt). Ни `WriteProblem` + (`platform/internal/httpapi/problem.go:22-28`), ни Deny-обработчик (`platform/cmd/tmplatformd/main.go:85`) + его не ставят, ни компонент `Unauthorized` (`openapi.yaml:624-628`) его не объявляет — при том что + в том же файле `headers:` объявлены дважды (`Location` на 202, `Retry-After` на 200). +- **`servers[0].url` — абсолютный плейсхолдер** `https://app.example.org/v0` (`:63-68`), тогда как + проза рядом требует обратного («a client that hard-codes one is a client that cannot be deployed + anywhere else»). Сгенерированный клиент целится в `app.example.org`; написанный руками вынужден + игнорировать объект `servers` (`frontend/src/api/client.ts:1-9`). Лечится относительным `url: /v0`. + +**Отдельно — заявленный, но не выдаваемый bearer.** Схема `bearerToken` объявлена на каждой операции +(`:564-570`) как равноправное удостоверение, а выдать его нечем: путь входа только ставит HttpOnly-куку +(`platform/internal/login/login.go:338`, `auth/cookie.go:72`), эндпойнта выпуска токена/PAT нет ни в +платформе, ни в контракте. То есть «портируемость к десктопу» сегодня не имеет носителя вовсе. + +**Цена. Ц1** (объявить заголовок машинно; отдавать `WWW-Authenticate`; развести две причины 403; +поправить множество методов и `servers.url`). +**Рекомендация — менять спеку и код**, плюс записать в спеке связь «защита работает, ПОКА нет CORS» — +чтобы её снятие требовало правки контракта, а не конфига; и назвать, чем выдаётся bearer, либо снять +схему до появления носителя. +**Контраргумент.** «Заголовок-маркер — известная рабочая защита» — да, и она остаётся; речь о том, что +её нет там, где проверяют машины. «403 такого класса не документируют» (Zalando: «403 … do not +document») — верно для авторизационного 403; здесь 403 отвечает на нарушение правила, которое ИЗОБРЁЛ +сам этот документ, поэтому минимум — одна фраза в описании схемы безопасности. + +--- + +### Б-16 (CONFIRMED · LOW). Требование HTTP/2 — ровно то, что стандарт советует не делать + +**Факт.** Спека предписывает «Required: HTTP/2 at the edge» (`openapi.yaml:386`). RFC 9205 §4.1: +«Requiring a particular version of HTTP makes it difficult to use in these situations and harms +interoperability. Therefore, it is **NOT RECOMMENDED** that applications using HTTP specify a minimum +version of HTTP to be used. However, if an application's deployment benefits from the use of a particular +version of HTTP (for example, HTTP/2's multiplexing), **this ought be noted**» +(https://www.rfc-editor.org/rfc/rfc9205.txt). Стандарт называет наш же случай и говорит: отметить, а не +потребовать. Рядом — `X-Accel-Buffering: no` (вендорный заголовок конкретного прокси в норме контракта) и +heartbeat без описания формы кадра (браузерный `EventSource` SSE-комментарии не наблюдает вовсе, так что +для ратифицированного клиента heartbeat невидим). + +**Цена. Ц0.** **Рекомендация — менять спеку:** требования деплоя — в примечание; форму heartbeat +описать. **Контраргумент.** «Без HTTP/2 шесть соединений на origin убивают вкладки» — верно как ПРИЧИНА +заметки; норма из этого не следует: на HTTP/1.1 сервис обязан работать хуже, а не не работать. + +--- + +### Б-17 (CONFIRMED · LOW). `TermOrigin` протаскивает через шов ровно тот словарь, который шов запрещает + +**Факт.** `TermOrigin: [seed, ruby, mined]` (`openapi.yaml:1075-1078`). `ruby` — японская типографская +фуригана, то есть паро-специфика; `mined` — имя стадии конвейера. Шапка того же документа объявляет: +«Pipeline vocabulary does not cross this boundary: no model names, no stage names» (`:20-21`), а канон +проекта — «книжный термин в общем пар-слое = утечка» (`CLAUDE.md`, гардрейлы). Клиент обязан нарисовать +слово для `ruby` в паре, где рубя не существует. + +**Цена. Ц1** (в БД `origin` — колонка с чеком, `00002_readmodel.sql:158`). +**Рекомендация — менять спеку:** назвать провенанс продуктово, движковые слова оставить движку. +**Контраргумент.** «Провенанс нужен подписывающему, чтобы понимать доверие к строке» — верно и +сохраняется: меняются слова, а не различение. + +--- + +### Б-18 (CONFIRMED · LOW). Спойлерное окно термина стоит на числе, которое сам контракт называет не-ключом + +**Факт.** `since_chapter`/`until_chapter` — целые, `0` значит и «с начала книги», и «без конца» +(`openapi.yaml:1116-1126`), и оба входят в ключ уникальности термина. Но `Chapter.number` — «**Not a +key:** numbering is dense … editing the source shifts every later chapter» и может быть `null` для книги +без нумерации (`:932-939`); плотность подтверждена движком (`backend/internal/chunk/chunker.go:132`). +Окно термина привязано к величине, которая сдвигается при пере-разборе и для легальной книги не +существует. + +**Цена. Ц1-Ц2.** **Рекомендация — менять спеку:** либо перевести окно на `chapter_id`, либо записать +явно, что окно живёт в системе координат нарезки и пересчитывается при смене эпохи, и развести два +смысла нуля. **Контраргумент.** «Так устроено у движка» — это происхождение, а не обоснование: движок +живёт внутри одной нарезки, клиент — поверх нескольких. + +--- + +### Б-19 (PLAUSIBLE · HIGH для планов). Глава контракта не выдерживает структуру глав, которую владелец поставил в очередь + +**Факт.** `Chapter` — `{id, number, heading, units_total, units_done, note_count}` (`openapi.yaml:926-965`). +Решение владельца 09.08 (`docs/research/27-chapter-detection.md:33-36,51,64,70`) требует большего: + +1. **Две метки.** «ОРИГИНАЛЬНЫЕ названия из данных книги + служебный рендер „Глава N“ ($0, локаль + интерфейса)». Контракт несёт одно поле и ЗАПРЕЩАЕТ клиенту синтезировать метку: «A client MUST NOT + synthesize a label from a template such as "Chapter {n}"» (`:944-948`). А служебный рендер обязан быть + клиентским — он в локали ИНТЕРФЕЙСА, которой сервер не знает (Accept-Language нет, Б-1). Два + ратифицированных решения смотрят в разные стороны. +2. **Тип узла** (глава / технический «фрагмент») — нет. +3. **Вердикт детекции** («структура не распознана») — нет. +4. **Живая структура** в черновой волне — наблюдать нечем (Б-7). +5. **Две фазы названий** (провизорный перевод на старте, ревизия после подписи банка) — метка легально + меняется дважды за прогон; правила «когда метке верить» нет. +6. **Ручная правка структуры** — записи нет (Б-5), а цена ошибки денежная: «Money is keyed by POSITIONS … + shifting ONE chapter boundary renumbers the tail and misses all its checkpoints» (`27:70`). + +Сегодняшний единственный производитель метки даёт не метку книги: `heading` манифеста — рендер по +правилу ЛАНГПАКА (для zh→ru это «Глава N»), пустой, когда у пары правила нет или у главы нет +структурного заголовка, и комментарий движка предупреждает «a consumer must not present it as one» +(`backend/internal/pipeline/manifest.go:80-86`). Настоящие названия — строка 160, в очереди D39.136 п.3. + +**Почему PLAUSIBLE, а не CONFIRMED.** Спека сама объявляет этот разрыв четырьмя строками ниже правила +(`:954-956`, форвард на К-2), а рабочие строки 160/161 уже перечисляют `title_raw`, тип +`chapter/fragment` и вердикт структуры и прямо резервируют «аддитивное расширение контракта 14» +(`docs/PROGRESS.md:107-108`). То есть находка — не «никто не заметил», а **тайминг и одно конкретное +противоречие**: пункт 1 выше (запрет на клиентскую служебную метку против решения владельца 09.08) +существует уже сегодня и требует правки независимо от этапа. + +**Цена. Ц0 сегодня** (чтение глав не построено) и резко дороже после P7. +**Рекомендация — менять спеку:** снять запрет на клиентскую служебную метку СЕЙЧАС (он противоречит +ратифицированному решению 09.08); `title_raw`, `kind`, структурную версию — заложить формой в 0.3.0 либо +явно передать дизайн-паку этапа 161 с записью «контракт 14 расширяется аддитивно этим паком». +**Контраргумент.** «Пока движок не отдаёт настоящих названий, поле будет пустым» — верно и нормально: +nullable-поле с честным `null` дешевле, чем смена смысла существующего `heading` после беты; смена +смысла — это мажор. + +--- + +### Б-19а (CONFIRMED · HIGH для планов). «Добавить главу в существующую книгу»: механизма нет, но экономика уже позволяет — если дописывать В КОНЕЦ + +Переписано 16.08 после разбора кода по требованию владельца («в бэкенде должен быть механизм… разберись +сначала в этом; костылей не будет»). Прежняя редакция этого буллета предлагала отдельную книгу-продолжение +— **снято владельцем и мной как неверное**. + +**Явного механизма нет нигде:** ни команды `tmctl`, ни флага, ни ручки платформы, ни ручки контракта +(грепом по `backend/**/*.go` на «append/add-chapters/source grew» — ноль). + +**Но экономика уже почти правильная**, потому что оплаченная работа адресуется ПОЗИЦИЕЙ: +- ключ вызова сворачивает `BookID, Chapter, ChunkIdx, Attempt, Stage, Role, Model, …, SnapshotID` + (`backend/internal/pipeline/render.go:306-309`), строка резолва — + `PRIMARY KEY (book_id, chapter, chunk_idx, stage)` (`backend/internal/store/migrate.go:126`); + контент-хеш — только guard быстрого пути, не ключ поиска; +- исходник не входит НИ В ОДИН снапшот (`backend/internal/pipeline/snapshot.go:426-450`); +- нарезка идёт ПО ГЛАВАМ, `ChunkIdx` рестартует внутри главы (`backend/internal/chunk/chunker.go:124-140`). + +**Следствия, и они противоположные:** +- **дописать В КОНЕЦ** — номера и индексы старых глав не двигаются, снапшот не двигается → хвост + резюмится за **$0**, платятся только новые главы; якоря читателя выживают, потому что `chapter.ID` — + хеш ТЕКСТА главы, а `unit.ID` производен от него (`backend/internal/pipeline/manifest.go:128-176`); + манифест признаётся протухшим и пересобирается $0-командой; +- **вставить в НАЧАЛО или СЕРЕДИНУ** — перенумеровывается весь хвост → промахиваются и ключ вызова, и + позиционная строка → **обе волны хвоста покупаются заново, и МОЛЧА**: консент-гейт сравнивает только + снапшоты и сам признаёт слепоту к позиционной оси (`backend/internal/pipeline/rebill.go:32-35,144-146,237-239`). + Единственный тормоз — потолок книги. + +**Цена, с числами** (exp08 v2 + поправка D30.4, книга 500 глав): дописать главу в конец — **$0,022-0,027**; +вставить главу в середину — **$8,8-11,0**. Разница ~400×. + +**Чего не хватает:** +1. **Движок.** Его собственная спека D15.2 требует, чтобы content-addressed reuse (`tm-request-v3` + + `guard_hash`) приземлился ДО режима дописывания; не построено — в коде до сих пор `tm-request-v2`. + Минимум на сейчас: гейт, который ловит позиционный сдвиг и требует явного согласия вместо молчания. +2. **Платформа.** Принять второй файл не может: `Accept` всегда минтит новый `book_id` и создаёт + директорию с `O_EXCL`; пере-разбора нет вовсе («there is no re-parse and no un-reject», + `platform/internal/books/parse.go:109`). +3. **Контракт.** Ручки нет, но ЯЗЫК для неё уже есть: курсор привязан к «поколению манифеста», полная + замена глав обязана отвечать `resync_required`. +4. **Мина для ja:** новая глава может принести новое ruby-чтение → алиас у сид-термина → `memory_version` + двинется → снапшот двинется → прогон упадёт без `--resnapshot`. + +**Рекомендация (в бэклог, исполнение — оркестратору):** форма `POST /books/{bookId}/parts` — дописать +файл к существующей книге; ответ до подтверждения называет, сколько глав добавится, сдвинулись ли +существующие и во что это обойдётся. Стройка — вместе с этапом структуры глав (строки 160-162): там уже +запланированы миграционные карты якорей `same/moved/split/merged/gone/new`. +**Контраргумент.** «Дописывание в конец работает и так, ручка не нужна» — не держится дважды: платформа +второй файл принять не может в принципе, а вставка не в конец сегодня перекупает книгу без вопроса. + +--- + +### Б-20 (PLAUSIBLE · MEDIUM для планов). Одна схема `Id` покрывает пять режимов стабильности + +**Факт.** Одно предложение — «Stability across runs is the platform's job when it persists the manifest» +(`openapi.yaml:663-668`) — распространяется на книгу, главу, юнит, прогон и экспорт, у которых +стабильность разная по природе. Для юнита обещание заведомо сильнее возможного: движковый id несёт +cut-tag, и «any chunker/budget/pipeline-shape change mints a new id for EVERY unit in the book, while +chapter ids survive» (`backend/internal/pipeline/manifest.go:102`). + +**Почему PLAUSIBLE.** Судейство справедливо отделило слои: манифестный id — ПРИВАТНЫЙ движковый, а +проводной `Unit.id` минтит платформа, и её политика ещё не написана — таблица `units` в проде не +заполняется никем (`platform/internal/ingest/manifest.go:12-16` прямо говорит, что читателя пер-юнитных +массивов на этой стороне нет). То есть обещание пока не нарушено — оно необеспечено. + +**Цена. Ц0** (правка формулировки) + зависимость строки 100. +**Рекомендация — менять спеку:** развести классы стабильности прямо в описании `Id` — `Chapter.id` +стабилен через пере-разбор, `Unit.id` стабилен только внутри структурной эпохи, и при её смене якоря на +юниты недействительны (а эпоху клиент видит — Б-7). Заодно: единственная выход-дверь контракта на случай +пере-нарезки — `resync_required` — существует ТОЛЬКО как кадр SSE; у не-стримящего клиента (и у всей +REST-поверхности) эквивалента нет. +**Контраргумент.** «Это работа строки 100» — работа её, но обещание даёт контракт, и клиент строит на +обещании. + +--- + +### Б-21 (PLAUSIBLE · MEDIUM). Карта «чтение → источник» нигде не ведётся, и поэтому устаревает + +Три чтения из шестнадцати операций не имеют полного источника, и в каждом случае причина ДРУГАЯ, чем написано в +контракте: +- **`GET /bank`** — движковая половина построена (`bankexport.go`), не хватает проекции платформы + (вход P7, строка 169). Текст спеки об этом устарел — Б-7а п.1. +- **`GET /notes`** — канал ЕСТЬ: `unit_done` несёт `flagged` и причину движка + (`backend/internal/runevents/runevents.go:126-135`), платформа их уже хранит + (`platform/internal/pgstore/sink.go:227-233`), и колонка `notes.reason` заведена именно под это + («The ENGINE's flag reason, stored and never projected. The product phrase … applied at read time from + the contract's map», `00002_readmodel.sql:139-142`). Не хватает ДВУХ вещей: карты «причина → фраза» + (приложение А компаньона — до сих пор ЗАГОТОВКА, `README.md:389-412`) и проекции. Это НЕ строка 49 + (annot-v1 — поверхность спанов для читалки, другая работа); буллет, поданный с неверной причиной, + заказал бы не ту работу. +- **`POST /exports`** — у движка только stdout-JSON и `--plaintext` (`backend/cmd/tmctl/invocation.go:107`), + носитель — строка 49/D29.1 «tmctl export-контракт». + +**Рекомендация — оставить форму как есть и завести дисциплину:** таблица «чтение → источник → строка +бэклога» в компаньоне, и правило «предупреждение о недостроенном обязано нести номер строки». Без неё +такие абзацы переживают своё основание — что уже произошло четырежды (Б-7а). +**Контраргумент к «оставить».** Можно было бы снять эти операции из контракта до появления источника — +не рекомендую: они уже сгенерированы в типы клиента и служат картой работ; вред от устаревшего +ОБОСНОВАНИЯ лечится дисциплиной, а не удалением операции. + +--- + +### Б-22 (PLAUSIBLE · LOW). Имя файла — несущая конструкция, о которой контракт молчит + +`file` описан как «Book file. The LAST part of the form» (`openapi.yaml:918-921`). Между тем +клиентское имя файла делает две вещи: становится названием книги (`platform/internal/books/books.go:141`) +и выбирает читателя движка по расширению (`:371-391`). Контракт не говорит ни того, ни другого, и клиент +вынужден ПРЕДСКАЗЫВАТЬ серверное правило названия, чтобы показать его в форме +(`frontend/src/showcase/AddBook.tsx:300`). **Рекомендация — менять спеку:** записать роль имени файла; +после введения `PATCH` (Б-5) первая половина перестаёт быть необратимой. + +--- + +### Б-23 (РЕШЕНО ВЛАДЕЛЬЦЕМ 16.08 · MEDIUM). Жанр книги — выкинуть + +**Что жанр делает сегодня.** Ни на чём не ветвится: во всём Go два функциональных места — +подстановка в шаблон (`backend/internal/pipeline/render.go:185`) и поле конфига +(`backend/internal/config/book.go:28`). Ни маршрутизации, ни чекеров, ни экранов, ни langpack, ни +майнера. В промпты попадает одной фразой шапки — «Жанр книги: {{genre}}. Аудитория: {{audience}}. +Книга: «{{title}}».» — в шести ролях из семи (`backend/prompts/zh-ru/*.md`); судья жанр не получает. +Валидации нет: поле необязательное, и при пустом значении в промпт уезжает «Жанр книги: . Аудитория: …». +Замера влияния нет ни одного. Жанровый СЛОВАРЬ (другая сущность) уже отменён владельцем как класс +26.07 — D39.47, код размотан. + +**Решение владельца 16.08: выкинуть.** Записано как его слово; планирование — за оркестратором. + +**⚠ Цена, которую надо знать до планирования (не возражение, а факт).** `genre` входит в `BriefHash` +(`backend/internal/config/book.go:279-309`), а тот сворачивается в оба снапшота волн и в каждый +`RequestHash`; изменение канона брифа = «the whole book is re-billed on the next resume» (`:284-290`). +Значит удаление поля из брифа — **перекупка всех существующих платных книг**, и дешёвое окно то же +самое, что у структуры глав: пока платная книга одна. При этом правка жанра НА ПЛАТФОРМЕ денег не стоит +вовсе: `book.yaml` пишется ровно один раз и никогда не перезаписывается +(`platform/internal/books/render.go:71-77`). + +**Форма удаления (предложение к планированию):** снять `{{genre}}` из шести промптов и поле из канона +брифа одним касанием вместе с этапом структуры глав (общее resnapshot-окно) · убрать `genre` из +`book.yaml`-рендера платформы · убрать поле из `BookIntake` и `Book` в контракте (Ц0/Ц1: поле никем не +читается, кроме карточки) · снять поле из формы интейка (зона фронта, разморозка). +**Контраргумент, который я рассмотрел:** «одна строка контекста может помогать модели держать регистр». +Возможно — но это ровно то, что не измерено ни разу за всё время, а цена наличия поля уже уплачена +(оно в `BriefHash`, то есть в стоимости пере-покупок). Решение владельца снимает вопрос; если позже +захочется вернуть — это новый вход с замером, а не восстановление статус-кво. + +--- + +## 3. Что ревью ПОДТВЕРДИЛО + +Четыре greenfield-дизайна, спеки не видевшие, независимо пришли к тем же решениям. Батчу их не трогать: + +1. **Прогресс — абсолютными снимками, пофазно, ETA nullable** (все четверо; дельты небезопасны при + at-least-once, счётчик легально идёт вниз). Наши `Counter`/`Progress`/`eta_seconds` совпали до поля. +2. **Денег на проводе нет ни в каком виде** — все четверо дошли до более жёсткой формулировки, чем наша + («поле не должно существовать в схеме»), и у нас именно так. +3. **Курсорная пагинация с непрозрачным курсором и `next_cursor` на каждом ответе.** +4. **Непрозрачные id; порядковый номер — только для показа.** +5. **Продуктовый словарь статусов вместо стадий конвейера.** +6. **Подпись банка как накапливаемый НАБОР РЕШЕНИЙ, а не правка строк** — по той же причине (банк + пересобирается). Уточнение в нашу пользу: id термина у движка уже контентный (SHA-256 от + `src+sense+since+until`, `bankexport.go:52`), то есть решение переживает пересборку; контракт этого + не требует — стоит записать. +7. **`verify_bank` и `ceiling_chapters` как параметры ПРОГОНА, не книги.** +8. **Отдельная ручка границ шкалы перед стартом** (у одного дизайна ещё quote-token; у нас это лечится + 409 — приемлемо). +9. **Свежесть текста на границах стадий, а не непрерывно** — не оспорено никем: живой SQLite движка + читать нельзя (D39.85/D39.106, `docs/research/23-engine-platform-seam.md:46-47`). +10. **Опрос для артефакта экспорта** — AIP-151 прямо запрещает стриминговый ответ для LRO; наш выбор + «SSE для прогона, опрос для экспорта» оказался textbook-верным, а не непоследовательным. + +--- + +## 4. REJECTED — и почему + +Отвергнутое — тоже результат. Девять из этих кандидатов опрокидывали ратифицированные решения +владельца, четыре — мои ошибки чтения стандарта или кода. + +| # | Кандидат | Почему отвергнут | +|---|---|---| +| 1 | 500 и `default` не документированы | Zalando прямо: «500 Internal Server Error · use · **do not document** · \». RFC обязанности не создаёт. Контракт последователен. | +| 2 | 429 не выражен | На `/v0` лимитера нет вовсе — нечего недо-объявлять. Форвард-вопрос платформы, не дефект контракта. | +| 3 | 404 несёт три смысла | Ратифицировано (PD-174, `:143-145`), и RFC 9110 §15.5.5 это прямо разрешает: 404 покрывает и «is not willing to disclose that one exists». Остаток — узкий: ратифицирован смысл только для `createBook`, а отвечают так семь операций; сворачивается в Б-2. | +| 4 | 405 не выдаётся / две формы 405 | Следствие метод-независимого catch-all (проверено по stdlib go1.26.6), а `/healthz` вне контракта по решению его владельца. | +| 5 | 413 только на интейке, иначе 400 | На тех маршрутах контракт объявляет 400 и не объявляет 413 — код отвечает единственным легальным. Объявлять 413 везде значило бы тащить порог в документ. | +| 6 | 409 «нет денег» одет фразой про потолок | **Моя ошибка.** `ErrInsufficientCredit` возникает ровно в гонке между чтением границ и взятием холда (`pgstore/credits.go:174-176`) — то есть в том самом случае, который фраза и описывает; ретрай с меньшим потолком работает. При исчерпанном балансе шкала отдаёт `max_chapters: 0` ещё до старта. | +| 7 | 408 без `Retry-After` и без `Connection: close` | **Моя ошибка дважды.** RFC 9110 §15.5.9 про `Retry-After` не говорит ничего; `Connection: close` net/http ставит сам на этом пути. | +| 8 | 503 без `Retry-After` | RFC 9110 §15.6.4 — «The server **MAY** send a Retry-After», а спека прямо пишет «no Retry-After is promised» (`:656-657`). Осознанно, ратифицировано D39.123. | +| 9 | `paused_reason` беднее реальности (три причины у платформы, одна в enum) | **Ратифицировано 15.08:** «PD-199 закрыт этим же решением: `null` на проводе подтверждён, слово в контракт не заводится» (D39.132 п.2а) — тем же решением, что убрало `day_usd` из шаблона. Пере-судил и не опрокидываю: с уходом дневной оси из платформы различие перестало быть продуктовым состоянием. **Выживший остаток вношу в батч:** `:879` по-прежнему пишет «`null` in every other state», а ратифицированное поведение даёт `paused` + `null` — правка описания, Ц0. | +| 10 | `EventCeiling` без `scope` | Тем же решением. Остаток: `halted: boolean`, всегда `true` — обязательное поле, не несущее информации. | +| 11 | Bearer-клиент не может читать SSE | **Моя ошибка:** `text/event-stream` — обычный HTTP-ответ; ограничение у браузерного `EventSource`, а не у контракта. Остаётся продуктовый вопрос (webview-десктоп), не дефект. | +| 12 | Два механизма для двух долгих операций | AIP-151: «The response must not be a streaming response» — опрос для артефакта и есть стандартная форма. | +| 13 | `min_chapters: 1` при `max_chapters: 0` | Документированный сентинел (`:1231-1233`), ратифицирован D39.115 §4, клиент его уже проверяет первым (`RunStart.tsx:122`). Остаток — общий: OpenAPI не выражает межполевой инвариант. | +| 14 | `default_chapters` = верх шкалы | Ратифицировано с явно названным компромиссом (D39.123 п.2е, D39.110 §2в). Не мой вызов; называю как конфликт интересов «продукт ↔ деньги». | +| 15 | Клиент обходит все страницы | Это КОНФОРМНОЕ поведение, предписанное спекой (`:99-100`). Остаток — у контракта нет НИЖНЕЙ границы страницы, и конформный сервер с 10 строками на страницу упрёт клиента в его потолок 50 страниц. | +| 16 | Страница глав 5000 при 2284 главах | Не дефект: страница больше коллекции — это чтение без разрывов. Остаток — два разных владельца одного числа (проза vs «platform's choice»), учтён в Б-2. | +| 17 | Курсор без эпохи (по коду библиотеки) | **Мой промах в адресации:** реализован только библиотечный курсор, которому эпоха не нужна. Дефект переформулирован как контрактный — Б-7. | +| 18 | `/usage` назван не тем, что отдаёт | Операция самоописательна («State of the credit balance»), переименование пути — ломающая правка ни за что. Остаток: `Usage.paused_reason` типизирован прогонным enum, тогда как PD-203 (D39.136 п.6а) развёл уровни — нужен свой словарь причин аккаунта либо снятие поля (в батч, Ц0). | +| 19 | Байт-копия спеки никем не сверяется | Неверно: сверка — обязанность лендинга и она исполняется (D39.135 п.2а); я сверил `cmp` — файлы идентичны. Остаток процессный: сверка ручная, не в CI. Пингом оркестратору, не в батч. | +| 20 | Обоснование `Retry-After` на 200 неверно читает RFC | Спорно в обе стороны: §10.2.3 начинается с общего правила, но специфицирует только 503 и 3xx. Решение (объявить) безвредно; довод не тяну. | +| 21 | Добавочные поля `BankTerm` (evidence/variants/conf, aliases) для экрана подписи S5 | **Слово владельца 15.08:** «Добавочные поля `BankTerm` НЕ заводить … смысла хватает» (D39.136 п.4б). Снято. Поправка к кандидату: у `BankTerm` девять полей, не восемь, и блокер S5 — отсутствующий канал ЧТЕНИЯ, а не набор полей. | +| 22 | `reject_reason` optional против `paused_reason` required — асимметрия | Спека аргументирует её в собственном тексте (`:826-831`), и довод состоятелен: требовать поле значило бы сломать деплой, который старше минора. Пере-судил; честная версия найдена в другом месте — `Usage.paused_reason` (см. Б-14). | +| 23 | `Unit.target` optional против `sense` required — «одна задача, два решения» | Не одна задача: `sense` входит в КЛЮЧ уникальности, `target` дизамбигуируется обязательным `state`. Форма исправления взята из К-11 (условная обязательность), а не из симметрии имён — она и вошла в Б-14. | +| 24 | Счётные имена не выровнены (`chapter_count` / `units_total` / `total`) | Правило есть и без исключений: короткое имя там, где у поля есть парная метрика в том же объекте. Остаток — два референта у слова `total` — перенесён в Б-14. | +| 25 | `src`/`dst` у банка против `source`/`target` у юнита | Выбор объяснён в схеме (`:1085-1087`) и в компаньоне историей ошибки, которую он исправляет: у движка `source` — это ПРОВЕНАНС. Переименование — самый дорогой класс правки ради стиля. | +| 26 | `TrustedOrigins` против «CORS-слоя нет вовсе» | Ратифицировано: D39.111 п.1 «В-кватер» (`:285`) — «кросс-origin не проектировать»; и это открытая строка СВОЕГО регистра платформы (PD-96), явно помеченная «НЕ трогать» тем же решением. Вне зоны этого ревью. Остаётся замечание о коде: комментарий `csrf.go:26-27` обещает отдельно развёрнутый фронт — мёртвая ручка с вводящим в заблуждение комментарием. | +| 27 | `/auth/*` вне контракта | Ратифицировано (`:37-39` + компаньон §2.14, D39.99) как механика сессии, а не контрактная поверхность. Выживает половина, и она НЕ контрактная: 401 — тупик (нет вызова, нет ссылки на вход, клиент рисует его как отказ) — строка бэклога фронта, которой сегодня нет. | +| 28 | Ставка $0.03/глава искажает шкалу | Не контрактная поверхность вовсе (пересчёт «главы → деньги» на провод не идёт, D39.84), свойство задокументировано в самом коде («It is an ESTIMATE and it is wrong for any particular book … Nothing depends on it being right»), носитель остатка — строка 166. | +| 29 | `ExportRequest.format` — свободная строка | Прозрачно отложено самой схемой («the set of formats is stage S7 work»), обе операции не построены. Остаток — «обязательное поле с неопределённым пространством значений» — учтён в Б-2 как часть документа возможностей. | + +--- + +## 5. Предложение состава батча 0.3.0 + +> **Состав уточнён 16.08 по решениям владельца (§8).** Добавлены Б-0 (утечка пайплайна) и жанр; снята +> история прогонов; форма ответа на В-1 выбрана — вариант B. + +**Ядро (без него остальное не имеет смысла):** +0. **Б-0. Убрать конвейер с провода** — `Progress` в один счётчик до остановки (закрывает и Б-13а), + `finalizing` свернуть, `verify_bank` переименовать, `TermOrigin`/`TermStatus` снять, описания + вычистить, завести гейт. Решение владельца 16.08: «переделываем». +1. **Б-1. Модель ошибок** — вариант B (решение владельца 16.08): машинный `code` (двухуровневый) + + `request_id`; `title`/`detail` объявить developer-facing и неотображаемыми; серверная локализованная + фраза — отдельным полем и только для неперечислимых причин; `errors[]` с указателем на поле; два + класса конкретности из §8а. +2. **Б-15. Полнота и машинность безопасности** — `X-TM-Client` как header-параметр, 403 в ответах (хотя + бы фразой в описании схемы), `WWW-Authenticate` на 401, множество небезопасных методов, относительный + `servers.url`, судьба заявленного bearer. +3. **Б-2. `GET /capabilities`** — версия · пары · порог интейка · форматы · размеры страниц. Снимает + Ф-57, половину Б-3 и «зашитые числа» разом. +4. **Б-7а. Вычистить устаревшие утверждения о движке** — три предупреждения снять, `resume`-абзац + переписать на действующее лечение (новый прогон с бо́льшим потолком). Дешевле всего в батче и + единственное, что сегодня отнимает у пользователя работающее действие. + +**Пока дёшево (операции не построены — Ц0):** +5. **Б-6. Поток** — перевесить на книгу; кадры конца разбора и конца прогона; `204` при переподключении + к завершённому; развести `id` кадра и ревизию; правило докачки; запрет склейки `note`. +6. **Б-4. Экспорт** — состояние вместо булева `ready`, `failure_code`, `expires_at`, эхо формата, + правило доступа к ссылке. +7. **Б-9 + Б-14а. Замечание и стоп подписи** — `id`, `created_at`, обязательная адресация; + `pending_decisions`/`complete` в чтение банка. +8. **Б-19. Глава** — снять запрет на клиентскую служебную метку СЕЙЧАС; `title_raw`, `kind`, структурная + версия — формой здесь либо явной передачей дизайн-паку этапа 161. +9. **Б-7. Структурная версия и условные чтения** — версия в ответах и в `hello`, `410` на исчезнувшую + главу, `If-None-Match`/`304`. +10. **Б-12. Агрегаты банка** — правило согласования со страницами. + +**Дёшево и на построенном (Ц1):** +11. **Б-3.** Запрет молчаливой потери; порядок свойств `BookIntake`; `Idempotency-Key`; `Location` на 201. +12. **Б-8 + Б-13а.** `failure_reason` у прогона; знаменатель прогона в главах (или явная запись, что + `Progress` — книжный). +13. **Б-13.** Отдельный `RunStatus`; правило старшинства; `book_id` в `Run`; судьба `finalizing`. +14. **Б-5.** `PATCH /books/{id}` (только `title` — жанр уходит из продукта, §8 п.6); `DELETE /books/{id}`; + `GET /runs/{runId}`. Записать явно: правка метаданных — ОТОБРАЖЕНИЕ, до движкового брифа она не + доезжает (иначе первый же пере-рендер `book.yaml` перекупит книгу). +15. **Б-10 / Б-11.** Максимум `limit` и подрезание вместо понижения; объявленный порядок коллекций; + `maxItems` у `decisions`. +16. **Б-14.** Одно правило «отсутствия значения»; условная обязательность `Unit.target`; имена конвертов. + +**Правки одной строкой:** Б-17 (`TermOrigin` → продуктовый словарь) · Б-18 (окно термина) · +Б-16 (требования деплоя — в примечание) · Б-20 (классы стабильности `Id`) · Б-22 (роль имени файла) · +описание `paused_reason` при `null` (остаток §4 №9) · `Usage.paused_reason` (остаток §4 №18) · +`Export.revision` · кэш-директивы в схему (сегодня платформа ставит `no-store` на ВСЕ ответы, +`middleware.go:38`, а контракт требует его только для ответов с переводом — контракт беднее кода). + +**Отвечено владельцем 16.08, вопросов не осталось:** «добавить главу» — дописывать в ТЕКУЩУЮ книгу, без +костылей (Б-19а, форма `POST /books/{id}/parts`) · дефолт шкалы остаётся, но клиент получает причину +`blocked: {code, book_id}` (В-6). + +✅ **Развилка снята владельцем 16.08: «экспорт будет».** Значит операции остаются, и пункт 6 становится +ОБЯЗАТЕЛЬНЫМ: без состояния отказа опрос «готово?» не завершается никогда. Предложение §5а снять обе +операции — отозвано. + +**Идёт отдельными строками бэклога (не правки спеки):** снятие жанра из брифа и промптов вместе с +этапом структуры глав (§8 п.6) · форма `POST /books/{id}/parts` и движковый гейт против молчаливой +перекупки (§8 п.7) · сжатие и условные чтения на платформе (§5б шаги 1-2) · сворачивание замечаний в +тексте под кнопку (§8 п.11) · обнуление гранта на бете (§8 п.14). + +**Не в батч, записать направлением:** двухшаговая загрузка · лента изменений структуры · поиск и фильтр +по банку и дереву · «грубая группа статуса» для эволюции словаря · дисциплина «предупреждение о +недостроенном несёт номер строки бэклога» (Б-21). **Снято решением владельца:** история прогонов +(в МВП не нужна). + +### 5а. Что РЕЗАТЬ (по вопросу владельца «не переусложнён ли контракт») + +Мерка: 1445 строк — **625 прозы против 760 формы (43 % файла — проза)**, и внутри прозы нормативного +всего ~37 предписаний (24 MUST, 3 MAY, 4 forbidden). Остальное — генезис: 12 ссылок на конкретный минор, +13 на компаньон, 15 «rather than», 21 «would». Спека сама пишет о себе «This file is normative for the +FORM; the companion explains where the form comes from» (`:12-13`) — и сама это нарушает: история +ратификации semver (`:47-53`), две апологии RFC 9110 (`:534-541`, `:644-651`), объяснение, почему имя +`source` не взято (`:1084-1087`). Всё это — в компаньон. + +**Мёртвое, режется без потерь (~186 строк формы плюс проза):** + +| Что | Почему мёртвое | +|---|---| +| ~~экспорт, 2 операции, 84 строки~~ | **ОТОЗВАНО решением владельца 16.08 «экспорт будет».** Кандидат был: снять до S7, потому что фиксируется «only the call shape», содержимое которого неизвестно. Раз экспорт в продукте — операции остаются, и вместо снятия делается Б-4 (состояние вместо булева `ready`, `failure_code`, `expires_at`, эхо формата, правило доступа к ссылке). Заодно решается К-12 | +| **`Problem.instance`** | структуры на платформе нет вовсе; реальная форма ответа — `{title, status}` | +| **`Note.unit_id`** | только в фикстуре мока; экраны берут `unit.note` вложенно и `note.chapter_id` | +| **параметр `limit`** | клиент не отправил его ни разу (все пять вызовов идут без `query`) | +| **`EventCeiling`** | единственное поле `halted`, всегда `true`, клиент его выбрасывает; дубль кадра `status`, который уже несёт `paused_reason` | +| **нагрузка 4 кадров из 8** | `EventBank`, `EventNote.note`, `EventHello.run_id/revision`, `EventResyncRequired.reason` передаются и игнорируются. Честная форма: имя кадра + `id`, нагрузка только там, где клиент патчит без чтения (`status`, `progress`, `chapter`) — три схемы вместо восьми | +| **`Bank.signed`** | доезжает до клиента и не рисуется | + +**Что оставить, оно несёт вес:** ревизия и её гард (лечит замеренную регрессию — прогресс откатывался на +refetch по фокусу), курсор (держит один ответ ограниченным), `verify_bank`, `ceiling_chapters`/ +`RunOptions`, `eta_seconds` (проходит сквозь всю цепь до экрана). + +**Слить формально:** пять конвертов списков (`Library`/`ChapterList`/`UnitList`/`NoteList`/`Bank`) +вручную повторяют `revision`+`next_cursor` — `allOf` сделает инвариант проверяемым; `EventChapter` — +ровно изменяемое подмножество `Chapter` с переименованным `id`→`chapter_id`, и клиент разбирает это +переименование руками. + +**Из восьми непостроенных операций** экспорт (обе) остаётся по решению владельца. Мои сомнения +сохраняются по двум другим: `listBankTerms` (форму задаст содержимое уже построенного сайдкара банка, а +не догадка до него; за этим чтением висит крупнейший блок файла — `BankTerm` плюс три словаря) и +`listNotes` (единственное содержательное поле — ненаписанная фраза, карта «причина → фраза» пуста, число +ступеней открыто). Это не предложение снять, а предложение НЕ доводить их форму до финала, пока источник +не построен. `listChapters`, `listUnits`, `streamRunEvents`, `submitBankDecisions` — оставить как есть: +это ридер, у него есть рабочий клиент и мок. + +**Зеркальный дефект:** `/usage` ПОСТРОЕН и не читается ни одним экраном (`usageQuery` встречается только +в тесте) — то есть в контракте одновременно есть и невостребованное построенное, и построенное +невыраженное. + +### 5б. Транспорт: менять ли REST (вопрос владельца 16.08 — «есть ведь ещё gRPC») + +Отдельная проверка независимым агентом другого тира (`fable`) с веб-ресёрчем; я перепроверил ключевые +цитаты по первоисточникам — дословны. Вывод совпал с моим. + +**Транспорт не менять. Лечить HTTP.** «REST умеет в сжатие» — верно, но сжатие это не свойство REST, а +слой HTTP (`Content-Encoding`), который надо включить явно: Go stdlib его не делает, и это уже записано +дырой в собственном доке платформы (`platform/docs/PLATFORM_DIRECTION.md:202-205`). + +**Почему не gRPC.** Наша нагрузка — художественный текст: глава 26,1 КБ это почти целиком сам текст. +Смена сериализации экономит на ключах и структуре, а не на тексте; после gzip разница между JSON и +protobuf на таком payload — единицы процентов. Цена: «gRPC-web clients connect to gRPC services via a +special proxy; by default, gRPC-web uses Envoy» и «Client-side and Bi-directional streaming is not +currently supported» (https://github.com/grpc/grpc-web) — новая инфраструктурная деталь на same-origin +продукте, вторая кодогенерация параллельно ратифицированному OpenAPI, потеря `If-None-Match`/304 и +читаемости в DevTools. **Connect** честно лучший из RPC-вариантов ровно потому, что возвращает обычный +кэшируемый HTTP GET + JSON («For RPCs that have no side effects, it is possible to use GET requests +instead… easy to cache in browsers, proxies, and CDNs», https://connectrpc.com/docs/protocol/) — то есть +платить миграцией за то, что у нас уже есть. **WebSocket** окупается только вместе с sync-движком (см. +ниже), **GraphQL** лечит over-fetching формы, а у нас болит частота перечитываний. + +**Наш паттерн «кадр без данных → клиент перечитывает» — легитимный, у него есть имя: poke/pull.** +Replicache: «A Replicache poke caries no data – it's only a hint telling the client to pull soon. This +enables developers to build their realtime apps in the standard stateless request/response style» +(https://doc.replicache.dev/byob/poke). Мейнтейнер React Query о том же для нашего стека: «I like to +send events from the backend instead of complete data objects… if we receive an event for an entity +that we are not interested in at the moment, nothing will happen» +(https://tkdodo.eu/blog/using-web-sockets-with-react-query). GitHub строит на этом же: +«If the data has not changed, you will receive a 304 Not Modified response, which does not count +against your primary rate limit» (https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api). +Дельта-пакеты вместо poke шлют те, у кого много одновременных писателей и local-first-редактирование +(Figma, Linear) — у нас писатель один (движок), клиент читает прогресс. + +**Но наш poke недоукомплектован по сравнению с нормой:** кадр не несёт СКОУПА (что именно изменилось), +поэтому клиент перечитывает всё; и перечитывание ничем не удешевлено. + +**План по эффекту на килобайт усилия:** + +| # | Шаг | Эффект на наших числах | +|---|---|---| +| 1 | сжатие ответов (middleware/edge; на SSE НЕ вешать) | глава −60 %, дерево −84 %, банк −90 %; кадр на 12 вкладок 562 → ~164 КБ | +| 2 | `ETag`/`If-None-Match` → 304 на всех списочных GET | фокус-рефетч 562 КБ → ~4 КБ заголовков, −99 % | +| 3 | скоуп в кадре (id сущности + версия, по-прежнему БЕЗ текста) | кадр стадии 767 КБ → ~50 КБ; неоткрытые вкладки не читаются вовсе | +| 4 | дельта-чтение банка/замечаний `?after_version=` + коалесинг на эмиттере | сценарий «кадр банка на главу» 372 МБ → 2-3 МБ | +| 5 | клиенту — поднять `staleTime` (после шага 2 фокус-рефетч почти бесплатен) | зона фронта | + +Шаги 1-2 — слой HTTP, контракта по семантике не касаются (но должны быть В НЁМ записаны, сегодня они +живут только в зонном доке). Шаги 3-4 — минорная правка контракта, не смена транспорта. + +**Чего НЕ делать:** менять транспорт · строить sync-движок уровня Linear/Figma (месяцы за то, чего +продукт не просит) · класть текст в кадры (второй источник истины и гонки с чтениями) · заводить ручку +«вся книга юнитами» (57 МБ) «раз уж будет сжатие» · включать сжатие на `text/event-stream`. + +**Версия.** Правки ломающие по смыслу (снимается опора на серверную фразу, меняется словарь `Problem`, +переезжает канал потока). По нынешнему правилу 0.x минор — лана для ломающих правок (`:47-50`), значит +формально **0.3.0**. Вопрос «не сделать ли его последним 0.x перед 1.0» задан и **закрыт владельцем +16.08: НЕТ — последний 0.x будет после беты.** То есть 0.3.0 — обычный ломающий минор, и право ломать в +0.x сохраняется на весь бета-период; окно «после беты» и есть место для 1.0. + +--- + +## 6. Диспозиции по обязательным входам промта + +| Вход | Диспозиция | +|---|---| +| **Ф-56** конец разбора | **Б-6(а).** Чинить не кадром, а перевеской канала на книгу: `uploading`/`parsing` прогону не принадлежат. Сегодня Ц0. | +| **Ф-57** пары языков | **Б-2.** Отдельной ручки пар не делать — один документ возможностей; плюс код отказа по паре на интейке. Худший случай прослежен: пар-промпты есть только для `zh-ru`, отсутствие промпта — жёсткая ошибка конфигурации, поэтому две из трёх предлагаемых пар умирают на старте прогона; денег это не стоит, но пользователь узнаёт об этом только `failed`-ом без причины в конце пути. | +| **Ф-61** хардкод-английский | **Б-1.** Машинный `code` + клиентская фраза; `Accept-Language` — только для неперечислимых причин. Закрывает половину слова владельца 15.08 о мультиязычности всех зон; вторая половина (фразы в логах и артефактах движка) вне контракта — носителя назвать отдельно. | +| **Ф-62** переименование | **Б-5.** `PATCH` только с `title` (жанр уходит из продукта — Б-23, решение владельца 16.08); `DELETE` обязателен рядом. Пара языков и структура — не метаданные. Правка названия — отображение: до движкового брифа она не доезжает, и это надо записать. | +| **Форма PD-172** | **Б-3. Протёкшая реализация в части МОЛЧАНИЯ, осмысленный контракт в части порядка.** Правило «файл последний» оставить, реакцию сменить на 400; порядок свойств в схеме исправить (сегодня он указывает нарушить правило). Двухшаговая загрузка — направлением. | +| **Пагинация и масштабы** | **Б-10, Б-11, Б-7.** Главное не размер страницы, а: не объявлен порядок, нет максимума `limit`, нет фильтра и поиска, нет наблюдаемой структурной эпохи. Масштабы сверены с движком (2284/7,78 млн; ~1,9 юнита на главу). | +| **Зашитые числа** | **Б-2.** В контракт как константы — нет (тут комментарий кода прав); в ответ документа возможностей — да. Исключение: максимум `limit` принадлежит контракту как предел формы. | +| **Паттерн-консистентность** | **Б-14.** Названная в промте пара (`paused_reason` / `reject_reason`) как раз ЗАЩИЩЕНА текстом спеки и снята (§4 №22); вместо неё найдено семь мест без обоснования нигде — включая то же имя `paused_reason` с двумя разными обязательностями на трёх схемах. Победитель — правило `Chapter.number`/`sense`. | +| **Идемпотентность, 408/413, 404-как-нет-интейка** | **Б-3 + §4 №3,5,7.** Идемпотентность — да, `Idempotency-Key` (RFC 9110 §9.2.2 прямо про наш случай: контракт советует ретрай и не даёт средства). 408 и 413 — коды верные, претензии сняты. 404 на `createBook` — ратифицирован и разрешён RFC; остаток про непостроенные маршруты сворачивается в Б-2. | +| **Версионная политика** | **Б-2 (часть).** Недостаточна: минорную версию не видит ни один не-потоковый клиент, а сегодня — вообще никакой (носитель не реализован). Минимум — версия в документе возможностей или в заголовке ответа. | +| **К-вопросы** | Таблица ниже. | +| **Спойлер (В-10)** | **ЗАКРЫТ владельцем 16.08: «вежливость».** Контракта не касается вовсе: `sense` остаётся обязательным, размытие — дело фронта, находимость текста через Ctrl+F — приемлемое следствие, а не дефект. Ревью его не трогало и по условию промта (слова «защита» не было), и по существу. | + +### К-вопросы компаньона: что закрывает это ревью + +| К | Статус | Вердикт ревью | +|---|---|---| +| К-2 (титул главы) | открыт | **Не закрывать формой «одно поле»** — Б-19: нужны два носителя, а нынешний запрет на клиентскую метку «Глава N» противоречит решению владельца 09.08 и должен быть снят. | +| К-4 (ревизия) | отвечен P0 | Подтверждаю, с поправкой Б-6(г): ревизия и позиция потока — разные величины, совмещать их в `id` нельзя. | +| К-6 (ступени замечания) | **ответ владельца 16.08** | «Показывать все; в тексте — сворачивать и раскрывать по кнопке; как именно — решим потом». Значит словарь ступеней проектировать заранее НЕ надо. Зависимость остаётся: без `Note.id` (Б-9) конкретное замечание нельзя свернуть и запомнить. В бэклог. | +| К-7 (пагинация) | отвечен P0 | Курсор верен; недостаёт порядка, максимума и фильтра (Б-10/Б-11). | +| К-9 (отказ прескрина) | **принято владельцем 16.08** | Прескрин — ещё одна ПРИЧИНА, а не двенадцатый статус: `rejected` + код. Владелец добавил рамку: отвечать максимально абстрактно, чтобы подбирающий не понял, где именно он валится — то есть ОДИН грубый код на весь модельный класс, без деталей и без вариации между попытками (§8а). | +| К-10 (пофазность у главы) | открыт | **Закрывается бесплатно:** обе фазы уже в read-модели (`00002_readmodel.sql:102-105`), это правка проекции. | +| К-11 (условная обязательность) | открыт | Частично закрывается Б-14: `Unit.target` — на правило `sense` (инвариант уже гарантирован чеками БД), `Note` — обязательная адресация. Остаток (генератор игнорирует `if/then`) — инструментальный, лечится сужением на шве клиента. | +| К-12 (завершение выгрузки) | отвечен P0 (опрос) | Ответ верен и подтверждён AIP-151. Но Б-4 важнее: без состояния отказа опрос не завершается никогда. | +| К-13 (`paused_reason` не различает) | открыт (владелец) | **Закрыт не мной:** D39.132 п.2а ратифицировал `null` на проводе. Остаток — противоречие в описании `:879` (в батч). | + +--- + +## 7. Не покрыто · препятствия · самопроверка (редакция 15.08; дополнения после разбора — §10) + +**Не покрыто по решению:** В-10/спойлер (нет слова владельца) · полигон (`eval/`, `docs/experiments/`) — +чужая живая зона, не читал · `/auth/*`, `/healthz`, `/readyz`, `/metrics` — вне контракта; смотрел только +там, где они касаются контрактной поверхности · административная поверхность (её в контракте нет, и это +правильно; три из четырёх greenfield-дизайнов её спроектировали — когда она появится, встанет вопрос +«одна спека или две»). + +**Попутно замечено, передаю пингом (не находки контракта):** `/metrics` слушает без аутентификации на +отдельном адресе, защищает только привязка к `127.0.0.1` (`platform/cmd/tmplatformd/main.go:200-202`); +лимитер входа один на процесс, не пер-адресный (`platform/internal/login/login.go:128-129,165`) — выбор +объяснён комментарием (`:170`, за прокси `RemoteAddr` один на всех), но следствие «один клиент упирает +вход всем» стоит назвать; сверка байт-копии спеки — ручная, не в CI. + +**Препятствия:** +- **Кросс-модельность опровергателей.** Требование промта §2.5 при границе «$0 по внешним провайдерам» + (§5) невыполнимо: все доступные харнессу агенты — семейства Claude. Компенсировал ролью-опровергателем, + разными тирами модели у аудитора улик и адвоката формы, проверкой каждой улики ИСПОЛНЕНИЕМ и + greenfield-панелью, спеки не видевшей. Это ослабляет ОЦЕНОЧНЫЕ вердикты (не фактические): засчитывать + их как независимое подтверждение нельзя. Настоящий кросс-семейный проход по этому докладу — отдельный + заказ полигонного контура. +- **Рантайм не поднимался** (по §5 промта). Всё, что от него зависит, помечено PLAUSIBLE: наблюдаемость + `problem+json` при 413/408 посреди тела, склейка кадров (кода + склейки не существует). +- **Масштаб замечаний не замерен никем** — ни один документ и ни один прогон не говорят, сколько + замечаний даёт настоящая книга. Страница 500 и книжный (не поглавный) доступ выбраны без числа. +- ~~Единицы юнитов~~ — **закрыто 16.08 замером**: манифест движка на настоящей книге даёт 2283 главы, + 4276 юнитов, 5071 чанк (то есть 1,87 юнита на главу — «~1,9» спеки верно, и фикстура фронта не выдумана). + +**Пере-проверка 16.08 (по прямому вопросу владельца «что поленился дочекать»):** +- прогнал ВСЕ 126 ссылок на строки спеки, встречающиеся в этом файле, против самого файла: вне диапазона + нет ни одной; глазная сверка содержимого каждой — совпадает, найдена и исправлена одна ошибка на + единицу (`:192` → `:193`); +- проверил три утверждения, взятые у агентов на веру: `ErrorBoundary` во фронте действительно нет + (грепом пусто) · сжатие и ETag действительно названы требованиями в `PLATFORM_DIRECTION.md:200-206`, + причём с той же цифрой «289 КБ против 46 КБ gzip» · основание цифры «26 КБ на главу» сходится по + `~/books/gu-zhenren/minirun/export.json`; +- прогнал линтер проекта по канону — `spectral lint docs/architecture/14-api-contract/openapi.yaml + --ruleset frontend/.spectral.yaml` → «No results with a severity of 'error' found!». То есть документ + валиден как OpenAPI 3.1 и чист по правилам проекта: **все находки этого доклада — смысловые, ни одной + синтаксической**. Раньше я его как машинный артефакт не прогонял — это была дыра в методе; +- **нашёл и исправил СВОЮ ошибку в HIGH-буллете Б-2**: механизм отказа по неподдерживаемой паре — не + «тихая деградация langpack до платных вызовов», а жёсткая ошибка валидации конфигурации из-за + отсутствующих пар-промптов, ДО единого платного вызова. Деньги не сгорают; неверная была только + атрибуция механизма и слова «доходит до платных вызовов». + +**Самопроверка (оси D39.120):** +1. *claim-fidelity* — перед сдачей пере-открыл исполнением 24 улики (не 10): `problem.go:22-28`, + `v0.go:104-132/242/250/297-302/352-378/384/505-510/518/544-572`, `server.go:99/120/134/143`, + `csrf.go:26-33/38/62`, `reqid.go:16-27`, `middleware.go:22/38/68`, `pricing.go:87/97`, + `pgstore/books.go:349/510/586-587/821-831`, `00002_readmodel.sql:91-199`, `sink.go:178-181/435`, + `runevents.go:45-102/151-153`, `manifest.go:77-102`, `bankexport.go:52/72`, `mining.go:317-322`, + `client.ts:87-115`, `queries.ts:64-75/141-151`, `stream.ts:17/97/131-139`, `Loaded.tsx:58-69`, + `Notes.tsx:41-43/63-66`, `AddBook.tsx:278-292`, `Status.tsx:21`, `languages.ts:14-16`, + `27-chapter-detection.md:33-73`. Все цитаты стандартов сверены по скачанным текстам как ДОСЛОВНЫЕ + подстроки. Пойманное этой проверкой на СВОЁМ тексте: контракт несёт **16 операций на 15 путях**, а не + 15 операций (`grep -c operationId:` — 16), и построено 8, не 7; неверно назывался эпизод с курсором + (реализован только библиотечный, эпоха ему не нужна — §4 №17); четыре ссылки на строки в кандидатах + указывали не туда (`runevents.go:64`, `books.go:96`, `middleware.go:111`, `Usage`-диапазон) и + исправлены по месту. +2. *анти-инерция* — у каждого «оставить как есть» записан контраргумент; в §4 девять отвержений + опираются на ратификации владельца, и в каждом сказано, что именно я пере-судил и почему не + опрокидываю. + Обратная сторона той же дисциплины: собственные буллеты, опровергнутые кодом или стандартом (№6-8, + 11, 15-17), сняты полностью, а не «понижены до low». +3. *полнота интересов* — у каждого буллета названы «чья боль» и класс миграционной цены; конфликты + интересов названы явно, не решены молча (дефолт шкалы «весь баланс под одну книгу»; порог интейка — + деплой против клиента; фразы отказа — сервер против клиента). +4. *leave-one-out по линзам* — мульти-линзовые буллеты держатся без каждой линзы по отдельности: Б-1 стоит + на коде и без RFC (три места печатают серверную строку в русском интерфейсе) и на RFC без greenfield; + Б-2 стоит на коде (`languages.ts` + отсутствие langpack) и без панели; Б-7 стоит на греп-факте + («epoch» встречается ровно один раз) и без панели. Буллетов, живущих ТОЛЬКО согласием линз, в докладе + нет. + +--- + +## 8. Решения владельца 16.08 по этому докладу (ратифицирует оркестратор; сессия отчёта строк не заводит) + +Разбор шёл вопрос-за-вопросом; ниже — что решено, дословный смысл решения и что это значит для работ. +**Это раздел для будущих сессий: он говорит, ЧТО именно правится и ГДЕ, чтобы не пришлось перечитывать +всю переписку.** + +| # | Вопрос | Решение владельца | Что это значит конкретно | +|---|---|---|---| +| 1 | Утечка пайплайна в контракт (Б-0) | **«Согласен, переделываем»** | `Progress{draft,edit}` → один счётчик до ближайшей остановки; `finalizing` свернуть; `verify_bank` → `stop_for_signing` без «before the final pass»; `TermOrigin`/`TermStatus` снять с провода; `Unit` описать без «edit unit»; вычистить конвейерные слова из описаний (они компилируются в исходники фронта); завести гейт на утечку. Затрагивает: спеку, `wireProgress` (`v0.go:92-96`), проекцию (`v0.go:473-477`), фронт (`format.ts:109-110`, `About.tsx:96`). Колонки платформы и словарь шва при этом МОЖНО оставить как есть — фазы остаются внутренним делом | +| 2 | Транспорт | **«Что сам думаешь»** → ответ §5б, проверен независимым агентом | Транспорт НЕ меняем. Порядок работ: сжатие → ETag/304 → скоуп в кадре → дельта-чтение банка/замечаний → `staleTime` у клиента. Записать сжатие и условные чтения В КОНТРАКТ, а не только в зонный док | +| 3 | Переусложнённость | **«Дампануть буллетами будущим сессиям»** | §5а: что резать (экспорт, `Problem.instance`, `Note.unit_id`, `limit`, `EventCeiling`, нагрузка 4 кадров, `Bank.signed`), что слить (`allOf` на конвертах), что оставить. Проза 43 % файла — генезис переносится в компаньон | +| 4 | В-1, кто пишет фразу отказа | **«Идём в Б путём гугла»** | Вариант **B**: машинный `code` (закрытый словарь, двухуровневый — корневой стабильный + расширяемый вложенный, как у Microsoft) + `request_id`; `title`/`detail` объявляются developer-facing и НЕ показываются; серверная локализованная фраза остаётся ОТДЕЛЬНЫМ полем и только для неперечислимых причин (как `google.rpc.LocalizedMessage`). Клиент уже почти готов: таблица фраз по статусам есть (`AddBook.tsx:282-288`) и просто перебивается серверной строкой | +| 5 | В-2, конкретность причин против брутфорса | **«Расширяемая/сужаемая категория, главное — архитектурно чистый пайплайн»** | Два класса, форма ниже (§8а). Не откладываю в бэклог: форма определена | +| 6 | Жанр | **«Выкидываем. Просто выкидываем»** | Б-23. ⚠ Планировать вместе с этапом структуры глав: `genre` в `BriefHash`, удаление двигает канон брифа и перекупает существующие платные книги; дешёвое окно — пока платная книга одна. Правка жанра на платформе денег не стоит (движковый `book.yaml` пишется один раз и не перезаписывается) | +| 7 | Добавление главы | **«Твоё предложение рассмотрит оркестратор»** | Б-19а: форма `POST /books/{bookId}/parts` + ответ «что сдвинется и почём» до подтверждения; стройка вместе со строками 160-162. Отдельно нужен движковый гейт против МОЛЧАЛИВОЙ перекупки хвоста при вставке не в конец | +| 8 | В-5, полоса прогресса | **«Ок, делаем так»** | Полоса до ближайшей остановки, после подписи банка начинается заново; знаменатель — по КУПЛЕННОМУ объёму, иначе прогон с потолком не доходит до конца. Совпадает с п.1 | +| 9 | В-6, «почему вторая книга не стартует» | **«ОК»** | `run-options` (и 409) получают `blocked: {code, book_id}`; платформа знает открытые холды с `book_id` (таблица `reservations`) | +| 10 | В-7, история прогонов | **«В МВП не нужно»** | Из батча снято. Хранить дополнительно ничего не надо: прогоны и попытки уже в Postgres, не удаляются, индекс `runs (book_id, started_at desc)` под этот запрос уже стоит — если поддержка попросит, это один read-путь | +| 11 | К-6, ступени замечаний | **«Показывать все; в тексте — сворачивать и раскрывать по кнопке. Как — решим потом, в бэклог»** | Словарь ступеней проектировать заранее НЕ нужно. Зависимость: чтобы конкретное замечание можно было свернуть и запомнить, нужен `Note.id` (Б-9) | +| 12 | К-9, отказ прескрина | **«Отвечать максимально абстрактно, чтобы юзер не догадался, в чём причина и где он валится»** | `rejected` + один грубый код, без деталей, без вариации между попытками. Двенадцатый статус НЕ заводить | +| 13 | В-10, спойлер | **«Вежливость»** | Контракта не касается вовсе. `sense` остаётся обязательным, размытие — дело фронта, находимость через Ctrl+F — приемлемое следствие | +| 15 | `finalizing` против лестницы владельца | **«Ну да, это чисто моя фраза была»** | Лестница «загрузка → разбор → перевод → подпись банка → финал → готово» — устная формулировка, а не норма продукта. Значит статус `finalizing` ею не связан и **сворачивается** (Б-0, Б-13). Если фаза когда-нибудь появится у движка — возвращается минором | +| 16 | Экспорт: снять операции или чинить | **«Экспорт будет»** | Операции ОСТАЮТСЯ. Предложение §5а снять их отозвано; вместо этого обязателен Б-4: `state` вместо булева `ready`, `failure_code`, `expires_at`, эхо формата, правило доступа к ссылке (ПТ-34). Заодно закрывается К-12 | +| 17 | 0.3.0 как последний 0.x перед 1.0 | **«НЕ последний. Последний будет после беты»** | 0.3.0 — обычный ломающий минор; право ломать в 0.x держится весь бета-период. Окно для 1.0 — после беты, и туда же уходит правило «с 1.0.0 минор аддитивен безусловно» | +| 14 | Грант новому аккаунту | **«Обнулить — это решение, которое я окнул»** | Дефолт `SignupGrantMicroUSD` → 0 на бете, начисление руками; возврат к $5 — вместе с суточным агрегатным потолком, когда появятся платежи. ⚠ В репозитории этого решения раньше НЕ БЫЛО: PD-104 везде помечен «ждёт слова владельца», а «дефолт в НОЛЬ» было предложением оркестратора. Теперь это слово владельца 16.08 — оркестратору закрыть PD-104 и строку в CURRENT-STATE. Подтверждено кодом: движок кредитами не занимается вообще, начисляет только платформа (`login.go:305`, `config.go:191`) | + +### 8а. Форма ответа на В-2 (конкретность причин против подбора) + +Опасение владельца точное: чем конкретнее отказ, тем лучше он работает как ОРАКУЛ для подбора входа, +который заставит систему делать не перевод. Поэтому правило не «насколько подробно», а **какого класса +причина**: + +- **Класс 1 — детерминированные, про вход пользователя и его счёт:** порядок частей формы, размер файла, + нечитаемый файл, неподдерживаемая пара, «не нашлось ни одного раздела», нет кредитов, идёт другой + прогон. Конкретика МАКСИМАЛЬНАЯ и безопасная: правила детерминированы, их можно хоть публиковать, + оракула не возникает. Сегодня этот класс недосказан — «The upload is incomplete» покрывает шесть + разных причин. +- **Класс 2 — модельные:** прескрин misuse, отказ провайдера, контент-фильтр. Здесь каждый различимый + код — бит обратной связи атакующему. Правило: **один грубый код на весь класс**, без причины, без + вариации между попытками, с лимитом попыток на аккаунт. Сегодня этот класс не отделён вовсе. + +Механизм расширяемости — двухуровневый код (стабильный корневой + расширяемый вложенный): новый +частный случай добавляется, не ломая клиентов, — ровно то, чем Microsoft закрывает «новый код = ломающее +изменение». + +--- + +### 8б. Оркестратору при лендинге (адресно, по слову владельца «скажи ему в своём ресёрче») + +Четыре вещи, которые сессия отчёта сделать не может, а без них батч приземлится криво. + +1. **Перенести решения §8 в D-ноту.** Сегодня они лежат в `docs/research/` — а по порядку источников + истины побеждает `05-decisions-log.md`. Пока переноса нет, исполняющие сессии будут действовать по + ресёрч-файлу: ровно тот класс, ради которого в проекте заведены ⚠-баннеры. Это первое действие + лендинга, до любой правки спеки. +2. **Компаньон правится ВМЕСТЕ со спекой, а не после.** Его §5 отвечает на собственный ревью-вопрос + «добавлена волна — править ли фронт?» словом «нет», и это опровергнуто исполнением (Б-0); его §3 + («у чтения `/bank` сегодня нет канала вообще») противоречит собственной строке 267, исправленной + 15.08 (Б-7а). Если править только `openapi.yaml`, зона фронта продолжит учиться по неправде — она уже + учится: Ф-43 в её бэклоге ссылается на устаревшую версию. +3. **Лендинг — два коммита в порядке, канон первым.** Канон и байт-зеркало `frontend/docs/api-contract/` + обязаны совпадать (сверял `cmp` — сегодня совпадают), но хук фронта запрещает смешивать зоны в одном + коммите, а сверка ручная и не в CI. Порядок и факт сверки стоит записать в саму процедуру лендинга, + иначе расхождение обнаружится на следующей генерации типов. +4. **Пинги по чужим зонам из §9 нужно раздать по журналам** — платформенные (незакрывающиеся расчёты, + флаг аккаунта от одной книги, `/metrics` без аутентификации, лимитер входа на процесс), движковый + (комментарий `snapshot.go:186-188` противоречит коду с пака-20) и фронтовый фикс-лист к разморозке. + Ни один из них не является дефектом контракта, и ни один не имеет сегодня носителя. + +Плюс два замечания о самом батче, которые видны только с моей стороны: + +- **Порядок исполнения в §5 не косметический.** Б-1 (модель ошибок) стоит первым не по важности, а + потому что от неё зависят формулировки в Б-2, Б-3, Б-8, Б-14а и §8а: если словарь кодов заведут + позже, эти правки придётся переписывать дважды. +- **Три буллета из двадцати девяти появились ПОСЛЕ доклада, из разбора с владельцем** (Б-0, Б-11а, + Б-23). Это стоит прочесть как сигнал о методе: вопросы владельца покрыли то, чего не покрыли ни + инвентарь кода, ни линза стандартов, ни greenfield-панель. Если у следующего ревью будет фаза + «разбор с заказчиком», её лучше ставить не в конец. + +--- + +## 9. Замечено попутно и НЕ вошло в контракт-ревью (владелец 16.08: «не замалчивай ничего») + +Это находки чужих зон, всплывшие в инвентаре 639 фактов. Контракта они не касаются, я их не судил +адверсариально — отдаю как пинги, с честной пометкой уверенности. + +**Платформа — деньги и надёжность:** +- **Расчёт может остаться незакрытым навсегда** в двух случаях: попытка, спавненная без базовой отметки + расхода — «settlement withheld: this attempt ran without a spend baseline», холд остаётся открытым + (`platform/internal/runs/reconcile.go:660-670`); и дев-путь без движка, где `settle()` возвращает nil + и прогон вечно остаётся в `UnsettledRuns`. Оба случая ЛОГИРУЮТСЯ громко, оба легаси-по-построению — + но бюджета «сколько холд может висеть» не существует. +- **Флаг «аккаунт остановлен» выводится сканированием последних прогонов всех книг пользователя** + (`platform/internal/pgstore/books.go:720-724`): одна книга с `credit_exhausted` зажигает состояние + всего аккаунта. После PD-203 (уровни разведены) это стоит перечитать. +- **`ReadRunForSpawn` читает ВСЕ живые прогоны и линейно ищет нужный** (`pgstore/runs.go:712`) — O(живых + прогонов) на каждую задачу спавна. +- **Проверено и НЕ является дефектом,** хотя выглядело им: авто-рестарт при exit 5 без записанного + намерения стопа. Код знает о компромиссе и объясняет его, и деньги не страдают — «the restart settles + the old attempt at what it actually spent and gives the new one what is left of the run's budget» + (`reconcile.go:540-549`). Снимаю. + +**Платформа — прочее:** `/metrics` слушает без аутентификации на отдельном адресе (защита — только +привязка к `127.0.0.1`, `platform/cmd/tmplatformd/main.go:200-202`) · лимитер входа один на процесс, не +пер-адресный (`login.go:128-129,165`; выбор объяснён комментарием про прокси, но следствие «один клиент +упирает вход всем» стоит назвать) · сырой Go-текст ошибки сохраняется как причина карантина в +пользовательской строке (`reconcile.go:320`) — на провод не идёт, но лежит в БД. + +**Движок:** комментарий `snapshot.go:186-188` утверждает, что auto/draft-строки банка исключены из +`memory_version`, а код с пака-20 сворачивает КАЖДУЮ строку любого статуса вместе с алиасами +(`memory.go:410-421,441-446`). Практическое следствие названо в Б-19а (ja-мина при дописывании главы). + +**Фронт (зона заморожена, чинить при разморозке):** мок отказов интейка триггерится МАРКЕРОМ В ИМЕНИ +ФАЙЛА (`frontend/src/mock/intake.ts:238`) — фикстурный переключатель без аналога на сервере · обработчик стопа в моке +игнорирует `runId` и всегда отвечает прогоном дефолтной книги (`frontend/src/mock/handlers.ts:190`) · в мире `scale` +чтения отвечают замороженной ревизией 4102, а кадры идут от счётчика с 1841 — чтения и поток расходятся +по построению (`frontend/src/mock/worlds.ts:136`) · в сценарии `loading` любой POST уходит мимо мока в реальную сеть +(`frontend/src/mock/handlers.ts:59`) · переполнение обхода страниц отдаётся в UI статусом 0 — тем же, что «сервер +недоступен» (`client.ts:114` → `Loaded.tsx:65`). + +**Процесс:** байт-сверка канона спеки и зеркала фронта — ручная обязанность лендинга, не гейт CI; при +этом хук фронта запрещает смешивать зоны в одном коммите, то есть атомарно обновить обе копии нельзя. + +--- + +## 10. Что осталось непокрытым после разбора 16.08 + +- **Кросс-семейность.** Долг частично закрыт: транспортный вопрос (§5б) проверен агентом другого тира + (`fable`), его цитаты я сверил по первоисточникам. Настоящая кросс-СЕМЕЙНАЯ проверка (не-Claude) при + границе «$0 по внешним провайдерам» по-прежнему невозможна и остаётся отдельным заказом. +- **Один агент ресёрча деградировал**: тема «индустриальные стандарты ошибок» вернулась пустой + (`answer: "test"`). Тему закрыл руками по четырём первоисточникам (RFC 9457, Microsoft, Google + `error_details.proto` + AIP-193, Stripe) — цитаты в Б-1 и в §8 п.4. +- **Масштаб замечаний по-прежнему не замерен** ни одним прогоном: сколько замечаний даёт настоящая + книга, не знает никто, а страница 500 и книжный (не поглавный) доступ выбраны без числа. +- **Влияние жанровой строки на качество не измерено** — и уже не будет: владелец решил поле убрать. +- **Рантайм не поднимался**: всё, что от него зависит, помечено PLAUSIBLE.