diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 3460a0a4..fff5a0fb 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -174,7 +174,331 @@ ## Бэкенд -#### Пак «гейт вместо прозы» (07.09, промт `docs/BACKEND_GATES_NOT_PROSE_SESSION_PROMPT.md`, вход HEAD `bd652fd`). НЕ КОММИЧУ — ждёт лендинга +#### Пак «правда оператору и деньги» (08.09, промт `docs/BACKEND_OPERATOR_TRUTH_SESSION_PROMPT.md`, вход HEAD `060b13b`). НЕ КОММИЧУ — ждёт лендинга + +**ЗАПИСКА-ПЛАН (§7), написана ДО первой правки и отправленная эхом оркестратору.** Порядок: 4.2 → 4.6 → 4.4 → 4.5 → 4.3 → 4.1 → прогон. Довод порядка: дешёвое и локальное вперёд, а единственная заказанная смена поведения (§4.1б) — последней, чтобы холодный прогон шёл по неподвижному коду; §4.7 запрещает менять код между двумя запусками из-за реплея чекпойнтов, и такой порядок делает запрет ненужным, а не соблюдаемым. Рискованным считала вырожденную фикстуру в §4.3 и §4.6 (на равных `Key`/`Src` мутация не краснеет) и то, что экономия §4.1 может оказаться артефактом обрезки бюджета, а не работой фильтра. + +**Итог одной строкой: движок перестал называть подписью то, что ею не является; спор, которого владелец не видел на самой частой форме сид-строки, доезжает до листа подписи; выбор ключа в шве сведён в одно место; и платный роль-вызов больше не покупает то, что банк уже несёт — при этом узкая форма фильтра сохранена ЧИСЛОМ, а не доводом.** + +##### Исход по каждому пункту §4 — механически, таблицей + +| Пункт | Исход | Чем предъявлено | +|---|---|---| +| §4.1(а) замер ДО | **сделано** | замер на корпусе исполнением (13 стоп-таблиц, 1365 кандидатов), числа ниже | +| §4.1(б) фильтр до оплаты | **сделано**, узкой формой | `TestABankSettledCandidateIsNotPaidFor` · `TestADisagreeingBankedCandidateIsStillPaidFor` · `TestTheFilterChangesNothingOnTheCANDIDATESITKEEPS` · `TestAResumeFindsItsCheckpointsAfterTheFilter` · 4 записи каталога мутаций | +| §4.1(б) ловушка резюма | **сделано**, лечение НЕ то, что я написала первым | `TestAResumeFindsItsCheckpointsAfterTheFilter`, разбор ниже | +| §4.1(в) замер ПОСЛЕ | **сделано** | секция холодного прогона | +| §4.2(1) `loadAutoBank` | **сделано** | `TestADropNamesItsHoldersSignature` (2 случая) | +| §4.2(2) ремарка `bankmaterialize` | **сделано** | `TestTheDropHeadlineDoesNotPromiseASignature` | +| §4.2(3) | **не делаю** — снято промтом перед выдачей, сверено, утверждения не было | — | +| §4.2(4) `Shown` | **сделано**, и носителей оказалось пять, а не один | `TestTheUnverifiedDenominatorIsNotACountOfRowsShown` · `TestARowWithNoRenderingNeverReachesTheDenominator` | +| §4.3 спор до листа подписи | **сделано**, разрезом кортежа | `TestTheCommonestSeedShapeStillReachesTheSigningSheet` + 2 в `membank` | +| §4.4 операторские тексты как класс | **сделано**, каталогом | `TestEveryOperatorMessageIsCatalogued` (113 сообщений) · `TestTheCatalogueFileIsWellFormed` | +| §4.5 путь записи статуса | **сделано**, мутацией; разрез на один путь НЕ брала | `TestASkippedRowSaysWHICHWriterWroteIt` (2 подслучая) | +| §4.6 шов `Contradictions`/`Conf` | **сделано**, ходом (карта убрана) | `TestEveryStopSheetFindingReachesTheRowItBelongsTo` · `TestTheStopSheetJoinIsNotFooledByTheRawSurface` | +| §4.7 холодный прогон | **сделано** | отдельная секция ниже | +| §4.8 чего в паке нет | **не делала**: строки 339 и 320(б), `Runner`, дробление `internal/pipeline`, секция предложений сайдкара; эскалация оставлена 0 | — | + +##### §4.1(а) — ЗАМЕР ДО. Число, которое решило форму фильтра + +Прибор — Go-тест на КОПИИ дерева (рабочее не трогалось), читает корпус `books/` только на чтение и считает ТЕМ ЖЕ нормализатором, что и движок (`text.NormalizeSourceKey`), а не своей питоновской копией. Команда: `go test ./internal/pipeline/ -run TestMeasureBankHeldCandidateShare -v` на копии с временным файлом `zzmeasure_test.go` (в дерево НЕ вносился — это прибор, а не гарантия). + +``` +stop tables read: 13 +owner seed rows: 58, of them seed SURFACES (what the emission guard keeps): 58 +candidates across every stop table: 1365 +stop-table runs that DECLARED a glossary_seed: 0 of 13 +CONTROL, matcher works: candidates found in their own run's bank export: 767 of 1365 +counterfactual, against the owner's signed seed: candidates whose surface it holds: 439 of 1365 + of those, split three ways: every observed draft AGREES 56 · at least one DISAGREES 366 · no draft observed at all 17 +``` + +⛔ **Первое число — ОТРИЦАТЕЛЬНОЕ, и оно печатается с контрольной величиной.** На корпусе КАК ОН ПРОГНАН доля кандидатов, которых банк уже несёт сид-поверхностью, — **0 из 1365**, потому что **0 из 13** прогонов со стоп-таблицей вообще объявляли `glossary_seed`: все сохранённые стоп-таблицы — холодные прогоны. Контроль, доказывающий, что вопрос задан существующему предмету и прибор умеет сопоставлять: тот же прибор находит **767 из 1365** кандидатов в банк-экспорте их же прогона. То есть ноль — про корпус, а не про прибор. + +⭐ **Второе число — контрфактическое и оно НЕСУЩЕЕ.** Против настоящего подписанного сида владельца (`guzhenren-seed-v2.yaml`, 58 строк, все 58 — сид-поверхности) банк держит поверхность **439 из 1365 кандидатов = 32,2 %**. Но из этих 439 у **366 (83,4 %)** хотя бы один черновик предложил НЕ то, что держит банк. **Слепой фильтр «банк держит — не платим» купил бы экономию ценой 366 замолчанных споров** — ровно того, что §4.3 этого же пака велит довезти до владельца. Узкая форма — не осторожность, а 366 строк листа подписи. Под фильтр попадает **56 из 1365 = 4,1 %** (плюс 17 без единого наблюдённого предложения — их я плачу, см. ниже). + +⚠ **Что этот замер НЕ говорит.** Он не говорит, сколько денег экономится: доля кандидатов ≠ доля батчей ≠ доля токенов, а батчи пакуются жадно, так что снятие 4 % кандидатов может не убрать ни одного вызова. Живое число — в секции холодного прогона. + +##### §4.1(б) — форма фильтра, и почему именно такая + +Фильтр стоит в `runTerminologist` ДО классификатора и до рендер-батчей (`dropBankSettled`, `terminologist.go`). Кандидат снимается только когда выполнены ОБА условия: + +1. банк держит его поверхность СИД-ПОВЕРХНОСТЬЮ — предикатом **тем же**, что у стража эмиссии, а не новым своим, и кандидат банкнотный-только: это ровно та популяция, которую `reverseSectionTerms` роняет на эмиссии, поэтому снятие выходно-эквивалентно; +2. **каждое** наблюдённое предложение черновика после `text.NormalizeTargetForm` равно dst банка. Расходится хоть одно — ПЛАТИМ. + +⚠ **Отступление от буквы промта, объявляю.** Промт оставляет неопределённым случай «наблюдённых предложений нет вовсе»; буквально «каждое наблюдённое равно» там истинно вакуумно. Я разрешила его в сторону ОПЛАТЫ: «нет улики» не должно читаться как «улика согласия». На банкнотных кандидатах случай не наступает по построению (банкнотный кандидат несёт предложение), поэтому цена решения нулевая, но правило записано так, как оно верно. + +**Один носитель предиката, а не второй.** «Строка движка, неподписанная» жила выражением `e.Source == "mined" && e.Status != "approved"` в `pipeline`. Заведён ОДИН экспортированный носитель `membank.IsEngineUnsigned`, и на него переведены все три места, которым надо согласиться: страж эмиссии, фильтр §4.1 и пропуск по кортежу §4.3. Мутация, выворачивающая носитель, краснит ДВА пакета — запись каталога `BANKSHEET-the-one-carrier-of-engine-unsigned-inverted`. + +**Ловушка резюма — и моё первое лечение было САМО багом.** Батчи пакуются жадно, снятие кандидата двигает состав хотя бы одного выжившего батча, а батч бэнк-роли адресуется хешем запроса ⇒ прогон с другим составом не находит чекпойнтов и покупает пас заново. Я написала «фильтр отступает для книги, которая уже платила» — и **исполнение показало, что лечение и есть болезнь**: прогон 1 фильтровал и платил под ОТФИЛЬТРОВАННЫМ составом, прогон 2 отступал, пересобирал ПОЛНЫЙ состав и перекупал всё. Правило, которое на втором прогоне решает иначе, — не правило резюма. Итоговое лечение: **фильтр безусловен**, состав становится чистой функцией кандидатов и банка и потому одинаков на каждом резюме; чекпойнты реплеятся за $0. Единственное, чему это не помогает, — книга, консолидированная ДО фильтра: её чекпойнты несут старый состав. Это ОДНА пере-консолидация, ограниченная `gates.terminology.budget_usd`, и она **объявляется вслух** предупреждением в момент, когда происходит, а не обнаруживается в леджере. Пин — `TestAResumeFindsItsCheckpointsAfterTheFilter`, и утверждает он ДЕНЬГИ: резюм-прогон делает ноль вызовов бэнк-роли и стоит $0. + +⚠ **Что владелец увидит на листе после правки и почему это не потеря.** Стоп-таблица рендерится по ПОЛНОМУ `cands`, поэтому отфильтрованный кандидат со стоп-листа НЕ исчезает — исчезает его колонка `dst` и классификация, потому что роль о нём не спрашивали. На его строке остаются `origin`, `freq`, spread, контексты и **предложения самих черновиков** (`drafts: …`) — а они по условию фильтра ВСЕ равны тому, что банк уже держит. То есть владелец видит: поверхность известна, черновики единодушны, банк согласен. Строки, где было бы что решать, под фильтр не попадают по построению. Пин на это — последние два ассерта `TestABankSettledCandidateIsNotPaidFor`. + +⛔ **Регрессия, которую я внесла сама и нашла сама — но НЕ тестами.** Первая редакция фильтра резала слайс вызывающего. Классификатор и скорер мутируют кандидатов ПО МЕСТУ (`applyTypes`, `ScoreVariants`), а лист подписи и дельту рендерит вызывающий из СВОЕГО слайса — то есть после фильтра на лист поехали бы эвристические типы и не пере-ранжированные варианты, и все счётчики читались бы чисто. **Ни один мой тест этого не видел: в каждой фикстуре пакета классификатор ВЫКЛЮЧЕН, а при выключенном классификаторе ни одна из двух мутаций не происходит вовсе.** Лечение: `paid` — отдельный слайс, `cands` остаётся целым. Пин, который пришлось написать с ВКЛЮЧЁННЫМ классификатором, — `TestTheClassifiersTypeStillReachesTheSheetThroughTheFilter`, плюс запись каталога `MONEY-the-filter-copies-the-candidates-and-loses-their-mutations`. + +##### §4.2 — четыре описания, и класс оказался ШИРЕ заказанного + +**Пункт 1 и 2 — «подпись, которой нет».** `loadAutoBank` принимал параметр `signed`, дока обещала «уже держит ПОДПИСАННАЯ строка», сообщение печатало `key held by the signed …`, а ремарка оператору — «the signed term wins». Популяция — весь собранный набор (сид + ruby + дельта владельца) ЛЮБОГО статуса: UNIQUE-ключ хранилища статусов не читает. Приведено к поведению: параметр `gathered`, дока называет реальную популяцию, каждая снятая строка печатает подпись держателя через ОДИН носитель формулировки (`membank.StatusLabel`, экспортирован специально, чтобы не завести вторую формулировку), заголовок говорит «the holder wins, whatever its status». + +⭐ **Знаменатель, посчитанный, а не заявленный.** Живых носителей фразы «коллизия ключа = подпись» было **5**: дока `loadAutoBank`, имя параметра, текст снятой строки, ремарка оператору, комментарий над ремаркой. Закрыты **5 из 5**. Плюс шестой, найденный собственным грепом уже после «готово»: доккомментарий `TestAutoBankKeyCollisionDoesNotCrashTheRun` формулировал МЕХАНИЗМ через «signed term» — исправлен, теперь говорит, что подписанность держателя есть случай фикстуры, а не правило. + +**Пункт 4 — `Shown`.** Дока обещала «сколько неподписанных строк реально ПОКАЗАНО модели». Sticky-перенос `continue`-ится раньше счёта, а sticky-запись В БЛОК ИНЪЕКЦИИ ПОПАДАЕТ — модель её видит. Значит фраза ложна. ⚠ **Исполнение поправило и меня:** первая редакция моей же правки называла второй причиной строку без `dst` — но `MaterializeBank` не даёт такой строке ни одной исходной поверхности, поэтому она не может ни сработать, ни быть перенесённой, и до `Postcheck` не доходит НИКОГДА (пин `TestARowWithNoRenderingNeverReachesTheDenominator`). Назвать её причиной сужения значило бы описать число так же неверно, как прежняя формулировка. Итог: `Shown` — «строки, на которых этот чанк МОЖНО судить», то есть сработавшие здесь. + +⭐ **Знаменатель.** Носителей описания было **5**, и они делились на две разные неправды: «put in front of the model» (`membank/mempostcheck.go`, `cmd/tmctl/render.go` — ложь про sticky) и «fired in this chunk» (`store/glossary.go`, `pipeline/quality.go` — ближе к правде). Пятый — `store/migrate.go`, где определение в буллете верно, а обрамляющий вопрос — нет. Плюс нарратив существующего теста `TestPostcheckUnverifiedChannelCountsExactly`, повторявший ту же фразу. Закрыто **6 из 6** (5 носителей + тест). Печатная строка оператору `row-showings=` переименована в `rows-checked=`: это описание, печатаемое человеку, а доля `model followed` есть дробь ИМЕННО от него. + +##### §4.3 — спор доезжает до листа подписи. Разрез кортежа, а не правило движения + +Пропуск «одна UNIQUE-строка ⇒ не спор» был безусловным. Его оправдание — «предложение, попавшее на кортеж, разрешается против строки, а не встаёт рядом» — верно только для предложения, которое МОЖЕТ туда попасть. На сид-поверхность предложение не попадает никогда: эмиссия роняет его раньше, банк остаётся при своём, спор постоянен и невидим. Разрез: пропуск действует, **только если строка банка — собственная неподписанная строка движка** (`IsEngineUnsigned`), то есть ровно та популяция, которую этот же прогон переписывает целиком. Отчёт стал согласован с тем, что сделает эмиссия. + +⚠ **Остаток НАЗЫВАЮ, а не заметаю.** Неподписанная строка в дельте ВЛАДЕЛЬЦА отвечает `IsEngineUnsigned` «да» — оба документа читаются `Source: "mined"`, и никакой предикат над строкой банка их не различает. Предложение на её кортеж тоже не разрешается против неё (его роняет `loadAutoBank` на следующем прогоне), и его спор всё ещё молчит. Это **уже** дыра, но она у́же закрытой: закрыта самая частая форма — рукописная сид-строка. ⚠ Число «три из сорока девяти» я получила из промта, поэтому ПЕРЕ-СНЯЛА его сама: `grep -c 'since_ch: *[1-9]'` против `grep -c '^- src:'` даёт **3 из 49** в `books/gu-zhenren/guzhenren-seed.yaml` и **3 из 58** в `guzhenren-seed-v2.yaml` — то есть окно по умолчанию несут 46 и 55 строк соответственно. Разделение двух документов требует, чтобы строка несла, откуда она пришла; сегодня не несёт. + +⛔ **Фикстура, которая обходила предмет, названа поимённо.** `TestConsolidationAgainstTheBankIsReportedAtTheStop` несёт сид `until_ch: 20`, и её собственный комментарий признаёт: «until_ch keeps the seed row off the proposal's UNIQUE key». Мой пин `TestTheCommonestSeedShapeStillReachesTheSigningSheet` — та же e2e-дорога с сидом БЕЗ окна и без sense, плюс отдельный ассерт «окно действительно дефолтное», чтобы тест не начал проходить по причине, не имеющей отношения к предмету, — как это случилось с его предшественником. + +##### §4.4 — операторские сообщения как КЛАСС. Каталог, а не ещё одна подстрока + +**Граница объявлена и она механическая.** Покрыто: каждое `Warn`/`WarnContext`/`Error`/`ErrorContext` на логгере пакета `internal/pipeline` и каждый `bankInputs.remark`, чьё сообщение — СТРОКОВЫЙ ЛИТЕРАЛ. Не покрыто сознательно: `Info`/`InfoContext` (прогресс, а не решение) и `fmt.Errorf` — сообщение, ОСТАНАВЛИВАЮЩЕЕ прогон, уже держит тест, утверждающий остановку, то есть у него есть второй читатель и оно не гниёт в одиночку. Не покрыто и названо, чтобы не спутали с покрытием: АРГУМЕНТЫ (переименование ключа структурного хвоста невидимо), и сообщения, СОБРАННЫЕ на ходу, — их **6**, каталог держит их место с `` вместо слов, так что НОВОЕ такое сообщение видно, и перевод литерала в собранную строку (способ уйти из-под гейта, не тронув его) читается как изменившаяся строка. + +Каталог — `internal/pipeline/testdata/operator-messages.txt`, **113 строк**, `filefuncquoted`. Ходит по ВСЕМУ пакету, а не по списку файлов: прецедент `flagseverity_test.go` в этом же пакете куплен ровно тем, что список файлов гниёт как код, который он проверяет. Гейт утверждает НЕ истину, а ОСОЗНАННОСТЬ: он делает день, когда кто-то меняет эти слова, днём, когда их перечитывают. + +⚠ **Первая редакция моего же теста запрещала дубли в каталоге, и исполнение показало, что довод был неверен:** сравнение идёт МУЛЬТИМНОЖЕСТВОМ, поэтому два одинаковых сообщения требуют двух одинаковых строк, и переформулировка любого краснеет. Запрет снят, довод записан в тесте. + +##### §4.5 — путь записи статуса: ЗАМЕР, которого не было, и он важнее правки + +⭐ **Мутацией измерено, а не прочитано.** Дока `store.ChunkStatus.UpdatedAt` называет ДВУХ писателей `skipped`-строки. Замер на копии: сломать `recordSkippedStages` → **красных 6** (`TestGoldenDeterminism`, `TestRebillProjectionExcludesSkippedAndUnchanged`, `TestABilledDecodeRowIsDisclosedAsOldMoneyEndToEnd`, `TestOrdinaryResumeMovesOnlyTheSkippedRow`, `TestRunnerFlagSkipContinueExit2`, `TestStatusAndRedrive`); сломать флагнутую ветвь `runStageSequence` → **пакет ЦЕЛИКОМ зелёный**. То есть половина того, что дока утверждала, не имела свидетеля ни с какой стороны, и никакая мутация не могла сказать читателю, какой путь сработает. + +Закрыто фикстурой, которой у пакета не было: волна с ДВУМЯ редакторскими стадиями (`projectOpts.secondEditStage`), где первая флагается по чёрному списку отказов, а вторую скипает именно `runStageSequence`. Двух писателей различает `Detail` — единственный долговечный след того, какой путь сработал. Пин `TestASkippedRowSaysWHICHWriterWroteIt` краснеет в ОБЕ стороны (обмен двух предложений местами), обе записи в каталоге. Разрез на один читаемый путь я НЕ брала: заказ его не требует, а он двигал бы раскладку волн — предмет отдельного решения. + +##### §4.6 — шов: ход, а не четвёртая заплата + +Три ключа пересекали границу файла картой: `Contradictions` ключевалась `cf.Src` у писателя и читалась `c.Src` у читателя, `Conf` читалась `c.Key`. Выбор ключа делался ДВАЖДЫ на находку, в двух файлах, и ничто не сравнивало эти два выбора. Ход тот же, что сосед сделал с `BankHolds`: карта убрана, находки едут НЕСГРУППИРОВАННЫМИ (`SelfConflictRows []terminology.ConsolidationConflict`), а сопоставление живёт в ОДНОМ месте — `terminologyResult.findingsFor`, которое заполняет все три поля строки листа. `ConsolidationConflict` теперь несёт `Key` (ключ, по которому находка НАЙДЕНА), а формулировки переехали на саму находку (`PartLabel`, `ConsolidationConflictMessages`) — чтобы лог и лист не могли назвать одну находку двумя способами. + +⛔ **Фикстура — традиционные написания.** `雲海空竅` фолдится в `云海空窍`, и две строки различаются ровно так, как различается боевая популяция. Второй тест ставит РАЗНЫЕ кандидаты так, что неверный ключ находит настоящую строку ЧУЖОЙ строки: сопоставитель, возвращающий пусто на неверном ключе, ловится первым тестом, а возвращающий находку СОСЕДА — только вторым, и это тот отказ, которого оператор не может заметить: лист полон, правдоподобен и не о том термине. + +##### Таблица мутаций — посаженная правка → где → красный тест → ТЕКСТ падения + +Все посадки — на КОПИИ дерева (`tar --exclude=./bin --exclude='./.env*'`; рабочее дерево не правилось ни разу, секретов в копии 0 — проверено `find -name '.env*'`). Ниже — то, что я сажала РУКАМИ и читала по тексту; те же посадки заведены записями каталога и прогоняются `make mutations`. + +| Мутация | Где посажена | Красный тест | ТЕКСТ падения (сокращён до сути) | +|---|---|---|---| +| `t.Conf[c.Key]` → `[c.Src]` | `mining.go:findingsFor` | `TestEveryStopSheetFindingReachesTheRowItBelongsTo`, `TestTheStopSheetJoinIsNotFooledByTheRawSurface` | «the role's confidence must reach the row it was stated for, want 2, got -1» · «row "雲海" took a confidence that is not its own: 9» | +| `cf.Key == c.Key` → `== c.Src` | `mining.go:findingsFor` | `TestEveryStopSheetFindingReachesTheRowItBelongsTo` | «the self-contradiction must reach the row and name the part it drops: []» | +| `cf.Src == c.Key` → `== c.Src` | `mining.go:findingsFor` | оба | «the bank's existing rendering must reach the row: []» · «row "雲海" took the wrong bank row: [approved "云海"→"Море Облаков"]» | +| `ConsolidationConflict{Key: c.Key}` → `{Key: c.Src}` | `terminology.go` | `TestEveryStopSheetFindingReachesTheRowItBelongsTo` | «the self-contradiction must reach the row…: []» | +| `membank.StatusLabel(prior.Status)` → `"signed"` | `mining.go:loadAutoBank` | `TestADropNamesItsHoldersSignature` | «the drop must name the holder's signature as "unsigned draft …"» · «the drop calls the holder "the signed", which it is not» | +| заголовок ремарки → прежний текст | `bankmaterialize.go` | `TestTheDropHeadlineDoesNotPromiseASignature` + каталог | «the headline states a signature the holder may not have: … (the signed term wins)» | +| `if p.Sticky` → `if false` | `membank/mempostcheck.go` | `TestTheUnverifiedDenominatorIsNotACountOfRowsShown` | «Shown counts only the unsigned rows whose key fired HERE, want 1, got 2» | +| ЧУЖОЕ операторское сообщение переформулировано | `seeding.go` | `TestEveryOperatorMessageIsCatalogued` | «IN THE CODE, NOT IN THE CATALOGUE (1): seeding.go seedGlossary "voice profile windows leave chapters uncovered"» + та же строка в «NOT IN THE CODE» | +| строка каталога УДАЛЕНА | `testdata/operator-messages.txt` | тот же | «IN THE CODE, NOT IN THE CATALOGUE (1): …» | +| `detail` писателя 1 → фраза писателя 2 | `waverun.go:recordSkippedStages` | `TestASkippedRowSaysWHICHWriterWroteIt` | «a unit whose member draft flagged is skipped by recordSkippedStages, and the row must say so: "skipped: an upstream stage was flagged"» | +| `detail` писателя 2 → фраза писателя 1 | `waverun.go:runStageSequence` | тот же | «a later stage of the same wave is skipped by runStageSequence, and the row must say so: "skipped: a member draft chunk…"» | +| `IsEngineUnsigned(b) &&` снято из пропуска | `membank/memseed.go` | `TestTheCommonestSeedShapeStillReachesTheSigningSheet` + 2 в `membank` | «the row the owner wrote must be named on the sheet he signs by: {… BankHolds:[]}» · «a proposal sharing a SEED row's tuple is dropped, not merged…: []» | +| пропуск снят целиком | там же | `TestConsolidationKeyConflicts`, `TestConsolidationTupleSkipMirrorsTheStoreKey` | «a proposal on the engine's own unsigned row's UNIQUE key replaces it and cannot contradict it: [{赵甲 …}]» | +| единственный носитель предиката вывернут | там же | те же + `bankchain`, `bankdecisions`, `bankfixpack` | «a set that decides one of 2 surfaces must report the rest as undecided: undecided=2» | +| `bankSettles(...)` → `false && …` | `terminologist.go` | `TestABankSettledCandidateIsNotPaidFor` | каталогом, зелёный `make mutations` | +| проверка расхождения черновиков отключена | `terminologist.go:bankSettles` | `TestADisagreeingBankedCandidateIsStillPaidFor` | каталогом | +| фильтр читает ВЕСЬ банк вместо сид-поверхностей | `terminologist.go:dropBankSettled` | `TestAResumeFindsItsCheckpointsAfterTheFilter` | «the resumed run: job test-book/ch1/edit was started under snapshot …, current config renders … — what moved: **memory_version** … already-paid checkpoints become invalid … re-run with --resnapshot» | +| `applyTypes(cands…)` → `applyTypes(paid…)` | `terminologist.go` | `TestTheClassifiersTypeStillReachesTheSheetThroughTheFilter` | каталогом | +| МОЯ фикстура шва сделана вырожденной | `stopsheetfindings_test.go` | сама себя | «degenerate fixture: "云海空窍" keys as itself, so a wrong key choice cannot be seen here» | +| МОЯ swap-фикстура сделана вырожденной | там же | сама себя | «degenerate fixture: the two candidates must have distinct keys and each differ from its own surface» | + +**ВЫЖИВШИЕ — называю обе.** + +1. ⚠ **`MONEY-the-filter-reads-a-bank-that-moves-between-runs` пережила ПЕРВУЮ редакцию моего пина, и это поймал `make mutations`, а не я.** Причина — фикстура: мой резюм-тест гнал первый прогон с остановкой на банк-границе, а **остановившийся прогон авто-банк не пишет** (`18-bank-ontology.md`), поэтому банк между двумя прогонами не двигался вообще и посадка «читай весь банк» была ненаблюдаема. Фикстура перестроена на ДВА авто-продолжающихся прогона, добавлен ассерт-предпосылка «первый прогон действительно оставил авто-банк», и посадка теперь красная — с текстом, который называет последствие ТОЧНЕЕ, чем моя запись каталога: двигается `memory_version`, то есть обесцениваются не только чекпойнты бэнк-роли, а оплаченная РЕДАКТОРСКАЯ волна, и прогон требует `--resnapshot`. Запись каталога переписана под замеренное последствие. +2. **`M11` — «из моей фикстуры удалён КОНТРОЛЬНЫЙ случай» — выжила, и это ожидаемо, а не дыра.** Удаление контрольного случая по построению не может покраснеть: оставшийся случай продолжает проходить. Держу в отчёте, потому что «выживших нет» без этой оговорки было бы неправдой. Сила оставшегося случая при этом не зависит от контроля: он требует дословного `unsigned draft "方源"→"Фан Юань"`, а эта строка может прийти только из `StatusLabel`. + +##### Находка ВНЕ ЗАКАЗА — класс мутаций, невидимый по построению + +⭐ **Замер, оба конца снятые командой, а не вычтенные из головы.** На входе смены (`git grep` по `060b13b`): классификатор включён в **6 фикстурах из 236**. В дереве сейчас: **7 из 245** — седьмую добавила я. Среди фикстур, вообще доходящих до бэнк-ролей (`terminology: true`, их 42), классификатор гоняют те же **7**. + +Почему это класс, а не одна дыра. Две правки, которые платная фаза делает НАД кандидатами по месту, — `applyTypes` (штамп типа от классификатора) и `ScoreVariants` (пере-ранжирование) — выполняются **только внутри `if len(classified) > 0`**. При выключенном классификаторе не происходит ни одна. Значит любая посадка, которая ломает доставку этих мутаций до вызывающего, зелена в 238 фикстурах из 245 и краснеет в 7 — и если ни одна из этих 7 не спрашивает про предмет посадки, класс невидим ЦЕЛИКОМ. Именно так моя собственная регрессия («фильтр режет слайс вызывающего») прошла мимо всех тестов, которые я к фильтру написала: я писала их на фикстурах с классификатором выключенным, потому что так устроены все соседние. + +Что я сделала: свой случай закрыла пином с ВКЛЮЧЁННЫМ классификатором (`TestTheClassifiersTypeStillReachesTheSheetThroughTheFilter`) плюс запись каталога. Чего НЕ сделала: не пересматривала остальные 238 фикстур — это отдельный заход, а не побочный эффект этого пака. + +##### Честная экономика фильтра — двумя числами, а не словом + +⛔ **Формулировка строки бэклога 335 «прямые деньги на КАЖДОМ прогоне майнинга» по ВЕЛИЧИНЕ неверна, и замер это показал.** Под узкий фильтр попадает 4,1 % кандидатов (56 из 1365), а не «всё банкованное»: 83,4 % банкованных кандидатов несут расхождение и обязаны быть оплачены, потому что расхождение и есть то, ради чего фаза построена. Механизм при этом остаётся — он построен, стоит дёшево и его цена уже уплачена; но носитель обязан говорить, что он покупает. + +**Порядок величины.** Терминологическая фаза ограничена книжными потолками `budget_usd` + `classify_budget_usd`. ⛔ **Первая редакция этого абзаца называла $0.05 + $0.02 = $0.07, и это было число ИЗ МОЕЙ ПРОБНОЙ КНИГИ, выданное за боевое** — ровно тот класс ошибки, который весь пак и чинит. Пере-снято по БОЕВОМУ файлу `backend/configs/pipeline-c1.yaml`: строка **173** `budget_usd: 1.00`, строка **175** `classify_budget_usd: 1.00` ⇒ **$2.00 на книгу**, в двадцать восемь раз больше моей ошибки. Это ПОТОЛОК, а не расход. Оба паса батчатся по рунному бюджету, поэтому 4,1 % кандидатов не переводятся в 4,1 % денег линейно: экономия появляется только там, где снятые кандидаты убирают ЦЕЛЫЙ батч. Отсюда две честные оценки: +- **на книге этого прогона** — число вызовов и денег ДО/ПОСЛЕ в секции холодного прогона ниже (это единственное живое число, которое у меня есть); +- **на книге в 1500 глав** фаза всё равно ограничена теми же книжными потолками — она платится ОДИН раз на книгу, а не на главу, — то есть верхняя граница экономии от этого фильтра составляет **доли $2.00 на книгу**, и ни при каком объёме книги она не растёт. Растёт она только с ДОЛЕЙ подписанного сида: на книге с богатым подписанным банком под фильтр попадает больше, но и там потолок фазы тот же. + +⇒ **Ценность правки не в деньгах, а в том, что платный вызов перестал задавать вопрос, ответ на который заведомо выбрасывается.** Так и надо её наследовать. + +⭐ **И обратное, что стоит записать: узкую форму фильтра оркестратору продиктовал довод (ревью старшего коллеги), а не число. Замер этой смены довод ПОДТВЕРДИЛ — 366 замолчанных споров.** Решение по доводу, подтверждённое замером задним числом, — это то, как в этом проекте должно выглядеть согласие; обратный порядок (замер, опровергающий довод) здесь тоже бывал. + +##### Находка → что сделано → чем предъявлено → **закрыт в N носителях из M** + +| Находка | Что сделано | Чем предъявлено | Носителей | +|---|---|---|---| +| «ключ держит подписанная строка» — ложь при любом статусе держателя | дока, имя параметра, текст строки, заголовок ремарки, комментарий приведены к поведению; формулировка подписи сведена в один носитель `membank.StatusLabel` | `TestADropNamesItsHoldersSignature` (2 случая) · `TestTheDropHeadlineDoesNotPromiseASignature` · 2 записи каталога | **6 из 6** (5 живых + доккомментарий чужого теста, найденный собственным грепом после «готово») | +| `Shown` = «показано модели» — ложь про sticky | все описания приведены к «сработало здесь»; печатная строка оператору `row-showings=` → `rows-checked=` | `TestTheUnverifiedDenominatorIsNotACountOfRowsShown` · `TestARowWithNoRenderingNeverReachesTheDenominator` · запись каталога | **6 из 6** (`membank/mempostcheck.go`, `store/glossary.go`, `pipeline/quality.go`, `cmd/tmctl/render.go`, `store/migrate.go`, нарратив `memory_e1_test.go`) | +| выбор ключа делается дважды на находку | карта убрана, находки едут несгруппированными, сопоставление в одном месте `findingsFor` | 2 теста · 4 записи каталога | **6 выборов из 6** сведены в **1 место** (3 находки × 2 стороны) | +| спор не доезжает до листа на самой частой форме сид-строки | пропуск по кортежу условен на `IsEngineUnsigned` | `TestTheCommonestSeedShapeStillReachesTheSigningSheet` + 2 в `membank` · 2 записи каталога | **1 из 1** носитель пропуска; остаток (неподписанная дельта владельца) НАЗВАН и открыт | +| операторские тексты запинены узко и несистемно | каталог всех сообщений пакета | `TestEveryOperatorMessageIsCatalogued` · `TestTheCatalogueFileIsWellFormed` · 2 записи каталога | **113 из 113** литеральных сообщений `internal/pipeline`; 6 собранных на ходу — местом, не словами | +| путь записи статуса не читается по коду | замер мутацией + фикстура второму писателю + пин на `Detail` | `TestASkippedRowSaysWHICHWriterWroteIt` (2 подслучая) · 2 записи каталога | **2 из 2** писателей; до пака свидетеля имел **1 из 2** | +| роль и классификатор платят за уже банкованное | фильтр до оплаты в узкой форме, один носитель предиката | 5 тестов · 5 записей каталога | **1 из 1** точка оплаты (`runTerminologist` — общий вход обоих платных пасов) | + +##### Правки существующих тестов, вызванные ЗАКАЗАННОЙ сменой поведения (`D39.183`) — объявляю все четыре + +1. **`pipeline.TestLoadAutoBankFiltersRejectsAndCollisions`** утверждал, что снятая строка содержит подстроку `"signed"`. §4.2 заказал привести текст к поведению, и теперь строка печатает подпись держателя (`approved`), а не слово «signed». Ассерт заменён на дословный `key held by the approved "方源"`. **Куда уехала гарантия:** «строка сообщает, что подпись существует» → «строка называет ФАКТИЧЕСКУЮ подпись держателя». Параметр `signed` в том же тесте переименован в `gathered` вслед за сигнатурой. +2. **`membank.TestConsolidationKeyConflicts`**, случай REPLACEMENT: утверждал 0 находок на предложении, делящем UNIQUE-кортеж с СИД-строкой. §4.3 заказал обратное. Ассерт разделён надвое: на кортеже сид-строки теперь 1 (спор постоянен), на кортеже собственной неподписанной строки движка — 0 (замена). **Гарантия:** «деление кортежа никогда не спор» → «деление кортежа не спор только там, где предложение МОЖЕТ на него попасть». +3. **`membank.TestConsolidationTupleSkipMirrorsTheStoreKey`**: та же смена, плюс добавлен КОНТРОЛЬ — тот же кортеж над строкой движка даёт 0, так что новое утверждение есть ответ провенанса строки, а не сломавшийся пропуск. +4. **`membank.TestPostcheckUnverifiedChannelCountsExactly`** — правка НЕ ассерта, а формулировок: его доккомментарий и текст падения повторяли ту же неверную фразу «put in front of the model», то есть были шестым носителем чинимой лжи. Числа теста не тронуты. + +Пятая правка теста — **аддитивная и ничего не переписывает**: `projectOpts.secondEditStage` (`runner_test.go`) добавляет ВТОРУЮ редакторскую стадию, без неё флагнутая ветвь `runStageSequence` недостижима ни одной фикстурой пакета. По умолчанию `false` — каждый существующий проект байт-идентичен. + +##### §4.7 — ХОЛОДНЫЙ ПРОГОН. Что было куплено, что найдено + +**Стенд.** Новый проект `coldrun-truth-0908` в скретчпаде (`books/` — чужая зона, из неё только ЧИТАЛИСЬ исходник и сид владельца). Исходник — побайтная копия среза `coldrun-v16` (sha256 `0b5f9b0266d8c32a…`, 57 838 Б, 10 глав). ⚠ **Сид ПОДКЛЮЧЁН нарочно**, в отличие от исторических холодных прогонов: фильтр §4.1 срабатывает только на сид-поверхностях, и без сида предмет прогона не исполняется вовсе. Сид — `guzhenren-seed-v2.yaml` владельца, 58 строк, 53 approved, `seed-lint OK`. + +⚠ **Прогон шёл на артефакте контраста, ВОССТАНОВЛЕННОМ локально, а не на том, что лежит в репозитории.** `eval/exp16/data/jieba_dict_general_zh.txt` на этой машине отсутствует (и в git его нет по построению). Воспроизведён по запиненному рецепту `docs/experiments/16-bank-mining.md` (jieba 0.42.1 `dict.txt`), **sha256 сошёлся с пином `7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8` дословно**; файл в скретчпаде, чужая зона `eval/` не тронута. Это живое подтверждение строки бэклога **251**. + +**Смета ДО прогона — не моя, а проекция движка** (`tmctl manifest`, $0): **$0.350075** на всю книгу, 10 глав / 14 юнитов / 20 чанков, из них $0.07 — книжный потолок терминологических пасов ЭТОГО стенда. Машинный гейт: `ceilings.book_usd: 1.50` + `day_usd: 2.00` в `book.yaml`, продублировано `--ceiling-usd 1.50` и `--max-units 14` на команде. `escalation.budget_usd: 0` вопреки комментарию боевого конфига — прямой заказ пака. + +**Два запуска, код между ними не менялся МЕХАНИЧЕСКИ:** оба сделаны одним заранее собранным бинарём `bin/tmctl`, sha256 `cd8b06a23340328b9461a2e95e665bde39c6e87f40644597f425921cdae1238e`. + +**ЧТО КУПЛЕНО — по СЫРОМУ ЛЕДЖЕРУ (`tmctl report`), а не по сумме лога.** + +``` +Book ledger: committed=$1.259217 reserved=$0.000000 +MONEY BY WHAT IT BOUGHT: shipped text $0.440828 (29 call(s)) · the book's TERMINOLOGY $0.004595 (2) + · superseded-by-a-later-call $0.534595 (11) · bought-nothing-shippable $0.279200 (4) — total $1.259217 +ROUTING/MONEY: escalation hops=0 · spend by model: deepseek-v4-flash=$0.121351 · deepseek-v4-pro=$1.137867 +``` + +- **Потолок ДЕРЖАЛ и ни разу не сработал:** $1.259217 против машинных $1.50, отказов резервации в логе ноль. +- ⛔ **Смета движка занижена в 3,6 раза: $0.350075 против $1.259217.** Причина названа самим движком: **$0.813795 из $1.259217 (64,6 %) не купили НИЧЕГО** — ни текста, ни терминологии; крупнейшая одиночная потеря — ch10/chunk0/edit на $0.205484. Из них 11 вызовов вытеснены более поздними (регенерации) и 4 не купили ничего отгружаемого. Регенерации пошли по двум документированным причинам: на черновой волне `reason=empty` ×2 (мина DeepSeek — рассуждение съедает бюджет вывода, `00-provider-quirks` §0731), движок сам поднял потолок 8496 → 16992; на редакторской — упор в длину с подъёмом до 32000 на `v4-pro`, где токен дороже втрое. +- **Эскалационных хопов 0** — заказ пака проверен отчётом движка, а не моим обещанием. + +**§4.1(в) — ЗАМЕР ПОСЛЕ, живьём.** + +``` +terminology: candidates the bank has already settled are NOT sent to the paid role … + skipped=1 of_candidates=30 still_paid=29 terms=沈嬷嬷 +terminology finished … bank_conflicts=2 batches_dropped=0 classify_batches_dropped=1 + consolidated=29 declined=0 unanswered=0 reclassified=2 +``` + +- **Фильтр снял 1 кандидата из 30 = 3,3 %** — на корпусном контрфакте было 4,1 %, то есть порядок сошёлся. +- ⛔ **Сколько это в ДЕНЬГАХ на этом прогоне — НЕ измерено, и я этого не заявляю.** Рендер-пас спланировал 2 батча на 29 кандидатах; спланировал ли бы он 3 на 30 — контрфакт, который стоит второго платного прогона и паком не санкционирован. Честный итог: фильтр убрал КАНДИДАТА, а убрал ли он ВЫЗОВ — неизвестно. Терминологическая фаза целиком стоила $0.013608 по строкам леджера (классификатор $0.009013 + два рендер-батча $0.002775 и $0.001820). +- ⚠ **Разделение «экономия фильтра / обрезка бюджета» на этом прогоне ВОЗМОЖНО и вот оно:** `batches_dropped=0` — рендер-пас бюджетом НЕ резался, значит его числа обрезкой не загрязнены. `classify_batches_dropped=1` — classify-пас обрезан, но это артефакт МОЕГО стенда (`classify_budget_usd: 0.02` против боевых `1.00` в `configs/pipeline-c1.yaml:175`), а не свойство движка. + +**§4.2 + §4.6 + §4.3 — предъявлены на ЖИВОМ листе подписи.** Из 30 строк листа две несут пометку, которую доставляет `findingsFor` и формулирует `StatusLabel`: + +``` +四代族长 глава рода четвёртого поколения + origin=banknote type=title freq=8 spread=1 conventions=1 confidence=95 + THE BANK ALREADY HOLDS: approved "四代族长"→"Четвёртый глава рода" [ch 1..end] +``` + +⭐ **Эта строка — живое доказательство §4.3, и оно сильнее любой фикстуры.** Кандидат несёт `src` `四代族长`; сид-строка владельца несёт ТУ ЖЕ поверхность БЕЗ `since_ch`/`until_ch` и без `sense` (проверено в `seed.yaml`). Значит они делят весь UNIQUE-кортеж `(src, sense="", 0, 0)`, и прежний безусловный пропуск `continue`-нулся бы раньше отчёта: **владелец подписал бы банк, не увидев, что роль перевела его собственную подписанную строку иначе.** Вторая пометка — `古月方源` против approved `方源`→«Фан Юань». + +**Что видит владелец на строке отфильтрованного кандидата — предъявлено, а не обещано:** + +``` +沈嬷嬷 — + origin=banknote type=title freq=5 spread=1 conventions=1 + why: neighbour + drafts: матушка Шэнь ×1 +``` +Колонка `dst` пуста (роль не спрашивали), предложение черновиков на месте. ⛔ **Но моё утверждение в первой редакции этого отчёта — «владелец видит: поверхность известна, черновики единодушны, банк согласен» — было ЛОЖНЫМ, и поймала это вторая линза приёмки.** Артефакт не говорил ни одного из трёх: `—` есть тот же глиф, что у «роль отказалась», «ни одна строка ответа её не покрыла» и «бюджет до неё не дошёл», а ранжирование обзора кладёт все четыре в один разряд. Утверждение не снято, а СДЕЛАНО правдой: строка листа теперь несёт `NOT ASKED(the bank already renders this surface and every draft agreed — nothing to decide)`. См. дофикс по второй линзе ниже. + +**Операторские сообщения — читаются ли человеком.** Печатная строка отчёта несёт МОЁ переименование: `UNSIGNED BANK: terms=32 · rows-checked=54 · model followed=44 (81%)`. Читается: «неподписанных строк в банке 32; проверить можно было 54 срабатывания; в 44 из них модель пошла за банком». Прежнее `row-showings` на этих же числах читалось бы как «показов», и 81 % был бы дробью не от того. + +**Что происходит ПОСЛЕ банк-стопа — названо, не сымпровизировано.** Запуск 1 вышел **exit 3** (сентинел стопа), написал карту подписи (17 420 Б), таблицу стопа (19 839 Б) и банк-экспорт; **авто-банк НЕ написан** (`ls` даёт «No such file») — подтверждает `mining.go:234` (`if !stopping`) и клаузу `18-bank-ontology.md`. Дальше законны ровно два пути: подпись владельца (`tmctl bank-apply`) — **не мой акт** (`D39.104`) — либо повторный запуск без флага. Взят второй: запуск 2 авто-продолжил с неподписанным банком, вышел **exit 2** (CompletedWithFlags). + +**Запись статуса.** Флагов в прогоне пять, и все доехали до `chunk_status` и до печатной секции FLAGS: `cjk_artifact` на черновике ch2/0 («CJK share 85% in output»), `off_target_lang` на ch2/1 («only 0% of 4034 letters are in the target's script»), два `decode_error` на редактуре ch5/0 и ch10/0 — и **`skipped` на ch2/0/edit с детализацией «skipped: a member draft chunk of this edit unit was flagged (cjk_artifact)»**. Последняя строка — живое предъявление §4.5: это ровно та фраза писателя `recordSkippedStages`, которую пин `TestASkippedRowSaysWHICHWriterWroteIt` теперь стережёт, и на живом прогоне сработал именно этот писатель. + +##### Найденное В ПРОГОНЕ — отдельной секцией, как заказано + +0. ⛔⛔ **САМАЯ ДОРОГАЯ НАХОДКА ПРОГОНА: треть денег куплена за нечитаемое тело ответа, и этого квирка нет там, где канон велит его искать.** Два вызова редактора вернулись `2xx` с оборванным JSON и были ОПЛАЧЕНЫ: ch5/0 `$0.202122` и ch10/0 `$0.205484` — **$0.407606 из $1.259217, то есть 32 % прогона**, дословно из FLAGS: «billed 2xx with an unreadable body: deepseek: 2xx body decode failed (call IS billed): unexpected end of JSON input». Движок обработал класс штатно и консервативно — у него есть `FlagDecodeError` (`internal/pipeline/disposition.go:79`, «2xx with an unreadable body — billed, conservatively settled, flagged»), и он же сам напечатал итог: «⚠ $0.813795 of that (64.6%) bought NOTHING … the largest single loss is ch10/chunk0/edit at $0.205484». ⛔ **Но в `docs/experiments/00-provider-quirks.md` об этом классе НЕТ НИ СТРОКИ** — греп по `decode`, `unexpected end of JSON`, `2xx` даёт ноль. Это та самая страница, которую канон велит читать ПЕРЕД правкой адаптеров и вызовов; сегодня она не предупреждает о поведении, которое на моём прогоне стоило трети бюджета. ⚠ Зона этого файла — **ПОЛИГОН**, не оркестратор и не бэкенд (зонная таблица `CLAUDE.md`); я сперва отдала находку «оркестратору», и это была ошибка по привычке, а не по таблице. Отдаю находкой, писать в файл — полигону. + +1. ⛔ **ДЕНЕЖНЫЙ ПУТЬ: спенд классификатора отчёт называет потерянным.** Бэнк-роли адресуются СИНТЕТИЧЕСКИМ чанком (глава 0, `chunk_idx` = номер батча), а разложение «за что заплачено» ключуется позицией `{глава, чанк, стадия}` (`internal/pipeline/paidtail.go:120-123`). Классификаторский батч 0 и терминологический батч 0 занимают ОДНУ позицию `{0, 0, terminology}`, поэтому более ранний (классификатор) попадает в ветвь `lastPaid[p] != i` и уезжает в **superseded-by-a-later-call**. На этом прогоне это $0.009013 из $1.259217 — мелочь; на боевом конфиге, где `classify_budget_usd: 1.00`, доля может быть большой. Улика — строки леджера: `0 0 terminology classifier $0.009013`, `0 0 terminology terminologist $0.002775`, `0 1 terminology terminologist $0.001820`, и печатное «the book's TERMINOLOGY $0.004595 (2)» = ровно два последних. **Не мой пак, не чинила.** +2. ⛔ **МОЁ СОБСТВЕННОЕ ОПЕРАТОРСКОЕ СООБЩЕНИЕ ОКАЗАЛОСЬ ЛОЖНЫМ НА ЖИВОМ ПРОГОНЕ — внутри пака, который чинит ложные операторские сообщения.** Предупреждение о разовой пере-консолидации я поставила ДО платного паса и обусловила фактом «книга уже платила». Но это верно для КАЖДОГО обычного резюма отфильтрованной книги: на запуске 2 оно сказало «пас будет куплен ещё раз», а терминологических вызовов было **ноль** (`grep 'attempt completed' | grep -c 'stage=terminology'` = 0, чекпойнты реплеились). Лечение: предупреждение переехало ЗА платный пас и обусловлено `run.fresh` — то есть говорит только тогда, когда пас ДЕЙСТВИТЕЛЬНО был куплен вопреки наличию прежних чекпойнтов. Пин, которого не было: `TestAResumeFindsItsCheckpointsAfterTheFilter` теперь читает лог резюма и краснеет, если предупреждение сработало на прогоне, ничего не перекупившем. +3. **ПРОВЕРЕННОЕ СВОЙСТВО СМЕТЫ, а не курьёз прогона: на книге с регенерациями проекция движка занижает втрое-с-лишним.** $0.350075 против $1.259217 по леджеру; 64,6 % денег, по словам самого движка, не купили ничего. Отсюда прямой довод за форму §4.7 «потолок ставится МАШИНОЙ, а не обещанием»: смета — единственное, чем сессия могла бы обещать, и она ошиблась в 3,6 раза, а машинный потолок отработал и не дал прогону выйти за $1.50. Всякая будущая смена, планирующая платный прогон по `tmctl manifest`, обязана знать: это НИЖНЯЯ граница, и регенерации её не учитывают. +4. **Мина DeepSeek `reason=empty` воспроизвелась дважды из двадцати черновых вызовов** (`finish=length`, `completion_tokens=8496`, тело пусто) — ровно то, что описано в `00-provider-quirks` §0731. Движок отработал штатно: поднял потолок и перекупил чанк. Наблюдение, не дефект. + +##### §10 — что НЕ удалось, что НЕ проверено, где сомневаюсь + +- ⛔ **Экономия §4.1 на КОРПУСЕ не измерена и измерена быть не могла: 0 из 13 прогонов со стоп-таблицей объявляли сид.** Число 4,1 % — КОНТРФАКТИЧЕСКОЕ (сид владельца, наложенный на кандидатов холодных прогонов), и я его так и называю. Живое число — только из прогона этой смены, то есть из ОДНОЙ книги и одного среза. +- **«Доля кандидатов» ≠ «доля денег».** Батчи пакуются жадно по рунному бюджету, поэтому снятие 4 % кандидатов может не убрать ни одного вызова. Разделение экономии фильтра от обрезки бюджета терминолога по протухшим июльским ценам — см. секцию холодного прогона: там названо, резал ли бюджет на этом прогоне, и если не резал, то эффекта обрезки в замере НЕТ, что есть свойство прогона, а не доказательство разделимости эффектов вообще. +- **Остаток §4.3 открыт и назван:** неподписанная строка в дельте ВЛАДЕЛЬЦА неотличима от строки авто-банка (обе `Source: "mined"`), и спор на её кортеже по-прежнему молчит. Закрытие требует, чтобы строка несла свой документ происхождения; сегодня не несёт. +- **Остаток §4.1 открыт и назван:** книга, консолидированная ДО фильтра, переплатит ОДНУ пере-консолидацию, ограниченную `gates.terminology.budget_usd`. Лечение — чекпойнт-идентичность, переживающая смену состава батча, то есть правка адресации денежного пути; в этот пак не входит. +- **Разрез §4.5 на один читаемый путь НЕ взят.** Заказ его не требовал, а он двигал бы раскладку волн. +- **Не проверено поведением, а только кодом:** что `HasCheckpointForStage` возвращает `true` на КНИГЕ ПОСЛЕ старого прогона (без фильтра) — такой книги на этой машине нет, и предупреждение о разовой пере-консолидации я видела только в тесте с искусственно созданными чекпойнтами. +- **Каталог операторских сообщений — постоянный налог, и я его называю:** любой будущий пак, меняющий формулировку любого из 113 сообщений `internal/pipeline`, обязан править строку каталога. Считаю это правильной ценой (гейт делает день правки днём перечитывания), но выбор границы — мой, и он не ратифицирован. +- ⚠ **Артефакт контраста майнинга я ВОСПРОИЗВЕЛА, а не нашла.** Его нет ни в дереве, ни на машине; без него `--verify-bank` отказывает и банковый контур не исполняется вовсе. Это ровно строка бэклога **251**. Мой файл лежит в скретчпаде и умрёт вместе с ним. + +##### Вопросы оркестратору + +1. **Граница каталога §4.4.** Я взяла ВЕСЬ пакет `internal/pipeline` (113 сообщений), потому что любая более узкая граница — список файлов, а он гниёт как код (прецедент `flagseverity_test.go` в этом же пакете куплен ровно этим). Цена — правка одной строки каталога при каждой правке любого варнинга пакета. Если считаешь налог чрезмерным, сузить можно только объявленным списком файлов, и тогда надо принять, что перенос сообщения в новый файл выводит его из-под гейта молча. Решение твоё, я не переигрываю без слова. +2. **Строка 251 и артефакт контраста.** Подтвердила её живьём и УТОЧНЯЮ формулировку — моя первая редакция была шире кода. По `internal/pipeline/mining.go:58-71`: тихое авто-продолжение (`return false, nil`, «mining not configured») наступает, только когда `pack == nil` ИЛИ `contrast_path` **ПУСТ**; если путь в конфиге ЗАДАН, а файла нет, срабатывает `os.Open` строкой ниже и прогон падает ГРОМКО — с флагом или без. Боевой `backend/configs/pipeline-c1.yaml` путь задаёт (строка **139**, `contrast_path: mining-contrast.zh.txt`). ⇒ точно: **на конфиге без `contrast_path` контур молча мёртв; на боевом отсутствие артефакта есть громкий отказ, то есть выкатка не стартует.** Первое — «фича тихо не работает», второе — «развёртывание не поднимется», и лечатся они по-разному. Дописывать в строку — твоя зона. +3. **Разовая пере-консолидация.** Книга, консолидированная до фильтра, купит терминологический пас второй раз (потолок `budget_usd`). Я сделала это громким, но НЕ спросила разрешения: считаю разменом внутри санкции §4.1(б). Если это надо было выносить владельцу — скажи, и я сниму фильтр под флаг. + +##### ДОФИКС ПО ПЕРВОЙ ЛИНЗЕ ПРИЁМКИ (09.09) — блокер и три находки, все закрыты + +⛔ **БЛОКЕР, и это ВТОРОЙ ВИТОК ОДНОГО БАГА: мой фикс первого ложного предупреждения породил второе.** Исправляя «предупреждение врёт на каждом резюме», я перенесла пробу `HasCheckpointForStage` ЗА платный пас — а тот уже записал чекпойнты ЭТОГО прогона. Значит для СВЕЖЕЙ книги, заплатившей впервые, «книга уже платила» тривиально истинно, и оператор читает «ты заплатил дважды» там, где заплатил один раз. Линза предъявила исполнением: один прогон, `WARN … skipped=1 of_candidates=3 cost_usd=0.001820`. Пере-проверила по коду сама: проба стояла на `:431`, `runBankRoleBatches` — на `:414`. + +⚠ **И мой собственный комментарий к этой правке утверждал ЛОЖЬ — ту самую, что пак чинит:** «the question cannot be answered BEFORE the pass … that is knowable only here». Наоборот: «платила РАНЬШЕ» знаемо ТОЛЬКО до первого платного вызова, а «заплатила ВСЁ РАВНО» — только после. Гарантии нужны обе половины, и каждая знаема ровно в одном месте. + +**Лечение.** `paidBefore` снимается ОДИН раз, выше обоих платных пасов; условие срабатывания — `BankSettled > 0 && run.fresh && paidBefore`. Ошибка чтения оставляет флаг ложным: предупреждение о деньгах не выдумывается из ошибки. + +⛔ **Страж, который я на это поставила, утверждение НЕ ловил, и линза права называть его вакуумным.** Он проверял только резюм, где ветвь недостижима по `run.fresh` — то есть отрицательное утверждение о недостижимой ветви. **Оба ложных предупреждения прошли по одной причине: их тест утверждал только МОЛЧАНИЕ, в фикстуре, где сработать было нечему.** Новый пин `TestTheOneTimeCostIsAnnouncedExactlyWhenItHappens` краснеет в ОБЕ стороны: «молчит на свежей книге, заплатившей однажды» и «СРАБАТЫВАЕТ, когда прежний состав действительно был оплачен». Вторую фикстуру пришлось построить: прогон 1 без сид-поверхности платит за ПОЛНЫЙ состав, затем владелец подписывает `方源` в mined-delta, и прогон 2 фильтрует — состав уехал, чекпойнты не находятся, предупреждение обязано прозвучать. Мутация «вернуть пробу за пас» краснит подслучай «молчит» с тем же текстом, что предъявила линза. + +**(а) Остаток МОЕГО фильтра назван числом, и комментарий приведён к правде.** Второе условие проверяет НАБЛЮДЕНИЯ ЧЕРНОВИКА, а не ответ роли: где черновики согласны с банком, а роль перевела бы иначе, кандидат снят и спор не родится — то есть лист подписи теряет пометку `bankHolds`. Комментарий утверждал «output-equivalent» без оговорки; теперь он говорит: **эквивалентно ДЛЯ ДЕЛЬТЫ и НЕ эквивалентно для листа подписи**. ⚠ Уточняю число оркестратора: верхняя граница — **56 из 1365** (не 73), потому что 17 кандидатов из тех 439 не несут ни одного наблюдённого предложения и по моему же правилу ОПЛАЧИВАЮТСЯ. Подмножество тех, у кого ответ роли действительно отличался бы, меньше 56 и не измеримо без оплаты. + +**(б) Вторая половина гарантии «правки по месту доезжают» запинена — и это про ПРАВДУ ОПЕРАТОРУ, а не про полноту каталога.** `ScoreVariants` был назван несущим в том же комментарии, что `applyTypes`, а свидетеля имел только второй. Замер обеими сторонами: верный код даёт у `青茅山` `Signals=[]`, посадка `ScoreVariants(&paid[i])` — `Signals=[conform]`, и пакет остаётся зелёным. Содержательно: классификатор сменил тип `place → term`, а конформность говорит только о name/place, поэтому пере-ранжирование обязано снять сигнал — иначе оператор читает на листе причину ранжирования, которую классификация только что отменила. Ассерт добавлен, запись каталога заведена. + +**Мелочи закрыты, кроме одной — объявленной.** Три условия `bankSettles` получили свидетеля поимённо (`TestTheSettledTestAnswersOnEachOfItsConditions`): банкнотное происхождение — потому что мина-кандидат ДОХОДИТ до дельты и снять его значило бы потерять строку артефакта; алиасы — потому что страж эмиссии их читает; и «одно расходящееся предложение из нескольких — уже находка». Мёртвая ветка «нет наблюдений» оставлена мёртвой и объявлена в доке. **Строка лога с числом экономии §4.1(в) стоит ВНЕ класса §4.4 сознательно** — каталог стережёт ТЕКСТЫ и говорит об этом, аргументы объявлены вне его; поэтому её ключи (`skipped=`, `of_candidates=`, `still_paid=`) запинены прямо, в `TestABankSettledCandidateIsNotPaidFor`: это единственная строка, по которой будущая смена прочтёт, сколько сэкономлено, и число без знаменателя там нечитаемо. + +⭐ **Что я забираю из этого дофикса.** Первое ложное предупреждение нашла я, второе — линза, и оба прошли по ОДНОЙ причине: тест утверждал молчание там, где сработать было нечему. Отрицательное утверждение о ветви, недостижимой в фикстуре, — это не пин, а его форма. Правило, которое я бы записала в норматив: **пин на условное сообщение обязан иметь фикстуру, в которой оно ОБЯЗАНО прозвучать, иначе половина утверждения вакуумна.** + +⚠ **МОЯ ОШИБКА В ХОДЕ ДОФИКСА, называю сама.** Проверяя исправленную запись каталога, я запустила `go run ./cmd/tmmutate -root .` по РАБОЧЕМУ дереву вместо копии — прямое нарушение контракта инструмента (его же шапка: «`-root` не предложение», сертифицировать можно только дерево, чистое НА МОМЕНТ старта). Дерево уцелело: отпечаток восстановил файл, `git status --short` даёт те же 25 позиций, следов посадки в `terminologist.go` ноль (`grep -c '_ = paidBefore\|paidNow'` = 0 при 1146 строках файла), сборка и целевой тест зелены. Но уцелело оно свойством инструмента, а не моей дисциплиной, и параллельная сессия в этот момент могла бы снять с испорченного дерева ложный замер — ровно тот случай, ради которого правило написано. + +⚠ **И ещё одна, поймана самим гейтом:** первая редакция записи `MONEY-the-one-time-cost-warning-reads-this-runs-own-checkpoints` меняла только условие, из-за чего `paidBefore` оставался неиспользуемым и пакет НЕ КОМПИЛИРОВАЛСЯ — `make mutations` отчитался «nothing ran». **Мутация, которая не собирается, — не пойманная и не выжившая, а неисполненная**, и без явного `unexpected outcome` она читалась бы как зелень. Запись переделана в ДВЕ правки (условие + `_ = paidBefore`), как я и сажала руками. + +##### ДОФИКС ПО ВТОРОЙ ЛИНЗЕ ПРИЁМКИ (09.09) — блокер, пять находок, все закрыты; одна отклонена доводом + +⛔ **БЛОКЕР — ТРЕТИЙ виток корня «копия против оригинала», и первые два чинила я же.** `applyTypes(cands, …)` штампует исправленный тип в `cands` (`:379`), а запрос к роли собирается из `paid` (`Batch(paid…)`, `:397`) — из КОПИЙ, когда фильтр сработал (`kept = cands[:0:0]`, `:1090`). Значит на книгах, ради которых фильтр и написан, классификатор ПОКУПАЕТСЯ (он покупается ровно за поле `type`, и поле уходит на провод для всего батча), прогон печатает `reclassified=N`, а провод несёт эвристическую догадку. Пере-проверила по коду сама: три адреса на месте. + +**Лечение убирает КОРЕНЬ, а не третий симптом.** `dropBankSettled` отдаёт теперь ИНДЕКСЫ, а не собственный массив; `paid` пересобирается из `cands` функцией `pickCandidates` ПОСЛЕ правок по месту. Копий не существует вовсе — лист и провод физически не могут разойтись, потому что читают одни и те же строки. Пин утверждает БАЙТЫ ЗАПРОСА (`type: term` есть, `type: place` нет) — утверждения о том, что уходит на провод, в репозитории не было ни одного. + +**1. Посылка моей фикстуры шва была ВЫДУМАНА.** Комментарий утверждал, что `NormalizeSourceKey` сворачивает `雲海空竅 → 云海空窍`. Пере-сняла живой функцией: `雲海空竅 → 雲海空竅`, свёртки НЕТ — ни `雲`, ни `竅` в `internal/text/data/trad2simp.txt` нет (0 и 0 при контроле: `長` и `鈴` есть по одному, всего 563 строки). Две строки различались только потому, что я их так набрала, а страж анти-вырожденности сравнивал два рукописных литерала — то есть подмена нормализатора тождеством оставила бы оба теста зелёными. Фикстура перестроена на `長空`/`長` (сворачиваются в `长空`/`长`), и **страж теперь зовёт настоящий нормализатор**: `text.NormalizeSourceKey(c.Src) != c.Key` → fatal. ⭐ Урок шире случая: **посылка фикстуры, проверенная литералом, удостоверяет литерал.** + +**2. Обе свёртки внутри денежного фильтра получили свидетеля.** `NormalizeSourceKey(e.Src)` при индексации сид-поверхностей и `NormalizeTargetForm(v.Dst)` при сравнении с dst банка переживали полную батарею. Цена первой названа в записи каталога: фильтр молча перестаёт срабатывать для всей традиционной и кана-популяции, а `BankSettled` показывает 0 — неотличимо от «нечего пропускать». Вторая — само денежное решение. Пин `TestTheFilterFoldsBothSidesTheWayProductionDoes`, две записи каталога. + +**3. Знаменатель денежной строки пере-запинен на ОДНОЙ строке.** Три `strings.Contains` по общему буферу удовлетворяются тремя РАЗНЫМИ строками, и обнуление знаменателя оставляло пакет зелёным. Теперь тест вырезает строку с `already settled` и требует `skipped=1`, `of_candidates=3`, `still_paid=2` В НЕЙ. Запись каталога заведена. + +**4. ОТКЛОНЕНА доводом, и оркестратор довод поддержал.** Утверждение «при исчерпанном пожизненном бюджете предупреждение структурно молчит» верно, но молчит оно ПРАВИЛЬНО: при `fits == 0` ничего не куплено, а моё предупреждение — про «пас куплен ВТОРОЙ раз». Сам факт извещается раньше и другим сообщением: `if fits < len(batches)` печатает `batches_planned` / `batches_running=0` / `batches_dropped` / `spent_usd` / `budget_usd` ДО первого вызова. Остаток, который я назвала сама (книга у пожизненного потолка не получит консолидации НИКОГДА, и об этом не говорит ни одно сообщение), — свойство `budget_usd`, а не фильтра; заведён строкой **356**. + +**5. Закрыта ПРАВКОЙ АРТЕФАКТА, а не снятием утверждения.** См. исправление выше по тексту: строка листа несёт теперь `NOT ASKED(…)`. ⚠ Секция предложений сайдкара НЕ тронута, и это проверено ПРЕЖДЕ добавления поля: рефлективный страж `bankexport_test.go:107-108` ходит по полям `BankExportProposal`, а не `BankStopRow`, и `projectBankProposals` перечисляет поля поимённо — новое поле в проекцию не попадает. Пин на пометку, на её ОТСУТСТВИЕ у оплаченного кандидата и на её появление в отрендеренной таблице; запись каталога. + +**Мелочи.** Список терминов в строке экономии обрезается на 20 — теперь рядом печатается `terms_listed=N`, так что обрезка перестала быть молчаливой. `TOTAL (this run)` не включает `ClassifyCostUSD` — предсуществующее, не моё, не трогала. Переименованная строка `rows-checked` отдельного стража не получила: её стережёт та же печатная секция отчёта, которую я и правила, а заводить ради одного слова второй механизм — не «least mechanism». + +⚠ **МОЯ ОШИБКА ПРОЦЕДУРЫ, названная оркестратором и признаю́ её.** Таблица чисел первой редакции была ВСПОМНЕННОЙ, а не снятой: пока линзы судили, дерево уехало под ними, и в отчёте стояло `248 · 37 · 1282`. Таблица ниже пере-снята командой после последней правки. + +##### Числа, снятые ПОСЛЕ последней правки — с командой у каждого + +Порядок: последняя правка `backend/` → `make battery` → `make mutations`. Между гейтами и после них дерево `backend/` не тронуто; правки `docs/PROGRESS.md` после гейтов были и Go-гейтов не касаются. + +| Число | Значение | Чем получено | +|---|---|---| +| Батарея | **19 `ok` · 0 `FAIL` · 4 «no test files» · 4 скипа · `0 issues` линта · exit 0** | `make battery` | +| Скипы поимённо | `TestMinerFullBookParity`, `TestCorpusBankKeyConflicts`, `TestHelperEventsRun`, `TestHelperKillLoop` | `grep -- '--- SKIP'` по логу батареи | +| Мутации | **47 запусков · 47 RED · выживших 0 · `0 unexpected outcome(s)` · `anchors swept: 0 of 258 entr(ies) rotten` · exit 0** | `make mutations` | +| Каталог мутаций | **258 записей, 47 в батарейном подмножестве** (на входе 232 / 21) | `python3 -c "json.load(...)"` по `cmd/tmmutate/mutations.json` | +| Тестов в зоне | **1267 → 1285, добавлено 18, удалено 0** | `git grep '^func Test' 060b13b` против `grep -r` по дереву, сверка `comm` ПО ИМЕНАМ | +| Каталог операторских сообщений | **113 строк** | `grep -vc '^#' internal/pipeline/testdata/operator-messages.txt` | +| Дерево | **25 позиций: 18 правок + 6 новых файлов в `backend/`, плюс `docs/PROGRESS.md`. Вне этих двух путей — 0** | `git status --short`, затем тот же вывод с `grep -v` по двум путям | +| Деньги пака | **$1.259217** (единственные платные вызовы — холодный прогон), потолок $1.50, отказов резервации 0 | `tmctl report` (сырой леджер) | + +**Изменённые файлы, поимённо.** `backend/`: `cmd/tmctl/render.go` · `cmd/tmmutate/mutations.json` · `internal/membank/{mempostcheck.go, memseed.go, memory_e1_test.go, memseed_test.go}` · `internal/pipeline/{bankmaterialize.go, mining.go, quality.go, terminologist.go, miningstop_join_test.go, resume_updatedat_test.go, runner_test.go}` · `internal/store/{chunkstatus.go, glossary.go, ledger.go, migrate.go}` · `internal/terminology/terminology.go`. Новые: `internal/membank/postcheckdenominator_test.go` · `internal/pipeline/{bankoperatortruth_test.go, banksettled_test.go, operatormessages_test.go, stopsheetfindings_test.go}` · `internal/pipeline/testdata/operator-messages.txt`. Плюс `docs/PROGRESS.md` — этот отчёт. + +**Круги сошлись.** Последний проход (собственный греп знаменателей + прогон + разбор леджера) дал ЧЕТЫРЕ новые находки, все закрыты или названы: пятый и шестой носители лжи `Shown`, ложное собственное операторское предупреждение (закрыто правкой и пином), и две находки вне заказа (оплаченный `2xx`, коллизия позиций бэнк-ролей) — отданы носителями, не чинились. Проход после них новых находок в коде пака не дал. + +**Работа завершена, править не планирую.** + +#### Пак «гейт вместо прозы» (07.09, промт `archive/prompts/BACKEND_GATES_NOT_PROSE_SESSION_PROMPT.md`, вход HEAD `bd652fd`). ✅ ЗАЛЕНДЕН и ПРИНЯТ — акт `D39.225`, три дофикса, оба верификатора окнули закрытие **ЗАПИСКА-ПЛАН (§7), написана ДО первой правки.** Пять швов держатся прозой и вырожденными фикстурами; ставлю под каждый гейт, боевой код трогаю ровно в одном месте (§4.2а — единственная заказанная смена поведения).