1670 lines
198 KiB
Markdown
1670 lines
198 KiB
Markdown
# 28 — Контракт-ревью API v0 (фронт ↔ платформа): доклад к батчу 0.3.0
|
||
|
||
> **⟶ СТАТУС (проставлен 05.09): ОТЧЁТ ОТРАБОТАЛ, ЕГО НАХОДКИ РАЗОБРАНЫ ПОШТУЧНО — но сам он
|
||
> ратификацией НЕ ЯВЛЯЕТСЯ.** Куда уехали выводы: К-статусы и Приложения — компаньон канона
|
||
> `docs/architecture/14-api-contract/README.md` (там же, что закрыто и чем); Б-находки — строки бэклога.
|
||
> ⚠ Читать через компаньон: часть предложений отчёта была отклонена или перекрыта минорами 0.4.0–0.10.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.
|
||
> 6. **Пост-ревью автора отчёта (16.08, по заказу владельца — ревью выданных промтов):** ⚠ Б-0, Б-11а и
|
||
> Б-23 добавлены ПОСЛЕ раунда адверсариального судейства — единственные три буллета без опровергателя
|
||
> исходной сессии; их основания проверять строже. Уже опрокинуто одно: **«TermStatus снять с провода»
|
||
> (Б-0 п.2 рекомендации) — НЕВЕРНО**, обоснование «ни один экран не рисует» подменяло «экрана подписи
|
||
> ещё нет»: ось продуктовая (`:1070-1072` — главный фильтр S5; шов клиента потребляет,
|
||
> `vocabulary.ts:159-163`) — ось ОСТАВИТЬ, слова переименовать (эррата 16.08-г D-лога). Вторая дыра
|
||
> носителя: Б-11а не входит в перечень §5 — правило кадра (скоуп/дельта-или-счётчик) и `?after_version=`
|
||
> (§5б шаги 3–4) заказаны батчу поверх §5 (промт батча, правка 16.08).
|
||
|
||
## 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/"<revision>"` берётся
|
||
из уже существующей ревизии.
|
||
|
||
**Рекомендация — менять спеку и код.** (а) Публиковать структурную версию в каждом ответе с главами или
|
||
юнитами и в `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** · \<all\>». 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.
|