Pin at the store that a parsed book has no provenance yet, and declare the two new rows that carry alarm markers

This commit is contained in:
heaven 2026-09-06 08:36:58 +03:00
parent 3435bd6eee
commit 3524e47d4b
6 changed files with 195 additions and 17 deletions

File diff suppressed because one or more lines are too long

View file

@ -349,6 +349,58 @@ r.ordered_units is not null then` — считают полосу в ЮНИТА
разойтись», который этот пак вычищал из денежного пути. Вынесен в `makeRecipe(t, target)`, оба гейта
читают один разбор; прежний тест зелен без правок.
### ⛔ ТРЕТИЙ ЗАХОД ПО НОРМЕ АВТОРСТВА — ОДНА ЭРА, ТРИ НОСИТЕЛЯ, ПРОТУХШИЕ ПО-РАЗНОМУ
Найдено не зоной: строитель нашёл, оркестратор проверил, зона пишет из пина (`D39.211` п.3а).
**⑴ `Book.structure` и `OrderOptions.structure` несли РАЗНОЕ, хотя канон утверждал обратное.**
В `OrderOptions.structure` я записала «the sibling `Book.structure` carries the same fact and says the
same in its own words; **the two cannot disagree**». Проверено — на тот момент это было заявление о
НАМЕРЕНИИ, а не о тексте: `Book.structure` говорил только «`null` while the book is still arriving» и
не различал `null` и `none` ВОВСЕ. ⇒ правило «нет провенанса ≠ есть, и он плохой» жило в схеме ЗАКАЗА,
а поле, которое клиент видит на карточке КНИГИ, о нём молчало.
**⑵ И «still arriving» ýже факта.** Книга может приехать целиком и отвечать `null`, потому что разрез
не гоняли. Это не рассуждение: пин, на котором стоит правило, берёт книгу в статусе `not_started`
полностью загруженную, без манифеста — и получает явный `null`.
**⑶ Третий носитель той же эры — МОЙ КОД.** `httpapi.wireBook.Structure` перечислял ТРИ значения
(`declared`/`detected`/`none`) и `delimited` не знал: комментарий остался от трёхзначной эры и читался
как обещание. ⇒ **три носителя одной эры протухли по-разному, и один из них мой.** Перечень из
комментария УБРАН совсем: словарь открытый и растёт аддитивно, список в комментарии протухает молча —
канон держит слова, комментарий держит форму.
**ПИН ВЫДЕЛЕН, А НЕ ПРОЦИТИРОВАН ЧУЖОЙ.** Утверждение о `null` на карточке книги держалось внутри
`TestTheCharacterCountSaysWhichNumberItIs` — постояльцем без своего имени, а канон, цитирующий пин по
имени, не может цитировать пин, названный про другое. Перенесено (не скопировано) в
`httpapi.TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`, туда же добавлено третье
утверждение: незнакомое слово (`delimited`) едет ВЕРБАТИМ, потому что сборка, переписывающая
непонятое, спрятала бы рост словаря от клиента, который обязан на нём деградировать.
**И правило, которое зона взяла себе из этого:** смысл значения нельзя списывать с комментария
провода — сегодня это ВТОРОЙ случай, когда так списанное утверждение оказалось ложным. Источник —
ПИН.
⛔ **И ТУТ ЖЕ — ЧЕТВЁРТЫЙ СЛУЧАЙ, ТОНЬШЕ ВСЕХ ПРЕДЫДУЩИХ: ПИН БЫЛ, А УТВЕРЖДЕНИЕ ВСЁ РАВНО УЕХАЛО.**
Я написала в канон, что `null` бывает у книги «fully uploaded, no manifest», и сослалась на пин, чей
фейк отдаёт `not_started` без структуры. Пин настоящий и держит РОВНО ТО, ЧТО ДЕРЖИТ: пустая колонка
доезжает до клиента явным `null`. Но это **тест ОТОБРАЖЕНИЯ** — он ничего не говорит о том, бывает ли
такая пара у настоящей книги. Я прочитала ФИКСТУРУ как продуктовое состояние. Поймал оркестратор
сверкой с соседним словарём: `BookStatus` обещает у `not_started` «cut», то есть манифест прочитан.
**Ответ дал пин уровня ХРАНИЛИЩА, заведённый под этот вопрос**
`pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`. `FinishParse` ставит `not_started` и
ВЗВОДИТ ДОЛГ НА ДЕРЕВО в одной транзакции (его собственный комментарий: «this is the transaction that
makes the book parsed, and a book that is parsed owes a tree»). ⇒ окно «разрезана, провенанса ещё нет»
СПРОЕКТИРОВАНО, а не гонка. Ложной оказалась **моя иллюстрация**, а не словарь статусов и не
различение `null`/`none`: `not_started` говорит, что разрезан ИСХОДНИК, а не что прочитан манифест.
Три дороги к тому же `null`: долг ещё не оплачен · манифест старше поля · книга заведена с уже
лежащего файла.
**Урок, который стоит отдельно от находки:** «есть пин» — не то же, что «пин доказывает сказанное».
Доказано было ОТОБРАЖЕНИЕ, сказано — про ЖИЗНЬ. Цитируя пин в утверждении о значении, спрашивать не
«зелен ли он», а «то ли он мерит».
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
@ -786,16 +838,33 @@ ordered_units: integer|null // НОВОЕ: сколько юнитов ку
make check → MAKE-EXIT=0 ⟵ ПЕРЕ-СНЯТО 06.09 ПОСЛЕ ДОФИКСА ПО КАНОНУ, красных НЕТ
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
ПАКЕТОВ 20: ok 20, FAIL 0
ТЕСТОВ верхнеуровневых 852: PASS 847 · FAIL 0 · SKIP 5
(847-й — новый гейт прибора, `TestTheBatteryCannotReportCleanlinessWithoutItsLog`)
⚠ ИСПР.: прежняя редакция этого блока писала «ПАКЕТОВ 21». Их ДВАДЦАТЬ. Двадцать первым
я посчитала лишнюю строку `FAIL` в выводе `make` — он печатает и падение ПАКЕТА, и
итоговое голое `FAIL`. Ошибка ровно того же рода, что ловил четвёртый заход: величина
взята из вывода прибора, не разобрав, что прибор печатает.
ТЕСТОВ верхнеуровневых 854: PASS 849 · FAIL 0 · SKIP 5
РЕГИСТР: 457 рядов · открытых 105 · major 3 · minor 35 · info 67
(847-й — гейт прибора `TestTheBatteryCannotReportCleanlinessWithoutItsLog`; 848-й —
выделенный пин провенанса `TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`;
849-й — пин ЖИЗНЕННОГО состояния `pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`,
заведённый потому, что прежний доказывал ОТОБРАЖЕНИЕ, а канон говорил про ЖИЗНЬ)
⚠ ИСПР.: прежняя редакция этого блока писала «ПАКЕТОВ 21». Их ДВАДЦАТЬ.
⛔ И ОБЪЯСНЕНИЕ, которое здесь стояло («двадцать первым я посчитала лишнюю голую
строку `FAIL`»), тоже НЕ ВЫДЕРЖАЛО ПРОВЕРКИ — это была третья догадка подряд об одном
числе. Пере-считано по сохранённым выводам: зелёный прогон даёт `ok` 20, `FAIL` 0 —
лишней строки нет ВОВСЕ; красный даёт `ok` 19 + `FAIL <пакет>` 1 + голых `FAIL` ДВА,
итого 22 строки при тех же 20 пакетах. ⇒ «21» получилось СЛОЖЕНИЕМ ДВУХ РАЗНЫХ
ПРОГОНОВ: `ok 20` взято из зелёного, `FAIL 1` — из красного. ⚠ **ИСПР. ПОВТОРНО:** здесь стояло «три
наблюдателя, три причины ОДНОГО числа». Неверно, и снято по замеру оркестратора: у него
`grep -c '^ok.*internal/money'` = **2** в одном логе и 1 после починки — дубль реален и
объясняет ЕГО двадцать один. **Чисел было ДВА, а не одно:** его — от дубля в общем логе,
моё — от сложения двух прогонов. Общего у них только величина. ⚠ Формулировка была
красивее правды и потому прожила дольше неё — ровно то, за чем этот отчёт весь день
гоняется.
⚠ счёт снят из `.check.log` (`make check` гонит `-v`); `go test` БЕЗ `-v` строк
`--- SKIP` не печатает вовсе, и греп по нему даёт ЛОЖНЫЙ НОЛЬ — этой ошибкой
четвёртый заход уже был пойман
ALARM PD-count: 12 (baseline 12) — ПЕРЕ-СНЯТА базой: PD-375 и PD-422 удалены из `alarmBaseline`
ALARM PD-count: 14 (baseline 14) — ⚠ ЧИСЛО ВЫРОСЛО 06.09 НЕ ОТ НОВОЙ ОПАСНОСТИ, а от того, что две
находки пака наконец получили РЯДЫ: `PD-455` и `PD-456` объявлены в `alarmBaseline` с
доводом, по прямой инструкции самого гейта. Маркеры они несут законно (холд, деньги,
молчание), ниже major стоят потому, что ни одна не теряет денег и не портит работы —
ломается продукт. Прежде: 12 (baseline 12), ПЕРЕ-СНЯТА базой: PD-375 и PD-422 удалены из `alarmBaseline`
по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ») и
прецеденту PD-168; было 14 (baseline 14) с двумя объявленными исключениями
@ -1051,7 +1120,13 @@ status` на входе сессии — clean). В него ВХОДИТ `81a89
мутациями (`SourceChars: 0` красит оба, потеря `BookOnce` — первый); ⑸ форма над книгой с ЖИВЫМ
прогоном отвечает `covers_all` и молчит о том, что клик получит `run_in_flight`; ⑹ глава
ЗАДРАФЧЕННАЯ, но не отредактированная, оценивается полностью (счётчики стадию не различают),
ошибка ВВЕРХ; ⑺ откат миграции стирает различение двух причин паузы, ради которого пак их и
ошибка ВВЕРХ ⟵ ⑵/⑸/⑹ **ПОЛУЧИЛИ РЯДЫ 06.09** (`PD-457`/`PD-455`/`PD-456`): до этого их
единственным носителем был ЭТОТ отчёт, а отчёт — хроника, и находка без ряда не находится.
**⑶ РЯДА НЕ ПОЛУЧИЛ И НЕ ДОЛЖЕН, но назван здесь строкой по просьбе оркестратора:** новые фикстуры
пишут колонки `pgstore` рукописным SQL ИЗ пакета `internal/runs`, куда не достаёт ни `sqlgate`
(читает свой каталог), ни `sqlcgate`. Предмет — ДЫРА В ПОКРЫТИИ ГЕЙТОВ, а не дефект поведения:
сегодня ни один сторож не судит эти вставки, и через месяц об этом никто не вспомнит. Кандидат в
расширение области `sqlgate`, не в ряд регистра; ⑺ откат миграции стирает различение двух причин паузы, ради которого пак их и
разделил — для отката приемлемо, но записано.
- **PLAUSIBLE, не проверено исполнением:** что `--resnapshot` на каждом продолжении не приводит к
заметной пере-оплате на РЕАЛЬНОЙ книге с растущим банком. Механизм я разобрала по коду движка и
@ -4273,7 +4348,7 @@ caught:` того теста, который она обязана валить
вне идемпотентности ключа запроса); повтор после успеха отвечает `run_in_flight` либо
`ErrRePassUnavailable` — факт погашен финишем (символ `ErrRePassUnavailable`:
`platform/internal/runs/runs.go:149`=`var ErrRePassUnavailable = errors.New(`; ветка провода —
`platform/internal/httpapi/v0.go:1149`=`case errors.Is(err, runs.ErrRePassUnavailable):`).
`platform/internal/httpapi/v0.go:1158`=`case errors.Is(err, runs.ErrRePassUnavailable):`).
Вырожденных дублей не нашёл, но специального пина нет.
- ⚠ **Для оркестратора — находка опровергателя P10, носителя ни в регистре, ни в бэклоге у неё нет:**
якоря §2.12 компаньона контракта (`docs/architecture/14-api-contract/README.md`, греп

View file

@ -73,6 +73,19 @@ var alarmBaseline = []string{
// ровно один) и о том, что отказ приходит МОЛЧА для пользователя, дважды нажавшего кнопку.
// Ниже major он стоит потому, что деньги не теряются: отказ случается ДО взятия холда.
"PD-448",
// PD-455 — вошёл в класс 06.09 вторым кругом пака «форма заказа». Маркеры законны: ряд про то, что
// форма МОЛЧИТ о живом прогоне, и про ХОЛД, потому что отказ случается ровно до его взятия. Ниже
// major он стоит по той же причине, по которой его цена терпима: денег не теряется и работа не
// портится — ломается обещание «вердикт ДО клика», то есть продукт, а не деньги.
"PD-455",
// PD-456 — вошёл в класс 06.09 вторым кругом пака «форма заказа». Маркеры несёт ЗАКОННО: ряд про
// деньги и про холд, потому что дефект — завышение оценки остатка (юнит, у которого сделан только
// черновик, входит в цену обеими волнами). Ниже major он стоит потому, что **ошибка направлена
// ВВЕРХ**: денег не теряется и работа не портится, у человека просят зарезервировать больше
// нужного. Цена продуктовая — завышенный холд сокращает покупаемое, а на коротком балансе делает
// покупку невозможной там, где она возможна. Лечение упирается в манифест движка (остаток по
// ВОЛНАМ, а не только по юнитам), то есть в чужую зону, и одной платформой не закрывается.
"PD-456",
"PD-94", "PD-107", "PD-162", "PD-201", "PD-212",
"PD-217", "PD-244", "PD-418", "PD-420", "PD-428", "PD-433",
}

View file

@ -209,10 +209,19 @@ type wireBook struct {
// destroy a meaning the contract already promises. Form ratified by the orchestrator, act
// D39.201 §5(б), on two lawful shapes the zone put up.
CharacterCountExact bool `json:"character_count_exact"`
// Structure is where the chapter boundaries came from: `declared` (the format drew them),
// `detected` (matched in the prose), `none` (the whole book is one chapter). Null until a
// manifest has said. A client needs it to know what a chapter NUMBER is worth before it offers an
// order phrased in one — see OrderOptions.chapter_orders.
// Structure is where the chapter boundaries came from, in the ENGINE's own word, passed through
// untouched. A client needs it to know what a chapter NUMBER is worth before it offers an order
// phrased in one — see OrderOptions.chapter_orders.
//
// ⚠ THE VOCABULARY IS OPEN AND GROWS ADDITIVELY, so this comment does not list it: an earlier
// edition named three words, the engine shipped a fourth (`delimited`, 06.09), and the list went
// stale in place while reading like a promise. The canon holds the words; this holds the shape.
//
// ⛔ NULL IS NOT A WORD OF THAT VOCABULARY AND NOT `none`. `none` is a book the engine LOOKED at
// and found one chapter in — a statement about the book. Null is the absence of any statement:
// no manifest has been read. ⚠ That is NOT the same as «still arriving»: a book can be fully here
// and still answer null, because the cut has not been run. Pinned by
// TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull, whose book is `not_started`.
Structure *string `json:"structure"`
AddedAt time.Time `json:"added_at"`
NoteCount int `json:"note_count"`

View file

@ -935,8 +935,21 @@ func TestTheCharacterCountSaysWhichNumberItIs(t *testing.T) {
}
})
}
// The cut's provenance travels beside it, verbatim, or as an explicit null when no manifest has
// spoken. The client needs it to know what a chapter NUMBER is worth before it offers an order.
}
// ⛔ THE CUT'S PROVENANCE ON THE BOOK CARD, and `null` there means «nobody has been asked», which is a
// different fact from every word the vocabulary carries.
//
// It used to be asserted inside the character-count test, as a tenant with no name of its own — and a
// contract that cites a pin by name cannot cite one named for something else. Moved rather than
// copied: the assertions are the same, the subject is now findable.
//
// ⚠ THE BOOK HERE IS `not_started`, WHICH IS THE POINT. It has fully arrived and still answers `null`,
// because no manifest has been read — so «null while the book is still arriving» is NARROWER than the
// fact and a client that waits for arrival waits for the wrong thing. And `null` is not `none`: `none`
// is a book the engine looked at and found one chapter in, which is a statement ABOUT the book;
// `null` is the absence of any statement.
func TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull(t *testing.T) {
got := decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))
book, _ := got["book"].(map[string]any)
if v, ok := book["structure"]; !ok || v != nil {
@ -947,6 +960,13 @@ func TestTheCharacterCountSaysWhichNumberItIs(t *testing.T) {
if book["structure"] != "declared" {
t.Errorf("structure = %v, want the engine's own word", book["structure"])
}
// An unknown word travels VERBATIM too: the vocabulary grows additively, and a build that
// re-wrote what it did not recognise would hide the growth from the client that must degrade on it.
got = decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started", Structure: "delimited"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))
book, _ = got["book"].(map[string]any)
if book["structure"] != "delimited" {
t.Errorf("structure = %v, want the engine's word passed through untouched", book["structure"])
}
}
// ⛔ A RETIRED MEMBER IS REFUSED, NOT IGNORED, and the difference is a purchase.

View file

@ -297,3 +297,61 @@ func TestEveryRunTheStoreHandsOutCarriesItsBooksRevision(t *testing.T) {
}
}
}
// ⛔ A BOOK IS `not_started` BEFORE ITS PROVENANCE EXISTS, and the window is DESIGNED rather than a
// race. `FinishParse` sets the status and ARMS THE READ-MODEL DEBT in one transaction — its own
// comment says why: «this is the transaction that makes the book parsed, and a book that is parsed
// owes a tree». The tree, and with it `structure`, arrive later, when the materializer pays that debt.
//
// ⚠ THIS PIN EXISTS BECAUSE A CONTRACT SENTENCE WAS WRITTEN OFF A DISPLAY TEST. The canon said
// `Book.structure` is `null` for a book that is «fully uploaded, no manifest», citing a test whose
// fake store returns a `not_started` book with no structure. That test is real and proves what it
// proves — an empty column reaches the client as an explicit `null` — but it is a test of RENDERING:
// it says nothing about whether a real book ever holds that pair. The illustration was a fixture read
// as a product state. Here the pair is produced by the intake itself.
//
// ⚠ And it is not the only way to reach it: a manifest older than the `structure` field leaves the
// column null for good (pgstore.Structure.Structure, «Empty when the engine did not say»), and
// AddBook — the route for a file already on disk — creates a book `not_started` with no manifest at
// all. Three roads, one pair; the status vocabulary's «cut, never run» is about the SOURCE having
// been cut, not about the platform having read a manifest.
//
// Mutation caught: arming the read-model debt after the status instead of with it; a status written
// only once the tree is materialised (the book would then be invisible to its owner while parsing
// completed).
func TestAParsedBookIsNotStartedBeforeItHasAnyProvenance(t *testing.T) {
s, ctx := testDB(t)
now := fundedAccount(t, s, ctx, "u1", "10")
b := upload(t, s, ctx, "u1", now)
if _, err := s.StartParsing(ctx, b.ID, 42, nil); err != nil {
t.Fatal(err)
}
claim, err := s.ClaimParse(ctx, b.ID, now, now.Add(-time.Hour))
if err != nil {
t.Fatal(err)
}
if _, err := s.FinishParse(ctx, b.ID, claim.At, ParsedBook{Chapters: 500, ChunkerVersion: "chunk-1"}); err != nil {
t.Fatal(err)
}
got, _, err := s.GetBook(ctx, "u1", b.ID)
if err != nil {
t.Fatal(err)
}
if got.Status != "not_started" {
t.Fatalf("a book whose parse just finished is %q", got.Status)
}
if got.Structure != "" {
t.Fatalf("a book that has not been materialised carries provenance %q: the pair this pin is "+
"about does not occur, and the contract's illustration of `null` is wrong in the other "+
"direction", got.Structure)
}
// …and it is not that the book is somehow unfinished: it owes a tree, and the debt is what will
// bring the provenance.
owed, err := s.BooksOwedReadModel(ctx, 10)
if err != nil {
t.Fatal(err)
}
if len(owed) != 1 || owed[0].ID != b.ID {
t.Fatalf("the parsed book does not owe a reading surface: %+v", owed)
}
}