# Доказательная база пака «входная дверь шва» (движковая половина D39.156) Бэкенд-сессия 24.08.2026. Промт архивирован — `docs/archive/prompts/BACKEND_SEAM_PACK_SESSION_PROMPT_2026-08-27.md`; записка-план — `SEAM_PACK_PLAN.md`. **Каждое число — с командой, которой получено;** приёмка пере-раняет выборочно. Команды из `backend/`. **Карта чтения — что здесь ЖИВОЕ, а что улика закрытого пака.** Четыре пака приняты и заленджены (D39.158), поэтому целиком файл читать не надо; входи по предмету: · **живые остатки и PLAUSIBLE** — §41, §24, §7, §9, хвосты §45 и §46 · · **пинги, правок по которым НЕ делали** — §10, §23, хвост §46 · · **решения зоны с доводами** — §21 (девять мелочей), §37 (модель флажка), §17 (выживший гейт) · · **провенанс чисел** — §18 (цена глагола), §29 п.1 (кап в количестве), §38 (хвост за потолком), §15 (цена рендера) · · **правила протокола, вынесенные инцидентами** — §11, §25, §35 · · **что построено** — §0, §28, §36 (состав трёх паков), §40 (счёт тестов). Попунктные чеки заказов (§12, §26, §43) вырезаны как прожитая приёмка — на их местах стоят адреса. ## 0. Состав пака **Новое:** `internal/membank/decisions.go` (чистая логика решений) · `internal/pipeline/bankdecisions.go` (обвязка: лок, чтения, записи, отчёт) · `cmd/tmctl/bankapply.go` (CLI) · `internal/seed/decode.go` (строгие декодеры схемы) · `internal/standdata/standdata.go` (тест-резолв корпуса) + 6 тест-файлов и 2 фаззера. **Тронуто:** `internal/config/book.go` (конвенционные дефолты) · `internal/pipeline/mining.go` (дефолт-осведомлённые чтения) · `internal/pipeline/status.go` (`status_version` + `artifacts{}`) · `internal/pipeline/export.go` (`export_version`) · `internal/pipeline/bankexport.go` (`bankTermID` → `membank.TermID`, одно определение на двоих) · `internal/pipeline/refusal.go` + `cmd/tmctl/main.go` (класс 14) · `internal/store/store.go` (+34 строки: `LockProject`) · `internal/membank/memseed.go` (строгий разбор) · `internal/seed/seed.go` (устав пакета) · `cmd/tmctl/invocation.go`, `dotenv.go` (`--keys-file`, `--decisions`). ## 1. Батареи ⚠ **Батарея НЕ детерминированно зелена на этой машине, и это НЕ пак** — читай до пере-рана, иначе красное будет отнесено на пак. Два пред-существующих `-race`-флейка в `internal/store` (§10 п.7) срабатывают примерно раз на 2–4 полных прогона: замерено `go test ./... -race -count=1` ×4 → прогон 1 чист, прогон 2 — `TestKillMinus9LosesAtMostOneCall` (`backend/internal/store/kill9_test.go:201=func TestKillMinus9LosesAtMostOneCall`). **Красное ИМЕННО в этих двух тестах ожидаемо; любое другое красное — регрессия.** ⚠ Частота — из ЭТОГО замера, а не из §31: там ДРУГОЙ прогон (чередующийся A/B против HEAD, 5 раундов, обе стороны 5×FAIL), и он закрывает АТРИБУЦИЮ — «флейк пред-существует паку», — а не частоту. На дереве пака: `make battery` — `EXIT=0` на чистом прогоне (skip: `TestMinerFullBookParity`, `TestHelperEventsRun`, `TestHelperKillLoop`); `make battery-stand` **без единого `TM_*_DIR`** — `EXIT=2`, красен ровно `TestMinerFullBookParity`, причина — отсутствующий `eval/exp16/data/jieba_dict_general_zh.txt`; `make lint` — `0 issues.`; `go vet ./... && go vet -tags live ./...` и `gofmt -l .` — чисто. Стенд-картина совпала с опорной точкой оркестратора ⇒ **критерий §4.6 выполнен буквально.** Батареи дерева, которое передавалось дальше, — §31 (дофикс), §38 (фикс2), §46 (фикс3). ## 2. Адверсариальные посадки: 16/16 краснеют Харнесс сажает мутацию → гоняет тесты НАЗВАННОГО пакета → откатывает; он же снимает sha256 целей до прогона и проверяет после (`TREE IS NOT PRISTINE`). ⚠ Пакет пина ≠ пакет правки. | # | посадка | пакет пина | что покраснело | |---|---|---|---| | M1 | дефолт срабатывает на НЕпустом ключе | `config` | `TestDeclaredDecisionPathIsNotDefaulted` — ⚠ пин СНЯТ вместе с предметом (ключи retired, D39.158 п.5; §40) | | M2 | частичный результат переживает отказ (чистый слой) | `membank` | `TestAllOrNothing` +2 | | M3 | частичная ЗАПИСЬ переживает отказ (обвязка) | `pipeline` | `TestBankApplyRefusesAPartialSet` | | M4 | повторный вызов пишет файл заново | `pipeline` | `TestBankApplyRepeatIsAByteNoOp` +2 | | M5 | лок берётся БЛОКИРУЮЩЕ | `pipeline` | `TestBankApplyRefusesABusyProject` (по 10-сек. границе) | | M6 | читающий глагол принимает ключи | `cmd/tmctl` | `TestOnlyTranslateAcceptsKeys` | | M7 | занятый лок отказывает классом 10 вместо 12 | `pipeline` | `TestBankApplyRefusesABusyProject` | | M8 | пишется `status: auto` вместо `approved` (мина №1 строки 199) | `membank` | `TestApproveWritesAnExplicitApprovedStatus` +2 | | M9 | решение, тронувшее только rejects, пере-рендерит и delta | `pipeline` | `TestADeclineDoesNotRewriteTheDelta` | | M10 | промоушен теряет алиасы строки банка | `membank` | `TestPromotionKeepsTheBankRowAliases` | | M11 | voice/address сида не сверяются с результатом | `membank` | `TestDecliningACharacterAVoiceProfileNamesIsRefused` | | M12 | два отказа по одной поверхности проходят как два терма | `membank` | `TestTwoDeclinesOfOneSurfaceAreIllFormed` | | M13 | пред-существующая поломка проглатывается молча | `membank` | `TestAPreExistingCollisionDoesNotBlockAnUnrelatedDecision` | | M14 | документ из одних комментариев снова не грузится | `seed` | `TestDecodeAcceptsAnEmptyDocument` | | M15 | ruby-чтения выброшены из судимого набора | `membank` | `TestApprovingARubyReadingOfASeedTermIsRefused` | | M16 | инертный отказ по алиасу дельты принимается | `membank` | `TestDecliningAnAliasOfAnApprovedTermIsRefused` | ## 3. Живые пробы на реальной книге стенда В КОПИИ вне рабочего дерева (`cp -r books/gu-zhenren/minirun-verify2 /live`, пути пере-указаны на копию). **В `books/` не записано ничего.** ⚠ Те же пробы пере-гонялись после дофикса (§19) и после фикс2 (§39) — там картина СВЕЖЕЕ; здесь оставлено то, что позже не пере-снималось. | проба | результат | |---|---| | занятый лок (flock держит сторонний процесс) | `exit=12`, `wall=0.02s` | | `decline` над рукописной дельтой | md5 дельты не изменился, комментарии владельца целы | | reject-файл ИЗ ОДНИХ КОММЕНТАРИЕВ | `exit=0` (до фикса R1 было `exit=10`, `seed: EOF`) | | все seed/delta/signature/auto-bank стенда под строгим загрузчиком | 21 из 21 чисто (независимо: 29/29 на более широкой выборке, включая книги dovodka/role-topology/tenant-panel); пере-снято после дофикса как 45 из 45 — §31 | ## 4. Замеры | что | число | вердикт | |---|---|---| | **цена глагола** (`bank-apply --dry-run`, `/usr/bin/time`) | ⚠ числа пака (5 000 термов → 1.60с/133МБ · 20 000 → 9.08с/350МБ) **не воспроизводятся, а по памяти занижены примерно вдвое — НЕ наследовать**: пере-замер §18, худший случай §29 п.1 | датум для таймаута платформенного воркера берётся из §18/§29, не отсюда. Что осталось верным: бюджет платформы на ОДИН вызов движка — 60с (`platform/internal/runs/reconcile.go:280`); реальная книга (кап майнера 200 за акт, сид стенда 58) — под секундой; цена — двойной прогон проверки документа, и он же не даёт пред-существующей поломке блокировать починку | | **контеншн** (20 одновременных `bank-apply`) | `2×0`, `18×12`, после гонки `seed-lint OK` | лок арбитрирует чисто; два нуля законны — второй взял лок после первого и оказался no-op | | **деньги** (sha256 базы до/после) | `7ea61ccecada341b` → идентичен | глагол не сдвинул ни байта хранилища. Сильнее SELECT по ledger: не требует sqlite и покрывает всё хранилище | | **детерминизм** (тот же документ в ДВУХ процессах, `cmp`) | побайтово идентичны | закрывает единственный реалистичный источник недетерминизма в Go — порядок обхода map | | **`FuzzDecisionDocument`** | **487 163 исполнения, 0 падений** | оракулы: не паникует · всё-или-ничего · **ни одно решение не теряется молча** (каждый входной индекс ровно один раз в accepted∪rejected) · принятый результат грузится реальным загрузчиком | | **`FuzzSeedDocumentRoundTrip`** | **199 896 исполнений, 0 падений** | `parse(render(A)) = A`, рендер — неподвижная точка; на этом держится байтовая идемпотентность | Оба фаззера исполняются обычным `go test` по seed-корпусу (15 кейсов) ⇒ входят в `make battery`; корпус живёт в `GOCACHE`, в репозиторий не попадает. ## 5. Дефекты, найденные и закрытые Все воспроизведены мной перед правкой, каждый под пином. | # | дефект | пин | |---|---|---| | R1 | **регрессия пака:** документ ИЗ ОДНИХ КОММЕНТАРИЕВ перестал грузиться (`seed: EOF`) — гард проверял «пусто после TrimSpace», а байты комментария не пусты. Бил по `glossary_seed` КАЖДОЙ книги. Тестируется EOF от декодера, пре-чек снят | M14 | | R2 | **стоил бы ДЕНЕГ:** `--decisions` принимался ВСЕМИ командами и молча игнорировался (флаги в одном `FlagSet`, гард стоял только у `--keys-file`) ⇒ `tmctl translate --decisions d.json` разбирался чисто и запускал ПЛАТНЫЙ прогон. **Класс: флаг, которого команда не исполняет, обязан отказывать, а не игнорироваться** | `TestOnlyBankApplyAcceptsDecisions` | | R3 | промоушен строки банка терял её АЛИАСЫ (кластер майнера); auto-bank строка затем дропается как key clash ⇒ терм молча сужается до своего src | M10 | | R4 | проверка «результат загрузится» не включала **ruby-чтения**: прогон грефтит kana-чтения на сид-термы как ФАЙРЯЩИЕ алиасы до проверки коллизий ⇒ approve на чтение сид-терма принимался и убивал прогон | M15 | | R5 | то же по **voice/address**: сид называет персонажа, чей терм живёт в дельте; отказ от терма принимался и валил следующий прогон | M11 | | R6 | отказ по поверхности, которая АЛИАС одобренного терма дельты, принимался как `applied`, а терм продолжал её рендерить | M16 | | R7 | **пред-существующая** поломка файлов проглатывалась молча: `applied, exit 0` книге, чей следующий прогон уже обречён. Поле `preexisting_problems` — сообщается, НЕ отказывает | M13 | | R8 | решение, тронувшее только reject-лист, пере-рендеривало рукописную дельту побочно | M9 | | R9 | два ОТКАЗА по одной поверхности с разными `sense` проходили как два терма: второй затирал note первого, оба отчитывались `applied` | M12 | | R10 | отказ ОТКАЗУ по терму сида выдавал довод от approve («строка дельты уронила бы UNIQUE») — для decline это ложь, он строки не пишет | `TestDecliningASignedSeedSurfaceIsRefused` (проверяет ТЕКСТ довода) | **Не подтвердились (опровергнуты исполнением; НЕ поднимать заново):** `--keys-file` не принимает `redrive` — механика верна, вывод «канала нет» ложен, конвенционный `.env` у него есть · `canonical_rewrite` предупреждает в том же вызове, что и выполняет — проекция это отдельный вызов, не пишущий ни байта · нормализатор на латинице удалил бы кейс-вариант — пред-существующая идентичность поверхности во всём движке, паком не введена · проекция молчит о пере-оценке редакторской волны — сумма квотируется цифрой и гейтится согласием в другом месте. ## 6. Изменение КОНТРАКТА, требующее ратификации **Заведён класс полосы 14 `decisions_rejected`.** Отвергнутый по существу набор решений ≠ сломанный деплой: `platform/internal/ingest/exit.go` `DeploymentFault` относит 10 к «условию, общему для всех книг хоста» (ось D39.134: при 12 действует время, при 10 человек — но НЕ тот же), и по 10 набор решений ОДНОГО пользователя был бы списан на оператора. ⚠ Уточнение, снятое мной: `DeploymentFault` покрывает 10, **12 и 13** одинаково, а зовут её только пути интейка (`readmodel.go:227`, `pgstore/books.go:568`) — у `bank-apply` ноль вызывающих на платформе, то есть мис-классификация перспективная, а не живая. Старый читатель деградирует безопасно: `Refused(14)` истинно ⇒ `OutcomeRefused`. 10 сохранён для документа, не прочитавшегося КАК документ решений. Гейт тотальности таблицы: `TestTheRefusalTableIsTotal` (класс без номера тихо падал бы в catch-all 19). ## 7. Что НЕ доказано - **Рантайм-вердикт по ПЛАТНОМУ пути — PLAUSIBLE.** Доказано: ключ из `--keys-file` доезжает до заголовка (`Authorization: Bearer fake-deploy-key`), и БЕЗ флага тот же прогон отказывает конфигом с именем незаполненного env. НЕ доказано: что реальный провайдер примет ключ и что SaaS положит файл туда, куда укажет флаг — это платформенная половина строки 211, чужая зона. - **Цепь целиком не гонялась** — §8. - Живой `translate` на стендовой книге не гонялся: санкции на платные вызовы у пака нет. ## 8. Цепь целиком — что пак замерить не смог, и чем это закрыто позже **Центральное продуктовое утверждение пака как ОДНО исполнение:** подписной стоп красный → `bank-apply` промотит все dst-несущие термы и отклоняет WHICH-only → резюм → **стоп ГАСНЕТ**. Части доказаны порознь (явный `status: approved` пишется — M8; результат грузится — sweep 21/21 и фаззер; `TestTheRunReadsTheConventionalDelta` доказывает, что прогон читает именно тот файл), **но цепь целиком паком не гонялась ни разу.** **Почему — проверено, не заявлено:** банк-стоп на этой машине не воспроизводится вовсе (`pipeline.yaml` требует `contrast_path: …/eval/exp16/data/jieba_dict_general_zh.txt`, `ls` → `No such file or directory` — та же причина, по которой красен парити), и даже с контрастом до стопа надо дойти ОПЛАЧЕННОЙ черновой волной. Синтетический контраст = подделать замер. ⚠ **Заказ приёмке снят исполнением дофикса:** цепь построена в репозитории (`internal/pipeline/bankchain_test.go`, $0 на фейковом провайдере, ОДИН проект от стопа до погасшего стопа) — но на СИНТЕТИЧЕСКОМ контрасте, и ограничение названо в комментарии самого теста и в §24. Живой стенд стоп по-прежнему не воспроизводит (§41). Что при этом НЕ дефект: свежая пачка ≤200 термов реверс-секции на большой книге — кап режется ПОСЛЕ фильтров (§22; постоянный якорь этого факта — `internal/miner/emissioncap_test.go`). ## 9. Остаточные риски — решение оркестратора, не моё - **Канонический пере-рендер необратим на уровне ДВИЖКА.** Смягчено трижды: проекция сообщает `canonical_rewrite` ДО мутации · ДАННЫЕ переживают round-trip полностью (теряются форматирование и комментарии шапки) · **на стенде восстановление УЖЕ есть** — репозиторий книг ВЕРСИОНИРУЕТ эти файлы (`git -C books ls-files | grep -c 'mined-delta\|mined-rejects'` → **30**, `check-ignore` → не игнорируется), откат через `git -C books checkout`; на SaaS каталог книги платформенный и снапшот — её сторона шва. Бэкап в движке не строил: механизм не заказан, носителя нет, существующий покрывает обе стороны. Владельцу: провенанс из комментариев шапки переносить в поле `note:`. - ~~Размер документа решений не ограничен~~ — **закрыто дофиксом:** два капа (1 МиБ на чтение и 5 000 решений на применение, оба классом 14). Довод и замер худшего случая — §29 п.1. ## 10. Пинги — предметы ВНЕ карты пака, правок не делал 1. **`rubyToCandidates` мёртв в проде — пере-проверено 02.09.** `grep -rn "rubyToCandidates" --include=*.go .` → 4 хита: определение, два комментария и ЕДИНСТВЕННЫЙ вызов — из теста (`backend/internal/membank/memseed_test.go:204=cands := rubyToCandidates`). ⚠ Адрес механизма из первой редакции («`seeding.go:36-38` строит `manualSrcs` и никуда не передаёт») МЁРТВ: ruby-путь переехал, и сегодня прогон читает чтения в `backend/internal/pipeline/bankmaterialize.go:84=RubyReadingsForBook(r.Book.BookID)` и отдаёт их ТОЛЬКО в `AttachRubyAliasesToManual` — аргумента `manualSrcs` в проде не строит вообще НИКТО, то есть кандидатная половина просто не подключена. Следствие прежнее: строк `Source:"ruby"` в банке не возникает, а платформа держит для них ветку словаря (`platform/internal/ingest/bank.go` `termOrigin` → `OriginAnnotated`). 2. **Гейт `TestDispatchCommandsCoversTheSwitch` слеп к `case` с КОНСТАНТОЙ** — считает только `*ast.BasicLit`, поэтому команда, объявленная константой, невидима гейту, чей смысл в том, что дефект строки 176 «отрастал трижды». Обнаружено посадкой; я вернулся к литералу (конвенция файла `if cmd == "seed-lint"`), поэтому дыра не эксплуатируется. 3. **`redrive` — платный путь без канала ключей аргументом.** Промт: «флаг только у `translate`», сделано ровно так. Канал у него есть (конвенционный `.env`), и платформа его не зовёт (`runner/engine.go` строит только translate/status/manifest/export). Развилка следующему паку. 4. **Проекция берёт тот же лок**, что и применение ⇒ «поправить термин при живом прогоне» даёт 12 даже на ПРЕВЬЮ, на всю длину прогона. Платформенный пак обязан это спроектировать, а не обнаружить. 5. **Пути `mined_delta`/`mined_rejects` опубликованы в `status --json`.** Законно по п.1 закона, но эти файлы версионного конверта не несут, а опубликованный путь читается как приглашение. Единственная изгородь против их парсинга платформой — комментарий. Самое мягкое место в «формат знает ОДНА сторона». 6. **Схема сида без `omitempty`** — `yaml.Marshal(seed.File{})` печатает все ключи, включая пустые. Уже существовало (`miner_emit.go` `DeltaYAML` рендерит так же), глагол наследует. Смена тегов = правка формата данных (`internal/seed/seed.go` это оговаривает). 7. **Два флейка в `internal/store`, пред-существующие.** `TestTheSeamIsNotChargedToTheStoreOperationBudget` и `TestKillMinus9LosesAtMostOneCall`; тесты НЕ трогал (D39.121: правка теста ради зелени недопустима). Оба меряют ВРЕМЯ (первый сознательно сжимает `opTimeout`, второй гоняет helper против таймера SIGKILL); покрытие независимо подтверждает, что `LockProject`/`Release` этими тестами не исполняются вовсе. ⚠ Первый A/B (3 раунда × 10) дал `kill9` 2/30 на HEAD против 0/30 на дереве пака — **это НЕ значит «пак улучшает»**: на таком числе событий это шум (Фишер p≈0.5); решающее и достаточное — что флейк живёт на дереве, которого пак не касался. Окончательная атрибуция — 5-раундовый A/B в §31. ## 11. Два правила протокола, вынесенные инцидентами сессии 1. **Эталон самопроверки снимается ДО первого изменения, и харнесс мутаций обязан фингерпринтить цели.** Незавершённый прогон `mutate.py` оставил посадку M4 в дереве ~40 минут, выглядящей продакшн-кодом, и сторонний ревьюер снял с этого дерева замеры (он же собрал `tmctl` из незакоммиченного дерева, не сняв отпечаток, — на класс наступили обе стороны). Посадку поймал и правило теперь держит пин `TestBankApplyRepeatIsAByteNoOp`: он же документирует и форму инцидента, и довод (инод плюс рукописный комментарий в шапке — то, что пере-рендер уничтожает, а проверка содержимого не заметила бы). ⚠ Класс повторился следующей сессией ВНУТРИ самого харнесса — §25; отпечаток целей и каталога у инструмента сегодня свой — §16. 2. **Одиночный прогон — не замер.** Атрибуцию флейков я сперва вывел из одного прогона («зелено в изоляции ⇒ нагрузка»), затем другой одиночный прогон дал ровно обратное. Решает только чередующийся A/B (§10 п.7). ## 12. Чек заказа и дифф тестов — сняты Попунктный чек шестнадцати пунктов промта и пофайловый дифф `^func Test` этого пака были прожитой приёмкой и вырезаны: сам заказ живёт в `docs/archive/prompts/BACKEND_SEAM_PACK_SESSION_PROMPT_2026-08-27.md`, доказательства по каждому пункту — в §1–§11 выше, а сводный счёт тестов по всем трём пакам («удалено НОЛЬ», обе команды, поимённые переименования) — в §40. --- # Дофикс той же двери (сессия 25.08.2026) Промт (16 пунктов §4) архивирован — `docs/archive/prompts/BACKEND_SEAM_FIX_SESSION_PROMPT_2026-08-27.md`; записка-план — `SEAM_FIX_PLAN.md`. Секции 0–12 выше — улика приёмки, не тронуты. ## 14. Батареи База ДО правок, на дереве пака: `make battery` → `EXIT=2`, красен ровно `TestKillMinus9LosesAtMostOneCall` — один из двух названных промтом пред-существующих `-race`-флейков. Флейк воспроизвёлся на дереве, которого дофикс ещё не касался: атрибуция своя, не наследованная. `make lint` → `0 issues.` Батареи ПОСЛЕ правок и обе стенд-строки — §31. ## 15. §4.1 — корень формата: замер, решение, цена **Триггер шире клейма промта и НЕ совпадает с поправкой записки прошлой сессии.** Прогнал матрицу «значение поля → рендер → разбор» на обеих схемах (проба вне рабочего дерева, `gopkg.in/yaml.v3 v3.0.1`): | значение | что рендерится | round-trip | |---|---|---| | `"\nтекст"`, `"\n\nтекст"`, `"\n\tx"` | `\|4-` + строки | **не разбирается** (`did not find expected key`) | | `"\n"` | `\|4+` | разбирается, значение становится `""` — **тихая потеря** | | `"\n x"` | `\|4-` + 10 пробелов | разбирается, значение становится `"x"` — **тихая потеря** | | `"текст\n"`, `"a\nb"`, `" x"`, `"\tx"`, `"\r\nx"` | скаляр/блок | ок | Два следствия, оба несущие. (1) Клейм промта «бьёт по ЛЮБОМУ свободному текстовому полю» **ВЕРЕН**; поправка записки пака («`dst` и `sense` не ломаются») — **НЕТ**, ломается любое строковое поле обеих схем (проверено на `src`/`dst`/`sense`/`note` и на `rejects[].note`). (2) Заказанная проверка `render → parse` две тихие потери **пропустила бы**: документ разбирается, значение другое, дверь отчитывается `applied`. Инвариант обязан быть «прочиталось **И совпало**». **Механика — сверена с исходником библиотеки, а не выведена.** Внутри элемента блочной последовательности эмиттер увеличивает отступ на жёсткую двойку, чтобы «пропустить индикатор `- `» (`emitterc.go:239-241`, комментарий там же), а индикатор отступа блочного скаляра печатает `best_indent` (`emitterc.go:1848-1852`). Расхождение — ровно ширина `- `. Апстрим починил это в линии-преемнике (yaml/go-yaml PR #150, ишью #65); `v3.0.1` — то, что в `go.mod`, и оно не поддерживается. **Путь «починить рендер стилями `yaml.Node`» ЗАКРЫТ:** `(*yaml.Node).Encode` внутри сам гоняет marshal→unmarshal и падает на том же входе — узел не построить (замерено). ⚠ **Популярный обход `enc.SetIndent(2)` НЕ применён и применять его нельзя:** на этом пине он делает документ разбираемым и **молча теряет ведущий перевод строки** (замерено: `"\nне термин"` → `"не термин"`, `"\n\necho a\necho b\n"` → `"\necho a\necho b\n"`). Это обмен громкой ошибки на тихую правку слов владельца — ровно тот класс, который канон запрещает абсорбировать. **Что сделано.** (а) `seed.File.Normalize()`/`RejectFile.Normalize()` — ОДНО правило на все текстовые поля обеих схем (`TrimSpace`, пустые коллекции → nil), применяется на РЕНДЕРЕ, не на чтении; (б) дверь так же нормализует свободный текст входящих решений — то же правило, которое она и загрузчик уже применяли к `src`/`sense`/`dst`; (в) `RenderSeedFile`/`RenderRejectFile` ДОКАЗЫВАЮТ байты: marshal → разбор настоящим строгим декодером → `reflect.DeepEqual`; провал — ошибка, никогда не байты; (г) байты, которые чистый слой проверил, он же и отдаёт наверх (`ApplyResult.DeltaBytes`) — второго рендерера в обвязке больше нет; (д) **характеризационный тест на саму библиотеку** (`TestYamlV3CannotReadBackWhatItWritesForALeadingNewline`) краснеет в день, когда дефект починят: обход перестаёт быть молчаливым. **(а) и (б) взаимно прикрывают друг друга** — это замерено, а не предположено: посадка, снимающая одну из них, выживала, пока пин не был написан на случай, до которого другая не достаёт (I → пред-существующее значение В ФАЙЛЕ; K → идемпотентность повторного решения с пробельным текстом). Оба теперь краснеют. **Цена инварианта — замерена, не оценена.** Два бинаря из одного дерева, второй с выключенной проверкой равенства, один документ на 10 000 решений, по 3 прогона (`/usr/bin/time -f '%e %M'`): **с проверкой 16.52 / 18.10 / 18.30 с, 391–392 МБ · без неё 14.46 / 17.27 / 17.39 с, 344–408 МБ.** По времени ⇒ ≈5 % (медиана 18.10 против 17.27). ⚠ **Эти секунды НЕ сравнимы с таблицей §18**, где тот же документ на 10 000 решений даёт 14.53 / 15.73 / 16.54 с на ТОМ ЖЕ бинаре с проверкой. Замеры сняты в разные моменты под разной внешней нагрузкой (параллельные сессии), и абсолютные числа между секциями складывать нельзя. ≈5 % выведены ТОЛЬКО из пары выше — она снята подряд, двумя бинарями из одного дерева, и внутренне согласована. По памяти пара с таблицей §18 сходится (391–392 МБ против 391.7/391.9). **По ПАМЯТИ разница НЕ измерена: диапазоны перекрываются, и максимум без проверки (408 МБ) выше любого прогона с ней.** Основная цена глагола — НЕ проверка (см. §18). ## 16. §4.11 + свои посадки: 36 мутаций, 35 краснеют, 1 выживает осознанно Харнесс — **в репозитории**: `cmd/tmmutate` + каталог `cmd/tmmutate/mutations.json`, у каждой записи названо СВОЙСТВО, за которое она отвечает. Три сессии подряд писали такой инструмент в скретчпад и теряли его вместе с сессией; каталог — это и есть память о том, что кто-то счёл достойным охраны. Прогон — на КОПИИ дерева (`cp -a`, равенство копии проверено побайтно), сам инструмент снимает отпечаток каждой цели и каталога целиком и краснеет на «TREE IS NOT PRISTINE». ⚠ **Число записей в этой секции — снимок дофикса (36); каталог с тех пор рос каждым паком.** Актуальное берётся командой `python3 -c "import json;print(len(json.load(open('cmd/tmmutate/mutations.json'))))"`. **База ДО пинов: 20 из 24 выжили** (⚠ снято на РАННЕЙ редакции каталога из 24 записей — исторический замер, а не команда, которую можно повторить) — то есть все десять слепых пятен §4.11 подтверждены исполнением, плюс новый код дофикса был не запинен. **Итог после всех итераций: 36 посадок, 35 краснеют на СВОЁМ пине, одна (G) записана как аргументированный выживший (§17).** Каталог поэтому не отчёт, а ГЕЙТ: исход, не совпавший с записанным, роняет прогон — в том числе неожиданно ПОЙМАННАЯ посадка, что значит «запись устарела, обнови её». ⚠ **Две собственные посадки поймали два своих же ВАКУУМНЫХ пина:** пин на позиционные субъекты держал строки без идентичности ПЕРВЫМИ, поэтому их позиции никогда не двигались; пин на полноту довода отказа искал имя команды там, где оно и так стоит — в обвинении «not by %q», а не в объяснении. Оба переписаны, оба теперь краснеют. Правило, подтверждённое этим дважды: **сначала посадка, потом пин** — пин, написанный первым, проверяет то, что и так верно. ``` $ cd <копия backend> && go run ./cmd/tmmutate -root . -logs ``` **Пофайловая таблица «посадка → пакет пина → что покраснело» здесь НЕ дублируется:** её живой и авторитетный носитель — сам каталог `backend/cmd/tmmutate/mutations.json` (у каждой записи `id`, `why` — свойство, за которое она отвечает, — и `package`), а имена пинов инструмент называет ПОИМЁННО в выводе прогона. Снимок в отчёте протух бы в первом же паке: записей было 36 на дату дофикса, 56 после фикс2, 60 после фикс3. ## 17. Единственная выжившая мутация — и почему пин на неё невозможен **G (байтовый гейт).** Заказ §4.11(ж) просит поведенческий пин. Его нет и быть не может: гейт **недостижим за первым**, и это доказывается, а не предполагается. `deltaChanged = DeltaTouched && changedDoc(диск, next, пусто)`. `DeltaTouched` ставится только когда решение реально изменило документ, а `next` — канонический рендер УЖЕ ИЗМЕНЁННОГО документа. Чтобы `changedDoc` вернул false, байты на диске должны БЫТЬ этим рендером — но тогда документ, разобранный из них, уже равен результату, никакое решение ничего не меняло, и `DeltaTouched` был бы false. Нормализация щели не открывает: файл, чьи значения несут окружающие пробелы, в себя же не рендерится. **Диспозиция:** гейт ОСТАВЛЕН как второй рубеж на случай, когда `*Touched` станет грубее сегодняшнего, и запинен как КОНТРАКТ ФУНКЦИИ (`TestChangedDocIsTheSecondFenceAndItsContractIsPinnedDirectly`), включая правило «отсутствующий файл + пустой документ — НЕ создавать». Снести его я не стал: изгородь, за которую я вижу не до конца, дешевле оставить, чем убрать. ## 18. Замеры — своей названной методикой **Методика:** `/usr/bin/time -f '%M %e'`, 3 прогона на размер, медиана, машина без нагрузки, `bank-apply --dry-run` (записи нет), документы из N `approve` с короткой заметкой. | N решений | размер документа | wall (мин/мед/макс) | maxRSS (мин/макс) | |---|---|---|---| | 1 000 | 82.9 КБ | 0.57 / 0.58 / 0.74 с | 51.9 / 52.1 МБ | | 5 000 | 423 КБ | 4.91 / 5.18 / 5.36 с | 171 / 215 МБ | | 10 000 | 848 КБ | 14.53 / 15.73 / 16.54 с | 391.7 / 391.9 МБ | | 20 000 | 1.72 МБ | 46.72 / 52.16 / 52.20 с | 740 / 762 МБ | **Разночтение по памяти между паком и приёмкой закрыто в пользу приёмки.** Пак дал 133/217/350 МБ на 5k/10k/20k, приёмка 199/395/798 МБ, мой замер — 203/392/745 МБ. Числа приёмки воспроизводятся, числа пака занижены примерно вдвое. **По времени сойтись не удалось:** пак дал 1.60/3.62/9.08 с, у меня 5.2/15.7/50 с — в 3–5 раз медленнее. Проверкой раунд-трипа это НЕ объясняется (§15: ≈5 %), бинарь пака я не пере-собирал, и объяснения у меня нет. **Число, которое поедет в платформенный пак как датум для таймаута, — моё или приёмочное, не паковское.** **Почему цена сверхлинейная — факт исходника, а не гипотеза:** каждый пер-решенческий помощник сканирует весь документ (`findTerm`, `dropTerms`, `addReject`, `aliasOwner`, `deltaHoldsSurface` — все `O(len(документа))`), поэтому проход по N решениям Θ(N²) по построению. **Чего я НЕ доказал:** что именно это доминирует в замеренном времени. Единственная проба-индекс, которую я на это потратил, была написана неверно (пере-строение индекса на каждом добавлении) и дала обратный результат; дальше я не пошёл — это вне заказа. Ускорение — пингом (§23), не правкой. **⚠ Эта таблица снята на документах из `approve` с заметкой (≈85 байт на решение) и потому НЕ описывает худший случай, который допускает кап в БАЙТАХ.** См. §29 п.1: минимальный `decline` — ≈35 байт, и 1 МиБ пускает 28 900 таких решений, то есть 308 секунд. Вывод «1 МиБ ≈ 12 000 решений ≈ 20 с» из первой редакции этого отчёта **неверен**, и кап пришлось делать двойным. **Деньги:** `sha256sum ` до и после всех живых проб — `7ea61ccecada341b…` идентичен. Ни один платный вызов не сделан и не запрашивался. ## 19. Живые пробы на КОПИИ книги стенда `cp -r books/gu-zhenren/minirun-verify2 /live/book`, все пути переписаны НА КОПИЮ (⚠ мёртвые абсолютные пути лежат в ТРЁХ файлах: `book.yaml`, `pipeline.yaml` и `pairs/zh-ru.yaml` — в последнем `prompts_root`). **В `books/` не записано ничего.** | проба | результат | |---|---| | немигрированный проект (схема 11 против 15) | `exit=13`, текст называет `tmctl migrate` — ратифицированный класс 13 живьём | | `tmctl migrate` на копии | `v11 -> v15`, пре-миграционный бэкап, `integrity_check green` | | **блокер §4.1: `approve` с `note`, начинающейся с `\n`** | `exit=0`, `changed=true`; в файле `note: проба ведущего перевода строки` | | **второй вызов по той же книге** (то, что раньше умирало) | `exit=0`; `seed-lint OK: 0 fail-louds, 0 shared-key collisions` | | проекция того же документа | `mode=projection`, **`canonical_rewrite_delta=true`, `canonical_rewrite_rejects=false`** (§4.13 п.1 виден живьём: раньше горело из-за файла, который вызов не открывает), md5 обоих файлов не изменились | | ручная строка владельца `陈博` после записи | `gender`/`decl`/`translit_policy` целы (мерж, не замена) | | повтор того же решения | `changed=false`, `already_applied`, **inode не изменился** | | **§4.2: починка ОДНОЙ из двух битых строк** | `exit=0` (было `exit=14`), отчёт называет обе оставшиеся поломки; вторая починка — тоже `exit=0`, файл грузится | | §4.5: документ 2.16 МБ | `exit=14`, «is over 1048576 bytes … split it», файлов не создано | | §4.6: `manifest --json` и `status --json` | несут ОДИН конверт `artifacts{}`, все четыре пути абсолютные | | §4.3: два ключа на один путь | `exit=10`, отказ называет оба ключа и путь | | §4.4: `--decisions=` / `--decisions ""` / `--keys-file=` | все три отказывают (было: проходили на ПЛАТНЫЙ путь) | | §4.14: `--keys-file` на `redrive` | довод теперь называет `redrive`, `backup`, `migrate` поимённо | ## 20. Фаззеры `go test ./internal/membank/ -run '^$' -fuzz -fuzztime=90s -parallel=2`, по 90 с каждый: | фаззер | исполнений | падений | |---|---|---| | `FuzzDecisionDocument` (оракул 4 переписан, §4.15) | 176 495 | 0 | | `FuzzSeedDocumentRoundTrip` | 83 663 | 0 | | **`FuzzDecisionFieldsThroughTheDoor`** (новый, заказ §4.1) | 32 713 | 0 | Крашеров в дерево не записано (`testdata/fuzz` отсутствует). **Новый фаззер закрывает СТРУКТУРНУЮ слепоту старого,** названную промтом: тот фаззит сырой YAML, то есть его корпус по построению состоит из строк, уже переживших YAML-разбор, а вход двери — JSON, на который YAML не накладывает ничего. Новый гоняет произвольные строки полей `Decision` через `ApplyDecisions` в рендер и обратно, и его оракул — «прочиталось И совпало», на ОБОИХ документах. **Оракул 4 старого фаззера снят с тавтологии** (§4.15): он пере-выполнял то, что дверь уже сделала внутри. Теперь он проверяет свойство, которого не проверяет ни один одиночный вызов: результат — НЕПОДВИЖНАЯ ТОЧКА, повторное применение тех же решений к записанным БАЙТАМ даёт `already_applied` по всем и не трогает ни одного документа. ## 21. §4.13 — девять мелочей, по каждой решение и довод | предмет | решение | довод | |---|---|---| | `canonical_rewrite` — ИЛИ по двум файлам | **ПОЧИНЕНО** | поля стали пер-файловыми (`canonical_rewrite_delta` / `..._rejects`), и каждое ложно для документа, которого вызов НЕ трогает. Общий флаг оставлен как ИЛИ — потребителю, которому нужно только поднять диалог, читать одно поле. Предупреждение про файл, который вызов не откроет, — предупреждение ни о чём | | `note` нельзя ОЧИСТИТЬ | **ПОЧИНЕНА НЕСОГЛАСОВАННОСТЬ, поведение названо** | две половины двери расходились: повторный `approve` заметку СОХРАНЯЛ, повторный `decline` СТИРАЛ. Одно правило: отсутствующий `note` значит «не решено», как `kind`. Цена названа в комментарии `Decision.Note`: очистить заметку через дверь нельзя, только заменить другими словами. Стирание провенанса по умолчанию хуже — и оно делается по случайности, а не намеренно | | `book_id` не проверяется на путевую безопасность | **ПОЧИНЕНО** | `book_id` — это ИМЯ ФАЙЛА для четырёх движковых файлов, два из которых теперь несут решения ВЛАДЕЛЬЦА. Требование: один элемент пути. Проверено на всех 22 `book.yaml` репозитория и стенда — нарушителей ноль, и платформенный `bk_…` проходит | | каталог книги без права записи → `exit 1` вне полосы | **ОТКЛОНЕНО, пинг** | воспроизведено: `exit=1`, `store: open lock file: permission denied`. `RefuseStoreOpen` относит в полосу только `ErrLocked`(12) и `SchemaMismatch`(13), всё прочее — 1, и так у ВСЕХ глаголов. Проблема прав хоста — это и есть инфраструктурный сбой; 1 здесь корректен. Спор не про класс движка, а про то, как платформа читает 1 (§23) | | проекция создаёт `.lock` | **ОТКЛОНЕНО, сказано вслух** | лок — арбитр (закон п.2), а проекция без него — проекция ни от чего. Инвариант «проекция не трогает ДОКУМЕНТЫ» держится; «не трогает ничего» никогда не было верно и теперь так и написано в шапке `ApplyBankDecisions` | | порядок двух записей пессимален для `decline` | **ОТКЛОНЕНО, комментарий исправлен** | правильного порядка нет: delta-first безопасен для `approve` и небезопасен для `decline`, обратный — наоборот. Прежний комментарий утверждал больше, чем доказывал, и это исправлено. Настоящий довод в другом: вызов всё-или-ничего и пере-вычисляется целиком, поэтому повтор сходится из ЛЮБОЙ половины, и каждая половина оставляет терм НЕРЕШЁННЫМ, а не решённым неверно. Атомарность пары требует журнала на два файла — механизма, которого эта дверь не заслужила | | `artifacts.bank_export` печатается безусловно | **ОТКЛОНЕНО, документировано** | поле именует МЕСТО, а не наличие; публиковать «существует ли» значит публиковать факт о моменте, который к чтению потребителя уже прошёл, и приглашать гонку check-then-open. Сказано в комментарии `StatusArtifacts` | | `aliasOwner` — первое совпадение | **ОТКЛОНЕНО** | приёмка расхождения не воспроизвела. Проверка идёт по РЕЗУЛЬТАТУ (после `dropTerms`), а «своя поверхность» живёт внутри самого `aliasOwner` — отдельного побега быть не должно (§30 п.2). Остаток — только чьё имя окажется в тексте отказа; сам отказ наступает в любом случае, а такой документ уже ловится `ApprovedSharedKeyCollisions` | | проекция отвечает «что», а не «почём» | **ПИНГ с рекомендацией (§23)** | это вопрос ДИСПОЗИЦИИ, как и сказано в заказе | ## 22. §4.9 — заказ исполнен не предсказанием, а фактом Дверь **не может** предсказать, погаснет ли стоп: следующий прогон пере-майнит источник, и его реверс-секция режется капом 200 ПОСЛЕ фильтров (`reverseSectionTerms`), то есть на большой книге приедет свежая карта. Множества кандидатов в этом процессе нет вовсе — считать его значит пере-ингестить и пере-нарезать книгу, а это то, что делает `status` дорогим, и не то, чем занимается $0-глагол решений. Что дверь **может** видеть — карту подписи, которую написал прошлый прогон, и решён ли каждый её сурфейс, ПО ПРАВИЛУ САМОГО ПРОГОНА (`ownerHandled(unsignedEngineSurfaces(...))`, вызывается тот же код, не его копия). Отсюда поле отчёта: ```json "signature": { "map": "<путь>", "surfaces": 2, "undecided": 0 } ``` Пустой `map` значит «прогон до границы банка ещё не доходил». В режиме проекции число — то, что БУДЕТ после применения (единственное чтение, которое может иметь превью). ⚠ **СТАРАЯ ФОРМУЛА ЭТОГО АБЗАЦА ОТМЕНЕНА, и вот действующая.** Пак писал «`undecided > 0` — факт, что стоп не погаснет»; под моделью флажка (D39.144, фикс2) поле **не отвечает на вопрос о стопе НИ В ОДНУ СТОРОНУ**, и это записано в самом коде: `backend/internal/pipeline/bankdecisions.go:92=IT DOES NOT ANSWER «погаснет ли стоп»`. Причина — механическая: стоп срабатывает, когда СВЕЖЕ-намайненная карта следующего прогона держит кластер вне предъявленной памяти (`hasUnpresentedCluster`, `internal/pipeline/mining.go`), а этого набора ещё не существует ни у кого за $0. Что поле ЕСТЬ: информационный счётчик для экрана подписи — «о скольких поверхностях последней карты владелец ещё не высказался», считанный ТЕМ ЖЕ правилом, которым прогон сворачивает решения в банк (`ownerHandled(unsignedEngineSurfaces(...))`). Пины сегодня — цепи `internal/pipeline/bankchain_test.go` (`TestTheChainOwnerDecidesTheWholeMapAndTheRunGoesOn` · `TestTheStopIsAFlagNotAGateOnDecisions`): решение по всей карте прогон НЕ останавливает, а стоп — флажок, а не гейт на решения. ## 23. Пинги — предметы ВНЕ карты дофикса, правок не делал 1. **`exit 2` при OOM читается платформой как успех** (заказ §4.5 прямо называет это пингом). Движок при нехватке памяти выходит `fatal error: out of memory` + код 2, а `platform/internal/ingest/exit.go` читает 2 как «команда СДЕЛАЛА работу». Класс общедвижковый. Кап §4.5 не даёт `bank-apply` стать первым, кто до него доедет, но самого класса не снимает. 2. **Каталог книги без права записи даёт `exit 1`**, и платформа спишет проблему прав ХОСТА в бюджет попыток КНИГИ. Класс движка корректен (см. §21); чинить надо чтение кода 1 на платформе. 3. **Проекция отвечает «что применится», а не «почём».** Рекомендация: **не строить**. Закон п.7 про «почём ДО подтверждения» уже исполнен там, где есть деньги (rebill-смета, `status --json`), а этот глагол $0 по построению — единственная его «цена» это пере-редактура, которая квотируется и гейтится в другом месте и другим согласием. Поле «$0» в отчёте было бы шумом. Решать оркестратору. 4. **Карта подписи не удаляется после погасшего стопа** (`os.Remove` в `mining.go` не встречается). Пред-существующее. Потребитель, читающий файл как ответ на «есть ли что подписать», читает уже подписанную карту. Зафиксировано ЯВНО тестом цепи как текущее поведение; честный ответ на тот же вопрос — `signature.undecided` (§22). 5. **Цена глагола Θ(N²) по построению** (§18) — это то, почему кап на КОЛИЧЕСТВЕ стоит на 5 000, а не выше. Лечится индексом по ключу терма в `applyOne`/`dropTerms` и по нормализованной поверхности в `addReject`/`dropReject`; вне заказа, не делал. Поднимать кап без этого нельзя. 6. **Апстрим-дефект `yaml.v3` (§15) лечится по-настоящему двумя способами,** и оба вне пака: (а) тип скаляра схемы с `MarshalYAML`, возвращающим узел с явным стилем (проверено по исходнику: явный стиль проверяется РАНЬШЕ ветки блочного скаляра, `encode.go:557-570`); (б) переезд на `go.yaml.in/yaml/v4` — там этот случай починен, но это релиз-кандидат и смена module path сразу в двух зонах. Мой обход несёт ссылку и характеризационный тест, а не «похоже, баг yaml». 7. **Цена доказывающего рендера** ≈5 % времени (§15); по памяти разница внутри разброса прогонов и НЕ измерена. Плюс `notCanonical` теперь тоже разбирает документ. Названо, чтобы следующий замер не приписал это чему-то другому. ## 24. Что НЕ доказано и что НЕ удалось - **Рантайм-вердикт по ПЛАТНОМУ пути остаётся PLAUSIBLE.** Санкции на платные вызовы у дофикса нет, и ни одного платного вызова не сделано (`sha256` базы стенда не сдвинулся). Это ожидаемый исход, а не дефект отчёта. - **Цепь §4.12 доказана на СИНТЕТИЧЕСКОМ контрасте**, а не на книге стенда: банк-стоп на этой машине не воспроизводится вовсе (`pipeline.yaml` требует jieba-контраста, которого клон не получает по построению — строка 123). Ограничение названо в комментарии самого теста: синтетика меняет, КАКИЕ кандидаты предложены, но не то, проходит ли подписанная строка фильтр подписи. Для вопроса «гаснет ли стоп» это корректно; для качества майнера — нет. - **Расхождение по ВРЕМЕНИ с замером пака (3–5×) не объяснено** (§18). Проверкой раунд-трипа оно не объясняется, бинарь пака я не пере-собирал. - **Что доминирует в сверхлинейной цене — не измерено** (§18): Θ(N²) следует из исходника, но какая доля замеренного времени приходится на неё, я не установил; единственная проба была написана неверно. - **Мутация G не имеет поведенческого пина** и не может его иметь (§17) — вместо него запинен контракт функции. - **Два пред-существующих `-race`-флейка в `internal/store`** не тронуты (D39.121). Атрибуция закрыта СВОИМ A/B против HEAD (§31), а не унаследована. - **Вклад дофикса во время `internal/pipeline` под `-race` не измерен** (§31): мои собственные тесты этого пакета — 1.9 % его времени, остальное между базой 474 с и позднейшими 558 с (оба под нагрузкой, см. §31) я не атрибутирую, потому что внешняя нагрузка между замерами не контролировалась. ## 25. Правило протокола, вынесенное инцидентом ЭТОЙ сессии **Инструмент, который сторожит от подсадки, сам подсадил.** Первая версия `tmmutate` читала-правила-писала файл ОДИН РАЗ НА ПРАВКУ: у мутации B две правки в ОДНОМ файле, «нетронутая» копия второй снималась с уже пропатченного файла, и sha256-самопроверка сравнивала пропатченное с пропатченным же хешем — **проходила**. ⚠ Цена: дерево копии осталось мутированным, и **два полных раунда измерений после этого шли по мутированному коду** — несколько пинов отчитались как поймавшие мутации, к которым не имели отношения. Вскрыто ровно тем, чему учит §11 п.2: один подозрительный «RED», пере-проверенный изолированным прогоном, не воспроизвёлся. **Починено дважды:** одно чтение и одна запись на файл (порядок объявлен контрактом в комментарии), плюс второй независимый рубеж — отпечаток ВСЕХ файлов каталога, снятый на заведомо чистом дереве и пере-проверяемый ПЕРЕД каждой мутацией и после последней. ⚠ Равенство копии рабочему дереву проверяется КОМАНДОЙ (`diff -rq --exclude=bin` плюс `sha256sum` по всем `*.go`/`*.json`), а не записанной константой: дерево с тех пор менялось, и повторяемо равенство двух сторон, а не число. **Общая форма правила, шире этого инструмента:** самопроверка, чей эталон снят ПОСЛЕ первого изменения, не проверяет ничего. Эталон снимается один раз, на состоянии, которое известно как чистое. ## 26. Чек заказа и дифф тестов дофикса — сняты Попунктный чек шестнадцати пунктов §4 и пофайловый дифф `^func Test` дофикса вырезаны как прожитая приёмка. Адреса: заказ — `docs/archive/prompts/BACKEND_SEAM_FIX_SESSION_PROMPT_2026-08-27.md` (комплектность сверялась механически: `grep -cE '^### 4\.' <промт>` → 16); доказательства по каждому пункту — §15–§25 выше; сводный счёт тестов и переименования — §40; каталог посадок — `backend/cmd/tmmutate/mutations.json`. ⚠ D39.158 называет «вход §26» — читатель, пришедший по этой ссылке, попал верно: адреса выше. ## 28. Состав дофикса **Новое:** `internal/seed/normalize.go` (канонический вид документа) · `internal/membank/problems.go` (вердикт загрузчика по субъектам) · `cmd/tmmutate/` (харнесс мутаций + каталог посадок; счёт записей на дату дофикса и команда актуального счёта — §16) · `internal/pipeline/bankchain_test.go`. **Тронуто:** `internal/membank/decisions.go` (доказывающий рендер, нормализация запроса, набор-осознанный алиас, перевёрнутое окно, байты результата, `note` не стирается) · `memseed.go`, `memvoice.go` (вердикт как список субъектов; `Error()` побайтно прежний) · `internal/pipeline/bankdecisions.go` (кап, контекст, пер-файловый `canonical_rewrite`, `signature{}`, байты из чистого слоя) · `mining.go` (`signatureMapPath` и `rejectSurfaces` — одно определение на двоих) · `status.go`, `manifest.go` (единственный производитель конверта путей; конверт на манифесте) · `internal/config/book.go` (попарно различные пути, `book_id` как элемент пути) · `cmd/tmctl/invocation.go` (гарды на присутствие, довод отказа), `bankapply.go`, `main.go` (контекст) · **`Makefile`** (пер-пакетный таймаут назван явно — §31, это единственная правка в самой батарее и она ничего не ослабляет) + семь тест-файлов. ## 29. Ревью старшей моделью: три дефекта, каждый воспроизведён МНОЙ и починен Ревью запрашивалось по решениям, где заказ оставлял форму на сессии. **Каждую находку сессия воспроизвела сама прежде, чем чинить** — чужой результат, взятый на веру, ровно так же становится нормой. У двух подтверждённых решений ревью дало довод СИЛЬНЕЕ, и довод заменён в комментариях кода: у конверта путей на манифесте — что бамп версии выбросил бы все сохранённые сайдкары и пере-нарезал каждую книгу ради аддитивного поля (компромисс назван вслух: манифест семантически про дерево глав, путь артефакта там гость); у SIGTERM — атомарность и порядок записей вместо длины непрерываемого участка. ### 1. Кап в БАЙТАХ не ограничивает ВРЕМЯ — мой довод был неверен Расчёт «1 МиБ ≈ 12 000 решений ≈ 20 с» брал ≈85 байт на решение из документа с заметками. Минимальное законное решение — `{"action":"decline","src":"t1"}`, ≈35 байт. **Пере-мерено тем же методом** (`/usr/bin/time -f '%e %M'`, документы из минимальных `decline`): | N | размер | wall | maxRSS | |---|---|---|---| | 5 000 | 174 КБ | **11.32 с** | 40 МБ | | 12 000 | 421 КБ | **57.96 с** | 70 МБ | | 20 000 | 709 КБ | **153.5 с** | 112 МБ | | 28 900 | **1 029 369 Б — ПОД капом** | **308.2 с** | 133 МБ | То есть байтовый кап пускал документ, который платформа убивает по своему 60-секундному бюджету на КАЖДОЙ попытке, навсегда, — а строка отказа «split it», ради которой класс 14 и заводился, не печаталась никогда. Это тот самый вечный ретрай, который полоса отказов существует чтобы превращать в действие. **Починено:** второй кап — на КОЛИЧЕСТВЕ решений, 5 000, тот же класс 14 (11 с в худшей замеренной форме, пятикратный запас внутри бюджета платформы). Байтовый кап остался пред-разборным сторожем и больше ничего про время не утверждает. Пин `TestTheDecisionCOUNTIsCappedBecauseTheBYTESCannotBound`, мутация W. ### 2. Субъект поломки нёс РЕНДЕРИНГ — тупик на оси, у которой нет двери Три проверки из четырёх называют `dst` в тексте сообщения (человеку он нужен), а `dst` — ровно то, что решение меняет. Ключом по тексту поломка про терм выглядела НОВОЙ в тот момент, когда дверь меняла рендеринг этого терма: строка дельты с `gender: badvalue` (ось, у которой в `Decision` НЕТ поля вообще) плюс решение, меняющее `dst` того же терма, давали `exit=14` — одна и та же поломка по обе стороны под двумя разными рендерингами, и починить её через дверь нельзя вовсе. Это §4.2 на уровень ниже. **Починено:** `subjectOf(class, src, sense, since, until)` — субъект строится ТОЛЬКО из класса поломки и идентичности терма, никогда из поля, которое дверь переписывает; экспортированные `[]string`-обёртки не изменились, ни один чужой вызов не тронут. Пин `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt`, мутация Y. ### 3. И то же самое ещё уровнем выше: поломки, которые ЗАГРУЗЧИК прячет Три проверки живут ПОД загрузчиком банка и на документе, который не грузится, не выполняются вовсе. Значит, как только решение чинит загружаемость, всё, что они впервые увидели, читается как новое — и владельца отвергают за починку того, что мешало всему. ⚠ Первая починка была ПРАВИЛОМ МАСКИ и оказалась ХУЖЕ дефекта: правило снято целиком, итоговое лечение и его довод — §30 п.1. ### 4. Латентная потеря данных на пути nil-байтов Если рендер ТЕКУЩЕГО документа падает и это пред-существующая поломка, она сообщается, а не отвергает (и это правильно) — но тогда `res.DeltaBytes == nil`, а обвязка считала `DeltaTouched && changedDoc(raw, nil, …)` → true и **записала бы файл нулевой длины поверх всех одобрений владельца**. До этого входа не дошёл ни ревьюер, ни сессия (130+ подобранных значений после нормализации прошли round-trip); но `renderProved` существует именно потому, что множество ломающих значений не известно целиком, и незащищённый `nil` противоречит его собственной предпосылке. **Починено одной строкой:** nil-байты не пишутся никогда. Поведенческого пина нет по той же причине, по которой его нет у G, — вход недостижим; инвариант чистого слоя (`accepted ⇒ DeltaBytes != nil`) держит `FuzzDecisionFieldsThroughTheDoor`. ### 5. Три комментария несли неверные доводы — исправлены В этом проекте комментарий — контракт, по которому действует следующая сессия, поэтому неверный ДОВОД при верном коде опаснее опечатки. Один из трёх стоит того, чтобы стоять и здесь: «путь `yaml.Node` закрыт» преувеличивало — закрыт `Encode`, а РУЧНАЯ сборка узла с явным стилем работает и отвергнута по ЦЕНЕ (второй рендерер всей схемы), а не по невозможности (§23 п.6). ### 6. Ещё две правки по ревью **Нечитаемая карта подписи** возвращалась как `{surfaces:0, undecided:0}` — побайтно то же, что «всё решено», единственный ответ, который это поле не должно давать по случайности. Заведено поле `unreadable`; пин `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided`, мутация Z. ⚠ Здесь закрыта только ошибка РАЗБОРА; ошибку ЧТЕНИЯ (`chmod 000`) закрыл §34. **У `tmmutate` опасное значение было ДЕФОЛТНЫМ** (`-root .` правит живое дерево). Флаг стал обязательным; в шапке названо и то, что отпечаток снимается на старте и потому сертифицирует только дерево, которое было чистым в тот момент. ## 30. Интервальная самоверификация субагентом: ещё пять дефектов, все воспроизведены и починены Ревью гонялось В СЕРЕДИНЕ работы против явных критериев, read-only, в копии; дерево под ним двигалось, поэтому его вердикты пере-исполнены на финальном снимке. Каждый дефект сессия воспроизвела своим прогоном прежде, чем чинить; воспроизведение каждого теперь живёт в его пине, а не здесь. ### 1. ⛔ Правило маски проглатывало ливлок, который вносил САМ вызов Собственная починка §29 п.3 («если до вызова документ не грузился, а после грузится, эти поломки сообщаются и не отвергают ничего») оказалась ХУЖЕ дефекта. На входе «битая строка + `Hero→Odin` с алиасом `Shared`» и решениях «починить битую строку + одобрить `Shared→Dva`» дверь диагностировала детерминированный ливлок матчера (`firing key "shared" is shared by different terms`), **записала документ** и отчиталась `exit 0`, назвав СВОЮ поломку чужой. Комментарий правила («состояние после такого вызова никогда не хуже, чем до») был ложью ровно здесь: до — банк не грузился (громкий стоп), после — грузится и инжектит ливлок. И два опубликованных поля (`Preexisting`, `preexisting_problems`, оба обещающие «этот вызов их не вызывал») становились ложными утверждениями. **Починено иначе и без исключений:** правило маски снято целиком; вместо него `ParseBankSeed` возвращает записи, которые он ПОСТРОИЛ, даже когда отказывает. Тогда три проверки под загрузчиком выполняются и на «до», и на «после», сравнение по субъекту работает как везде, и оба случая решаются одним правилом: починка загружаемости больше не отвергается (поломка видна с обеих сторон), а внесённый ливлок отвергается (его субъекта на «до» нет). Пины — обе половины `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS`, мутация AD. ### 2. Побег §4.7 принимал инертный отказ `ownsSurface(in.Delta, …)` пропускал проверку, когда поверхность была термом дельты сама по себе: на дельте `Nick→Nik` + `Hero→Nik2 (alias Nick)` отказ `Nick` принимался, строка `Nick` снималась, а `Hero` ПРОДОЛЖАЛ фарить `Nick` — ровно тот вред, ради которого §4.7 и заказан. Замерено дополнительно: во всех остальных входах побег — no-op (после `dropTerms` алиасом никто не владеет), то есть он менял ответ только во вредную сторону, и зеркальный случай, ради которого он якобы существовал, работает без него. **Починено:** побег снят — ⚠ `ownsSurface` в коде НЕТ, и заводить заново не надо. Пин `TestADeclineIsRefusedWhenTheSurfaceGOESONFiringAsAnotherTermsAlias`, мутация AB. ### 3. И то же правило ОТВЕРГАЛО законный отказ, с доводом, который неверен `aliasOwner` не смотрел на СТАТУС: на дельте `Hero (status: auto, alias Nick)` отказ `Nick` отвергался доводом «"Nick" is an ALIAS of the approved term … already excluded from proposals». Обе половины довода ложны — строка не approved, а `unsignedEngineSurfaces` выбрасывает mined-строки со статусом ≠ `approved`, то есть её поверхности из предложений НЕ исключены и отказ как раз осмыслен. `status: auto` в дельте — не угол, а ровно то состояние, о котором говорит первая мина формата. **Починено:** `aliasOwner` судит только подписанные строки (пустой статус = approved, как дефолтит загрузчик). Пин `TestDecliningTheAliasOfAnUNSIGNEDRowIsAccepted`, мутация AC. ### 4. §4.14 был исполнен неполно — и тем же способом, против которого заказан Довод отказа `--keys-file` не описывал `bank-apply` — глагол, который этот пак и добавил. А тест, написанный ЗАКРЫТЬ §4.14, перечислял четыре команды ручным списком — ровно слабость, против которой поднят §4.11(в), воспроизведённая в тесте про неё же. **Починено:** довод называет все девять; тест перечисляет из `dispatchCommands` и требует, чтобы имя каждой команды встречалось в её собственном отказе. Мутация AF. ### 5. Гейт тотальности читал ОДИН файл и одну форму объявления `refusalClassesFromSource` разбирал `refusal.go` и требовал явного `vs.Type == RefusalClass`. Класс, объявленный в соседнем файле пакета или конверсией `RefusalX = RefusalClass("x")`, был ему невидим — та же слепота, ради снятия которой гейт и переписан. **Починено:** `parser.ParseDir` по всему пакету и обе формы объявления. ### Плюс два, которые ревью нашло, а я закрыл превентивно **Позиционные субъекты** («record N has no identity») не были запинены вовсе — добавлен `TestTwoRecordsWithNoIdentityDoNotBlockEachOthersRepair` и мутация AE. **Фаззер двери возвращался рано на отказе** и потому не видел бы возврата блокер-класса: оракул отказа усилен — отказ обязан не выдать байтов, не отчитаться о принятых решениях и не изменить ни один документ. ### Что ревью подтвердило исполнением (не переделывал) Нормализация идемпотентна на 50-строчном адверсариальном корпусе и КОПИРУЕТ (проверено мутацией выхода при неподвижном входе) · ложного провала `renderProved` найти не удалось (400 000 случайных строк по алфавиту управляющих/пунктуации/CJK, невалидный UTF-8, NUL, C0/C1, одиночные суррогаты) · оба капа не off-by-one · после второй проверки контекста ничего не пишется, `defer lock.Release()` стоит сразу за захватом · общности не нарушено ни одной новой строкой · `TestDecisionByBankID` и пин на инъективность действительно независимы (все три константы пере-считаны в Python по спецификации из комментария `TermID`, а не из его же выхода — `backend/internal/membank/decisions_test.go:348=Computed outside Go, from the spec in TermID's own comment`) · `SeedProblems.Error()` побайтно равен прежнему склеенному сообщению. **Общность — проверена грепом по девяти тронутым файлам:** в Go, которую дофикс тронул, нет ни одного литерала пары/письменности/орфографии; нормализация — `strings.TrimSpace`, определённый Юникодом, а не скриптом. ⚠ **Унаследованное замечание, не дефект пака:** всё, на чём эта дверь ключуется, проходит через `internal/text/norm.go`, где таблица `trad2simp` и ветка катакана→хирагана живут В GO, а не в langpack'е. Пара, которой в репо нет, деградирует до NFKC+lowercase — то есть не блокирована, но фолд Go-резидентный. Пингом, не правкой: это шире двери. ## 31. Батареи после всех правок — и одна правка в самой батарее Сняты ПОСЛЕ рефакторинга §33 и после всех правок по воркфлоу-ревью §34 — то есть описывают дерево, которое передавалось: `make lint` → `0 issues.` · `go vet ./...` + `go vet -tags live ./...` + оба прохода `-vettool` → чисто · `gofmt -l .` → пусто · **`make battery` → `EXIT=0`, полностью зелено**, включая оба названных промтом `-race`-флейка · **`make battery-stand` → `EXIT=2`, красен ровно `TestMinerFullBookParity`** (нет jieba-контраста `eval/exp16/data/jieba_dict_general_zh.txt` — он лежит по внутрирепозиторному пути, но вне git по построению). Больше ничего. **ОДНО утверждение о детерминизме, чтобы приёмка не гадала.** Два store-теста (`TestKillMinus9LosesAtMostOneCall`, `TestTheSeamIsNotChargedToTheStoreOperationBudget`) ведут себя так: в ПОЛНОЙ батарее на ненагруженной машине проходят; под нагрузкой параллельных сессий падают; а запущенные ИЗОЛИРОВАННО через `-run` падают **детерминированно и на обеих сторонах A/B** — они сознательно сжимают `opTimeout`, и в одиночку сжатого времени не хватает. То есть: красное в этих двух — ожидаемо и НЕ пак; зелёное в них — тоже нормальный исход; красное в любом ДРУГОМ тесте — регрессия. **Корпус под строгим загрузчиком — 45 из 45 чисто**, двумя командами: ``` $ for f in $(find books backend -name "*seed*.yaml" -o -name "*mined-delta*.yaml" \ -o -name "*auto-bank*.yaml" -o -name "*mined-signature*.yaml" | grep -v '\.bak'); do tmctl seed-lint --seed "$f" >/dev/null || echo "FAIL $f"; done # → 30 OK / 0 FAIL $ find books backend -name "*mined-rejects*.yaml" | wc -l # → 15 $ … | while read f; do python3 -c 'import sys,yaml;d=yaml.safe_load(open(sys.argv[1]))or{}; assert set(d)<={"rejects"}; [__import__("sys").exit(1) for r in (d.get("rejects") or []) if set(r)-{"src","note"}]' "$f" \ || echo "FAIL $f"; done # → 15 OK / 0 FAIL ``` ### Атрибуция красного — ЗАМЕРОМ против HEAD, а не «промт сказал» `git worktree add --detach HEAD` — дерево БЕЗ пака вообще (`internal/membank/decisions.go` там нет). Чередующийся A/B, 5 раундов, `-race -count=1`: два названных store-теста — `MINE 5×FAIL · HEAD 5×FAIL`; `pipeline/TestManifestOfAnOlderDocumentVersionIsRefused` — `MINE 5×ok · HEAD 5×ok` (в стенд-батарее он падал ЧИСТО от нагрузки). Изолированный прогон обеих сторон даёт БУКВАЛЬНО одинаковый вывод: `migrate_test.go:41: store: apply migration 9: context deadline exceeded`. Атрибуция закрыта — ни пак, ни дофикс тут ни при чём, и это СВОЙ замер, а не унаследованное утверждение. Тесты не тронуты (D39.121). ### ⚠ Одна правка В САМОЙ батарее, и вот замер, из-за которого она сделана `Makefile`: `go test ./... -race -count=1` → `… -timeout=20m` (в обеих целях). **Почему.** Go по умолчанию даёт пакету 10 минут. `internal/pipeline` под `-race` идёт **557.7 с** (`/usr/bin/time`, машина с ТРЕМЯ параллельными агент-сессиями, load average 3–12 в моменты замера), и под этой нагрузкой два прогона батареи подряд закончились `panic: test timed out after 10m0s` по всему пакету — это читается как падение и падением не является. ⚠ На действительно свободной машине приёмка намерила **92.8/97.6 с**: разброс ×6 даёт сама нагрузка, оба замера настоящие, и довод в `Makefile` поэтому записан диапазоном с условиями — без слова idle и без привязки к машине. **Это НЕ подгонка гейта под зелень:** таймаут — не утверждение. Ни один тест не ослаблен, ни один не удалён; те же самые тесты просто доходят до конца. Пакет, который действительно ЗАВИС, всё так же падает — на двадцатой минуте вместо десятой. **Чего НЕ доказано:** сколько из 557.7 с добавил дофикс. База в начале сессии на нетронутом дереве пака дала 474.2 с, но внешняя нагрузка между двумя замерами не контролировалась, поэтому разница НЕ атрибутируется. Что измерено точно: **все одиннадцать новых тестов этого пакета стоят 10.5 с** под `-race` из 557.7 с, то есть 1.9 % — не они. ## 32. Записка предыдущей сессии — попунктно, включая одно ОПРОВЕРЖЕНИЕ Записка бэкенд-сессии пака (жила в хвосте её промта, ныне архивированного) пройдена целиком и уехала в работу почти вся: онбординг в код, три ловушки стенда (мёртвые абсолютные пути в трёх файлах, схема v11, нет jieba — все три сработали ровно как описано), правило «сначала посадка, потом пин», требование пере-мерить память своей командой и проверить красное в `internal/store` ЗАМЕРОМ против HEAD. Проверка против HEAD была не лишней: она дала более сильный результат, чем ожидала записка (§31). **Одно место оказалось ВРЕДНЫМ.** Записка «поправляла» промт: «`dst` и `sense` с ведущим `\n` round-trip НЕ ломают». **Опровергнуто замером §15: ломается любое строковое поле обеих схем.** Прав был ПРОМТ, а не записка, — и если бы сессия поверила записке, фикс был бы заплаткой на одно поле. **Вывод для оркестратора:** записка не помечена «проверено исполнением» построчно, а её самый уверенный абзац оказался неверным. Полезен ФОРМАТ такой записки, а не её авторитет. ## 33. Рефакторинг по итогам вопроса о качестве Go-кода Две точки входа двери были вдвое длиннее всего остального в тех самых файлах, которые они представляют, и обе разобраны на именованные фазы — чистое извлечение, без изменения поведения: ``` ApplyDecisions → resolveAll · refuseSeedConflicts · foldAccepted · refuseInertDeclines · refuseNewFaults ApplyBankDecisions → openDecisionRequest · readBookState · decisionReport · changedFiles · writeDecisionFiles ``` ⚠ Фаза `decisionReport` позже заменена единым конструктором исходов `finishReport` (фикс2, §36–§37) — грепать надо его. Тело `ApplyDecisions` стало последовательностью из пяти судящих фаз, из которых **ровно одна** что-то меняет (`foldAccepted`): это и есть контракт двери, теперь видимый в самом коде, а не только в комментарии. ⚠ **Считать длину функции надо по ТЕЛУ** (от `^func` до первой строки ровно `}`), а не от `^func` до следующего `^func`: второе правило на методах даёт мусор, и именно им была получена несуществующая «норма репозитория в 75 строк» — такой нормы в проекте НЕТ. **Замерено по ТЕЛУ:** две точки входа — **92** и **90** строк против **42** у следующей по длине функции тех же двух файлов; максимумы пакетов совсем другие — **218** (`Status`, `internal/pipeline`) и **105** (`loadVoiceSections`, `internal/membank`); после разбора самая длинная функция обоих файлов — **44** (`signatureState`). ⚠ **И вот чем доказательство эквивалентности ОКАЗАЛОСЬ слабее, чем здесь было написано.** Первая редакция утверждала «весь каталог пере-прогнан, как записано». На деле рефакторинг сломал ДВЕ записи каталога — они ссылались на переменную, которую он убрал из области видимости, — а инструмент считал провал СБОРКИ красным, то есть две записи не проверяли ничего, а отчёт называл тесты, которые не могли отработать. Нашло воркфлоу-ревью; обе записи пере-выражены, у инструмента появился третий вердикт — §34. ## 34. Воркфлоу-ревью дофикса: шесть осей, все находки воспроизведены и закрыты Третье и последнее ревью дофикса — **воркфлоу**, а не одиночный агент (форма — §45). Полосы выбраны по тому, чего не касались два предыдущих ревью: гонка двух `bank-apply` · последствия новых полей для платформы (read-only) · эквивалентность только что сделанного рефакторинга · сам `cmd/tmmutate` (он теперь в репозитории, значит продакшн-код) · честность хендофф-документов · общность и безопасность данных. Каждая находка перепроверена своим прогоном прежде, чем чинить; правки доводов в комментариях и правки в самом этом документе живут по месту, здесь — только то, что осталось фактом. ### ⛔ Главное: две посадки каталога НЕ КОМПИЛИРОВАЛИСЬ — и это внёс мой же рефакторинг `P-alias-judged-on-the-set` и `AB-alias-escape` патчат тело `refuseInertDeclines`, ссылаясь на `in.Delta`, которое рефакторинг §33 убрал из области видимости, — а инструмент считал их КРАСНЫМИ, потому что `red` был буквально `terr != nil`, и провал СБОРКИ выглядел так же, как поймавший пин. **Починено на двух уровнях.** Обе записи пере-выражены под новую сигнатуру и краснеют на своих пинах поимённо; у инструмента появился ТРЕТИЙ вердикт (`NOTHING` — ни один тест не назвал себя, гейт падает) плюс обязательная зелёная базовая линия пакета ДО первой посадки. ⚠ Оба механизма и провенанс инцидента живут в самом инструменте — `cmd/tmmutate/main.go`, комментарии «THE PACKAGE MUST BE GREEN BEFORE ANYTHING IS PLANTED» и «NOT a red …»; второй копии этот отчёт не держит. ### Продуктовые дефекты — все воспроизведены мной | что | как это выглядело | починка | |---|---|---| | **остановленный вызов ПЕЧАТАЛ, что применил** | на отмене контекста отчёт уходил в stdout с `mode:"apply"`, `changed:true` и `state:"applied"` — при нуле записанных байт. Отличал их только код выхода | `mode:"stopped"`, `changed:false`, и `accepted[]` очищается так же, как на пути отказа (это поле потребитель перебирает). Пин `TestAStoppedCallDoesNotPRINTThatItApplied`, мутация AG бьёт по всем трём полям | | **нечитаемая карта подписи ≠ «всё решено»** | фикс §29 п.6 закрыл ошибку РАЗБОРА, но не ошибку ЧТЕНИЯ: `chmod 000` возвращал нулевое состояние — побайтно то же, что «нечего подписывать». Тест назывался ровно про это свойство и его не покрывал | `os.IsNotExist` отделён от прочих ошибок чтения; пин расширен случаем `chmod 000`, мутация AH | | **отказ предупреждал о перезаписи, которой не будет** | откат «всё-или-ничего» не сбрасывает `*Touched`, поэтому `canonical_rewrite` горел на ОТВЕРГНУТОМ вызове. Диалог «сейчас будут уничтожены комментарии» для вызова, который ничего не пишет, — так настоящее предупреждение перестают читать | поля обнуляются при отказе; пин `TestARefusedCallDoesNotWarnAboutARewriteThatWillNotHappen`, мутация AI | | **отказ по алиасу сида говорил неправду** | «declining it would change nothing» — но если дельта несёт СВОЮ строку с этой поверхностью, отказ её сбрасывает и чинит ровно ту коллизию, которую тот же отчёт печатает в `preexisting_problems`. А предписанное «remove it from glossary_seed» строку дельты не убрало бы. Воспроизведено | отказ сужен до случая, где его собственная фраза верна; пин `TestDecliningASeedAliasIsAcceptedWhenTheDeltaHoldsThatRow`, мутация AJ | ### Что ревью ПОДТВЕРДИЛО (и это тоже результат) **Рефакторинг §33 — эквивалентен**: порядок фаз, мутация `rs[i].ok` через слайс и место отката проверены отдельной полосой, расхождений нет. **Платформа не ломается**: аддитивные поля отчёта, конверт `artifacts{}` на манифесте без бампа версии, код 14 и размерные капы — прослежены до её кода, дефектов нет. Исправлены при этом два комментария, утверждавших про чужую зону неверное, и вот верное: код 5 платформа пишет как `stopped` (и только при зафиксированном намерении остановиться, иначе `failed`), а отчёт двери печатается НЕ в каждом исходе — четыре отказа (10, 12, 13 и 14 до чтения книги) не печатают ничего, как и два не-отказных случая (5 и 1). ## 35. Проверка ЗАКРЫТИЯ — отдельным воркфлоу, и она нашла, что закрыто не всё На вопрос «все замечания исправлены?» сессия не отвечала по памяти: независимые агенты прошли по всем пережившим находкам §34 и по живому дереву решали КАЖДУЮ — закрыта / не закрыта / частично / отклонена осознанно. Закрыто оказалось не всё; все остатки доделаны, и их результат живёт не здесь, а в исправленных по месту секциях (§15, §16, §18, §31, §33) — второй копии этот отчёт намеренно не держит. **Два остатка были признаны вне полномочий пака, и оба закрылись позже:** межкнижная гонка за один `mined_delta` — снятием объявляемых ключей (D39.158 п.5; остаток «второй конфиг ТОЙ ЖЕ книги» заведён строкой бэклога 222), запись карты подписи обычным `os.WriteFile` — атомарной записью с конвертом (§36, §41). ⚠ **Вывод, который стоит унести дальше самого пака.** Это было ТРЕТЬЕ ревью подряд, и каждое находило настоящие дефекты, которых не видели предыдущие: одиночный старший ревьюер — три (§29), интервальное — пять (§30), воркфлоу — двадцать шесть, проверка закрытия — ещё десять остатков (из 28 её пунктов: 16 закрыто, 2 отклонено осознанно, 7 частично, 3 не закрыто). **Ни одно «все замечания исправлены» в этой сессии не оказалось верным при проверке.** Вывод не «ревью бесполезны», а обратный: закрытие надо ПРОВЕРЯТЬ отдельно от починки, и не тем, кто чинил. --- # ДОФИКС-2 (13 пунктов §4; промт архивирован — `docs/archive/prompts/BACKEND_SEAM_FIX2_SESSION_PROMPT_2026-08-27.md`) — секции 36+ ## 36. Состав фикс2-пака **Новое:** `internal/store/stopmemory.go` + миграция v16 (`bank_stop_presented`) · `internal/seed/signaturemap.go` (конверт карты: `map_version` + `map_id` от содержимого) · `internal/pipeline/bankreport_test.go` · `internal/pipeline/stopflag_test.go` · `internal/store/stopmemory_test.go` · `internal/seed/normalize_test.go`. **Тронуто:** `internal/pipeline/mining.go` (предикат новизны · разделение «стоп»/«авто-провод» · запись памяти · атомарная запись карты с конвертом · снос формул старой модели) · `internal/pipeline/bankdecisions.go` (вердикт/исход, `finishReport` — единственная точка фиксации; staged-запись обоих файлов + fsync каталога; кап-отчёты; `written_delta`/`written_rejects`; пере-док `SignatureState`) · `internal/pipeline/refusal.go` (два яруса полосы + класс `write_incomplete`) · `internal/pipeline/artifact.go` (staged-вариант + `syncDir`) · `internal/config/book.go` (`filepath.Clean` абсолютных + retired-отказ ключей `mined_delta`/`mined_rejects`) · `internal/membank/decisions.go` (`RenderSignatureMap` с доказательством; версия отчёта → **v2**) · `internal/miner/miner_emit.go` (`DeltaFile`) · `cmd/tmctl/main.go` (exit **15**), `bankapply.go`, `render.go` · `internal/pipeline/waverun.go` (док стопа) · тесты: `bankchain_test.go` (переписан под модель флажка, две цепи), `miningstop_join_test.go` (два теста переписаны с старой семантики), `internal/config/decisionpaths_test.go` (переписан под retired-модель), `internal/membank/decisions_test.go` (+3 пина) · `cmd/tmmutate/mutations.json` (36 → **56** записей: 20 новых FX* + 6 ре-якорено после рефакторинга). ## 37. Решения §4.1–§4.3, каждое с доводом — и названные остатки - **Память** — плоское множество нормализованных поверхностей всех предъявленных кластеров (Src + Aliases), таблица `bank_stop_presented (book_id, surface)`. Кластерность живёт в ПРЕДИКАТЕ (пересечение), не в схеме: для вопроса «есть ли кластер вне памяти» объединение множеств поверхностей эквивалентно хранению кластеров, а строк меньше и мигрировать нечего. Рост — только вширь (`INSERT OR IGNORE`), обрезки нет по букве промта. - **Двусторонний остаток кластерной памяти** записан пином, а не только словами (`TestAClusterSharingAnAliasWithTheMemoryReadsAsPresented`): мягкая сторона — лишний стоп на новом написании; вредная — новая сущность, делящая поверхность-алиас с виденным кластером, предъявлена НЕ будет (поедет редактору авто-строкой). Обе — принятые остатки решения, смена трейда = новое решение. - **Порядок записи**: карта (АТОМАРНО, §4.10 сделан первым) → память, в момент стопа. **Отказ записи памяти НЕ валит состоявшийся стоп**: карта на диске, пауза настоящая; цена — один доброкачественный повторный стоп (ERROR-строка в логе называет это). Валить прогон значило бы превратить законную паузу в аварию из-за бухгалтерской строки. Между файлом и SQLite общей транзакции нет — принятый остаток, направление потерь безопасное (повторный стоп, не молчаливая потеря). - **Остановившийся прогон авто-банк НЕ пишет** — названное решение (§4.3 ⚠): стоп — граница владельца, и запись банка на ней меняла бы состояние, которое владелец как раз рассматривает; возобновление без решений доводит авто-провод на следующем прогоне (сходится, проверено цепью Б, прогон 3). Чекпоинты терминолога делают оба варианта $0-эквивалентными — выбран тот, что сохраняет артефактный набор стопа прежним. - **Черновая волна (draft-only)**: карта пишется, стопа нет, памяти нет (по букве §4.2); авто-провод теперь исполняется и при поднятом флаге (раньше — нет; это изменение поведения, названо здесь). - **Флаг-поднят-нового-нет** — новое состояние: WARN-строка «--verify-bank is raised and the delta is non-empty, but every cluster... already presented» + авто-провод + полноценное продолжение. Молчание больше не спутать с «майнинг не сконфигурирован». - **Миграция**: v15→v16 — новая таблица `CREATE TABLE IF NOT EXISTS` (дешёвая форма из промта; ALTER не трогали, долг 49а не задет). ⚠ Голова схемы двигается: `OpenReadOnly` отказывает классом 13 всем непере-мигрированным книгам до `tmctl migrate` — порядок деплоя «migrate первым» это покрывает (D39.134 п.3), живьём проверено на копии стенда (v11 → v16, пре-миграционный бэкап, integrity green). После апгрейда память пуста → ОДИН доброкачественный стоп на первом verify-прогоне каждой книги — миграционная нота в журнал. ## 38. Заявление = команда: замеры фикс2 - **§4.4 пере-замер своей командой** (числа промта 2110/442/117/325 первоисточника в дереве не имеют — не цитирую). Первоисточник ТЕПЕРЬ В ДЕРЕВЕ (упрёк проверки закрытия принят): постоянный якорь-пин `internal/miner/emissioncap_test.go` (`TestTheEmissionCapPrecedesTheFiltersAndTheTailIsUnreachable`) — тот же корпус (фамилии langpack × имена, 600 имён по 6 вхождений в 6 разных рамках), утверждает ТЕКУЩЕЕ поведение и краснеет в день сдвига потолка или порядка фильтров, приводя двигающего к пингу (прецедент — токенование якоря пинга живым фактом, коммит 0f4c556). Замер той сессии: `ranked=2354 · годных во всём алфавите=209 · годных в окне потолка=11 · невидимых постоянно=198`; отклонение ВСЕХ 11 годных окна (9 эмитированных + алиасы) → второй прогон: `emitted=0, fresh_surfaces=0` — ни один из 198 не поднялся. Структурная причина — `miner_emit.go` (`ranked = ranked[:emitRankCap]` ДО `emissionEligible`, сид- и реджект-скипы тратят слоты ВНУТРИ окна). Картина промта подтверждена на моих числах; заказ — не лечить, пинг в журнале. - **Каталог мутаций**: `go run ./cmd/tmmutate -root <копия>` → **56 записей, 0 неожиданных исходов** (55 RED + записанный выживший G-byte-gate). Все 20 новых FX-посадок краснеют СВОИМИ пинами (в выводе инструмента поимённо); ре-якорены после рефакторинга: E3 (пере-выражена: предмет «declared» умер с §4.9(б), новая — «unreadable ≠ undecided»), H, S, Z, AH, AG, AI. - **Пины §4.11, проверенные посадкой ДО записи в каталог** (правило «сначала посадка, потом пин»): посадка `subjectOf → class` вскрыла, что мой первый «межтермовый» пин держится гардом shared-key (его класс-строка несёт идентичность) и для этой мутации ВАКУУМЕН — довод пина переписан честно, мутацию ловит оконный пин (`TestANewFaultIsNotMaskedByTheSameTermsFaultInAnotherWindow`), что и записано в каталоге. - **Батарея**: `go build ./... && go vet ./... && go test ./... -count=1` — зелёная целиком; `make lint` — 0 issues; `make battery` — EXIT=0. Замер времени не публикую: прогоны шли под параллельной нагрузкой (агент-ревью и мутационный харнесс), а время под нагрузкой этот проект уже дважды обжигало (§31). ## 39. Живые пробы фикс2 — на КОПИИ книги стенда (minirun-verify2), в books/ не записано ничего Копия вне дерева; мёртвые пути переправлены в трёх файлах (book.yaml, pipeline.yaml, pairs/zh-ru.yaml); две строки снятых ключей удалены в копии (сам стенд — зона оркестратора, он в курсе). jieba-контраста на машине нет — стоп/цепь кроются харнессом (`miningstop_join_test.go`, `bankchain_test.go`), как и в прошлый раз. | проба | результат | |---|---| | немигрированный проект (v11 против v16) | `exit=13`, текст называет `tmctl migrate` | | `tmctl migrate` | `v11 -> v16`, пре-миграционный бэкап, integrity green | | approve+decline одним документом | `exit=0`, отчёт **v2**: `mode=apply`, `written_delta=true`, `written_rejects=true`, оба файла по конвенции, note с ведущим `\n` записана и читается | | повтор того же документа | `exit=0`, `changed=false`, оба `already_applied`, `written_*=false` | | проекция | `exit=0`, `mode=projection`, `written_*=false` | | решение о ПОДПИСАННОМ сид-терме | `exit=14`, `mode=refused`, причина называет сид и цену (base snapshot) | | снятый ключ `mined_delta:` в book.yaml | `exit=10`, отказ называет retired + конвенцию + лечение | | документ на 5001 решение | `exit=14` И отчёт на stdout: `mode=refused`, `rejected[0].index=-1`, `signature.unreadable=true` | | запись в несписываемый каталог (книга без базы, лок пред-создан) | **`exit=15`**, `mode=write_incomplete`, `written_*={false,false}`, `accepted=[]`; после chmod повтор ТОГО ЖЕ документа → `exit=0` | | абсолютный `project_db` со вставкой `/./` на конвенционный путь дельты | `exit=10`, отказ называет оба ключа | | sha256 стендовой базы | не сдвинулся; `git -C books status` чист | ## 40. Дифф `^func Test` — исполнением ``` $ git diff -U0 -- . | grep -cE "^-func (Test|Fuzz)" → 0 # удалено: НОЛЬ $ git ls-files --others --exclude-standard -- . | grep '_test.go$' \ | xargs grep -h '^func Test' | wc -l → 117 # было 97: +20 тест-функций (фаззеров по-прежнему 3) ``` **Сводно по трём пакам:** пак оставил 59 тест-функций + 2 фаззера, дофикс добавил 38 + 1, фикс2 — ещё 20; **удалено за все три пака НОЛЬ**, и это проверено дважды — грепом по диффу и поимённо по пинам, которые отчёты паков называют по имени. Переименовано (не удалено): `TestTheStopClearsWhenTheOwnerDecidesTheWholeSignatureMap` → `TestTheChainOwnerDecidesTheWholeMapAndTheRunGoesOn` (замена по заказу §4.12, новая семантика; вторая цепь — `TestTheStopIsAFlagNotAGateOnDecisions` — и есть пин старой формулы) · `TestDecisionPathsDefaultByConventionOnAnEmptyKey` → `TestDecisionPathsAreAPureFunctionOfTheConvention`; сняты вместе с предметом (§4.9(б), снятие заказано промтом, не подгонка): `TestDeclaredDecisionPathIsNotDefaulted`, `TestDeclaredAndUnreadableDecisionPathStillRefuses`, `TestADeclaredDecisionFileThatVANISHESIsStillAnError` (→ `TestAnAbsentDecisionFileIsUndecidedAndAnUnreadableOneIsAnError`), `TestTwoKeysOnOnePathAreRefused` (→ `TestDeclaredKeysCannotLandOnTheDecisionFiles`); переписаны под модель флажка (старая семантика была их премиссой): `TestMiningStopWHATSurvivesResume`, `TestTerminologistResumeIsFree`. ⚠ Промт §4.11 говорил «семантика §4.1 не краснит НИ ОДИН тест» — на деле три теста краснели, потому что пинили СТАРУЮ семантику как премиссу; их переписывание — часть заказа §4.12, и теперь возврат старой формулы краснит ТРИ теста — `TestTheStopIsAFlagNotAGateOnDecisions`, `TestMiningStopWHATSurvivesResume`, `TestTerminologistResumeIsFree` (вывод tmmutate по FX2 поимённо). ## 41. Что НЕ удалось и что осталось PLAUSIBLE (§10) - **Атомарность ПАРЫ против смерти процесса не построена и не заявлена**: окно между двумя rename остаётся; закрывает его только журнал на два файла — механизм, которого дверь не заслужила (довод в `writeDecisionFiles`). Покрытие остатка — сходимость повтора (приёмка мерила; жива и в моей пробе: retry после exit 15 сходится) и то, что убитый процесс отчёта не печатает. - **Долговечность fsync каталога проверена только на ОТКАЗЕ открытия** (chmod 0333): что rename переживает реальное падение хоста после syncDir — PLAUSIBLE, живого крэша не было и не будет за $0. - **exit 15 живьём воспроизведён только формой «не легло ничего»**: половинная посадка (`delta лег, rejects нет`) на живом CLI требует отказа ВТОРОГО rename при успехе первого — среда одного каталога такого не даёт без root; половина покрыта юнитом `TestHalfLandedIsNamedPerFile` (rename на непустой каталог) и конвергентностью. - **Стоп на живом стенде не воспроизводится** (нет jieba-контраста на машине — то же ограничение, что у прошлых паков): семантика флажка целиком доказана харнессом `miningstop_join_test.go` и цепями `bankchain_test.go` на фейковом провайдере; рантайм-вердикт по ПЛАТНОМУ пути — PLAUSIBLE, как и заказано §4.13. - **«Один лишний стоп» вредной стороны кластерной памяти** (новая сущность на общем алиасе молча не предъявляется) в интеграции не воспроизведён — у фикстуры нет двух сущностей с общим прозвищем; запинен на юнит-уровне предиката (`stopflag_test.go`) как ПРИНЯТЫЙ трейд с направлением потерь. - **Гипотеза §4.13 о пере-оплате терминолога при подписании** механизмом не подтверждена и выигрышем не заявляется: память предъявленного не трогает ни состав кандидатов, ни ключ чекпоинта (хеш отрендеренных сообщений); `TestTerminologistResumeIsFree` держит $0-резюм в обеих семантиках. - **Поимённой разметки новизны в карте подписи НЕТ**: оператор видит РАЗМЕР карты, а не число новых термов (баннер стопа больше не выдаёт одно за другое — `renderSignatureStop`, §45). Пер-термная новизна — продуктовая поверхность, не этот пак. ## 42. Интервальная самоверификация субагентом (§5.1): четыре находки, все minor Ревью шло против явных критериев, адверсариально, с исполнением (точечные тесты + посадки в своей копии). Код сошёлся по всем; находки — класс «довод/покрытие», ровно тот, что предыдущая сессия назвала своим худшим. Три из них — неверные доводы в докстроках при верном коде (отменённая механика «промотируй/отклони, потом re-run» у `renderSignatureStop`; предикат v16 в `migrate.go`, читавшийся как «любая поверхность вне памяти», тогда как код стреляет только на кластере, чья ВСЯ поверхность вне; описание стоп-окна в `bankapply.go`), все переписаны по месту. ⚠ Третья правка сама внесла ложь про «pre-lock check» и была исправлена воркфлоу-ревью: ОБЕ проверки контекста в `bankapply.go` пост-локовые (§45). Две находки с живым остатком: | # | находка | диспозиция | |---|---|---| | 4а | грань домена §4.1 «предикат по итоговой карте, не по пулу ДО потолка» не краснится ни одной посадкой: вечный стоп невоспроизводим в фикстурах (2 терма при потолке 200), а одно-строчной мутации не существует — карта, предикат и память читают ОДИН список `mined` после потолков, расхождение требует сознательной перестройки | **аргументированный не-пин** по прецеденту G: сама структура (один список) и есть защита; записано здесь, чтобы следующая сессия не считала свип дырявым | | 4б | новизна из ОБРАТНОЙ секции не была покрыта: в стоп-фикстурах terminology-гейт выключен и reverse пуста | закрыта поведенческим пином `TestNoveltyFromTheReverseSectionAloneTripsTheFlag`: run 1 с пред-отклонённой banknote-only поверхностью (карта без неё), run 2 со снятым отказом — стоп обязан сработать на новизне, пришедшей ТОЛЬКО через обратную дверь | **Что ревью подтвердило исполнением (не переделывал):** предикат/карта/память — один список после обеих секций и потолков · все шесть исходов отчёта проходят через `finishReport`; исходные поля присваиваются ровно в ДВУХ конструкторах — `finishReport` и до-локовом `capRefusalReport` (у капа `bookState` ещё не существует) · `TestTheRefusalTableIsTotal` невакуумен для класса 15 (AST-словарь + сверка длины таблицы) · retired-ключи ловятся полями, а не голым strict-decode · конверт карты читают все движковые читатели, `DecodeFile` на карту не наведён · дверь память не читает · в новом Go-коде ветвлений по паре/книге нет. ## 43. Чек заказа фикс2 — снят Попунктный чек тринадцати пунктов §4 фикс2 вырезан как прожитая приёмка. Адреса: заказ — `docs/archive/prompts/BACKEND_SEAM_FIX2_SESSION_PROMPT_2026-08-27.md` (комплектность сверялась механически: `grep -cE '^### 4\.' <промт>` → 13); решения с доводами и названные остатки — §37; замеры — §38; живые пробы — §39; дифф тестов и переименования — §40; что НЕ удалось — §41; сужения воркфлоу-ревью — §45. ⚠ D39.158 называет «вход §43» — читатель, пришедший по этой ссылке, попал верно: адреса выше. ## 44. Проверка ЗАКРЫТИЯ — отдельным агентом, не чинившим (§5.1) Прогнана по презумпции «каждое заявленное закрытие ложно, пока не увидено исполнением»: все 13 пунктов заказа фикс2 (§36–§41, §45) и все 4 диспозиции §42 подтверждены в дереве (13 точечных тестовых прогонов + 3 полных пакета — зелёные; носители по file:line; реверс-пин признан невакуумным с названным механизмом падения). Расхождения нашлись и здесь (⚠-вывод §35 держится и на этой сессии), все закрыты по месту — правки видны в §38, §40 и §42. Одно стоит того, чтобы его запомнили: пере-замер §4.4 жил только в скратчпаде — то есть был «числом без первоисточника» ровно того класса, за который заказ корил чужие числа; первоисточник положен в дерево постоянным якорем-пином `internal/miner/emissioncap_test.go`. ## 45. Воркфлоу-ревью фикс2: две кодовые находки и три остатка, которые оно сузило Форма: шесть параллельных полос-искателей под антипаттерны проекта (неверные доводы при верном коде · вакуумные пины · семантика флажка глубоким чтением · дверь/отчёт глубоким чтением · общность движка · шов/контракт), каждая находка — через трёх адверсариальных скептиков с разными линзами (корректность трассой · достижимость · «не принятый ли уже остаток»), выживание — большинство. Все выжившие находки закрыты; правки доводов в комментариях и документах живут по месту, здесь — то, что осталось фактом. **Кодовые (2), обе закрыты правкой + пином + посадкой:** | находка | закрытие | |---|---| | **`tmctl translate --dry-run` молча игнорировался и запускал ПЛАТНЫЙ прогон** — третье написание денежной дыры, которую это же дерево уже закрывало для `--decisions=`/`--keys-file=`; пред-существует фикс2, нигде не был записан | гард присутствия: `--dry-run` принимают только `bank-apply` и `redrive`, остальным — громкий отказ с ценой в тексте; пин `TestDryRunIsRefusedWhereNothingProjects`; посадка FX21 | | **Повтор после exit 15 из-за отказа `syncDir` сходился в exit 0, так и не пере-доказав долговечность** — retry по контракту класса шёл byte-no-op-веткой мимо единственного места с синком | no-op-ветка теперь пере-доказывает долговечность: не открывающийся на синк каталог держит класс 15 (`written_*={false,false}`, текст называет непроверенное) до выздоровления среды; пин `TestARetryAfterAFailedDirectorySyncReProvesDurability`; посадка FX22 | **Что сужено и осталось живым:** - **Якорь-пин §4.4 НЕ краснел при сдвиге потолка** — премиссы не фиксировали значение 200 (150/300/400 проходили зелёными), вопреки трём носителям клейма. Значение потолка теперь запинено премиссой с текстом, ведущим двигающего к пингу; проверено: смена 200 → красный. - **Довод арбитра сужен**: «межкнижная гонка недостижима по построению» была шире правды — остался второй конфиг ТОЙ ЖЕ книги (один каталог/`book_id`, другой объявленный `project_db`): пишет те же файлы под другим локом, мимо flock живого прогона (ревью воспроизвело живьём). Межкнижная версия снята; остаток заведён строкой бэклога **222** (D39.158 п.9). - **Клауза «ничего не произошло» у классов 10–14 сужена до «состояния КНИГИ»**: пред-полётный бэкап `translate` пишется ДО отказа 12/13. И **`preexisting_problems: []` в кап-отчёте ≠ «проверено чисто»** — оговорено на самом поле: при отказе по капу проверка не выполнялась. - **Баннер стопа выдавал РАЗМЕР карты за число НОВЫХ термов** — под моделью флажка карта намеренно смешивает новое и ранее предъявленное-нерешённое, поэтому счётчик не имеет права называться «new». `WaveSignatureStop.Error` переписан, а `renderSignatureStop` печатает нейтральное `backend/cmd/tmctl/render.go:171=(%d term(s) in the map)`; довод стоит рядом с кодом (`backend/cmd/tmctl/render.go:169=must not be labelled «new»`). ⚠ Поимённой разметки новизны в карте НЕТ — продуктовая поверхность, не этот пак; сюда и ссылается §41. --- # ФИКС3 (6 пунктов; промт архивирован — `docs/archive/prompts/BACKEND_SEAM_FIX3_SESSION_PROMPT_2026-08-27.md`) — секция 46 ## 46. Фикс-лист приёмки дофикса-2: четыре пина, один снос, зачистка двух контрактов Комплектность: `grep -cE '^### 4\.' docs/archive/prompts/BACKEND_SEAM_FIX3_SESSION_PROMPT_2026-08-27.md` → **6**. | пункт | статус | чем закрыт | |---|---|---| | **4.1** пин «карта → память» | ✅ | мутация приёмки ВОСПРОИЗВЕДЕНА до пина (отказ записи карты → WarnContext: батарея зелёная во всех трёх кандидатных пакетах — pipeline, tmctl, store). Пин приёмом приёмки: путь карты занят непустым каталогом → `TestTheMemoryIsNotWrittenWhenTheMapCannotBe` требует ошибку прогона (не стоп и не успех) и ПУСТУЮ память книги. Проверен посадкой: красный на мутации, зелёный на дереве. Каталог: **FX23** | | **4.2** отчёт 15 мерит по диску | ✅ | утверждение панели ВОСПРОИЗВЕДЕНО (docsReRead → docsFromResult на write_incomplete: вся батарея зелёная — существующий пин кормит конструктор пустыми состояниями, источники неразличимы). Пин — брат stopped-пина: карта с нерешённой поверхностью + документ, решающий её + «не легло ничего» → `signature.undecided` обязан быть 1 (диск), не 0 (намерение): `TestAWriteFailedCallMeasuresTheSignatureAgainstTheDiskNotTheIntent`. Проверен посадкой. Каталог: **FX24** | | **4.3** старый пол в двух носителях | ✅ | `runner.go` (openRunner) и `cmd/tmctl/panic_exit_test.go` приведены к двух-ярусной букве: пол полосы + «nothing was written» как клауза классов, с явной оговоркой про класс 15. Контрольный греп: `grep -rn "nothing was written" --include='*.go' .` → **два** хита, оба — клаузы КЛАССОВ, не пола: `ErrDecisionsRefused` (класс 14, там она истинна целиком) и сообщение отказа синка каталога («nothing was written BY THIS CALL…», класс 15 — пер-вызовная клауза с оговоркой) | | **4.4** отменённая формула | ✅ грепом | греп-формы: `re-?fires? the stop` · `stop clears only` · `clears? once every` · `promoted or rejected` · `stop.*forever` · `livelock` рядом с decline · `переспраш`. Найдено и переписано **восемь** носителей (пять из списка промта + `DecodeRejects`-док + док и оракул `TestMiningStopRejectClearsDelta`): правда из ноты — отказ исключает сущность из ЭМИССИИ (терм не возвращается в карту и в авто-банк; про стоп отказ не говорит ничего — стоп это вопрос памяти). Тест реджектов НЕ вакуумен под памятью (его второй проект СВЕЖИЙ — память пуста, продолжение решенческое), и это теперь сказано его доком + явным оракулом `lastMinedCount == 0`. Контрольный греп после зачистки: остались только носители НОВОЙ модели (память/`re-fire on what it already presented`) и история row-199 в цепях | | **4.5** снос `.bank-stop.json` | ✅ | свой греп читателей по backend/platform/frontend/eval ДО сноса: в коде читателя нет ни в одной зоне (у платформы — только упоминания в её ЖУРНАЛАХ, пинг ниже). Снесено: писатель `writeBankStopTableJSON`, `clearBankStopTableJSON` (+ его вызов на not-configured-пути), пустая перезапись на empty-delta, `bankStopTableJSONPath`, `bankStopTableVersion`, оба DTO, `emptyIfNil`/`emptyVariantsIfNil`, json-теги `BankStopVariant`, поле `WaveSignatureStop.TableJSONPath`, строка CLI «Machine table:», лог-поле `table_json`, четыре теста таблицы (предмет снесён заказом — не подгонка). `.bank-stop.txt` НЕ тронут (читатель — оператор). Контрольный греп `bank-stop.json|TableJSONPath|bankStopTableJSON|tm-bank-stop` по `*.go` → 0 | | **4.6** якорь свежести банк-экспорта | ✅ утверждение подтверждено, признак заведён | подтверждение: `at` ехал ТОЛЬКО в лог-строке (`exportBank`), при отказе записи артефакт молча оставался прошлым — в документе отличить нечем. Форма: аддитивные поля `as_of` (метка границы) + `run_id` (тот же id, что стрим `events.jsonl`; эмиттер авторитетен — он может пере-минтить id; пустой = «идентичности нет», не «текущий»). Версия `tm-bank-v1` не двигалась (добавление аддитивно, по букве промта). Пин `TestTheBankExportCarriesItsFreshnessAnchor`: последняя граница прогона · смена границы двигает якорь · без идентичности — пусто. Детерминизм не задет: id приходит из контекста/эмиттера, не из часов | **Что НЕ удалось / остатки (§10):** якорь §4.6 отличает прогоны по `run_id` и границы по `as_of`, но две записи ОДНОЙ границы одного прогона (не бывает сегодня: каждая граница пишется однажды) были бы неразличимы — принято, механизм счётчика был бы новым механизмом против буквы промта. Снос §4.5 оставляет упоминания `.bank-stop.json` в ЖУРНАЛАХ платформы (их зона) — пинг, не правка. **Пинги этого пака (в журнал):** платформенные доки (`platform-PROGRESS.md`, `STACK_DECISIONS.md`) называют `.bank-stop.json` атомарно-читаемым сайдкаром — файла больше нет, упоминания устареют при лендинге · потребители банк-экспорта получают `as_of`/`run_id` — аддитивно, ломать нечего. **Прогоны итогового дерева (§5):** `make battery` → **EXIT=0** (fmt+vet+тесты+линт) · `make battery-stand` → красен ровно `TestMinerFullBookParity` (jieba вне git — ожидаемая картина промта) · каталог мутаций `go run ./cmd/tmmutate -root <копия>` → **60 записей, 0 неожиданных исходов** (перегнан ПОСЛЕ форматирования — якоря каталога сверены с финальными байтами). Дифф тестов: снесены четыре теста машинной таблицы (предмет снесён заказом §4.5), добавлены четыре пина (§4.1, §4.2, §4.6, оракул `lastMinedCount` в тесте реджектов); `git diff -U0 -- . | grep -cE "^-func (Test|Fuzz)"` → **4** — ровно четыре снесённых `TestBankStopMachineTable*` — снос легитимен: предмет снесён заказом §4.5, других удалений диффа нет. **Проверка закрытия (не-чинившим):** все шесть пунктов подтверждены исполнением — обе мутации (FX23, FX24) посажены им в своей копии и убиты пинами, греп-оси §6 пере-сняты; найденные расхождения исправлены по месту выше.