From 75ae6f06ca60efa6150e7ab6bf227fcfe6a745b9 Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 6 Sep 2026 01:30:50 +0300 Subject: [PATCH] Make the contract describe the wire it announces: the order options, the money slider, the precision signal, the second pause reason and the retired ceiling schema --- .../architecture/14-api-contract/openapi.yaml | 189 ++++++++++++++++-- 1 file changed, 168 insertions(+), 21 deletions(-) diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml index 449119af..25b370b9 100644 --- a/docs/architecture/14-api-contract/openapi.yaml +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -17,9 +17,11 @@ info: **Nothing about HOW a book is translated crosses this boundary** — no model names, no phase or stage names, no internal vocabularies, and no cost of any model, stage or call. ⚠ **Money the ACCOUNT owns is not in that set** (owner, 05.09): the balance, an order's ceiling and its hold are - the user's own figures and WILL be shown as amounts — the fields land with the order-form minor, and - until they do, `Usage` and `CeilingBounds` below still carry the retired rule (⚠ read them together - with this paragraph, not instead of it). The earlier blanket «no money sums» was the owner's own + the user's own figures and WILL be shown as amounts — ⚠ **ИСПР. 06.09: условие ИЗРАСХОДОВАНО.** Минор формы + заказа ЗАЛАНДЕН (`0.11.0`), суммы на проводе: `balance_micro_usd`, `limit.{min,max}_micro_usd`, + `estimate.{expected,hold}_micro_usd`. `Usage` ретированное правило по-прежнему НЕСЁТ и это верно + (её провод не изменился); `CeilingBounds` — РЕТИРОВАНА целиком и больше не отдаётся ни одной + операцией. The earlier blanket «no money sums» was the owner's own rule of 02.08 and he RETIRED it on 05.09; what stays banned is the structure of OUR spend. The projection is an allowlist: a field not named here never reaches the client. The rule binds the PROSE too — every description here is compiled into the generated client's source. @@ -1569,11 +1571,17 @@ components: PausedReason: type: string description: | - Machine reason a run is paused; the client draws the phrase. One value today. A client MUST - tolerate an unknown one under a minor bump and MUST render a paused run whose reason is - `null` — the ordinary answer when the service has no word for what stopped it — showing the - neutral "halted, continuable" state. - enum: [credit_exhausted] + Machine reason a run is paused; the client draws the phrase. **TWO values since the + order-form minor (0.11.0), and the distinction is load-bearing because the remedies are + OPPOSITE:** `credit_exhausted` — the ACCOUNT has no money, the remedy is to top up; + `run_limit_reached` — the account is fine and the RUN delivered everything that was ordered, + the remedy is to order more. Reading the second as the first sends a paying user to a + payment page they do not need. + + A client MUST tolerate an unknown one under a minor bump and MUST render a paused run whose + reason is `null` — the ordinary answer when the service has no word for what stopped it — + showing the neutral "halted, continuable" state. + enum: [credit_exhausted, run_limit_reached] AccountHaltReason: type: string @@ -1616,6 +1624,8 @@ components: - chapter_count - chapters_done - character_count + - character_count_exact + - structure - added_at - note_count - shape_epoch @@ -1679,8 +1689,34 @@ components: **The chosen cure is a PRECISION SIGNAL BESIDE the number, not a change to what `null` means** — `null` already says "still arriving", a live client renders it as `0`, and overloading it would break a reader that is deployed. The field lands with the - order-form pack, which is the one that makes this number a PRICE; until it does, this - description is the only place that says the number can be wrong. + order-form pack, which is the one that makes this number a PRICE. ⚠ **ИСПР. 06.09: ПРИЕХАЛО + ЛЕНДИНГОМ 0.11.0** — сигнал точности стоит рядом полем `character_count_exact`, и это + описание больше не единственное место, где сказано, что число может быть неверным. + character_count_exact: + type: boolean + description: | + Whether `character_count` is the source's own characters (`true`) or a proxy this side + could not decode — runes of an EPUB's ZIP container, or of a stream in an encoding the + engine re-reads (`false`). ⚠ **Added by the order-form minor (0.11.0)**, because the + same minor turns this number into a PRICE: selling by characters against an archive's + byte count would charge for the container. + + Beside the number, never inside it: `null` on `character_count` keeps its one meaning, + "still arriving", and a deployed reader that renders `null` as `0` is not broken by this. + structure: + oneOf: + - type: string + - type: 'null' + description: | + Where this book's chapter boundaries came from, passed through from the engine's + manifest; `null` while the book is still arriving. ⚠ **Added by the order-form minor + (0.11.0)** so a client can tell an order in chapters from an order it must phrase in + characters. + + ⚠ **An OPEN vocabulary that grows additively** — today `declared`, `detected`, `none`. + A value this build does not know is read as "not recognised" and is never promoted into + trust. A client MUST do the same: switch on the values it knows and treat the rest as + "no usable structure", rather than compile an exhaustive list that a minor will outgrow. added_at: { type: string, format: date-time } note_count: type: integer @@ -1769,12 +1805,32 @@ components: cleared by a `resume` that SUCCEEDS: continuing IS the withdrawal of the request, and a run that was asked to continue is no longer a run someone asked to stop. A refused `resume` clears nothing — nothing was withdrawn. - ceiling_chapters: + ordered_chapters: type: integer minimum: 1 description: | - The limit this run was started with, in CHAPTERS — a property of the RUN. Present so a - reloaded screen can name the limit the user chose and read `progress.total` against it. + What this run was ORDERED to deliver, in chapters — a property of the RUN. Present so a + reloaded screen can name what the user bought and read `progress` against it. + + ⚠ **Renamed from `ceiling_chapters` by the order-form minor (0.11.0).** The old name said + "limit" when the figure is a PURCHASE: a ceiling is what a run may not exceed, an order is + what it owes. The retired name is rejected on the way IN with a pointer to `/chapters`, + never silently reinterpreted. + delivered_chapters: + oneOf: + - type: integer + minimum: 0 + - type: 'null' + description: | + Whole chapters this run has delivered, or `null` when the question does not apply — + an order expressed in CHARACTERS stops inside a chapter and closes none, and `0` there + would read as "nothing happened" when work was in fact done and paid for. + term_consistency_funded: + type: boolean + description: | + Whether THIS run's reservation carries the book-wide terminology pass. It is a flat bond + on the book, not a per-chapter cost, so a small order can be short of it while a large + one is not — and the screen must say so BEFORE the click, not after. progress: { $ref: '#/components/schemas/Progress' } paused_reason: oneOf: @@ -2504,7 +2560,7 @@ components: 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 + 2. `RunOptions.order.verdict: covers_none` 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`); @@ -2514,9 +2570,16 @@ components: A client that read them in the opposite order would refuse to offer a run the server would have accepted. - required: [ceiling, blocked] + required: [order, balance_micro_usd, limit, blocked] properties: - ceiling: { $ref: '#/components/schemas/CeilingBounds' } + order: { $ref: '#/components/schemas/OrderOptions' } + balance_micro_usd: + type: integer + minimum: 0 + description: | + What the account can still spend, in micro-dollars. ⚠ A quantity, not arithmetic: a hold + is a debit when taken, so this figure ALREADY excludes the holds open against it. + limit: { $ref: '#/components/schemas/MoneyLimit' } blocked: oneOf: - $ref: '#/components/schemas/Blocked' @@ -2526,6 +2589,86 @@ components: an account's second book shows a shrunken scale with no way to learn that its own first book is the reason. + OrderOptions: + type: object + description: | + What can be ordered on this book right now, and whether the balance covers it. ⚠ **Added by + the order-form minor (0.11.0).** + required: [chapters_left, affordable_chapters, verdict, chapter_orders, structure, source_chars, term_consistency_funded, estimate] + properties: + chapters_left: + type: integer + minimum: 0 + description: Chapters of this book not yet delivered. + affordable_chapters: + type: integer + minimum: 0 + description: | + How many of those the balance covers, already clamped to `chapters_left`. A client MUST + NOT clamp it again. + verdict: + type: string + enum: [covers_all, covers_part, covers_none] + description: | + The one-word answer the start screen draws BEFORE the click. Computed by the same + arithmetic that admits the order, so the verdict and the click cannot disagree. + chapter_orders: + type: boolean + description: | + Whether an order may be phrased in CHAPTERS at all. `false` when this build does not + trust the book's chapter boundaries — then the only partial order is in characters, and + the screen must say so in words rather than offering a slider against a cut nobody made. + structure: + oneOf: + - type: string + - type: 'null' + description: | + Where the chapter boundaries came from, passed through from the engine's manifest. + ⚠ **An open vocabulary that GROWS additively** — today `declared`, `detected`, `none`. + A value this build does not know is read as "not recognised" and never promoted into + trust; a client MUST do the same rather than switch on an exhaustive list. + source_chars: + type: integer + minimum: 0 + description: Characters of the source this order is priced against. + term_consistency_funded: + type: boolean + description: | + Whether the balance covers the book-wide terminology bond ON TOP of this order. A flat + bond on the book, so a small order can fall short of it while a large one does not — + and the degradation must be visible BEFORE the click. + estimate: { $ref: '#/components/schemas/Estimate' } + + Estimate: + type: object + description: What an order is expected to cost, and what will be reserved for it. + required: [expected_micro_usd, hold_micro_usd] + properties: + expected_micro_usd: + type: integer + minimum: 0 + description: | + The engine's a-priori projection for this order, in micro-dollars — derived from the + text before a single call is made, not a running total. + hold_micro_usd: + type: integer + minimum: 0 + description: | + What is actually reserved: the expectation with headroom, plus the engine's largest + INDIVISIBLE step so the last call of the order cannot be refused for want of room. + + MoneyLimit: + type: object + description: | + Bounds of the money slider. ⚠ **These two are NOT a range.** The minimum is the engine's + indivisible step — the smallest order any run could move under — and it can exceed the + maximum when the balance does not carry even one reservation. That is the honest shape of + "nothing can be bought", and `order.verdict: covers_none` says it in a word. + required: [min_micro_usd, max_micro_usd] + properties: + min_micro_usd: { type: integer, minimum: 0 } + max_micro_usd: { type: integer, minimum: 0 } + Blocked: type: object description: | @@ -2551,11 +2694,15 @@ components: Bounds of the run-limit scale, in CHAPTERS. The conversion to money lives on the platform and is not exposed here in any form. - ⚠ **THIS PARAGRAPH IS THE RETIRED RULE, KEPT UNTIL ITS FIELDS LAND (05.09).** The owner retired - the blanket «no money on screen» ban; an order now states its ceiling AS MONEY and the account - shows a balance. The money fields arrive with the order-form pack — the schema is NOT moved - ahead of the code, so what is written above still describes what this version serves. What is - NOT retired and never was: the price of a model, the cost of a stage, the price of one call. + ⛔⛔ **RETIRED BY THE ORDER-FORM MINOR (0.11.0), 06.09 — THIS SCHEMA IS SERVED BY NOTHING.** + `GET /books/{bookId}/run-options` no longer carries a `ceiling` member at all; its answer is + `order` + `balance_micro_usd` + `limit` + `blocked` (see `RunOptions`). Everything above is + kept as the history of the chapter-scale era and describes NO live wire. + ⚠ The paragraph that stood here until 06.09 said the opposite — «the schema is NOT moved ahead + of the code, so what is written above still describes what this version serves». It was written + expecting the canon never to lag the code; the landing did the reverse, and the sentence + outlived its condition. What is NOT retired and never was: the price of a model, the cost of a + stage, the price of one call. `max_chapters` is what the account can still spend, ALREADY clamped to what is left of the book; a client MUST NOT clamp it again. ⚠ A quantity, not arithmetic: a hold is a debit when