// Package pricing answers the two questions the order form asks: what will this order cost, and how // much money does the platform have to hold for it. // // ⛔ THERE IS NO PER-CHAPTER RATE HERE ANY MORE, and its absence is the point of the package rather // than a tidy-up. Until 05.09 a chapter was worth a CONSTANT — `DefaultPerChapter`, $0.03, provenance // carefully documented and measured to be 4.47× too low the moment DeepSeek re-priced (D39.179 §1) — // and everything downstream inherited its error. A run's new headroom was `chaptersLeft × $0.03`, so // the LAST chapters of any book were bought with less money than one editor call reserves (~$0.07): // the run died in ten seconds having moved nothing, and it did so at any balance. That is PD-440, // and no calibration of a constant could remove it, because the constant shrinks with the book while // the cost of one indivisible call does not. // // What replaced it is the engine's own projection, published per book and per chapter since landing // 81a89e9 (act D39.206): `expected_usd` — what the work will be BILLED, computed with the settlement // formula over the pair's own fertility calibration — and `step_max_usd` — the largest single // INDIVISIBLE reservation, computed with the reservation formula at worst case. Two numbers, two // arithmetics, and the engine states in its own file why publishing one for both is what produced // «this book cannot be translated at any price». // // A book the engine could not price is REFUSED rather than sold at a guess. That refusal is what // makes the deletion of the constant safe: there is now exactly one source for what a book costs, // and when it is silent the platform says so instead of inventing the number that broke. // // PAIR-AGNOSTIC BY CONSTRUCTION. Nothing in this file knows a language, a book or a model: it adds // up figures the engine derived from the pair's own data. A pair that is not in this repository is // priced by these same lines (общность §0.1). package pricing import ( "fmt" "textmachine/platform/internal/money" ) // DefaultHoldFactorPercent is `k`: the cushion the hold carries over the engine's expected bill, // as a percentage. // // ⚠⚠ IT IS DERIVED AND NOT MEASURED, and it is labelled so here because a number that reads like a // measurement while being an argument is the class this project keeps paying for (D39.202 §2в). The // derivation, so a reader can re-do it rather than trust it: // // - `expected_usd` EXCLUDES retries, escalation, repair and additive reasoning by construction // (backend/internal/pipeline/priceprojection.go, projectUnit). It is therefore a LOWER bound of // the bill the moment anything regenerates, and a factor of exactly 1 would latch a run on the // first regeneration of every purchase. // - The only live figures there are: two chapters of one paid run (31.08) came in at $0.093 and // $0.151 — a ratio of 1.62. ⚠ TWO POINTS, NOT A DISTRIBUTION. It bounds the guess; it does not // measure a spread, and the next reader must not quote it as one. The measurement that replaces // this constant is unified backlog row 281. // - The DIRECTION of the error is ratified and it changed on 05.09 with D39.206: a money stop is no // longer a crash. A refusal now LATCHES the wave — what was admitted finishes, the run pauses and // says how much was missing — so a hold that is too small costs the buyer one top-up, while a // hold that is too large freezes credit they cannot spend elsewhere. Err LOW. // // 125 is above 1 (ordinary regeneration does not latch every purchase) and well below the 1.62 seen // (the error stays on the low side, as ratified). const DefaultHoldFactorPercent = 125 // Model is the platform's half of the arithmetic: one operator-set cushion, and nothing else. type Model struct { // HoldFactorPercent is `k` as an integer percentage — CONFIGURATION and not a literal in the // logic, because the measurement of row 281 must arrive as data rather than as a code change. HoldFactorPercent int } // New validates the cushion. func New(holdFactorPercent int) (Model, error) { if holdFactorPercent < 100 { // Below the engine's own expected bill is not a cushion, it is a discount: every purchase // would latch before it delivered what it quoted. return Model{}, fmt.Errorf("pricing: the hold factor must be at least 100%%, got %d", holdFactorPercent) } return Model{HoldFactorPercent: holdFactorPercent}, nil } // maxExpected bounds the projection the arithmetic will act on: one million dollars, which is four // orders of magnitude above the most expensive book anybody has measured ($10.94 for a 500-chapter // webnovel at the standard mix) and far below where `expected × 125` leaves int64. A figure above it // did not come from a book. const maxExpected = money.MicroUSD(1_000_000) * money.PerUSD // Unit is one output unit of a book still to be delivered — the granularity a CHARACTER order is // both expressed against and PRICED from. type Unit struct { // ID is the boundary a character order is stored as: a unit id carries the cut in its own bytes. ID string // Expected is this unit's own bill, and SourceChars its own text. Expected money.MicroUSD SourceChars int64 } // Chapter is one chapter of a book as an order is priced against it: what the engine expects it to // cost, how much text is in it, and how many output units it ships. type Chapter struct { // ID is the chapter's identity — what an order phrased in chapters is ANCHORED on. Carried // through the arithmetic so the quote and the stored order name the same boundary by the same // fact; the ordinal beside it is a label (migration 00033). ID string // Number is the DISPLAY ordinal — the label an order is phrased in («перевести до главы N»). Number int // Units is how many output units this chapter ships. It is what `translate --max-units` counts. Units int Expected money.MicroUSD SourceChars int64 } // Book is what is left of a book to buy, in the order the engine will deliver it. type Book struct { // StepMax is the largest single reservation any one call of this book can ask for. It is the // FLOOR of every hold: a ceiling below it admits nothing at all, whatever else is true. StepMax money.MicroUSD // BookOnce is the BOUND on the book-level passes — the terminology consolidation and its // classifier, which read the whole book's drafts once. It is flat: $2.00 on the shipped arm // whatever the book's length. BookOnce money.MicroUSD // Remaining are the chapters still to be delivered, ascending by Number. Remaining []Chapter } // Verdicts, exactly as the contract's OrderOptions carries them. const ( // VerdictCoversAll — the balance covers the whole of what is left of this book. VerdictCoversAll = "covers_all" // VerdictCoversPart — it covers some chapters but not all of them. VerdictCoversPart = "covers_part" // VerdictCoversNone — it does not cover even one chapter, so no run can start. VerdictCoversNone = "covers_none" ) // Quote is one possible order, priced. It is what the buyer is shown BEFORE the click, and the // numbers in it are what the admission then acts on. type Quote struct { // Chapters is how many of the remaining chapters this order buys; 0 with Units 0 means the order // buys nothing (there is nothing left, or nothing affordable). Chapters int // ThroughChapterID and ThroughChapter are the last chapter bought: its IDENTITY, which is what // the order is stored as, and its ordinal, which is the label beside it. Both empty/zero when the // order is the whole remainder — «the whole book» must not freeze into any boundary at all. ThroughChapterID string ThroughChapter int // Units is how many output units the order buys — the figure `--max-units` is derived from. Units int // UnitShaped says the order was phrased in CHARACTERS, and therefore is measured in units all the // way through: the bar counts units, and `delivered_chapters` is null. // // ⚠ IT IS ABOUT THE PHRASING, NOT ABOUT WHERE THE ORDER LANDED — an earlier edition of this line // said «does not close whole chapters», and that is measurably false: `UnitsFor` returns the first // unit whose running sum covers what was asked, so an order lands on a chapter boundary roughly as // often as chapters divide units (a third of them at three units a chapter), and such an order // closes whole chapters while staying unit-shaped. Pinned by // `runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`. // // The reason to measure in the unit the buyer SPOKE, rather than in whatever the order happens to // cover, is that two purchases a person cannot tell apart must not render differently. The reason // it cannot be chapters is older: a chapter counts only once every unit in it is done, so an order // that stops inside one would read `0/N` for its whole life. UnitShaped bool // Expected is what the engine expects the ordered work to be BILLED. It is the honest half of the // pair a buyer is shown («ожидаемо ≈ …, зарезервируем до …»). Expected money.MicroUSD // Hold is what the account reserves — the other half of that pair, and the number that becomes // the engine's ceiling increment. Hold money.MicroUSD // BondFunded says whether Hold has room for the book-level passes on top of the ordered work. // // ⚠ FALSE IS NOT A FAILURE AND IT MUST NOT BE SILENT. The book-level passes feed the memory bank, // and the bank is the mechanism behind consistency of terms across a whole book — the owner's // first priority, stated as «100% консистентность, никаких разнопереведённых терминов» (D39.198). // They DEGRADE rather than halt when a ceiling refuses them (the engine says so itself: // priceprojection.go, stepMaxForUnit — «a ceiling too small for them costs quality, not // progress»), which is exactly what makes the failure invisible: the book arrives, and only its // terms wander. So the fact travels to the buyer BEFORE the click. Ratified 05.09 by the // orchestrator on this pack's own finding. BondFunded bool } // Hold is the money one order reserves, and the whole formula lives here. // // hold = k × expected + stepMax [+ bookOnce when the balance carries it] // // ⛔ THE `+ stepMax` TERM IS ADDITIVE AND NOT A FLOOR, and that is what removes the last of PD-440 // rather than merely widening it. The engine compares its ceiling against the book's CUMULATIVE // committed + reserved on EVERY reservation, so as an order is delivered the room left over shrinks // by what has been billed. The LAST call of the order therefore needs one whole reservation of room // ON TOP of everything the order has already spent — and `max(k × expected, stepMax)` does not give // it that: on any order whose bill exceeds one reservation the maximum is the bill, the last call is // refused, and the book stops one call short of what was bought. Additive, the room for that call is // there by construction at every point of the order. // // ⛔ `bookOnce` IS NOT IN THE BASE, and the reason is that it is a BOUND rather than a forecast and a // FLAT one — $2.00 on the shipped arm for a book of three chapters and for a book of three hundred // alike (backend/configs/pipeline-c1.yaml: gates.terminology.budget_usd $1.00 plus // classify_budget_usd $1.00). Folded into the base it would put a two-dollar threshold under every // purchase, and a five-chapter book expected to cost twenty-five cents would be unbuyable — PD-440's // wall again, wearing different clothes. So it is added only when the balance carries it on top of // the order, and when it is not added the caller is told, because a silently unfunded consistency // pass is a refusal that looks exactly like normal work (D39.202 §3). func (m Model) Hold(expected, stepMax, bookOnce, balance money.MicroUSD) (hold money.MicroUSD, bondFunded bool) { if expected <= 0 { // Nothing left to buy. The pair «expected 0, reserve one whole reservation plus a flat // book-level bound» is arithmetic rather than an offer, and it reads on a screen as a price // for a book that is finished. Zero across the board says what is true. return 0, true } // ⚠ THE MULTIPLICATION IS BOUNDED BEFORE IT HAPPENS, not after. micro-USD is an int64 and the // cushion multiplies BEFORE it divides — deliberately, because dividing first throws away the // cents — so an absurd projection ($7.4·10¹⁰ and up, which `money.ParseUSD` will accept) would // wrap and hand back a hold SMALLER than the bill it is supposed to cover. Not a wrap into the // negative, which the ledger would refuse: a plausible, small, wrong number. No book reaches it; // a projection read out of a document that is not a manifest could. if expected > maxExpected { expected = maxExpected } base := expected*money.MicroUSD(m.HoldFactorPercent)/100 + stepMax if bookOnce <= 0 { // The deployment runs no book-level pass at all, so there is nothing to fund and nothing to // warn about. `true` is the honest answer: no consistency work is going unpaid. return base, true } if base+bookOnce <= balance { return base + bookOnce, true } return base, false } // Quote prices an order of the first `chapters` remaining chapters. `chapters` at or above what is // left — and zero — means the whole remainder, which is the default order (D39.196 §1: «не тронута // ни одна ручка ⇒ заказ = вся книга»). func (m Model) Quote(b Book, balance money.MicroUSD, chapters int) Quote { if chapters <= 0 || chapters > len(b.Remaining) { chapters = len(b.Remaining) } q := Quote{Chapters: chapters} for _, c := range b.Remaining[:chapters] { q.Expected += c.Expected q.Units += c.Units } if chapters > 0 && chapters < len(b.Remaining) { // A PARTIAL order names the chapter it stops at; the whole remainder deliberately does not, so // that «the whole book» keeps meaning the whole book after a re-cut moves the numbers. q.ThroughChapterID = b.Remaining[chapters-1].ID q.ThroughChapter = b.Remaining[chapters-1].Number } q.Hold, q.BondFunded = m.Hold(q.Expected, b.StepMax, b.BookOnce, balance) return q } // Affordable is the largest number of remaining chapters this balance can buy. // // It WALKS the chapters rather than dividing by an average, because there is no average: the whole // point of reading the engine's projection is that a chapter of a dense Han source and a chapter of // an alphabetic one are very different money, and so are two chapters of one book. // // ⚠ The walk carries a RUNNING SUM rather than re-quoting each prefix. Re-quoting re-adds the // prefix every step, which is quadratic in the chapter count — and this runs on the options read, // which a client polls, over books of a couple of thousand chapters. The arithmetic is the same one // Quote performs; what is not repeated is the summing. func (m Model) Affordable(b Book, balance money.MicroUSD) int { n, expected := 0, money.MicroUSD(0) for i, c := range b.Remaining { expected += c.Expected if hold, _ := m.Hold(expected, b.StepMax, b.BookOnce, balance); hold > balance { break } n = i + 1 } return n } // Options is the whole answer of the order form, before the click. type Options struct { // ChaptersLeft is what is still untranslated. ChaptersLeft int // AffordableChapters is how many of those this balance can buy. AffordableChapters int // Verdict answers the buyer's real question — «хватает на всю книгу или нет». Verdict string // Whole is the quote for the default order: everything that is left. Whole Quote // Balance is the account's money as it is (D39.115 §2a — holds are already debited from it). Balance money.MicroUSD // MinHold is the smallest hold that can buy anything at all: one indivisible reservation. It is // the minimum of the money slider, so the slider cannot express an impossible order (row 276). MinHold money.MicroUSD } // Order computes the order form for a book and a balance. func (m Model) Order(b Book, balance money.MicroUSD) Options { out := Options{ ChaptersLeft: len(b.Remaining), AffordableChapters: m.Affordable(b, balance), Whole: m.Quote(b, balance, 0), Balance: balance, MinHold: b.StepMax, } switch { case out.ChaptersLeft > 0 && out.AffordableChapters >= out.ChaptersLeft: out.Verdict = VerdictCoversAll case out.AffordableChapters > 0: out.Verdict = VerdictCoversPart default: // Includes the book with nothing left to translate: there is no order it can carry, and // `chapters_left: 0` is what tells the two apart on the wire. out.Verdict = VerdictCoversNone } return out } // UnitsFor is the shortest prefix of `units` that reaches `want` characters — the CHARACTER order, // which is the only partial order a book with no chapter structure can carry (D39.196 §1: знаки, // когда книга не распозналась). // // The answer is a UNIT count, because units are the granularity the engine ships and the granularity // `--max-units` counts; characters are what the buyer expresses and are never a stopping point of // their own. // // Rounds UP to a whole unit — a buyer who asks for ten thousand characters gets the unit that // contains the ten-thousandth, not one short of it — and asking for more than the book has buys the // book. // // ⚠ A UNIT OF UNKNOWN SIZE DOES NOT ADVANCE THE SUM, and that is the difference between «this order // covers everything» and «this build could not read the sizes». Zero sizes make every prefix fall // short, so the loop would run off the end and quietly answer «the whole book» — a partial order // silently becoming a total one. The caller is refused instead: sizes are witnessed before a book is // sold at all (ingest.Priced), and this is the second line of that defence rather than a repeat. func UnitsFor(units []Unit, want int64) (int, bool) { if want <= 0 || len(units) == 0 { return 0, false } sum := int64(0) for i, u := range units { if u.SourceChars <= 0 { return 0, false } sum += u.SourceChars if sum >= want { return i + 1, true } } return len(units), true } // QuoteUnits prices an order of the first `n` remaining UNITS — the character order, resolved. // // ⛔ IT PRICES THE UNITS AND NOT THE CHAPTERS THEY FALL IN, which the first edition of this path got // backwards. Quoting a unit order at the price of every chapter it touches makes the order // meaningless exactly where it is the only one available: a book with no chapter structure is ONE // chapter, so buying a thousand characters of it reserved the whole book. Measured on a five-unit // `none` book at $0.03 a unit: the honest hold for one unit is $0.107328 and the chapter-priced one // was $0.257328 — and a buyer holding $0.12 was refused a purchase they could afford. // // `chapters` is carried for the RUN's own bar, which is still counted in chapters (canon §Progress), // and it is the coarser figure of the two: a character order can stop inside a chapter. What the // engine is given is the unit count, which is not coarse. func (m Model) QuoteUnits(b Book, balance money.MicroUSD, units []Unit, n, chapters int) Quote { if n > len(units) { n = len(units) } q := Quote{Chapters: max(chapters, 1), Units: n} for _, u := range units[:n] { q.Expected += u.Expected } q.Hold, q.BondFunded = m.Hold(q.Expected, b.StepMax, b.BookOnce, balance) return q }