Pin that a re-pass answers zero on both surfaces a run is handed out on, and that a book with nothing left is refused rather than sold
This commit is contained in:
parent
32a60509fe
commit
33b896b1ac
3 changed files with 156 additions and 0 deletions
|
|
@ -248,6 +248,94 @@ r.ordered_units is not null then` — считают полосу в ЮНИТА
|
|||
под прежним именем (`…ReportsTheChapterSpan…`) и ПЕРЕИМЕНОВАН вместе с переворотом — см. п.0
|
||||
заявления `D39.183`.
|
||||
|
||||
### ⛔ ДОФИКС ПО КАНОНУ 06.09 — ДВА УТВЕРЖДЕНИЯ О ЗНАЧЕНИЯХ, ПЕРЕПИСАННЫЕ ЗОНОЙ ИЗ СВОИХ ПИНОВ
|
||||
|
||||
Заказ оркестратора по норме авторства (строка бэклога 309 / `D39.208`): **утверждение канона о
|
||||
ЗНАЧЕНИИ пишет ЗОНА из своих пинов и называет пин.** Оба утверждения были написаны им без прогона и
|
||||
оказались ложными. Правился ТОЛЬКО текст и схема канона; поведение не менялось ни в одном месте.
|
||||
|
||||
1. **`Run.ordered_chapters` нёс `minimum: 1` при собственной прозе «`0` is legal and means a
|
||||
re-pass».** Схема противоречила своему же описанию, и валидирующий клиент, сгенерированный по ней,
|
||||
отверг бы законный ответ ре-прохода. Сверено по проводу: `runOrderedChapters` для ре-прохода
|
||||
отдаёт `ceiling_chapters`, а он ноль. ⇒ `minimum: 0`. ⚠ Нижняя граница НЕ снята, хотя `null` уже
|
||||
отдельная ветвь типа: ноль — это ПОЛ, а не «сегодняшнее наименьшее», отрицательное отвергает сам
|
||||
допуск стора, и схема теперь говорит вслух то, что сервис и так гарантирует.
|
||||
⚠ **Пина на это не было — завела.** `pgstore.TestARePassRunsBarIsOneUnitOfWork` расширен: он
|
||||
держал только полосу `0/1 re_pass`, теперь держит и `ordered_chapters: 0`, и `ordered_units: null`,
|
||||
и `delivered_chapters: 0` — **на ОБЕИХ поверхностях**, стартовой квитанции и пути чтения, потому
|
||||
что это два разных SQL-выражения. Мутация (`delivered_chapters` → null) ловится.
|
||||
⚠ **Это ВОЗВРАТ, а не сочинение — эррата `06.09-г`.** Верная граница `minimum: 0` и абзац о ней в
|
||||
каноне УЖЕ СТОЯЛИ; их снёс один заход оркестратора, писавший канон 0.12.0, — он заменил `0` на `1`
|
||||
и пере-написал прозу в редакцию, говорящую ровно то же самое. То есть половина поля, которую
|
||||
читает ЧЕЛОВЕК, всё это время держала правду, а половина, которую читает ГЕНЕРАТОР КЛИЕНТА, —
|
||||
ложь, и стояли они в шести строках друг от друга. Разница для урока существенна: не «зона написала
|
||||
правило», а «зона вернула правило, снесённое вместе с прозой, которая его же и повторяла». ⚠ Свою
|
||||
сверку я вела от ПРОВОДА и ПИНА, а не от его письма, поэтому вывод от авторства не зависел.
|
||||
|
||||
2. **Абзац `Progress` объявлял единственным различителем `Run.delivered_chapters`, и это ложь.** Для
|
||||
ре-прохода `runDelivered` отдаёт `0` — ЧИСЛО, — значит по этому правилу клиент прочёл бы ре-проход
|
||||
как ГЛАВЫ и нарисовал «0 of 1 chapters», ровно то, что абзац запрещает двумя строками ниже.
|
||||
⇒ переписано на пару, описывающую ЗАКАЗ, с явным порядком проверок: `ordered_chapters == 0` ⇒
|
||||
ре-проход · `== null` ⇒ юниты · иначе главы. Порядок несущий: проверка ре-прохода обязана быть
|
||||
ПЕРВОЙ.
|
||||
⚠ **Отклонена вторая половина предложения оркестратора** — он назвал `stage: re_pass` равноправной
|
||||
альтернативой. `stage` — ОТКРЫТЫЙ словарь (новые значения версию не двигают), и клиент обязан ему
|
||||
деградировать, а не решать по нему. В канон вписано «corroborates; does not discriminate».
|
||||
|
||||
⛔ **И КРАЙ, КОТОРЫЙ ВСКРЫЛСЯ ТОЛЬКО ОТ ЭТОЙ ПРАВКИ — правило эксклюзивности едва не оказалось
|
||||
неверным.** Канон теперь утверждает «ноль ⇒ ре-проход и ничто иное». Проверено: `pricing.Quote`
|
||||
клампит заказ всей книги к остатку, поэтому над ПОЛНОСТЬЮ ДОСТАВЛЕННОЙ книгой он квотит **ноль глав**,
|
||||
а допуск стора отвергает ноль только при `Resnapshot == false` — которое любое продолжение над
|
||||
сдвинутым банком ставит в `true`. То есть без гварда `quote.Chapters == 0` в `runs.Start` такой прогон
|
||||
лёг бы с `ceiling_chapters = 0` и был бы НЕОТЛИЧИМ от ре-прохода на проводе: полоса `0/1`,
|
||||
`stage: re_pass`, `delivered_chapters: 0`. Гвард есть и стоит верно — ПОСЛЕ ветви ре-прохода и ДО
|
||||
взятия холда, — но **не был пиньнут ничем**. Заведён
|
||||
`runs.TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun`; мутация (снятие гварда)
|
||||
ловится, деньги при отказе не двигаются.
|
||||
|
||||
⚠ **Чего НЕ обнаружилось:** расхождения на стороне ПРОВОДА. Оба раза лгал документ. `delivered_chapters: 0`
|
||||
у ре-прохода — не дефект: он купил ноль глав и доставил ноль, и `ordered_chapters: 0` рядом делает
|
||||
это чтение однозначным; `null` там был бы ХУЖЕ, потому что `null` уже занят под «меряется в юнитах».
|
||||
|
||||
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
|
||||
|
||||
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
|
||||
**всем 193 свойствам схем канона**, три правила: граница `minimum > 0` при прозе, говорящей про ноль ·
|
||||
проза обещает `null`, а тип его не допускает · тип допускает `null`, а проза о нём молчит. Семь
|
||||
флагов, **пять — ложные срабатывания** эвристики (`target` прямо говорит «never `null`»;
|
||||
`RunRequest.chapters`/`characters` несут `minimum: 1` при прозе, которая ноль ЗАПРЕЩАЕТ;
|
||||
`character_count_exact` и `kind` упоминают `null` СОСЕДНЕГО поля). Настоящих два:
|
||||
|
||||
1. ⛔ **`OrderOptions.structure` — тип `oneOf [string, null]`, а проза не говорила, КОГДА бывает `null`
|
||||
и что он значит.** Зеркало разобранного случая: про null знал генератор, не знал человек. Провод
|
||||
его шлёт — `wireOrderOptions.Structure *string`, «null when no manifest has answered yet»
|
||||
(`internal/httpapi/v0.go`, греп `Structure \*string`), — и это пиньнуто:
|
||||
`httpapi.TestABookWhoseCutCannotBeSoldAgainstOffersCharactersAndSaysSo` проверяет ЯВНЫЙ `null`, а
|
||||
не выдуманное слово. Смысл дописан из пина: **`null` — не четвёртый вид разреза и не `none`**;
|
||||
`none` — книга, на которую движок посмотрел и нашёл одну главу, `null` — книга, которую он ещё не
|
||||
спрашивали. Соседний `Book.structure` то же самое говорил, а этот молчал.
|
||||
2. ⚠ **`Chapter.number` — `minimum: 1` при `type: [integer,'null']` и прозе «`null` законен». НЕ
|
||||
ТРОНУТО:** противоречие уже объявлено ЭРРАТОЙ 05.09 внутри самого поля, с верным разбором и
|
||||
адресом (структурный пак). Сверено, что довод эрраты ЖИВ на моей стороне: `ingest.Whole()`
|
||||
по-прежнему отвергает `c.Number < 1` (`internal/ingest/manifest.go`, греп `chapter numbering is
|
||||
1-based`), то есть книга, которую абзац называет законной, через шов не проходит. Добавлять нечего.
|
||||
|
||||
⚠ `ordered_units` с `minimum: 1` проверен оркестратором и верен (заказа на ноль юнитов нет, «не
|
||||
юнитами» несёт `null`) — не тронут.
|
||||
|
||||
### ⚠ И НАХОДКА О САМОМ СПОСОБЕ ПЕРЕДАЧИ, отданная оркестратору
|
||||
|
||||
**Номер строки, посланный сообщением, протухает от следующей правки того же файла и ничем не
|
||||
сторожится.** Я дала ему три номера для чужих якорей, потом сама же дописала в канон два блока — и все
|
||||
три соврали на четыре строки. Поймала это тем, что пере-считала перед отправкой итога, а не тем, что
|
||||
что-то покраснело. ⇒ надёжная форма передачи — ТОКЕН, а не номер: `counts.py --lint` сверяет по
|
||||
СОДЕРЖИМОМУ и единственный способен сказать, попал ли пере-нацеливший.
|
||||
|
||||
⚠ **Мои правки канона сдвинули ТРИ чужих якоря** (`docs/PROGRESS.md:237` и `:244`,
|
||||
`frontend/docs/frontend-PROGRESS.md:162`) — все целят в канон. Чужие журналы не трогаю; точные новые
|
||||
строки отданы оркестратору вместе с ТОКЕНАМИ, чтобы он пере-нацелил их тем же лендингом и пере-считал
|
||||
номер линтом в момент правки, а не взял мой из письма.
|
||||
|
||||
✅ **ЧТО ПРОВЕРЕНО КАК ЗАКРЫТОЕ ОРКЕСТРАТОРОМ** (по его дереву, а не со слов): канон `info.version:
|
||||
0.11.0` · `delivered_chapters` нуллабелен и объяснён · `ordered_chapters: minimum 0` с объяснением
|
||||
нуля · `ceiling_chapters` deprecated + «REFUSED 400» · `run_limit_reached` пере-формулирован ТОЧНЕЕ
|
||||
|
|
|
|||
|
|
@ -1403,6 +1403,18 @@ func TestARePassRunsBarIsOneUnitOfWork(t *testing.T) {
|
|||
t.Fatalf("a fresh re-pass opens at %d/%d %q, want 0/1 re_pass",
|
||||
run.Progress.Done, run.Progress.Total, run.Progress.Stage)
|
||||
}
|
||||
// ⛔ THE TWO VOLUME FIGURES A RE-PASS PUTS ON THE WIRE, pinned because the CONTRACT asserts them
|
||||
// and a contract may only assert what a pin holds. Both are ZERO and neither is null: a re-pass
|
||||
// was ordered in chapters (it buys none) and it is not measured in units, so `ordered_units` is
|
||||
// the one that is absent. This is what makes `ordered_chapters == 0` the re-pass's own mark — the
|
||||
// FIRST check a client owes, before it reads `delivered_chapters`, which here is a number and
|
||||
// would otherwise be taken for a chapter count.
|
||||
if run.OrderedChapters == nil || *run.OrderedChapters != 0 {
|
||||
t.Errorf("the start receipt of a re-pass reports ordered_chapters %v, want 0", run.OrderedChapters)
|
||||
}
|
||||
if run.OrderedUnits != nil {
|
||||
t.Errorf("a re-pass reports ordered_units %d; it is not measured in units", *run.OrderedUnits)
|
||||
}
|
||||
exec(t, s, ctx, `update runs set status = 'ready', finished_at = $2 where id = $1`,
|
||||
run.ID, at.Add(time.Minute))
|
||||
got, err := s.ReadRun(ctx, "u1", run.ID)
|
||||
|
|
@ -1413,6 +1425,22 @@ func TestARePassRunsBarIsOneUnitOfWork(t *testing.T) {
|
|||
t.Fatalf("a finished re-pass reads %d/%d %q, want 1/1 re_pass",
|
||||
got.Progress.Done, got.Progress.Total, got.Progress.Stage)
|
||||
}
|
||||
// ⚠ AND THE READ PATH SAYS THE SAME AS THE RECEIPT — they are two different SQL expressions
|
||||
// (`insertRun`'s RETURNING and `runOrderedChapters` over the lateral), and a contract that
|
||||
// promises one shape must be held to it on both.
|
||||
if got.OrderedChapters == nil || *got.OrderedChapters != 0 {
|
||||
t.Errorf("a re-pass reads back ordered_chapters %v, want 0", got.OrderedChapters)
|
||||
}
|
||||
if got.OrderedUnits != nil {
|
||||
t.Errorf("a re-pass reads back ordered_units %d", *got.OrderedUnits)
|
||||
}
|
||||
// ⛔ ZERO, NOT NULL — and this is the value that makes `delivered_chapters` USELESS as the sole
|
||||
// discriminator of the bar's unit: a re-pass answers with a NUMBER here, so the rule «a number
|
||||
// means the counters are chapters» would have a client render a re-pass as «0 of 1 chapters»,
|
||||
// which the contract's own Progress paragraph forbids.
|
||||
if got.DeliveredChapters == nil || *got.DeliveredChapters != 0 {
|
||||
t.Errorf("a re-pass reads back delivered_chapters %v, want 0 rather than null", got.DeliveredChapters)
|
||||
}
|
||||
if _, err := s.StartRun(ctx, StartRunInput{UserID: "u1", BookID: book, OrderedChapters: 0,
|
||||
Ceiling: money.MicroUSD(100_000), Now: at.Add(time.Hour)}, 0, nil); err == nil {
|
||||
t.Fatal("a zero-chapter run without the re-pass consent was admitted")
|
||||
|
|
|
|||
|
|
@ -787,3 +787,43 @@ func TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn(t *testing.T) {
|
|||
t.Errorf("nothing has been delivered yet and the bar reads %d", got.Progress.Done)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ A BOOK WITH NOTHING LEFT IS REFUSED, AND THAT REFUSAL IS WHAT MAKES `ordered_chapters: 0` MEAN
|
||||
// «RE-PASS» AND NOTHING ELSE.
|
||||
//
|
||||
// The contract now tells a client to read the bar's unit off that field, and its FIRST case is «zero
|
||||
// ⇒ a re-pass, `total` is 1, do not render it as chapters». That rule is only safe because no other
|
||||
// shape can put a zero there — and one nearly can: `pricing.Quote` clamps a whole-book order to what
|
||||
// remains, so over a fully delivered book it quotes ZERO chapters, and the store's own admission
|
||||
// check refuses a zero only when `Resnapshot` is false, which a continuation over a moved bank sets
|
||||
// to true. The guard in `Start` is what closes that door, ahead of the store.
|
||||
//
|
||||
// Without it such a run would land with `ceiling_chapters = 0` and be INDISTINGUISHABLE FROM A
|
||||
// RE-PASS on the wire: bar 0/1, `stage: re_pass`, `delivered_chapters: 0`. The buyer would be shown
|
||||
// somebody else's shape of work.
|
||||
//
|
||||
// Mutation caught: dropping the `quote.Chapters == 0` guard; moving it after the hold is taken;
|
||||
// letting Quote return the remainder count without clamping.
|
||||
func TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun(t *testing.T) {
|
||||
f := newFixture(t, "10", 500)
|
||||
book := multiUnitBook(t, f, "done", 3, 2)
|
||||
// Every unit of every chapter delivered in the wave the book is measured in.
|
||||
if _, err := f.store.Pool().Exec(f.ctx, `
|
||||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||||
select $1, c, u - 1, 'edit', true, false, now()
|
||||
from generate_series(1, 3) c, generate_series(1, 2) u`, book); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book})
|
||||
if err == nil {
|
||||
t.Fatal("a fully delivered book was sold a run; on the wire it would be indistinguishable " +
|
||||
"from a re-pass, because both carry ordered_chapters 0")
|
||||
}
|
||||
if !errors.Is(err, ErrCeilingOutOfBounds) {
|
||||
t.Errorf("the refusal came as %v; the client needs the one that means «re-read the options»", err)
|
||||
}
|
||||
// …and the money did not move: the refusal is ahead of the hold.
|
||||
if got := f.account(t).Reserved; got != 0 {
|
||||
t.Errorf("a refused order reserved %s", got.USD())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue