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

This commit is contained in:
heaven 2026-09-06 01:30:50 +03:00
parent 358a473a29
commit 75ae6f06ca

View file

@ -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