Make the signing sheet's ranking and its empty renderings part of what the tests assert

This commit is contained in:
heaven 2026-09-08 02:30:23 +03:00
parent be46bc9972
commit 0abfad82d0
4 changed files with 126 additions and 22 deletions

View file

@ -2881,5 +2881,31 @@
"replace": "\treturn m.providerReasoning(model) != \"\""
}
]
},
{
"id": "BANKSTOP-the-sheets-order-is-the-stops-ranking",
"why": "the section's own contract says its order IS the stop's ranking, so which row an owner reads first is information rather than layout. Nothing asserted it until 08.09: the fold's one-row fixture made order unobservable by construction, and every other assertion found its row by Src instead of by position, so a reversal shipped a sheet ranked backwards and left the package green",
"package": "./internal/pipeline/",
"battery": true,
"edits": [
{
"file": "internal/pipeline/bankexport.go",
"find": "\t\tout = append(out, p)\n\t}\n\treturn out\n}",
"replace": "\t\tout = append(out, p)\n\t}\n\tfor i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {\n\t\tout[i], out[j] = out[j], out[i]\n\t}\n\treturn out\n}"
}
]
},
{
"id": "BANKSTOP-nothing-consolidated-is-published-as-nothing",
"why": "Dst is \"\" when the role consolidated nothing — the field's comment says so in as many words — and that empty must reach the sheet. Substituting the source surface publishes 青茅山 -> 青茅山, a decision where none was made, in the one place whose whole purpose is to show what has yet to be decided. Invisible against any fixture whose rows all carry a rendering, which was every fixture in this package until 08.09",
"package": "./internal/pipeline/",
"battery": true,
"edits": [
{
"file": "internal/pipeline/bankexport.go",
"find": "\t\tout = append(out, p)",
"replace": "\t\tif p.Dst == \"\" {\n\t\t\tp.Dst = row.Src\n\t\t}\n\t\tout = append(out, p)"
}
]
}
]

View file

@ -14,11 +14,19 @@ import (
// while the projection lies. Measured on 08.09: four mutations planted at once (Freq×10, Invented forced
// false, Contradicts↔BankHolds swapped) left the whole package green in 16.8 s.
//
// So the row below gives EVERY field a value that is distinguishable from every other field's: the two
// string lists differ in length AND in content, Freq/Spread/Conf are three different non-zero numbers,
// So the first row below gives EVERY field a value that is distinguishable from every other field's: the
// two string lists differ in length AND in content, Freq/Spread/Conf are three different non-zero numbers,
// Kind and Channel are two different strings, and the rendering is one no draft proposed.
//
// And the table is TWO rows, not one, for two reasons a single row cannot carry. ORDER: the section's own
// contract (bankexport.go — "Order is the stop's own RANKING") says which row an owner reads first, and a
// one-row table makes a reversal unobservable by construction — measured 08.09, reversing the fold left
// the whole package green. NOTHING CONSOLIDATED: the second row's Dst is "", the state the field's comment
// names in as many words, and no fixture in this package carried one — so a fold substituting the source
// surface for the missing rendering published 青茅山 → 青茅山, a decision where there is none, and stayed
// green too.
func TestEveryDecidingFieldSurvivesTheProjection(t *testing.T) {
row := BankStopRow{
decided := BankStopRow{
Src: "方源",
Dst: "Фан-Юань",
Type: "name",
@ -46,28 +54,52 @@ func TestEveryDecidingFieldSurvivesTheProjection(t *testing.T) {
Signals: []string{"majority"},
}
got := projectBankProposals([]BankStopRow{row})
if len(got) != 1 {
t.Fatalf("one row projected to %d proposal(s)", len(got))
}
want := BankExportProposal{
Src: "方源", Dst: "Фан-Юань", Kind: "name", Channel: "banknote",
Freq: 37, Spread: 4, Conf: 61, Invented: true,
Contradicts: []string{`this run also renders 方 as "Фан"`},
BankHolds: []string{
`unsigned draft "方源"→"Странник" [ch 1..20]`,
`approved "方源"→"Фан Юань" [ch 21..40]`,
// The row where the role consolidated NOTHING. Dst stays empty all the way to the sheet: an owner
// reading 青茅山 → 青茅山 would be reading a decision that was never made. Conf is negative because
// the role stated none, which the field's comment calls a different fact from "0% sure".
undecided := BankStopRow{
Src: "青茅山",
Dst: "",
Type: "place",
Origin: "mined",
Freq: 5,
Spread: 2,
Conf: -1,
Variants: []BankStopVariant{
{Dst: "гора Цинмао", Chunks: 3},
},
Variants: []string{"Фан Юань ×9", "Фан ×2 (proposed for 方)"},
}
if !reflect.DeepEqual(got[0], want) {
t.Errorf("the projection is not the row it projects:\n got %+v\n want %+v", got[0], want)
}
// AND: no field of the proposal is left at its zero value. The comparison above pins the fields that
// exist today; this pins the ones added tomorrow — a field appended to BankExportProposal and never
// filled by the fold ships as an absent key in the sidecar, and every assertion written against
// today's fields stays green while the signing screen loses a column.
got := projectBankProposals([]BankStopRow{decided, undecided})
want := []BankExportProposal{
{
Src: "方源", Dst: "Фан-Юань", Kind: "name", Channel: "banknote",
Freq: 37, Spread: 4, Conf: 61, Invented: true,
Contradicts: []string{`this run also renders 方 as "Фан"`},
BankHolds: []string{
`unsigned draft "方源"→"Странник" [ch 1..20]`,
`approved "方源"→"Фан Юань" [ch 21..40]`,
},
Variants: []string{"Фан Юань ×9", "Фан ×2 (proposed for 方)"},
},
{
Src: "青茅山", Dst: "", Kind: "place", Channel: "mined",
Freq: 5, Spread: 2, Conf: -1,
Variants: []string{"гора Цинмао ×3"},
},
}
// Compared as a WHOLE SLICE, not row by row found by Src: the ranking is the information, so the
// position each row holds is part of what is being asserted.
if !reflect.DeepEqual(got, want) {
t.Fatalf("the projection is not the table it projects:\n got %+v\n want %+v", got, want)
}
// AND: no field of the proposal is left at its zero value — read off the FIRST row, the one built to
// have every field set (the second is deliberately half empty, that being its whole point). The
// comparison above pins the fields that exist today; this pins the ones added tomorrow — a field
// appended to BankExportProposal and never filled by the fold ships as an absent key in the sidecar,
// and every assertion written against today's fields stays green while the signing screen loses a
// column.
//
// ⚠ Its limit, named: this catches a field the PROPOSAL grew and the fold ignored. A field BankStopRow
// grows that ought to be projected and is not cannot be caught mechanically — whether a new fact is

View file

@ -72,6 +72,24 @@ type miningStopOpts struct {
// candidate (the join's non-empty intersection — the whole point of the fixture) and one that is not.
const bankBlockForMining = bankSeparator + "\n方源\tФан Юань\tname\n青茅山\tгора Цинмао\tplace"
// srcsOfProposals and srcsOfRows render the two orders side by side when they disagree: a failure that
// says "row 2 differs" without showing both sequences sends the reader back to the debugger.
func srcsOfProposals(ps []BankExportProposal) []string {
out := make([]string, 0, len(ps))
for _, p := range ps {
out = append(out, p.Src)
}
return out
}
func srcsOfRows(rows []BankStopRow) []string {
out := make([]string, 0, len(rows))
for _, r := range rows {
out = append(out, r.Src)
}
return out
}
// setupMiningStopProject builds a zh→ru fixture with the bank-mining stop CONFIGURED (contrast artifact +
// langpack) and the banknote channel ON, so both halves of the WHAT→WHICH join are live. Local
// constructor by the repair_integration_test.go precedent: the shared setupProjectOpts stays untouched.
@ -969,6 +987,16 @@ func TestBankStopPublishesItsProposalsInTheReadOut(t *testing.T) {
if want := projectBankProposals(stop.Rows); !reflect.DeepEqual(exp.Proposed, want) {
t.Fatalf("the published section is not the fold of the table it came from:\n in the file %+v\n from the rows %+v", exp.Proposed, want)
}
// ORDER, anchored against the ROWS rather than against the fold — the one thing the comparison above
// structurally cannot see. The section's contract is that its order IS the stop's ranking, so which
// row an owner reads first is information and not layout; a fold that reversed it would pass every
// comparison that finds its row by Src instead of by position.
for i := range exp.Proposed {
if exp.Proposed[i].Src != stop.Rows[i].Src {
t.Fatalf("published row %d is %q, the stop ranked %q there — the sheet's order is the ranking, and this one is not it:\n file %v\n rows %v",
i, exp.Proposed[i].Src, stop.Rows[i].Src, srcsOfProposals(exp.Proposed), srcsOfRows(stop.Rows))
}
}
var fy *BankExportProposal
for i := range exp.Proposed {
if exp.Proposed[i].Src == "方源" {

View file

@ -699,6 +699,24 @@
**Две мелочи.** `snapshot_wave_test.go` говорил «four callers», соседний `snapshot.go` — «five call sites in four test files»: оба верны, единица не названа; теперь единица названа в обоих. `backend/README.md` печатал диапазон цены гейта `87104 %`, снятый на СТАРОМ составе из 11 записей, и об этом не говорил — пере-снят формулировкой без процента, вместе с отзывом моего же числа «123 %» (см. поправку выше по секции: парный замер спина-к-спине дал 102 %, то есть 123 % были фоновой нагрузкой, а не свойством цели).
#### ТРЕТИЙ (и по правилу остановки ПОСЛЕДНИЙ) ДОФИКС — порядок секции и «ничего не консолидировано» (08.09, к акту `D39.225`)
Две линзы независимо нашли две выживших мутации в свёртке предложений. Пере-мерены моей рукой на чистых копиях: **разворот порядка**`ok` 15,8 с; **`if p.Dst == "" { p.Dst = row.Src }`** — `ok` 16,1 с. Обе настоящие, и корень у них один: тест свёртки проецировал ОДНУ строку, поэтому порядок был ненаблюдаем по построению, а фикстуры пакета все несли непустой `Dst`, поэтому класс «роль не консолидировала ничего» был невидим целиком.
Бьёт это по свойству, которое код объявляет своими словами (`bankexport.go`: «Order is the stop's own RANKING… which row to read first is the information here») и которое не утверждал ни один тест; и по классу, который комментарий поля называет дословно («`""` when nothing was consolidated»). На листе подписи вторая мутация показала бы `青茅山 → 青茅山` — исходную поверхность как рендеринг ровно там, где решения нет.
**Лечение.** Тест свёртки проецирует ТАБЛИЦУ ИЗ ДВУХ строк в известном ранжировании и сверяет **срез целиком**, а не найденную перебором строку: позиция — часть утверждения. Вторая строка — та, где роль не решила ничего (`Dst: ""`, `Conf: -1` — «роль не назвала уверенности» есть другой факт, чем «уверена на 0 %»). Рефлективный страж нулевых полей остался на ПЕРВОЙ строке и это сказано на месте: вторая полупуста намеренно, в этом её смысл.
**И порядок отдельно закреплён в интеграционном — против стоп-СТРОК, а не против свёртки.** Сверка «секция есть свёртка той же таблицы» структурно слепа к развороту: оба берега едут вместе. Добавлен цикл, сверяющий `i`-е опубликованное предложение с `i`-й строкой стопа, и при расхождении печатающий обе последовательности целиком — «строка 2 не та» без обеих раскладок отправляет читателя в отладчик.
**Предъявлено ИСПОЛНЕНИЕМ, по одной посадке на чистой копии:** разворот → краснеет НА ОБОИХ уровнях (`bankexport_test.go:94` «the projection is not the table it projects» и `miningstop_join_test.go:996` «published row 0 is "青茅山", the stop ranked "方源" there»); пустой `Dst` → краснеет на юнит-тесте. ⚠ Вторую интеграционный НЕ ловит и поймать не может: в его фикстуре нет строки с пустым `Dst`, а его сверка — свёртка против самой себя. Разделение то же, что и кругом раньше, и названо там же.
**Каталог: 230 → 232** (+2, `battery` 19 → 21): `BANKSTOP-the-sheets-order-is-the-stops-ranking` и `BANKSTOP-nothing-consolidated-is-published-as-nothing`. Битых якорей 0 при 232 записях.
**Числа, снятые ПОСЛЕ последней правки (порядок — мутации, затем батарея; между ними дерево не тронуто).** `make mutations` → exit 0, **5m39.4s**, 21 запись, 21 RED, выживших **0**, `0 unexpected outcome(s)`, `anchors swept: 0 of 232 entr(ies) rotten`. `make battery` → exit 0, **3m17.2s**, `0 issues`, `ok` 19 / `FAIL` 0 / «no test files» 4, скипов 4 — те же. Тестов в зоне **1267** (два новых теста свёртки; третий дофикс имён не добавил, он пере-сделал существующий).
**Правило остановки применено оркестратором, и я его принимаю без спора.** Три круга подряд находили в одной новой секции всё более узкие вещи; у секции сегодня ноль читателей, и цена следующего круга растёт быстрее, чем то, что он покупает. Найденное после этого дофикса идёт СТРОКОЙ бэклога, а не работой: «контракт секции предложений запинен по полям и порядку, но не исчерпывающе».
### Пак «ЗАКОН БАНКА — ЧЕСТНО» (06.09, промт `docs/BACKEND_CONSISTENCY_SESSION_PROMPT.md`, вход HEAD `e4097cb`). НЕ КОММИЧУ — ждёт лендинга
**Итог: закон банка описан тем, что код делает, и найдена дыра ШИРЕ заказанной — стадия схлопывания не сверяла свою выработку со строкой банка на тот же ключ ни при какой подписи.** Закрыто в двух местах (схлопывание + страховка на слиянии), репортом, без смены поведения платного прогона. **ШЕСТЬ кругов самопроверки нашли 43 находки В МОЕЙ ЖЕ работе** (18 + 8 + 5 + 6 + 4 + 2); круг 6 сказал «сошлись» — новых находок в коде пака нет. Шесть находок — ложные утверждения в моих же комментариях, то есть ровно тот класс, который пак чинил; две внесены лечением предыдущего круга; один и тот же шов «писатель ↔ читатель» ловился ТРИ круга подряд, после чего был убран целиком, а не подпёрт четвёртой заплатой. Все разобраны, списки ниже.