diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml index c845f466..89ca7a63 100644 --- a/docs/architecture/14-api-contract/openapi.yaml +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -1448,11 +1448,40 @@ components: description: | How far THIS RUN has got through the whole of its own work, as ONE monotonic fraction. - ⚠ **THE UNIT FOLLOWS HOW THE ORDER WAS PHRASED, and there is exactly one discriminator — - `Run.delivered_chapters`:** a number ⇒ the counters are CHAPTERS · `null` ⇒ the order was - placed in characters and the counters are the engine's own shipping units, the same `Unit` - this contract already carries · a re-pass ⇒ `total` is `1`, a declared shape and not a count - of anything. **Do not render a unit noun without checking it** — "3 of 10 chapters" drawn + ⚠ **THE UNIT FOLLOWS HOW THE ORDER WAS PHRASED, and it is read off `Run.ordered_chapters` + and `Run.ordered_units` — the pair that describes the ORDER — in this order, first match + wins:** + + 1. **`ordered_chapters == 0` ⇒ a RE-PASS.** `total` is `1` and `done` is `0` until it + finishes clean, then `1`: a declared shape, not a count of anything. Do not render it as + "0 of 1 chapters". Held by `pgstore.TestARePassRunsBarIsOneUnitOfWork`, and its + EXCLUSIVITY — that nothing else can put a zero here — by + `runs.TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun`: a whole-book order + over a fully delivered book quotes zero chapters, and the service refuses it rather than + admitting a run that would wear the re-pass's shape without being one. + 2. **`ordered_chapters == null` ⇒ the order was placed in CHARACTERS**, and the counters are + the engine's own shipping units — the same `Unit` this contract already carries. The size + bought stands in `ordered_units`. Held by + `runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought` and + `runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`. + 3. **Otherwise (`ordered_chapters >= 1`) ⇒ the counters are CHAPTERS.** Held by + `runs.TestAChapterOrderKeepsTheBarItAlwaysHad`. + + ⛔ **THE ORDER OF THOSE CHECKS IS LOAD-BEARING, and `Run.delivered_chapters` is NOT the + discriminator** — an earlier edition of this paragraph said it was, and it was wrong in the + one case that matters. A re-pass answers `delivered_chapters: 0`, which is a NUMBER, so the + rule "a number means chapters" sends a client to case 3 and it draws the very "0 of 1 + chapters" this paragraph forbids. `delivered_chapters: null` does mark case 2 — it is derived + from the same fact — but it cannot separate case 1 from case 3, so a client that starts there + gets a re-pass wrong. Measured, not reasoned: the re-pass row of + `pgstore.TestARePassRunsBarIsOneUnitOfWork` asserts `delivered_chapters: 0` beside + `ordered_chapters: 0`. + + ⚠ **Do not key on `stage` either.** `stage: re_pass` is true of case 1 today, but `stage` is + an OPEN vocabulary whose new values move no version — a client owes it degradation, not + decisions. It corroborates; it does not discriminate. + + **Do not render a unit noun without running the three checks** — "3 of 10 chapters" drawn over units is a lie the client tells on our behalf. A client that draws a PERCENTAGE needs none of this: the fraction is meaningful in every case, which is why no ready-made percentage is shipped. A run may make more than one pass over the chapters it bought, and it may stop @@ -1818,7 +1847,7 @@ components: `resume` clears nothing — nothing was withdrawn. ordered_chapters: type: [integer, 'null'] - minimum: 1 + minimum: 0 description: | ⚠ **`null` when the order was NOT placed in chapters** — an order phrased in characters carries its size in `ordered_units` instead. **Exactly one of the two is @@ -1827,6 +1856,17 @@ components: ⚠ **`0` is legal and means a re-pass:** it buys no chapters while being a perfectly ordinary run. A client that treats this field as at-least-one will reject valid answers. + ⚠ **The bound was `minimum: 1` until this was measured, and it contradicted the line + above it** — a generated validating client would have rejected every re-pass. `null` was + never at risk (a bound constrains numbers, not the null branch); the value it excluded + was the zero. Held by `pgstore.TestARePassRunsBarIsOneUnitOfWork`, which asserts `0` on + BOTH surfaces a run is handed out on — the start receipt and the read path — because they + are two different expressions and a promise made once must hold on both. + + The bound is kept rather than dropped: `0` is the floor and not merely today's smallest + value. A negative is refused at admission by the service itself, so the schema says out + loud what the store already guarantees. + ⚠ **Changed in 0.12.0.** Until then this field carried, for a character order, the SPAN of chapters the order reached into — a number that could exceed what was bought (an order of one unit out of four reported `1`). That was a defect: the field is named for what was @@ -2696,7 +2736,17 @@ components: - type: string - type: 'null' description: | - Where the chapter boundaries came from, passed through from the engine's manifest. + Where the chapter boundaries came from, passed through from the engine's manifest, or + `null` when NO MANIFEST HAS ANSWERED YET. + + ⚠ **`null` is not a fourth kind of cut, and it is not `none`.** `none` is a book the engine + looked at and found one chapter in; `null` is a book it has not been asked about. Having no + provenance and having a poor one are different facts, and only the second says anything + about the book. Held by + `httpapi.TestABookWhoseCutCannotBeSoldAgainstOffersCharactersAndSaysSo`, which asserts an + explicit `null` rather than an invented word. ⚠ The sibling `Book.structure` carries the + same fact and says the same in its own words; the two cannot disagree. + ⚠ **An open vocabulary that GROWS additively** — today `declared`, `delimited`, `detected`, `none` (⚠ `delimited` added by the engine 06.09: the file drew a separator — an EPUB spine, a form feed — but calling it a CHAPTER is the engine's assumption, not the format's word). 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.