Land the batch dofix: all ten FB findings closed in the canon, report numbers re-run after the last edit, prose-share conclusion honestly reversed
This commit is contained in:
parent
4a8c64ec60
commit
86720cfa9f
3 changed files with 404 additions and 86 deletions
|
|
@ -362,7 +362,18 @@ rejected» (`pipeline/mining.go:201`). Отсюда: решение на тер
|
|||
последней применённой. Хуже: кадр `bank` предписывал читать строки «ревизией этого кадра», что при
|
||||
строгом сравнении всегда возвращало пустоту. Поймано холодным потребителем, исправлено: чтение
|
||||
включительно, повторно пришедшая строка безвредна (строка заменяется по `id`), а водяной знак
|
||||
следующего чтения — `revision` конверта, а не величина, выведенная из строк. Устаревший водяной знак
|
||||
следующего чтения берётся из КОНВЕРТА, а не выводится из строк.
|
||||
|
||||
⚠ **Дофикс 16.08 (ФБ-4): у многостраничного обхода конверт не один.** Формулировка «водяной знак —
|
||||
`revision` конверта» была однозначна ровно до тех пор, пока чтение умещалось в одну страницу; на
|
||||
рваном чтении она сталкивалась со вторым правилом того же раздела — «ревизия рваного списка это
|
||||
ревизия СТАРЕЙШЕЙ страницы». Два правила давали два разных числа, и клиент, взявший новейшее, молча
|
||||
терял строки, изменившиеся между первой страницей и последней. Сведено в одно: **и гард свежести, и
|
||||
водяной знак — это НАИМЕНЬШАЯ ревизия, увиденная за обход**; на одностраничном ответе это его
|
||||
собственная. Направление выбора — безопасное: лишнее перечитывание бесплатно (строка заменяется по
|
||||
`id`), пропуск строки — нет. Там же заявлено равенство `BookDetail.revision` и
|
||||
`BookDetail.book.revision`: карточка собирается одной транзакцией с книгой, и клиент вправе брать
|
||||
любое. Устаревший водяной знак
|
||||
(коллекцию заменили целиком) отвечает `400` c `cause.code: version_too_old` — тем же ответом и с тем
|
||||
же смыслом, что мёртвый курсор.
|
||||
|
||||
|
|
@ -582,7 +593,8 @@ the original request was never applied». Каждый `POST /books` созда
|
|||
|---|---|---|---|
|
||||
| `GET /books`, `GET /books/{id}` | read-модель платформы | ПОСТРОЕНО | — |
|
||||
| `GET /books/{id}/run-options`, `POST /runs` | шкала + холды | ПОСТРОЕНО | — |
|
||||
| `POST /runs/{id}/stop`, `/resume` | реконсилятор | ПОСТРОЕНО (кнопок на экране нет) | зона фронта |
|
||||
| `POST /runs/{id}/stop` | реконсилятор | ПОСТРОЕНО (кнопки на экране нет) | зона фронта |
|
||||
| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **поведение на паузе по потолку 0.3.0 МЕНЯЕТ**: сегодня платформа отвечает `202` и возвращает прогон в то же состояние (`runs/reconcile.go:906` — сама `Resume`; ветка `case "paused"` `:919` доходит до `reopen`, 409 только на дневном потолке движка `:931`), а контракт теперь требует `409` `run_not_resumable` · `cause.code: ceiling_reached`. Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) |
|
||||
| `GET /usage` | кредиты | ПОСТРОЕНО, **не читается ни одним экраном** | зона фронта |
|
||||
| `GET /capabilities` | конфигурация деплоя | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
|
||||
| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
|
||||
|
|
@ -715,7 +727,10 @@ the original request was never applied». Каждый `POST /books` созда
|
|||
схема `Counter` (одна полоса вместо двух) · `Book.genre` и `BookIntake.genre` (Б-23) · значение
|
||||
`finalizing` · прогонный поток `GET /runs/{id}/events` (перевешен на книгу) · зашитые числа размеров
|
||||
страниц из прозы операций · генезис-проза (история ратификации semver, две апологии RFC 9110,
|
||||
объяснение имени `source`) — перенесена в этот файл, не удалена.
|
||||
объяснение имени `source`) — **ужата НА МЕСТЕ до ссылки на источник, а не перенесена сюда**: в спеке
|
||||
остались `semver §4` и одна строка про `Retry-After` на `200`, целиком выброшенного текста нет.
|
||||
Формулировка «перенесена в этот файл» держалась ровно один раунд и исправлена дофиксом (ФБ-10):
|
||||
проверяется грепом — этих абзацев в компаньоне нет.
|
||||
|
||||
**Резать отказались, с контраргументом на каждое:**
|
||||
|
||||
|
|
@ -739,6 +754,71 @@ the original request was never applied». Каждый `POST /books` созда
|
|||
|
||||
---
|
||||
|
||||
## 6б. Дофикс-раунд 16.08 (заказ приёмки D39.142): что изменилось в каноне и почему
|
||||
|
||||
Десять находок ХОЛОДНОГО ПОТРЕБИТЕЛЯ — линзы, которой у селф-ревью батча не было: только финальная
|
||||
спека и проба генератором, без ревью, без компаньона, без диффа. Ниже — не пересказ правок, а их
|
||||
основание; сами правки в каноне.
|
||||
|
||||
**Форма кадра была двусмысленна (ФБ-1).** `EventEnvelope{event,id,data}` читался и как объект на
|
||||
проводе, и как описание SSE-фрейминга — оба чтения соответствовали тексту, и клиент по второму
|
||||
прочтению искал бы JSON с полем `data` внутри. Теперь на схеме стоит дословный пример кадра и
|
||||
сказано прямо: `event` и `id` — ПОЛЯ SSE, телом кадра является `data` и только оно. Это тот класс
|
||||
дефекта, который не ловится ни линтером, ни генератором: документ валиден в обоих прочтениях.
|
||||
|
||||
**Книга в покое могла крутить переподключение вечно (ФБ-2).** Служебные кадры несут id последнего
|
||||
кадра истории — а у книги, которая ещё ничего не производила, такого кадра нет. Клиент оставался без
|
||||
`Last-Event-ID`, каждый его запрос был «новым потоком», сервер отвечал `hello`+`end`, браузер
|
||||
переподключался — и так по кругу. Закрыто самой дешёвой из возможных мер: история нумеруется с `1`,
|
||||
служебные кадры пустой книги несут `0`, а `Last-Event-ID: 0` на книге в покое попадает под уже
|
||||
существующее правило `204`. Последовательность стала конечной: `hello` → `end` → закрытие → одно
|
||||
переподключение → `204` → браузер останавливается.
|
||||
|
||||
**Обещание «`note` не теряется» не переживало разрыв (ФБ-3).** Внутри одного соединения кадр-добавление
|
||||
защищён запретом склейки; между двумя соединениями — ничем: буфер не обещан, `resync_required` за
|
||||
обычный реконнект не полагается. Обещание не расширено (буфер обещать нечем), а названа обязанность
|
||||
клиента: после КАЖДОГО переподключения — дельта-чтение `/notes?after_version=…`. Механизм для этого
|
||||
уже был; не хватало записи, что он обязателен, а не удобен.
|
||||
|
||||
**Пустые члены `allOf` давали необитаемые типы (ФБ-5).** `EventEnd` и `EventResyncRequired`
|
||||
описывались как `EventBase` плюс пустой объект — генератор выводил `Record<string, never>`, и
|
||||
пересечение становилось типом, значение которого построить нельзя. Заменено на чистый `allOf` из
|
||||
одного члена с описанием на самой схеме; проверено генератором — оба теперь `EventBase`. Попутно
|
||||
записано то, что раньше подразумевалось: эти два кадра НЕРАЗЛИЧИМЫ по форме, диспетчеризация только
|
||||
по имени события.
|
||||
|
||||
**«Новый прогон» предписывался, а условия — нет (ФБ-6).** Спека велела предлагать новый прогон как
|
||||
лечение стопа по потолку, но нигде не перечисляла, что делает `resume` в каждом останавливающем
|
||||
статусе, а `paused_reason: null` описывался как «нейтрально, продолжаемо» — из чего клиент мог
|
||||
заключить, что при `null` надо звать `resume`. Добавлена таблица по всем статусам и сказано прямо:
|
||||
новый прогон легален при ЛЮБОМ `paused`, включая `null`; причина паузы — подсказка о прошлом, а не
|
||||
разрешение на будущее. Значение enum при этом не заводилось — ратификация D39.132 п.2а не двигается.
|
||||
|
||||
**Пол под моделью ошибок (ФБ-7).** Весь §Errors описывал ответы, которые СООТВЕТСТВУЮТ контракту;
|
||||
что делать с ответом прокси, HTML-страницей или оборванным телом — не говорил никто, а именно там у
|
||||
клиента нет ни `code`, ни `request_id`. Записано правило того же вида, что и для известных кодов:
|
||||
не показывать из такого ответа ни байта, рисовать свою нейтральную фразу.
|
||||
|
||||
**Пачка мелких (ФБ-8).** `title: null` в merge-patch — объяснено, почему это `400`, а не удаление
|
||||
члена по RFC 7386 (книги без названия у этой поверхности не бывает) · `X-TM-Client` — записано, что
|
||||
`required: true` стоит при живом исключении для bearer, потому что условной обязательности по схеме
|
||||
безопасности OpenAPI не выражает, и валидатор не должен читать законное отсутствие как нарушение ·
|
||||
список отказов интейка помечен неисчерпывающим (авторитет — `code` ответа) · правило резолюции
|
||||
`Location` · тождество `Idempotency-Key` на multipart считается по объявленным частям, а `408` не
|
||||
считается состоявшейся попыткой · `min_chapters` при `max_chapters: 0` — не диапазон, клиент проверяет
|
||||
максимум первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» — снято
|
||||
как обещание без носителя, заменено на наблюдаемое (`BankPage.signed` против `total`) · `409` у
|
||||
экспорта объяснён как ключевой, а не книжный · неизвестный `term_id` в подписи отклоняет весь батч, а
|
||||
не молча пропускает строку.
|
||||
|
||||
**Слова, которые называли не то (ФБ-9).** `parser_unavailable` называл наш компонент — переименован в
|
||||
`processing_failed`, по эффекту: имя значения это то, на что клиент вешает фразу. ⚠ Проводное имя
|
||||
теперь отличается от внутреннего словаря платформы — там значение зовётся `parser_unavailable`
|
||||
(`platform/internal/books/parse.go:66`), и проекция обязана отобразить одно на другое; рядом там же
|
||||
живут `schema_mismatch` и `storage_unavailable`, которые на провод не идут вовсе. Вход P7. Из описаний вычищены внутренние ссылки (`research/28 §…`,
|
||||
«companion К-6»): описания компилируются в исходники клиента, и ссылка на ревью в чужом репозитории
|
||||
там — мусор; носители остались здесь.
|
||||
|
||||
## 7. Эксплуатационные примечания — НЕ норма контракта
|
||||
|
||||
Вынесено из спеки в 0.3.0 (Б-16). RFC 9205 §4.1 прямо про наш случай: «Requiring a particular version
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ openapi: 3.1.0
|
|||
info:
|
||||
title: TextMachine API
|
||||
version: 0.3.0
|
||||
summary: Ratified contract between the frontend and the TextMachine platform (D39.99, D39.138).
|
||||
summary: Ratified contract between the frontend and the TextMachine platform.
|
||||
description: |
|
||||
**RATIFIED contract.** Canonical copy: `docs/architecture/14-api-contract/`;
|
||||
`frontend/docs/api-contract/` is a byte-mirror and a divergence is a defect of one of the two.
|
||||
|
|
@ -36,6 +36,12 @@ info:
|
|||
**Signing in is not part of this surface**: session mechanics live outside the version prefix and
|
||||
the flow starts at `GET /auth/login` (companion). A client that meets `401` sends the user there.
|
||||
|
||||
## `Location`
|
||||
|
||||
Every `Location` here is a URI reference resolved against the request's URL (RFC 9110 §10.2.2);
|
||||
it may be relative and usually is. A client follows it as given and does not rebuild the address
|
||||
from an identifier of its own.
|
||||
|
||||
## Conditional reads
|
||||
|
||||
Every collection read and the book card answer an `ETag` and honour `If-None-Match` with `304`. A
|
||||
|
|
@ -65,6 +71,13 @@ info:
|
|||
`title` and `detail` are for a developer and a log: a client MUST NOT show either, and draws the
|
||||
phrase from `code` — a neutral one for a code it does not know. See `Problem`.
|
||||
|
||||
**A failure that is NOT this shape is still a failure a user sees.** A gateway answering `502`
|
||||
with an HTML page, a connection that dies mid-body, a body that does not parse: there is no
|
||||
`code` to dispatch on and there may be no `X-Request-Id`. The rule is the same as everywhere —
|
||||
**the client shows NOTHING from that response, not one byte of its body**, and draws its own
|
||||
neutral phrase from what it does know: the status class if there is one, "the service could not
|
||||
be reached" if there is not. This is not an error class of this API; it is the floor under it.
|
||||
|
||||
## Versioning
|
||||
|
||||
Semver. A client MUST ignore unknown fields and MUST tolerate unknown enum values without
|
||||
|
|
@ -178,10 +191,19 @@ paths:
|
|||
wire. `Location` names the book card. Parsing has no numeric progress; its END arrives on the
|
||||
book's event stream as an ordinary status change.
|
||||
|
||||
Refusals: `400` unreadable form, missing or late part, over-long part, too many parts,
|
||||
malformed language code, or a pair this deployment cannot translate (`code` and `errors[]`
|
||||
say which) · `404` this deployment takes no books at all (`intake_enabled`) · `408` the body
|
||||
did not finish in time, retry · `413` over `intake_max_bytes`.
|
||||
Refusals, the common ones and NOT an exhaustive list — the authority is the response's own
|
||||
`code`, and every code of `ErrorCode` may answer here: `400` unreadable form, missing or late
|
||||
part, over-long part, too many parts, malformed language code, or a pair this deployment
|
||||
cannot translate (`code` and `errors[]` say which) · `404` this deployment takes no books at
|
||||
all (`intake_enabled`) · `408` the body did not finish in time, retry · `413` over
|
||||
`intake_max_bytes`.
|
||||
|
||||
**`Idempotency-Key` on a multipart body.** "The same request" is compared over the DECLARED
|
||||
parts — the metadata and the file's name and size — and never over the bytes themselves: a
|
||||
server does not hold a book in memory to compare it, and a retry of an interrupted upload
|
||||
re-sends the same file. Consequence, and it is the case that matters: **a `408` is not a
|
||||
completed attempt**, so the same key may be presented again; the retry is a repeat of the
|
||||
first call, not a second book.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ClientHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
|
|
@ -241,8 +263,14 @@ paths:
|
|||
is written once at intake and never rewritten. The language pair is NOT patchable: changing
|
||||
it is a re-translation, not an edit.
|
||||
|
||||
Accepted while a run is live: a rename touches nothing a run reads. `title: null` is refused
|
||||
with `400`.
|
||||
Accepted while a run is live: a rename touches nothing a run reads.
|
||||
|
||||
⚠ **`title: null` is refused with `400`** (`invalid_request`, `errors[]` pointing at
|
||||
`/title`) — and that is a deliberate narrowing of RFC 7386, where `null` means "remove this
|
||||
member". A book without a title is not a state this surface has: the field is required on
|
||||
`Book`, and the intake fills it from the file name when the user gives none. "Remove the
|
||||
title" therefore has no meaning to grant, and answering `400` is honest where silently
|
||||
restoring the derived name would be a rename the user did not ask for.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ClientHeader'
|
||||
requestBody:
|
||||
|
|
@ -430,6 +458,12 @@ paths:
|
|||
|
||||
Submission is PARTIAL and accumulates on the server — a closed tab must not cost an hour of
|
||||
work.
|
||||
|
||||
**A `term_id` this book's bank does not hold** — a stale row from before a rebuild, or one
|
||||
belonging to another book — refuses the WHOLE call: `400`, `code: invalid_request`, with an
|
||||
`errors[]` entry pointing at the item (`/decisions/2/term_id`, item code `unknown`). Refusing
|
||||
the batch rather than skipping the row is deliberate: a silently dropped decision reads on
|
||||
the screen as a decision that was saved.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ClientHeader'
|
||||
requestBody:
|
||||
|
|
@ -481,7 +515,7 @@ paths:
|
|||
description: |
|
||||
`stop_for_signing` and `ceiling_chapters` are parameters of the RUN, not of the book: they
|
||||
travel with the start and do not outlive it. With `stop_for_signing` the run waits for the
|
||||
bank to be signed; without it the unsigned bank is carried forward marked as unverified.
|
||||
bank to be signed; without it the run does not stop and uses the bank as it stands.
|
||||
|
||||
**409** answers a limit that no longer fits — the bounds are read by
|
||||
`GET /books/{bookId}/run-options` and may move in between. When another book's hold is the
|
||||
|
|
@ -541,6 +575,14 @@ paths:
|
|||
later one of its kind, and a client MUST tolerate counters that jump. `note` is an ADDITION:
|
||||
it MUST NOT be coalesced or dropped — a lost one is lost silently and forever.
|
||||
|
||||
⚠ **That guarantee covers one connection, not a gap between two.** Nothing is buffered for a
|
||||
client that is not attached, the live buffer above is not promised, and a `resync_required`
|
||||
is not owed for an ordinary reconnect — so a note raised while the client was away can be
|
||||
missing from the stream. **After EVERY reconnect the client MUST close that gap with a delta
|
||||
read** — `GET /books/{bookId}/notes?after_version=<the revision it last applied>` — before
|
||||
trusting its list. The same read repairs any state frame lost with it, and costs one request
|
||||
against a validator when nothing changed.
|
||||
|
||||
**Reconnect.** The client sends `Last-Event-ID`. The server MAY resend frames it still holds
|
||||
in a short live buffer after that id and MUST NOT replay history beyond it; the buffer's size
|
||||
is not declared and a client MUST NOT depend on any frame being resent. If the server cannot
|
||||
|
|
@ -551,6 +593,13 @@ paths:
|
|||
book is at rest, is answered `204` — which is how SSE is told to stop reconnecting. A request
|
||||
WITHOUT `Last-Event-ID` always opens a new stream.
|
||||
|
||||
⚠ **A book at rest must not become an endless open-and-close.** Every frame carries an `id`,
|
||||
`hello` included — `0` on a book that has produced no history yet (`EventEnvelope.id`) — so a
|
||||
client always has something to present on the reconnect a browser makes by itself after
|
||||
`end`, and that reconnect is the one answered `204`. The sequence is therefore bounded:
|
||||
`hello` → `end` → close → one reconnect → `204` → the browser stops. A server that omitted
|
||||
the id on `hello` would leave the client with nothing to send and reopen the stream forever.
|
||||
|
||||
One stream per book being watched; there is no library-wide stream, and a list screen does
|
||||
NOT open one per row. A `chapter` frame for a chapter the client does not hold is IGNORED — a
|
||||
frame is never a reason to page a collection. A deleted book ends its stream, and a reconnect
|
||||
|
|
@ -625,15 +674,25 @@ paths:
|
|||
operationId: resumeRun
|
||||
summary: Continue a stopped run.
|
||||
description: |
|
||||
Clears the bank-signing stop and continues after a stop the user asked for.
|
||||
Clears the bank-signing stop and continues after a stop the user asked for. **What it does
|
||||
in every other state, so a client never has to guess which stop it is looking at:**
|
||||
|
||||
**409 while the set of bank decisions is incomplete** — the stop clears only on a complete
|
||||
set; `cause.code: bank_decisions_incomplete`.
|
||||
| the run stands in | this call answers | the remedy a client offers |
|
||||
|---|---|---|
|
||||
| `awaiting_bank`, decisions complete | continues it — `202` | this call |
|
||||
| `awaiting_bank`, decisions incomplete | `409` `run_not_resumable`, `cause.code: bank_decisions_incomplete` | finish signing, then this call |
|
||||
| `paused` — ANY reason, `null` included | `409` `run_not_resumable`, `cause.code: ceiling_reached` | a NEW run with a larger `ceiling_chapters` |
|
||||
| `stopped` — the user's own stop | continues it — `202` | this call |
|
||||
| `failed`, `ready` | `409` `run_not_resumable`, no `cause` — it is not a stopped run | a NEW run |
|
||||
| `translating` | `409` `run_not_resumable`, no `cause` | nothing; it is already running |
|
||||
|
||||
⚠ **A run stopped at its limit is NOT continued by this call** — `409`,
|
||||
`cause.code: ceiling_reached`. The limit travels with the START of a run and nothing changes
|
||||
it afterwards. The remedy is a NEW run with a larger `ceiling_chapters`: the paused book is
|
||||
startable and finished work is not bought again. A client offers that, not this call.
|
||||
⚠ **A NEW run is legal from ANY paused book, whatever `paused_reason` says — `null`
|
||||
included.** The limit travels with the START of a run and nothing changes it afterwards, so a
|
||||
run stopped at one is never continued by this call; the reason is a hint about WHY the work
|
||||
stopped, never a gate on what may be started next. The book is startable,
|
||||
`POST /books/{bookId}/runs` takes a larger `ceiling_chapters`, and finished work is not
|
||||
bought again. A client that waits for a particular reason value before offering that strands
|
||||
the user on the commonest stop there is.
|
||||
|
||||
**503 answers a deployment that cannot run at all** — continuing a run is starting a process.
|
||||
parameters:
|
||||
|
|
@ -687,6 +746,11 @@ paths:
|
|||
Completion is POLLED: the `202` names the status resource in `Location` and the status read
|
||||
carries `Retry-After` while the build runs. Repeating with the same `Idempotency-Key` returns
|
||||
the original `202` and `Location` rather than building a second copy.
|
||||
|
||||
**The `409` here is about the key, not about the book** — `idempotency_conflict`, when a key
|
||||
is re-used for a different request or while the first is still running. Nothing about a book's
|
||||
state conflicts with exporting it: a book still being translated may be exported, and a second
|
||||
export under a different key is a second artifact, not a conflict.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ClientHeader'
|
||||
requestBody:
|
||||
|
|
@ -768,7 +832,7 @@ components:
|
|||
is established in middleware only; no endpoint may assume a cookie.
|
||||
|
||||
⚠ **Nothing issues such a token today** — the server accepts one, but no call here or in the
|
||||
session mechanics hands one out. Carrier: research/28 §2 (Б-15); companion §3.
|
||||
session mechanics hands one out. The companion carries who owes that and when.
|
||||
|
||||
headers:
|
||||
RequestId:
|
||||
|
|
@ -816,8 +880,13 @@ components:
|
|||
Present on every unsafe request presented by session cookie; the value is arbitrary (see
|
||||
the `sessionCookie` scheme). Absent, such a request is `403` with `code: forbidden`.
|
||||
|
||||
Declared required because the browser client always has to send it; a client presenting a
|
||||
bearer token is exempt by the security scheme.
|
||||
⚠ **Declared `required: true` although one class of client is exempt.** The rule is "required
|
||||
on the COOKIE path", and OpenAPI has no way to make a parameter conditional on which security
|
||||
scheme was used. Declaring it required is the choice that makes a generated browser client
|
||||
send it; a client presenting `Authorization: Bearer` legally omits it, and a validator
|
||||
checking requests against this document must not read that omission as a violation. The
|
||||
server enforces the real rule: the header is demanded only of a request carrying the session
|
||||
cookie.
|
||||
schema: { type: string, minLength: 1 }
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
|
|
@ -865,8 +934,9 @@ components:
|
|||
one revision but several ROWS. Re-reading a row already held costs nothing — a row is
|
||||
replaced by its `id`.
|
||||
|
||||
**The watermark for the next delta read is the `revision` of the envelope just received**;
|
||||
rows carry no version of their own.
|
||||
**The watermark for the next delta read is the revision of the WALK just completed** — the
|
||||
lowest envelope revision seen across its pages, which for a single-page answer is that page's
|
||||
own (`Revision`). Rows carry no version of their own.
|
||||
|
||||
A DELETION cannot be expressed this way. Two answers close that: `resync_required` on the
|
||||
stream when a collection is replaced wholesale, and `400` with
|
||||
|
|
@ -1017,15 +1087,26 @@ components:
|
|||
that book carry the same number. The library has its own scope, and a revision is never
|
||||
compared across scopes.
|
||||
|
||||
**One number per ENVELOPE.** A response carries the revision at which the server built THAT
|
||||
response; a frame carries it inside `data`. `BookDetail.revision` is the revision of the
|
||||
envelope, and `BookDetail.book.revision` is the same number — the card is built in one
|
||||
transaction with the book it carries, and a client may use either.
|
||||
|
||||
**Discarding a stale read is the CLIENT's duty**: a read whose revision is lower than what it
|
||||
has already applied MUST be dropped rather than rendered, or the interface rolls progress
|
||||
backwards on every refetch. The comparison is per SCOPE and per COLLECTION — never against
|
||||
the library's, never across two collections of one book, and an export poll is never dropped
|
||||
for carrying an older number than the chapter tree.
|
||||
|
||||
**A list read across several pages is torn**, and its revision is that of the OLDEST page —
|
||||
stamped with the newest, a list whose head predates an applied frame would pass the guard
|
||||
above and overwrite it.
|
||||
**A list read across several pages is torn**, and ONE number stands for the whole walk: the
|
||||
LOWEST revision seen across its pages. Stamped with the newest, a list whose head predates an
|
||||
applied frame would pass the guard above and overwrite it.
|
||||
|
||||
That single number is also **the watermark for the next delta read** of that collection.
|
||||
Taking the newest envelope of the walk instead would skip every row changed between the first
|
||||
page and the last; taking the lowest re-reads a few rows, which costs nothing because a row
|
||||
is replaced by its `id` (`after_version`). A one-page read is the same rule with one page in
|
||||
the walk.
|
||||
|
||||
**Catch-up after a reconnect reads `revision >= R`, not `> R`**: one transaction is one
|
||||
revision but SEVERAL frames.
|
||||
|
|
@ -1223,15 +1304,16 @@ components:
|
|||
again;
|
||||
- `not_configured` — this deployment has nothing to read the book AGAINST. A state of the
|
||||
service, and retrying alone does not clear it;
|
||||
- `parser_unavailable` — the service failed on the file repeatedly and gave up. Also a state
|
||||
of the service, and temporary;
|
||||
- `processing_failed` — the service tried this file repeatedly and gave up. Also a state of
|
||||
the service and a temporary one: named by what happened, not by which component of ours
|
||||
was unwell, because the name is what a client keys a phrase on;
|
||||
- `content_refused` — the service will not translate this book. **One coarse reason for a
|
||||
whole class**: it never says which check refused, never varies between attempts, and gives
|
||||
nothing to search against. A client shows one neutral phrase and does not invite a retry.
|
||||
|
||||
A client MUST tolerate an unknown value under a minor bump and MUST render a rejected book
|
||||
that carries no reason at all: `null` is legal.
|
||||
enum: [source_unreadable, not_configured, parser_unavailable, content_refused]
|
||||
enum: [source_unreadable, not_configured, processing_failed, content_refused]
|
||||
|
||||
PausedReason:
|
||||
type: string
|
||||
|
|
@ -1582,7 +1664,8 @@ components:
|
|||
type: string
|
||||
description: |
|
||||
How much attention the note asks for. Two steps today; how many there ought to be is an open
|
||||
product question (companion K-6), and a client MUST tolerate a new value under a minor bump.
|
||||
product question the companion tracks, and a client MUST tolerate a new value under a minor
|
||||
bump.
|
||||
enum: [attention, glance]
|
||||
|
||||
Note:
|
||||
|
|
@ -1665,7 +1748,8 @@ components:
|
|||
type: object
|
||||
description: |
|
||||
A memory bank row. Provenance is `origin` and the term's two surfaces are `src`/`dst`; the
|
||||
name `source` is deliberately unused — companion §2.8.
|
||||
name `source` is deliberately unused here, because it means something else on the other side
|
||||
of this boundary and a field name must not mean two things.
|
||||
required: [id, src, dst, kind, status, origin, sense, since_chapter, until_chapter]
|
||||
properties:
|
||||
id:
|
||||
|
|
@ -1816,7 +1900,9 @@ components:
|
|||
type: boolean
|
||||
description: |
|
||||
Stop when the book's terms are ready and wait for them to be signed; without it the
|
||||
unsigned bank is carried forward marked as unverified.
|
||||
run does not stop for it and uses the bank as it stands. Nothing on this surface marks a
|
||||
book as having been translated against an unsigned bank: what is observable is the bank
|
||||
itself — `BankPage.signed` against `BankPage.total`.
|
||||
ceiling_chapters:
|
||||
type: integer
|
||||
minimum: 1
|
||||
|
|
@ -1829,6 +1915,24 @@ components:
|
|||
|
||||
RunOptions:
|
||||
type: object
|
||||
description: |
|
||||
What a run may be started with, read immediately before starting one.
|
||||
|
||||
**Five places on this surface can say "there is no credit", and they answer different
|
||||
questions. In this order:**
|
||||
|
||||
1. the `409` of `POST /books/{bookId}/runs` (`ceiling_unavailable`) — the only authority on
|
||||
whether THIS start may happen; every read below can be stale by the time it is used;
|
||||
2. `RunOptions.ceiling.max_chapters == 0` and `RunOptions.blocked` — whether a start is worth
|
||||
offering AT ALL right now, and what is holding it. This is what the start screen draws;
|
||||
3. `Run.paused_reason: credit_exhausted` — why a run that already ran stopped. History, not a
|
||||
gate: a new run may still be startable (see `resumeRun`);
|
||||
4. `Usage.halt_reason` — the ACCOUNT is halted, which is a state of the account and not of
|
||||
any book;
|
||||
5. `Usage.state: exhausted` — the account screen's own summary, the coarsest of the five.
|
||||
|
||||
A client that read them in the opposite order would refuse to offer a run the server would
|
||||
have accepted.
|
||||
required: [ceiling, blocked]
|
||||
properties:
|
||||
ceiling: { $ref: '#/components/schemas/CeilingBounds' }
|
||||
|
|
@ -1872,6 +1976,13 @@ components:
|
|||
|
||||
`max_chapters: 0` means no run can start — the client shows the exhausted state instead of a
|
||||
scale, and `RunOptions.blocked` may say what is holding it.
|
||||
|
||||
⚠ **When `max_chapters` is `0` the three numbers are not a range.** `min_chapters` stays `1`
|
||||
— it is the smallest limit this service will ever accept, not a bound derived from what is
|
||||
affordable — so the pair reads `min 1, max 0`, which is empty by construction. **A client
|
||||
tests `max_chapters == 0` FIRST** and never treats `min_chapters` as the start of a scale
|
||||
before it has done so. The alternative, moving `min` to `0`, would make "one chapter" and
|
||||
"nothing at all" the same value.
|
||||
required: [min_chapters, max_chapters, default_chapters]
|
||||
properties:
|
||||
min_chapters:
|
||||
|
|
@ -1958,7 +2069,7 @@ components:
|
|||
description: |
|
||||
Machine reason when `state` is `failed`, `null` otherwise; the phrase is the client's.
|
||||
Not enumerated — how an export can fail depends on formats that do not exist yet. It
|
||||
becomes an enum with the first built format. Carrier: research/28 §2 (Б-4).
|
||||
becomes an enum with the first built format; the companion carries why it is open.
|
||||
url:
|
||||
type: [string, 'null']
|
||||
format: uri
|
||||
|
|
@ -1972,7 +2083,29 @@ components:
|
|||
EventEnvelope:
|
||||
type: object
|
||||
description: |
|
||||
An SSE frame. OpenAPI does not type stream frames, so the mapping is fixed here:
|
||||
An SSE frame.
|
||||
|
||||
⚠ **This schema is a DESCRIPTION, not the shape of a JSON object on the wire.** OpenAPI
|
||||
cannot type `text/event-stream`, so the three members below are written as one object; on the
|
||||
wire they are not one. A frame is exactly:
|
||||
|
||||
```
|
||||
event: progress
|
||||
id: 57
|
||||
data: {"revision":1841,"structure_version":3,"progress":{...}}
|
||||
|
||||
```
|
||||
|
||||
`event` and `id` are SSE FIELDS of the frame (`EventSource` exposes them as `event.type` and
|
||||
`event.lastEventId`); the JSON body of the frame is `data` ALONE, and it is the `data` schema
|
||||
of the table below — never an object containing `event`/`id`/`data`. A heartbeat is a comment
|
||||
line (`:` and a newline) and is not a frame.
|
||||
|
||||
The two numbers in that example are deliberately unlike: `id` is this frame's position in the
|
||||
book's event history, `revision` is the book's state. They are not the same counter and a
|
||||
client never substitutes one for the other.
|
||||
|
||||
OpenAPI does not type stream frames, so the mapping is fixed here:
|
||||
|
||||
| `event` | `data` schema | When |
|
||||
|---|---|---|
|
||||
|
|
@ -2007,12 +2140,22 @@ components:
|
|||
consumes none of its own, so the same id legally appears more than once in one stream. A
|
||||
client stores the id it last saw and sends it back; it never counts with it.
|
||||
|
||||
**A book that has produced no history at all** — never run, still arriving — has no last
|
||||
frame, and its connection frames carry `0`. History numbering starts at `1`, so `0` is
|
||||
"nothing yet" and can never collide with a real frame. This is what stops the loop on a
|
||||
book at rest: the client has an id to send, presents `Last-Event-ID: 0`, and is answered
|
||||
`204` instead of another empty stream.
|
||||
|
||||
**A gap is legal** — coalescing removes frames, and a client MUST NOT read a skipped
|
||||
number as a lost frame.
|
||||
data:
|
||||
description: |
|
||||
Frame payload. `anyOf` and not `oneOf`: dispatch is by the event NAME, and two frames
|
||||
legally carry the same shape.
|
||||
Frame payload — the JSON body of the SSE frame, per the table above.
|
||||
|
||||
`anyOf` and not `oneOf`: dispatch is by the event NAME, and two frames legally carry the
|
||||
same shape. **`resync_required` and `end` are exactly that case** — both are `EventBase`
|
||||
and nothing more, so a client that tried to tell frames apart by their members would
|
||||
confuse them. Dispatch on `event` first, always; the shape only validates what arrived.
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/EventHello'
|
||||
- $ref: '#/components/schemas/EventStatus'
|
||||
|
|
@ -2114,22 +2257,25 @@ components:
|
|||
complete: { type: boolean }
|
||||
|
||||
EventResyncRequired:
|
||||
description: |
|
||||
The server cannot resume from the presented `Last-Event-ID`, or the book's collections were
|
||||
replaced wholesale. The client MUST re-read what it holds in full: a delta cannot express a
|
||||
deletion.
|
||||
|
||||
Payload is exactly `EventBase` — no member of its own. Told apart from `end` by the frame's
|
||||
`event` name and by nothing else; see the dispatch rule on `EventEnvelope.data`.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EventBase'
|
||||
- type: object
|
||||
description: |
|
||||
The server cannot resume from the presented `Last-Event-ID`, or the book's collections
|
||||
were replaced wholesale. The client MUST re-read what it holds in full: a delta cannot
|
||||
express a deletion.
|
||||
|
||||
EventEnd:
|
||||
description: |
|
||||
Nothing further will arrive: no run is live and no intake is in flight. The client closes and
|
||||
does NOT reconnect automatically; it opens a new stream when it has a reason to watch again.
|
||||
|
||||
Payload is exactly `EventBase` — no member of its own. Told apart from `resync_required` by
|
||||
the frame's `event` name and by nothing else.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EventBase'
|
||||
- type: object
|
||||
description: |
|
||||
Nothing further will arrive: no run is live and no intake is in flight. The client closes
|
||||
and does NOT reconnect automatically; it opens a new stream when it has a reason to watch
|
||||
again.
|
||||
|
||||
Problem:
|
||||
type: object
|
||||
|
|
@ -2195,8 +2341,8 @@ components:
|
|||
$ref: '#/components/schemas/LocalizedMessage'
|
||||
description: |
|
||||
A phrase written by the SERVER, to be shown as it is — the single exception, for causes
|
||||
that cannot be enumerated in advance. No code in this version carries it. Carrier:
|
||||
research/28 §8 п.4.
|
||||
that cannot be enumerated in advance. No code in this version carries it; the companion
|
||||
carries the rule for when one may.
|
||||
|
||||
ErrorCode:
|
||||
type: string
|
||||
|
|
@ -2228,8 +2374,9 @@ components:
|
|||
being cut, or was rejected;
|
||||
- `run_not_stoppable` (409) — this run is not running;
|
||||
- `run_not_resumable` (409) — this run cannot be continued. `cause.code` says why:
|
||||
`bank_decisions_incomplete` — terms are still undecided; `ceiling_reached` — the run stopped
|
||||
at its limit, and the remedy is a NEW run with a larger one, not this call;
|
||||
`bank_decisions_incomplete` — terms are still undecided; `ceiling_reached` — the run
|
||||
stopped at a limit, the chapters it bought or the credit behind them, and the remedy is a
|
||||
NEW run rather than this call. A run that is not a stopped run at all carries no `cause`;
|
||||
- `ceiling_unavailable` (409) — the limit asked for cannot be started. `cause.code`: `bounds_moved`
|
||||
— the bounds changed between the read and this call; `credit_held` — the account's credit
|
||||
is held elsewhere, and `blocked` names the book holding it;
|
||||
|
|
@ -2295,7 +2442,8 @@ components:
|
|||
minLength: 1
|
||||
description: |
|
||||
`missing` · `missing_or_late` (absent, or sent after the file) · `malformed` ·
|
||||
`too_long` · `unsupported_pair` · `out_of_range`. Not closed, like `cause.code`.
|
||||
`too_long` · `unsupported_pair` · `out_of_range` · `unknown` (an identifier this book
|
||||
does not hold). Not closed, like `cause.code`.
|
||||
examples: ['missing_or_late']
|
||||
|
||||
LocalizedMessage:
|
||||
|
|
|
|||
|
|
@ -12,36 +12,36 @@
|
|||
|
||||
## 0. Что сделано, одним экраном
|
||||
|
||||
| | было (0.2.3) | стало (0.3.0) |
|
||||
| | было (0.2.3) | стало (0.3.0, после дофикса) |
|
||||
|---|---|---|
|
||||
| операций / путей | 16 / 15 | **20 / 17** |
|
||||
| строк файла | 1445 | **2290** |
|
||||
| форма (структурные строки) | 828 | **1336** (+61 %) |
|
||||
| проза (строки внутри описаний) | 472 | **757** (+60 %) |
|
||||
| доля прозы | 36 % | **36 %** |
|
||||
| причина отказа на проводе | английская фраза, показываемая «as-is» | машинный `code` (15 корневых) + `cause` + `request_id` + `errors[]` |
|
||||
| строк файла (`wc -l`) | 1444 | **2463** |
|
||||
| форма (структурные строки) | 760 | **1183** |
|
||||
| проза (строки внутри описаний) | 540 | **1055** |
|
||||
| доля прозы | 42 % | **47 %** |
|
||||
| причина отказа на проводе | английская фраза, показываемая «as-is» | машинный `code` (16 корневых) + `cause` + `request_id` + `errors[]` |
|
||||
| прогресс | `{draft{done,total}, edit{done,total}}` по всей книге, в юнитах | ОДНА полоса до ближайшей остановки, в главах, знаменатель — купленный объём |
|
||||
| поток | `GET /runs/{runId}/events` | `GET /books/{bookId}/events` + кадр `end` + `204` |
|
||||
| условные чтения | нет ни одного | `ETag`/`If-None-Match`/`304` на 7 чтениях + дельта `?after_version=` |
|
||||
| «что умеет деплой» | нигде | `GET /capabilities` |
|
||||
|
||||
Команды, которыми получены числа:
|
||||
Команды, которыми получены числа (пере-ран после ПОСЛЕДНЕЙ правки дофикса — требование ФБ-10):
|
||||
```
|
||||
grep -c 'operationId:' openapi.yaml # 16 → 20
|
||||
grep -cE '^ /' openapi.yaml # 15 → 17
|
||||
wc -l openapi.yaml # 1445 → 2290
|
||||
git show HEAD:...openapi.yaml > /tmp/old.yaml # снимок «было» для всех сверок
|
||||
wc -l openapi.yaml # 1444 → 2463
|
||||
awk-счёт значений ErrorCode # → 16
|
||||
python3 /tmp/prose_form.py (скрипт приложен в §4.4) # проза/форма обеих версий
|
||||
git show 1022567:…openapi.yaml > /tmp/old.yaml # снимок «было» для всех сверок
|
||||
```
|
||||
Проза/форма меряны одним скриптом по обоим файлам (лист-скан: непустая строка внутри блочного
|
||||
скаляра `description:|`/`summary:|` — проза, остальные — форма; пустые не считаются). Число «43 %» из
|
||||
research/28 §5а получено другой меркой и с этими двумя не сравнивается — сравнимы только они между
|
||||
собой.
|
||||
|
||||
**Форма выросла на 61 %, проза — на 60 %, доля прозы не сдвинулась.** Это и есть ответ на вопрос §8
|
||||
п.3 «не переусложнён ли контракт»: батч добавил четыре операции и шестнадцать схем, и текст вырос
|
||||
ровно пропорционально поверхности, а не быстрее неё. Как этого добились — §4.4; первая редакция
|
||||
батча давала 43 % и 2587 строк, то есть проза росла быстрее формы, и это было исправлено отдельным
|
||||
проходом.
|
||||
**Форма выросла на 56 %, проза — на 96 %; доля прозы выросла с 42 % до 47 %.** Это ответ на
|
||||
вопрос §8 п.3 «не переусложнён ли контракт», и он честнее прежнего: батч добавил четыре операции и
|
||||
шестнадцать схем, а каждая находка трёх линз селф-ревью и десять пунктов дофикса закрываются
|
||||
ПРАВИЛОМ — а правило это проза. Прежний вывод «доля не сдвинулась» держался на мерке, где строка
|
||||
`description: |` считалась формой; скрипт теперь приложен (§4.4), число можно оспорить командой, и
|
||||
утверждать «вернул к уровню 0.2.3» я больше не могу. Что резалась вода, а не норма, доказано отдельно
|
||||
и от мерки не зависит: типами и мультимножеством модальных глаголов (§4.4).
|
||||
|
||||
## 1. Записка-план: каждый ID заказа с диспозицией
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ research/28 §5а получено другой меркой и с этими д
|
|||
| | из ВСЕХ описаний вычистить конвейерные слова | **ИСПОЛНЕНО**, проверено грепом (§4.2) | весь файл |
|
||||
| | счётчик ГЛАВЫ той же сегментной логикой | **ИСПОЛНЕНО.** `ChapterProgress.units_done` — «в текущем сегменте», с явным «иначе дерево читало бы ноль всю первую волну» | `§ChapterProgress.units_done` |
|
||||
| | завести гейт на утечку | **ЧАСТИЧНО — по границе зон, как заказано промтом §3.** Правило записано в компаньон как ревью-вопрос каждой правки со списком грепа; ТЕСТ — половина фронта, носитель — пинг фронту 16.08, исполнение при разморозке | компаньон §5 |
|
||||
| **§5.1 / Б-1** | модель ошибок, вариант B | **ИСПОЛНЕНО ПЕРВОЙ** (порядок §3 промта соблюдён: словарь кодов определён до формулировок Б-2/Б-3/Б-8/Б-14а). `code` (15 корневых, закрытый) + `cause{code}` (второй уровень, НЕ закрытый) + `request_id` + `errors[]{pointer,code}` + `localized` + `blocked`; `title`/`detail` объявлены developer-facing и неотображаемыми, `title` получил `minLength:1` | `§Problem`, `§ErrorCode`, `§ErrorCause`, `§ErrorItem` |
|
||||
| **§5.1 / Б-1** | модель ошибок, вариант B | **ИСПОЛНЕНО ПЕРВОЙ** (порядок §3 промта соблюдён: словарь кодов определён до формулировок Б-2/Б-3/Б-8/Б-14а). `code` (16 корневых, закрытый) + `cause{code}` (второй уровень, НЕ закрытый) + `request_id` + `errors[]{pointer,code}` + `localized` + `blocked`; `title`/`detail` объявлены developer-facing и неотображаемыми, `title` получил `minLength:1` | `§Problem`, `§ErrorCode`, `§ErrorCause`, `§ErrorItem` |
|
||||
| | два класса конкретности §8а | **ИСПОЛНЕНО.** Класс 1 — максимальная конкретика (`errors[]` + `cause`); класс 2 — ОДИН код `content_refused` без `cause`, без `errors`, без вариации между попытками; тем же правилом заведено значение `RejectReason.content_refused` (К-9) | `§ErrorCode.content_refused`, `§RejectReason` |
|
||||
| **§5.2 / Б-15** | `X-TM-Client` машинно · 403 · `WWW-Authenticate` · множество методов · `servers.url` · судьба bearer | **ИСПОЛНЕНО ПОЛНОСТЬЮ.** Заголовок — параметр на 9 небезопасных операциях; `403` — компонент-ответ; `401` несёт обязательный `WWW-Authenticate`; методы приведены к коду и RFC (`OPTIONS` освобождён); `servers[0].url: /v0`; bearer ОСТАВЛЕН с явной записью «сервер принимает, выдать нечем» + носитель | `§ClientHeader`, `§responses.Forbidden`, `§responses.Unauthorized`, `§sessionCookie`, `servers`, `§bearerToken` |
|
||||
| **§5.3 / Б-2** | `GET /capabilities` | **ИСПОЛНЕНО.** `contract_version` · `language_pairs[]` со статусом · `intake_max_bytes` · `export_formats[]` · `page_size_default`. Один плоский ответ на деплой (предупреждение судьи о соразмерности учтено: ни ETag-мультитенантности, ни пер-аккаунтных оверрайдов) | `/capabilities`, `§Capabilities` |
|
||||
|
|
@ -71,7 +71,7 @@ research/28 §5а получено другой меркой и с этими д
|
|||
| ID | Заказ | Диспозиция | Где |
|
||||
|---|---|---|---|
|
||||
| **§5.5 / Б-6** | перевесить поток на книгу | **ИСПОЛНЕНО** — `GET /books/{bookId}/events`, `streamBookEvents` | `/books/{bookId}/events` |
|
||||
| | кадры конца разбора и конца прогона | **ИСПОЛНЕНО ИНАЧЕ, чем буквально:** конец разбора — обычная смена статуса на книжном потоке (кадр не нужен, и Б-6(а) сам это говорит: «чинить не кадром, а перевеской»); конец ПОТОКА — новый кадр `end` | `§EventEnd` |
|
||||
| | кадры конца разбора и конца прогона | **ОТСТУПЛЕНИЕ, аргумент:** конец разбора — обычная смена статуса на книжном потоке (кадр не нужен, и Б-6(а) сам это говорит: «чинить не кадром, а перевеской»); конец ПОТОКА — новый кадр `end` | `§EventEnd` |
|
||||
| | `204` при переподключении к завершённому | **ИСПОЛНЕНО** с точной границей: `204` только на запрос С `Last-Event-ID`; без него всегда открывается новый поток | `/books/{bookId}/events` → `204` |
|
||||
| | развести `id` кадра и ревизию | **ИСПОЛНЕНО.** `id` — позиция потока, форма зафиксирована `^[0-9]+$`; `revision` — в `data` каждого кадра через `EventBase` | `§EventEnvelope.id`, `§EventBase` |
|
||||
| | правило докачки — одно | **ИСПОЛНЕНО:** короткий живой буфер разрешён, реплей истории запрещён, размер не объявляется | `/books/{bookId}/events` → Reconnect |
|
||||
|
|
@ -106,6 +106,9 @@ research/28 §5а получено другой меркой и с этими д
|
|||
| **§5.15 / Б-10+Б-11** | максимум `limit` и подрезание | **ИСПОЛНЕНО:** `maximum: 1000` (= `maxPage`, `pgstore/books.go:587`) + «CLAMPED DOWN, never answered with the default instead» | `§Limit` |
|
||||
| | объявленный порядок коллекций | **ИСПОЛНЕНО для всех пяти** (библиотека · главы · юниты · замечания · банк) | описания пяти list-операций |
|
||||
| | `maxItems` у `decisions` | **ИСПОЛНЕНО** (1000) | `§BankDecisionsRequest` |
|
||||
| **Б-23 / §3 промта** | жанр — выкинуть из `BookIntake` и `Book` | **ИСПОЛНЕНО** (слово владельца 16.08). Поля нет ни в интейке, ни в карточке; остальные половины удаления — снятие `{{genre}}` из промптов пары и из канона брифа — строка 184 и НЕ моя зона. ⚠ Строка отсутствовала в записке-плане первой редакции и дописана дофиксом (ФБ-10) | `§BookIntake`, `§Book` |
|
||||
| **К-10 / D39.138 п.5** | пофазность у главы — НЕ строить | **ИСПОЛНЕНО КАК «НЕ СТРОИТЬ».** Фазы не выходят на провод ни у книги, ни у главы: `ChapterProgress.units_done` — один счётчик «в текущем сегменте». Колонки `units_draft_done`/`units_edit_done` read-модели остаются внутренним делом платформы. Строка дописана дофиксом (ФБ-10) | `§ChapterProgress` |
|
||||
| **В-6 / D39.138 п.2з** | `blocked: {code, book_id}` на run-options и 409 | **ИСПОЛНЕНО** обоими носителями: `RunOptions.blocked` (required, nullable) и расширение `Problem.blocked` у `ceiling_unavailable`; `cause.code: credit_held` называет причину, `book_id` — держателя. Строка дописана дофиксом (ФБ-10) | `§RunOptions.blocked`, `§Blocked`, `§Problem.blocked` |
|
||||
| **§5.16 / Б-14** | ОДНО правило кодирования отсутствия | **ИСПОЛНЕНО: правило ЗАПИСАНО** отдельным разделом шапки и применено к `Usage.halt_reason`, `Run.finished_at`/`paused_reason`/`failure_reason`, `Book.character_count`/`reject_reason`, `Progress.eta_seconds`, `Unit.note`, `RunOptions.blocked`, `Export.*` | info §Absence of a value |
|
||||
| | условная обязательность `Unit.target` | **ИСПОЛНЕНО** — `if/then` + слова (К-11: генератор `if/then` игнорирует) | `§Unit.target` |
|
||||
| | имена конвертов | **ИСПОЛНЕНО:** `Library/ChapterList/UnitList/NoteList/Bank` → `BookPage/ChapterPage/UnitPage/NotePage/BankPage` на общем `Page` через `allOf` | `§Page` и др. |
|
||||
|
|
@ -138,7 +141,7 @@ research/28 §5а получено другой меркой и с этими д
|
|||
| слить формально пять конвертов на `allOf` | **ИСПОЛНЕНО** |
|
||||
| `EventChapter` = подмножество `Chapter` | **ИСПОЛНЕНО:** выделен `ChapterProgress`, `Chapter` и `EventChapter` собраны из него через `allOf` — переименования `id`→`chapter_id` больше нет, клиент не разбирает его руками |
|
||||
| не доводить до финала `listBankTerms`/`listNotes` | **УЧТЕНО:** словарь ступеней не проектировался (К-6), поля `BankTerm` не добавлялись (D39.136 п.4б), `Note.code` НЕ сделан enum до написания фраз, число страницы замечаний не выдумывалось |
|
||||
| генезис-проза (semver-история, две апологии RFC 9110, объяснение имени `source`) | **ПЕРЕНЕСЕНО в компаньон**, не удалено (промт §5д) |
|
||||
| генезис-проза (semver-история, две апологии RFC 9110, объяснение имени `source`) | **УЖАТО НА МЕСТЕ, не перенесено** — отступление от §5д промта, аргумент в §1.7(д) |
|
||||
| зеркальный дефект `/usage` построен и не читается | **ЗАПИСАНО** в таблицу компаньона §3 |
|
||||
|
||||
### 1.6. §5б шаги 3–4 и Б-11а (заказ ПОВЕРХ §5)
|
||||
|
|
@ -157,7 +160,7 @@ research/28 §5а получено другой меркой и с этими д
|
|||
| **(б)** таблица «чтение → источник → строка бэклога» + правило «предупреждение несёт номер строки» | **ИСПОЛНЕНО** — компаньон §3, 19 строк; правило поставлено над таблицей |
|
||||
| **(в)** провенанс-классы ✓/◆/○ под 0.3.0 | **ИСПОЛНЕНО** — компаньон §0, «Пере-разметка 0.3.0»: четыре класса сдвинуты, `Progress` ✓→◆ с объяснением, почему прежняя ✓ и была дефектом |
|
||||
| **(г)** приложение А на словарь кодов Б-1, слова не заполнять | **ИСПОЛНЕНО.** Карта переведена на «причина движка → КОД контракта → фраза ⬜»; класс 2 схлопнут в один код (нормативно, §8а); коды помечены ◆ и в спеку как enum НЕ занесены. Добавлено приложение А-2 — карта кодов ОШИБОК, заполненная целиком с грунтом `file:line` на каждый |
|
||||
| **(д)** генезис-проза из спеки — в компаньон | **ИСПОЛНЕНО** (27 блоков перенесено/сжато) |
|
||||
| **(д)** генезис-проза из спеки — в компаньон | **ОТСТУПЛЕНИЕ, аргумент.** Фактически проза УЖАТА НА МЕСТЕ до ссылки на источник (`semver §4`, одна строка про `Retry-After` на `200`), а не перенесена в компаньон: переносить в спутник абзац, от которого осталась ссылка, значит плодить третью копию. Проверяется грепом — этих абзацев в компаньоне нет. Ярлык «ИСПОЛНЕНО (27 блоков перенесено)» был неверен и исправлен дофиксом (ФБ-10); компаньон §6а поправлен там же |
|
||||
|
||||
## 2. Решения, которые я принял сам (промт §4)
|
||||
|
||||
|
|
@ -189,7 +192,7 @@ research/28 §5а получено другой меркой и с этими д
|
|||
### 2.4. `promote` → `approve`
|
||||
|
||||
Б-0 называет `promote`/`decline` глаголами оператора майнера (они дословно из
|
||||
`pipeline/mining.go:201`). Плюс `promote` порождал `status: approved` — два слова на один акт.
|
||||
`pipeline/mining.go:81,243`). Плюс `promote` порождал `status: approved` — два слова на один акт.
|
||||
`decline` оставлен: обычное слово, к именам файлов майнера не привязано.
|
||||
|
||||
### 2.5. Порядок работ
|
||||
|
|
@ -292,7 +295,7 @@ cd frontend && npx spectral lint ../docs/architecture/14-api-contract/openapi.ya
|
|||
```
|
||||
cd frontend && npx openapi-typescript ../docs/architecture/14-api-contract/openapi.yaml \
|
||||
-o <scratchpad>/schema-probe.ts
|
||||
→ EXIT=0, 2916 строк
|
||||
→ EXIT=0; на заленденном каноне 2608 строк, после дофикса 16.08 — 2798
|
||||
```
|
||||
Прогонялся трижды: после первой редакции, после де-утечки, после правок по селф-ревью.
|
||||
|
||||
|
|
@ -309,12 +312,49 @@ cd frontend && npx openapi-typescript ../docs/architecture/14-api-contract/opena
|
|||
|
||||
### 4.4. Резка прозы — отдельный проход, проверенный типами
|
||||
|
||||
Первая редакция батча дала 2587 строк при доле прозы 43 % (у 0.2.3 — 36 %). То есть я исполнил букву
|
||||
⚠ **Пере-счёт 16.08 (ФБ-10): исходный вывод «доля прозы не сдвинулась» держался на методе, который
|
||||
не был приложен, и другим методом не воспроизводится.** Скрипт приложен ниже, чтобы число можно было
|
||||
оспорить командой, а не поверить на слово:
|
||||
|
||||
```python
|
||||
# /tmp/prose_form.py — проза = строки блочных скаляров description/summary (и строка ключа),
|
||||
# форма = всё прочее непустое; конец блока определяется по возврату отступа на уровень ключа.
|
||||
import sys, re
|
||||
def measure(path):
|
||||
lines = open(path).read().split('\n'); prose = form = 0; in_block = False; key_indent = 0
|
||||
for ln in lines:
|
||||
if not ln.strip(): continue
|
||||
indent = len(ln) - len(ln.lstrip())
|
||||
if in_block:
|
||||
if indent > key_indent: prose += 1; continue
|
||||
in_block = False
|
||||
m = re.match(r'^(\s*)(description|summary):\s*\|', ln)
|
||||
if m: prose += 1; in_block = True; key_indent = len(m.group(1)); continue
|
||||
if re.match(r'^\s*(description|summary):\s+\S', ln): prose += 1; continue
|
||||
form += 1
|
||||
return prose, form
|
||||
```
|
||||
|
||||
```
|
||||
0.2.3 : проза 540 · форма 760 · доля 42 %
|
||||
0.3.0 заленденный: проза 930 · форма 1185 · доля 44 %
|
||||
0.3.0 + дофикс : проза 1055 · форма 1183 · доля 47 %
|
||||
```
|
||||
|
||||
Честный вывод вместо прежнего: **доля прозы выросла — с 42 % до 44 % при батче и до 47 % после
|
||||
дофикса**, потому что каждая находка холодного потребителя закрывается ПРАВИЛОМ, а правило это проза.
|
||||
Абсолютные числа (форма 760 → 1183) не спорны ни при каком методе; спорна именно доля, и утверждать
|
||||
«вернул к уровню 0.2.3» я больше не могу. Что резалась вода, а не норма, доказано отдельно — типами и
|
||||
мультимножеством модальных глаголов ниже, и эти два доказательства от метода мерки не зависят.
|
||||
|
||||
Первая редакция батча дала 2586 строк при доле прозы 50 % (приложенным скриптом). То есть я исполнил букву
|
||||
§5а (вынести названный генезис в компаньон) и провалил её предмет: объяснял ПОЧЕМУ прямо в описании
|
||||
поля, хотя рядом лежит компаньон ровно под это.
|
||||
|
||||
Проход по всему файлу: в описании остаётся правило и обязанность потребителя, «потому что» уходит в
|
||||
компаньон либо снимается. Итог — 2290 строк, проза 1011 → 757 (−25 %), доля 43 % → 36 %.
|
||||
компаньон либо снимается. Итог прохода, ТЕМ ЖЕ скриптом: 2586 → 2315 строк, проза 1163 → 930
|
||||
(−20 %), доля 50 % → 44 %. Дофикс потом вернул часть — 1055 и 47 %, — потому что каждый его пункт
|
||||
это новое ПРАВИЛО; ниже видно, что правил прибавилось, а не воды.
|
||||
|
||||
**Что резалась именно вода, а не норма, проверено двумя способами исполнением:**
|
||||
|
||||
|
|
@ -345,7 +385,8 @@ python: для каждого MUST / MUST NOT / MAY / SHOULD взять мода
|
|||
|
||||
```
|
||||
grep -niE "draft|wave|stage names|mined|miner|ruby|finaliz|chunk|langpack|sanitiz|flagged|verdict|prompt|engine|pipeline|glossar|promote|snapshot|CJK" openapi.yaml
|
||||
→ ровно 1 вхождение: строка 21 — сама формулировка запрета в шапке.
|
||||
→ ровно 1 вхождение: строка 18 — сама формулировка запрета в шапке.
|
||||
(пере-ран после дофикса 16.08: по-прежнему ровно одно, там же)
|
||||
```
|
||||
Список составлен из слов, названных Б-0, плюс найденные мной самостоятельно: `glossar` (два слова на
|
||||
одну сущность — «bank» и «glossary»), `snapshot` (у движка это конкретный артефакт ре-биллинга),
|
||||
|
|
@ -387,13 +428,13 @@ failed_reason`/`ready_at`) · `runs/runs.go:187-231` (`paused` в allowlist ст
|
|||
не шёл искать). Мандат: «ты пишешь клиента с нуля». Вернул 47 находок, ранжированных по цене
|
||||
реализатору.
|
||||
|
||||
**Ноль находок не случился ни у одного, и это правильный исход.** Принято и исправлено 26 находок;
|
||||
**Ноль находок не случился ни у одного, и это правильный исход.** Пере-считано по таблицам ниже (ФБ-10): двумя линзами найдено **57** позиций — 46 исправлено (§5.1) и 11 отклонено с причиной (§5.2); третьей, кросс-модельной, ещё **12** (§5.3). Итого **69 находок, из них 57 исправлено**;
|
||||
четыре из них — дефекты, которые прошли бы в код P7 и стоили бы там на порядок дороже (F1, F8/F9,
|
||||
F32, C2). Обе линзы независимо нашли одно и то же в трёх местах (структурная версия · `X-TM-Client`
|
||||
против bearer · `Book` без ревизии в ответе на запись) — это и есть подтверждение, что находки не
|
||||
шум.
|
||||
|
||||
### 5.1. Исправлено (26 позиций)
|
||||
### 5.1. Исправлено (46 позиций)
|
||||
|
||||
| # | Линза | Находка | Что сделано |
|
||||
|---|---|---|---|
|
||||
|
|
@ -444,12 +485,12 @@ F32, C2). Обе линзы независимо нашли одно и то ж
|
|||
| **C6** | а | Правила докачки опираются на тождество, которого `id` кадра не несёт: «позиция в потоке» не уникальна между потоками одной книги, а обе нормы (буфер докачки и `204`) решают именно «тот ли это поток» | `id` переопределён как позиция в истории событий КНИГИ; `204` переформулирован без «поток, названный `Last-Event-ID`» |
|
||||
| C4 / B1 | а | `Chapter.units_done` как сегментный счётчик противоречит «`Book.chapters_done` никогда не идёт назад» и не определён для главы ВНЕ текущего сегмента | Формулировка уточнена: счётчик ПРОХОДА (`0` — не дошли, `units_total` — прошли, глава вне прохода держит прежнее) + явная разводка с `Book.chapters_done`. Сама сегментная логика — прямой заказ промта батча §3, а не самодеятельность: опровергатель промта не видел |
|
||||
|
||||
### 5.2. Отклонено — с причиной (12 позиций)
|
||||
### 5.2. Отклонено — с причиной (11 позиций)
|
||||
|
||||
| # | Линза | Находка | Почему отклонено |
|
||||
|---|---|---|---|
|
||||
| A2 | а | Три строки таблицы §5а не вырезаны: `Note.unit_id`, `Bank.signed`, нагрузка кадров | **Диспозиция была записана ДО ревью** — §3.3, §3.5, §3.6 этого отчёта и §6а компаньона, с контраргументом на каждое. Опровергатель отчёта и прозы компаньона не видел по условию мандата; его вердикт «без прикрытия» основан на неполном входе. Аргументы стоят |
|
||||
| **A3** | а | Файл вырос на 63 %, доля прозы выросла | **ПРИНЯТО и ИСПРАВЛЕНО отдельным проходом** (§4.4), после того как владелец назвал ту же вещь прямо. Итог: 2587 → 2290 строк, проза −25 %, доля 43 % → 36 % — ровно уровень 0.2.3, при форме, выросшей на 61 %. Проверено, что резалась вода: генерённые типы до и после резки идентичны с точностью до JSDoc, нормативных правил потеряно ноль. Заказанное содержание НЕ сокращалось |
|
||||
| **A3** | а | Файл вырос на 63 %, доля прозы выросла | **ПРИНЯТО и ИСПРАВЛЕНО отдельным проходом** (§4.4), после того как владелец назвал ту же вещь прямо. Итог прохода: 2586 → 2315 строк, проза −20 %, доля 50 % → 44 %. ⚠ Прежняя приписка «вернул ровно к уровню 0.2.3 (36 %)» СНЯТА дофиксом: она держалась на мерке, где ключевая строка `description: |` считалась формой, и приложенным скриптом не воспроизводится — уровень 0.2.3 это 42 %, и до него проход не дошёл. Что резалась ВОДА, а не норма, доказано независимо от мерки: генерённые типы до и после резки идентичны с точностью до JSDoc, нормативных правил потеряно ноль |
|
||||
| B2 | а | `Book.reject_reason` сделан обязательным вопреки §4 №22 | Сам опровергатель называет правку justified: единое правило отсутствия (§5 п.16 того же заказа) не держится с неперепроверенным исключением, а прежний довод («деплой старше этого минора») внутри ЛОМАЮЩЕГО минора пуст |
|
||||
| B3 | а | `Note.message` снят, введён `Note.code` | Признано justified самим опровергателем; аргумент — §2.3. Вынесено вопросом в §8 |
|
||||
| B4 / B6 | а | Контентность `BankTerm.id` записана; дисциплина Б-21 применена в спеке | Обе — прямые «стоит записать» из research/28 §3 п.6 и Б-21 |
|
||||
|
|
@ -470,7 +511,9 @@ F32, C2). Обе линзы независимо нашли одно и то ж
|
|||
у первых двух линз не было, потому что резка случилась после них.
|
||||
|
||||
Результат: **1 HIGH и 4 MEDIUM, все — дефекты моей новой формы; ни одного неисполненного пункта
|
||||
заказа.** Все девять находок приняты и исправлены.
|
||||
заказа.** В таблице ниже **12 строк: 11 приняты и исправлены, одна отклонена осознанно** (числа
|
||||
интейка — аргумент в самой строке). Прежняя формулировка «все девять» не сходилась с таблицей и
|
||||
исправлена дофиксом (ФБ-10).
|
||||
|
||||
| # | Находка | Что сделано |
|
||||
|---|---|---|
|
||||
|
|
@ -518,7 +561,7 @@ F32, C2). Обе линзы независимо нашли одно и то ж
|
|||
§5а-оговорка. Коды предложены в компаньоне (◆), enum появится вместе с фразами.
|
||||
5. **`Export.failure_code` не enum** — по той же причине: способы упасть зависят от форматов, которых
|
||||
не существует (S7). Носитель записан в самой схеме.
|
||||
6. **Рантайм не поднимался.** Ни платформа, ни фронт не запускались: платформа 8 из 20 операций не
|
||||
6. **Рантайм не поднимался.** Ни платформа, ни фронт не запускались: платформа 12 из 20 операций не
|
||||
отвечает вовсе, а фронт заморожен и живёт на моках. Всё, что я утверждаю о поведении сервера,
|
||||
выведено чтением кода (§4.3), а не наблюдением. Соответственно НЕ проверено исполнением:
|
||||
реальная форма `problem+json` при 413/408 посреди тела; поведение `Idempotency-Key` (не построен);
|
||||
|
|
@ -557,8 +600,7 @@ F32, C2). Обе линзы независимо нашли одно и то ж
|
|||
| `getUsage` | `paused_reason` необязателен, типизирован прогонным enum | `halt_reason` required+nullable, свой `AccountHaltReason` |
|
||||
| `createExport` / `getExport` | `{id, ready, url?}`; опрос не завершался | `{id, revision, state, format, expires_at, failure_code, url}`; формат из `capabilities`; правило доступа к ссылке |
|
||||
|
||||
**Проход резки прозы (§4.4):** 2587 → 2290 строк, доля прозы 43 % → 36 %; типы не изменились ни на
|
||||
поле.
|
||||
**Проход резки прозы (§4.4):** 2586 → 2315 строк, проза −20 %; типы не изменились ни на поле.
|
||||
|
||||
**Прочее, изменённое селф-ревью:** `BookIntake.title` стал ОБЯЗАТЕЛЬНЫМ (пустая строка = «назови по
|
||||
файлу») — в схеме не осталось необязательных членов, и порядок частей больше не зависит от повадки
|
||||
|
|
@ -592,3 +634,51 @@ F32, C2). Обе линзы независимо нашли одно и то ж
|
|||
полях СМЫСЛА («смысла хватает, но не слишком подробно»), а это поле — состояние подписи, то есть
|
||||
предмет Б-14а, который в батч входит. Но чтение — не право сделать иначе. Вопрос владельцу; если
|
||||
ответ «заводить», правка стоит четырёх строк схемы и одной строки P7.
|
||||
|
||||
---
|
||||
|
||||
## 9. Дофикс-раунд 16.08 (заказ приёмки D39.142): диспозиция по каждому ФБ
|
||||
|
||||
> ⚠ **Провенанс дерева.** Часть правок дофикса приехала в рабочее дерево от ПАРАЛЛЕЛЬНОЙ сессии,
|
||||
> которую владелец остановил. Я не принимал их на веру: каждую сверил с заказом и с кодом, три
|
||||
> дефекта в них нашёл и починил, один невыполненный пункт доделал. Ниже — состояние ПОСЛЕ этой
|
||||
> сверки; там, где я правил чужую правку, это названо.
|
||||
|
||||
| ФБ | Что требовалось | Диспозиция |
|
||||
|---|---|---|
|
||||
| **1** (HIGH) | Wire-форма кадра неоднозначна: `EventEnvelope{event,id,data}` как объект против SSE-фрейминга | **ИСПОЛНЕНО.** На схеме стоит дословный пример кадра и сказано прямо: `EventEnvelope` — ОПИСАНИЕ, а не форма JSON на проводе; `event` и `id` — поля SSE (`event.type`/`event.lastEventId` у `EventSource`), телом кадра является `data` и только оно; heartbeat — комментарий, не кадр |
|
||||
| **2** (HIGH) | Книга в покое без истории: `hello` без id → клиент без `Last-Event-ID` → вечный цикл | **ИСПОЛНЕНО.** История нумеруется с `1`, служебные кадры пустой книги несут `0`; `Last-Event-ID: 0` попадает под уже существующее правило `204`. Последовательность конечна: `hello` → `end` → закрытие → одно переподключение → `204` → браузер останавливается |
|
||||
| **3** (HIGH) | «`note` не теряется» не переживает reconnect | **ИСПОЛНЕНО.** Обещание не расширено (буфер обещать нечем) — названа ОБЯЗАННОСТЬ клиента: после КАЖДОГО переподключения дельта-чтение `/notes?after_version=<последняя применённая ревизия>` до того, как доверять списку |
|
||||
| **4** (HIGH) | `Revision` противоречит себе; `BookDetail.revision` vs `book.revision` | **ИСПОЛНЕНО.** Одна модель: одно число на КОНВЕРТ; у многостраничного обхода за всё чтение отвечает НАИМЕНЬШАЯ увиденная ревизия — и как гард свежести, и как водяной знак следующей дельты (лишнее перечитывание бесплатно, пропуск строки — нет); равенство `BookDetail.revision` и `BookDetail.book.revision` заявлено явно |
|
||||
| **5** (MED) | Пустые `allOf`-члены → необитаемые типы | **ИСПОЛНЕНО и ПРОВЕРЕНО ГЕНЕРАТОРОМ.** `EventEnd` и `EventResyncRequired` теперь `allOf` из одного члена с описанием на самой схеме; в генерённых типах оба — `components["schemas"]["EventBase"]`, `Record<string, never>` в наших схемах не осталось (два вхождения в файле — служебные `webhooks`/`$defs` генератора). Записано и то, что раньше подразумевалось: эти два кадра неразличимы по форме, диспетчеризация только по имени |
|
||||
| **6** (MED) | Что делает `resume` по каждому статусу; «новый прогон» легален при ЛЮБОМ `paused` | **ИСПОЛНЕНО, таблица ИСПРАВЛЕНА мной.** В пришедшей редакции таблица клала КОРНЕВОЙ код в слот причины (`cause.code: run_not_resumable`) и оставляла три строки вовсе без кода. Исправлено: корневой код `run_not_resumable` во всех 409, `cause.code` только там, где причина есть (`bank_decisions_incomplete`, `ceiling_reached`), а «это вообще не остановленный прогон» идёт БЕЗ `cause`. Расширен и сам `ceiling_reached`: он покрывает и купленные главы, и кредит за ними — иначе строка `credit_exhausted` таблицы указывала на код, который её не описывает. Значение enum не заводилось (D39.132 п.2а не двигается) |
|
||||
| **7** (MED) | Ответ НЕ `problem+json` — потерянный пункт Б-1 рек. 4 | **ИСПОЛНЕНО.** Записан пол под моделью ошибок: из такого ответа клиент не показывает ни байта и рисует свою нейтральную фразу — по классу статуса, если он есть, иначе «сервис недоступен» |
|
||||
| **8** (MED, 10 позиций) | Пачка недоопределённостей | **ИСПОЛНЕНЫ все десять:** `title: null` → 400 с объяснением сужения RFC 7386 · разводка `X-TM-Client` `required` против освобождения bearer записана в самом параметре · список отказов интейка помечен неисчерпывающим (авторитет — `code`) · правило резолюции `Location` отдельным разделом шапки · тождество `Idempotency-Key` на multipart по объявленным частям + `408` не считается состоявшейся попыткой · `min_chapters` при `max_chapters: 0` — не диапазон, максимум проверяется первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» СНЯТО как обещание без носителя, заменено на наблюдаемое (`BankPage.signed` против `total`) · `409` у `createExport` объяснён как ключевой, а не книжный · неизвестный `term_id` отклоняет весь батч решений с указателем на позицию |
|
||||
| **9** (LOW) | `parser_unavailable` по эффекту; вычистить внутренние ссылки из описаний | **ИСПОЛНЕНО, ДОДЕЛАНО мной.** Переименование в `processing_failed` на месте; ⚠ проводное имя теперь расходится с внутренним словарём платформы (`parse.go:66`) — проекция это вход P7, записано в компаньон. Ссылки: пришедшая редакция сняла три (`research/28 §2 Б-15`, `§8 п.4`, `§2 Б-4`, «companion K-6»), но пропустила две — цитату D-нот в `info.summary` и `companion §2.8` у `BankTerm`; обе сняты мной. Греп по спеке на `research/28|companion §|К-N|D39.` — ноль |
|
||||
| **10** (отчёт) | Пере-ран чисел ПОСЛЕ последней правки + хвосты | **ИСПОЛНЕНО, ДОПРАВЛЕНО мной.** Пришедшая редакция закрыла: `ErrorCode` 16 (сверено `awk`-счётом), якорь `mining.go:81,243` (сверено — цитата «for EACH term either promote … OR decline» стоит на `:243`), «строка 21» → 18, «12 из 20 не отвечает» (сверено: `contractRoutes` регистрирует 8 путей из 20 операций), строки записки-плана Б-23/К-10/В-6, README §6а «ужаты на месте», скрипт мерки прозы приложен. **Осталось и починено мной:** §0 противоречил сам себе (заголовок «проза выросла быстрее», а следующим предложением «выросла ровно пропорционально») · §0 держал старую долю 36 % против новой мерки в §4.4 · заголовки §5.1/§5.2 говорили «26 позиций»/«12 позиций» против фактических 46/11 в самих таблицах · ярлык §5.5 «ИСПОЛНЕНО ИНАЧЕ» не был заменён на «ОТСТУПЛЕНИЕ, аргумент» · все числа пере-раны после ПОСЛЕДНЕЙ правки, а не после предпоследней |
|
||||
|
||||
### 9а. Проверка дофикса (кросс-модельная, по слову владельца)
|
||||
|
||||
Одна линза другой модели, узкий мандат: закрыт ли каждый ФБ · нет ли регрессий · правда ли то, что
|
||||
дофикс УТВЕРЖДАЕТ о коде. **Вердикт: ФБ-1..9 закрыты (ФБ-8 — 10/10 подпунктов), регрессий в каноне
|
||||
не найдено, все кодовые утверждения подтверждены, кроме одного якоря.** Неприкосновенное цело —
|
||||
проверено грепом на месте: прогонного потока нет, `Note` несёт `code`, `content_refused` в обоих
|
||||
enum, `page_size_default` единственный носитель размера страницы. Нормативных правил заленденный
|
||||
канон → нынешний: 44 → 45, потеряно ноль.
|
||||
|
||||
| Находка | Диспозиция |
|
||||
|---|---|
|
||||
| **Битый якорь в компаньоне:** `Resume` приписана `runs/runs.go`, а живёт в `runs/reconcile.go:906` (ветка `case "paused"` — `:919`, дневной потолок — `:931`). Пакет верный, файл нет | **ИСПРАВЛЕНО.** Моя ошибка, не чужая: строку писал я. Прочие ссылки на `runs/runs.go:227-231` (`readyToTranslate`) проверены и ВЕРНЫ — там функция действительно живёт |
|
||||
| LOW: пример кадра давал `id: 1841` и `"revision":1841` — одно число на два счётчика, которые батч специально разводил; холодный потребитель мог склеить их обратно | **ИСПРАВЛЕНО:** в примере теперь `id: 57` при `revision: 1841`, плюс строка «эти два числа намеренно непохожи и одно не подменяет другое» |
|
||||
| LOW: код позиции `unknown` введён в `submitBankDecisions`, но отсутствует в иллюстративном списке `ErrorItem.code` — потребитель его там не найдёт | **ИСПРАВЛЕНО:** добавлен в список |
|
||||
| Наблюдение: версия осталась `0.3.0`, хотя после лендинга переименовано значение enum (`parser_unavailable` → `processing_failed`) — «0.3.0» теперь обозначает два разных содержания | **ПРИНЯТО КАК ЕСТЬ, называю оркестратору.** Дофикс заказан САМОЙ нотой приёмки как часть того же раунда (D39.142 п.3), потребителей у 0.3.0 нет ни одного (зеркало фронта на 0.2.3, платформа на 0.2.3), и бампать минор внутри приёмочного раунда значило бы плодить версию, которую никто не видел. Если оркестратор читает иначе — бамп стоит одной строки |
|
||||
| Требование «пере-ран чисел ПОСЛЕ последней правки» на момент проверки снова не сходилось (дерево правилось при ней) | **ЗАКРЫТО финальным прогоном** — числа §0 сняты после САМОЙ последней правки и сверены; проверка это и предсказывала |
|
||||
|
||||
**Ратификации приёмки, которые дофикс НЕ двигал** (проверено грепом по канону): прогонный поток снят,
|
||||
канал один — книжный · `Note.code` вместо `message` · `content_refused` остаётся значением
|
||||
`RejectReason` · единый `page_size_default` вместо пер-коллекционных.
|
||||
|
||||
**Что дофикс добавил в долг P7, сверх читающей поверхности:** `POST /runs/{runId}/resume` — построенный
|
||||
путь, и 0.3.0 меняет его ответ на паузе по потолку с `202`-без-изменений на `409`
|
||||
`cause.code: ceiling_reached`. Записано строкой в таблицу компаньона §3, потому что это единственное
|
||||
место дофикса, где правится УЖЕ РАБОТАЮЩИЙ код, а не проекция, которой ещё нет.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue