# Доказательная база пака «входная дверь шва» (движковая половина D39.156) Бэкенд-сессия 24.08.2026. Промт — `docs/BACKEND_SEAM_PACK_SESSION_PROMPT.md`, план — `SEAM_PACK_PLAN.md`. **Каждое число — с командой, которой получено;** приёмка пере-раняет выборочно. Команды из `backend/`. ## 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`. **Красное ИМЕННО в этих двух тестах ожидаемо; любое другое красное — регрессия.** | что | команда | результат | |---|---|---| | хермет. батарея | `make battery` | `EXIT=0` на чистом прогоне; skip: `TestMinerFullBookParity`, `TestHelperEventsRun`, `TestHelperKillLoop`. См. предупреждение выше | | стенд-батарея, **без единого `TM_*_DIR`** | `make battery-stand` | `EXIT=2`, красен ровно `TestMinerFullBookParity`, причина — отсутствующий `eval/exp16/data/jieba_dict_general_zh.txt` (строка 123) | | линтер | `make lint` | `0 issues.` | | vet под оба тег-набора · формат | `go vet ./... && go vet -tags live ./...` · `test -z "$(gofmt -l .)"` | чисто | Стенд-картина совпала с опорной точкой оркестратора ⇒ **критерий §4.6 выполнен буквально.** ## 2. Адверсариальные посадки: 16/16 краснеют Харнесс сажает мутацию → гоняет тесты НАЗВАННОГО пакета → откатывает; он же снимает sha256 целей до прогона и проверяет после (`TREE IS NOT PRISTINE`). ⚠ Пакет пина ≠ пакет правки. | # | посадка | пакет пина | что покраснело | |---|---|---|---| | M1 | дефолт срабатывает на НЕпустом ключе | `config` | `TestDeclaredDecisionPathIsNotDefaulted` | | 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/` не записано ничего.** | проба | результат | |---|---| | глагол на НЕмигрированном проекте | `exit=13`, `schema_mismatch found=11 expected=15 … run tmctl migrate` | | проекция (`--dry-run`) | `exit=0`, `mode=projection`, `canonical_rewrite=true`, md5 обоих файлов НЕ изменились | | применение | `exit=0`, `changed=true`; ручной `陈博` сохранил `gender`/`decl`/`translit_policy` | | повтор того же решения | `changed=false`, `already_applied`, **inode и md5 не изменились** | | конфликт с подписанной строкой сида (`春秋蝉`) | `exit=14`, отчёт называет строку сида ПОИМЁННО, файлы не тронуты | | занятый лок (flock держит сторонний процесс) | `exit=12`, `wall=0.02s` | | `decline` над рукописной дельтой | md5 дельты не изменился, комментарии владельца целы | | reject-файл ИЗ ОДНИХ КОММЕНТАРИЕВ | `exit=0` (до фикса R1 было `exit=10`, `seed: EOF`) | | загружаемость результата | `seed-lint OK: 0 fail-louds, 0 shared-key collisions` | | конвенционный дефолт (ключи из `book.yaml` удалены) | `artifacts.mined_delta = /.mined-delta.yaml`, файлов на диске нет; `approve`+`decline` одним вызовом создают оба | | все seed/delta/signature/auto-bank стенда под строгим загрузчиком | 21 из 21 чисто (независимо: 29/29 на более широкой выборке, включая книги dovodka/role-topology/tenant-panel) | ## 4. Замеры | что | число | вердикт | |---|---|---| | **цена глагола** (`bank-apply --dry-run`, `/usr/bin/time`) | 5 000 термов → 1.60с/133МБ · 10 000 → 3.62с/217МБ · 20 000 → 9.08с/350МБ; ×2.3–2.5 на удвоение | **датум для таймаута платформенного воркера.** Бюджет платформы на ОДИН вызов движка — 60с (`platform/internal/runs/reconcile.go:280`), до него ~80k термов. Реальная книга (кап майнера 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` доказывает, что прогон читает именно тот файл), **но цепь целиком не гонялась ни разу**, а мина №1 строки 199 — livelock именно уровня ЦЕПИ, которого частичные тесты по построению не видят. **Почему я его не снял — проверено, не заявлено:** банк-стоп на этой машине не воспроизводится вовсе (`pipeline.yaml` требует `contrast_path: …/eval/exp16/data/jieba_dict_general_zh.txt`, `ls` → `No such file or directory` — та же причина, по которой красен парити), и даже с контрастом до стопа надо дойти ОПЛАЧЕННОЙ черновой волной. Синтетический контраст = подделать замер. **Команда:** на КОПИИ книги, припаркованной на exit 3, скормить `bank-apply` документ на всю карту подписи (id из банк-экспорта), затем возобновить `translate --verify-bank`. **ДЕФЕКТ** — стоп снова с ТЕМИ ЖЕ поверхностями. **НЕ дефект** — свежая пачка ≤200 термов реверс-секции на большой книге (мина №3 строки 199: кап режется ПОСЛЕ фильтров). ## 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:`. - **Размер документа решений не ограничен** (`os.ReadFile`), тогда как платформа свои чтения от движка капает (`maxStatus` 16МБ / `maxBank` 256МБ). Дотянуться могут: на SaaS — воркер самой платформы (ограничен её API-капами), локально — оператор, роняющий свой процесс. Документ на 10 000 решений ≈2МБ ⇒ кап в диапазоне 1–16МБ стоит одной константы плюс строки отказа. **Однострочник при лендинге.** ## 10. Пинги — предметы ВНЕ карты пака, правок не делал 1. **`rubyToCandidates` мёртв в проде.** `grep -rn "rubyToCandidates" --include=*.go .` → 4 хита: определение `internal/membank/memseed.go:301`, два комментария и ЕДИНСТВЕННЫЙ вызов — `memseed_test.go:204`. `internal/pipeline/seeding.go:36-38` строит `manualSrcs` (аргумент этой функции) и никуда не передаёт; Go молчит, потому что запись в map считается использованием. Следствие: строк `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). Чередующийся A/B на `git worktree` HEAD, 3 раунда × 10: `kill9` — 2/30 на HEAD против 0/30 у меня; `seam` (`-race`, 30) — 2/30 против 1/30; БЕЗ `-race` — 0/30. Покрытие независимо подтверждает: `LockProject`/`Release` этими тестами не исполняются вовсе. ⚠ **0/30 против 2/30 НЕ значит «пак улучшает»** — на таком числе событий это шум (Фишер p≈0.5); решающее и достаточное — что флейк живёт на дереве, которого пак не касался. Воспроизведение: два дерева, `-race -count=10`, чередуя, под параллельным `go test ./...`. ## 11. Три правила протокола, вынесенные инцидентами сессии 1. **Харнесс мутаций обязан фингерпринтить цели.** Прогон `mutate.py` был убит таймаутом инструмента, `finally` не отработал, и посадка M4 (снятый гейт байтового no-op) просидела в `internal/pipeline/bankdecisions.go` ~40 минут, выглядя продакшн-кодом; сторонний ревьюер снял с этого дерева замеры и доложил «дефект гейта идемпотентности». Поймал посадку пин `TestBankApplyRepeatIsAByteNoOp` на первом прогоне после. Харнесс починен (sha256 целей до и после). Пин усилен по следу: повтор проверяется над файлом с РУЧНЫМ комментарием в шапке — это то, что пере-рендер уничтожает и чего не заметила бы проверка содержимого — плюс что `changed:false` не врёт. 2. **Снимай отпечаток дерева ПЕРЕД сборкой бинаря замера.** Симметричная половина того же класса: ревьюер собрал `tmctl` из незакоммиченного дерева, не сняв отпечаток, и инструмент унаследовал посадку. За сессию на это наступили обе стороны. 3. **Одиночный прогон — не замер.** Атрибуцию флейков я сперва вывел из одного прогона («зелено в изоляции ⇒ нагрузка»), затем другой одиночный прогон дал ровно обратное. Решает только чередующийся A/B (§10 п.7). ## 12. Попунктный чек промта | пункт | статус | |---|---| | §2 зона записи только `backend/` + секция «Бэкенд» в `docs/PROGRESS.md`; не коммитить; чужое не трогать | ✅ `git status` подтверждает | | §4.1 дефолты пустых ключей; гард ЯВНОГО пути не ослаблен; отсутствующий дефолт = «решений нет» | ✅ M1 + `TestDeclaredAndUnreadableDecisionPathStillRefuses` | | §4.2 глагол $0; байтовый no-op; отзыв = ЗАМЕНА; всё-или-ничего; два решения по терму = отказ; конфликт с сидом С ИМЕНЕМ; проекция БЕЗ мутации; занятый лок РОВНО 12; flock неблокирующе; версия в ответе; `depth` явным полем | ✅ M2–M8 + `TestDeclineWritesTheRejectAndWithdrawsTheApproval`, `TestOneCallDecidesATermOnce`, `TestDecisionOnASignedSeedTermIsRefusedByName`, `TestBankApplyDryRunWritesNothing`, `TestBankApplyDeclineThenApproveConverges`; §3, §4 | | §4.2 НЕ проектировать «глубину до черновика» | ✅ `depth: edit_wave`; `TestTheRunReadsTheConventionalDelta` доказывает `Source:"mined"` | | §4.3 `--keys-file` только у `translate`; деплой-файл ПЕРВЫМ; нечитаемый = отказ конфигом; ничего «на вырост» | ✅ M6 + `TestKeysFileWinsOverTheConventionalEnv` + `TestAnUnreadableKeysFileRefusesWithTheConfigClass` | | §4.4 строгость сид-загрузчика + тест на unknown-key | ✅ 2 кейса в `TestSeedLintCatchesDefects` (`gendr`, `termz`) | | §4.5 путь артефакта аддитивно + версия документа; НЕ тащить пере-именование банк-экспорта (ЛОМАЮЩЕЕ, окно — строка 161) | ✅ `artifacts{}` + `status_version`; пин `rep.Artifacts.BankExport == r.bankExportPath()` | | §4.6 дефолт корпуса маркерным резолвом, env-переопределения старше | ✅ §1 | | §4.7 платных вызовов НОЛЬ; `--keys-file` фейковыми ключами; вердикт PLAUSIBLE | ✅ sha256 базы (§4) + `TestTheKeysFileActuallyReachesTheProvider` (сверяет `Authorization` на fake-провайдере, и БЕЗ флага тот же прогон отказывает конфигом — проверка не вакуумна); §7 | | §5 батарея после каждой правки; красный тест не подгонять; посадки ВНЕ списка промта; посадка называет ПАКЕТ; живая проба в КОПИИ; артефакт-находок | ✅ 16 посадок, 8 сверх списка; флейки охарактеризованы и НЕ тронуты; §2, §3 | | §5.1 последний абзац — план или обещание? сделать СЕЙЧАС | ✅ всё найденное ревью закрыто в этом дереве; единственное отложенное — §8, вне моих возможностей по деньгам | | §5.1 дифф `^func Test` исполнением; интервальная самоверификация субагентом | ✅ §13; ревью гонялось в СЕРЕДИНЕ пака против явных критериев | | §6 оси: шов (инвентарь ЧУЖОГО кода) · общность · деньги | ✅ читал `artifacts.go`/`bank.go`/`engine.go`/`spawn.go`/`resync.go`/`exit.go` сам; проверил, что ни один декодер платформы не строгий ⇒ аддитивные поля безопасны; Go не ветвится по паре/книге; деньги — sha256 базы | | §8 число с командой · §9 эхо до работы · §10 «что НЕ удалось» · §11 развилки пингом | ✅ §1–§4 · выдано · §7 · §10 | ## 13. Дифф `^func Test` — исполнением ``` $ git diff -U0 -- backend | grep -E "^[+-]func Test" +func TestTheRefusalTableIsTotal(t *testing.T) { $ git ls-files --others --exclude-standard -- backend | grep '_test.go$' | xargs grep -h '^func Test' | wc -l 59 $ ... | xargs grep -h '^func Fuzz' | wc -l 2 ``` **Добавлено:** 59 тест-функций в шести НОВЫХ файлах + `TestTheRefusalTableIsTotal` в существующем `cmd/tmctl/exitcontract_test.go` + 2 фаззера. **Удалено:** ноль. **Тронуто в существующих файлах — шесть, ни у одной сигнатура не изменилась:** - `exitcontract_test.go` — в список полосы добавлены `exitSchemaMismatch` и `exitDecisionsRejected` (иначе гейт молча перестал бы покрывать новый код) + тест тотальности; - `invocation_test.go` — frozen-строка usage расширена именем `bank-apply`. Объявленное расширение контракта тем же механизмом, которым строка росла пять раз до меня (её собственный комментарий их перечисляет), и второй половине того же теста («usage обязана называть каждую dispatch-команду») иначе не удовлетворить; - `checks/labelharness_test.go`, `membank/labelharness_test.go`, `miner/miner_parity_test.go` — §4.6: три копии резолвера заменены на `standdata` (−120 строк, +62); - `membank/seedlint_test.go` — два кейса unknown-key (§4.4). --- # Дофикс той же двери (сессия 25.08.2026) Промт — `docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (16 пунктов §4), план — `SEAM_FIX_PLAN.md`. Секции 0–13 выше — улика приёмки, не тронуты. **Каждое число ниже — с командой, которой получено;** команды из `backend/`. Дерево не коммичено, лендит оркестратор. ## 14. Батареи | что | команда | результат | |---|---|---| | **база ДО правок** (дерево пака) | `make battery` | `EXIT=2`, красен ровно `TestKillMinus9LosesAtMostOneCall` — один из двух названных промтом пред-существующих `-race`-флейков. Флейк воспроизвёлся на дереве, которого я ещё не касался: атрибуция моя, не наследованная | | батарея после правок | `make battery` | см. §31 | | стенд-батарея | `make battery-stand` | см. §31 | | линтер | `make lint` | `0 issues.` | ## 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 МБ) выше любого прогона с ней.** Первая редакция публиковала здесь «≈13 %», сравнивая минимум с минимумом, — снято. Основная цена глагола — НЕ проверка (см. §18). ## 16. §4.11 + свои посадки: 36 мутаций, 35 краснеют, 1 выживает осознанно Харнесс — **в репозитории**: `cmd/tmmutate` + каталог `cmd/tmmutate/mutations.json` (36 записей — `python3 -c "import json;print(len(json.load(open('cmd/tmmutate/mutations.json'))))"`, у каждой названо СВОЙСТВО, за которое она отвечает). Три сессии подряд писали такой инструмент в скретчпад и теряли его вместе с сессией; каталог — это и есть память о том, что кто-то счёл достойным охраны. Прогон — на КОПИИ дерева (`cp -a`, равенство копии проверено побайтно), сам инструмент снимает отпечаток каждой цели и каталога целиком и краснеет на «TREE IS NOT PRISTINE». **База ДО пинов: 20 из 24 выжили** (⚠ снято на РАННЕЙ редакции каталога из 24 записей; нынешние 36 её не воспроизводят — это исторический замер, а не команда, которую можно повторить) — то есть все десять слепых пятен §4.11 подтверждены исполнением, плюс мой собственный новый код был не запинен. **Итог после всех итераций: 36 посадок, 35 краснеют на СВОЁМ пине, одна (G) записана как аргументированный выживший (§17).** Каталог поэтому не отчёт, а ГЕЙТ: исход, не совпавший с записанным, роняет прогон — в том числе неожиданно ПОЙМАННАЯ посадка, что значит «запись устарела, обнови её». ⚠ **Две мои собственные посадки поймали два моих же ВАКУУМНЫХ пина** (последняя итерация): пин на позиционные субъекты держал строки без идентичности ПЕРВЫМИ, поэтому их позиции никогда не двигались; а пин на полноту довода отказа искал имя команды в тексте, где оно и так стоит — в обвинении «not by %q», а не в объяснении. Оба переписаны и оба теперь краснеют. Это второе за сессию подтверждение правила «сначала посадка, потом пин»: пин, написанный первым, проверяет то, что и так верно. ``` $ cd <копия backend> && go run ./cmd/tmmutate -root . -logs ``` | # | посадка (свойство) | пакет пина | что покраснело | |---|---|---|---| | A | лок двери — тот же flock, что держит ПРОГОН | `pipeline` | `TestBankApplyRefusesAProjectAN_RUNIsHolding` | | B | `canonical_rewrite` + `preexisting_problems` (2 правки в 1 файле) | `pipeline` | `TestTheReportCarriesTheTwoFieldsThePackMitigatesItsRisksWith` | | C | класс отказа без номера уезжает в catch-all 19 | `cmd/tmctl` | `TestTheRefusalTableIsTotal` | | D | нормализация в REPLACEMENT-половине отказа | `membank` | `TestADeclineWithdrawsAnApprovalWrittenInAnotherOrthography` | | E1 | глубокая копия алиасов в `withRubyAliases` | `membank` | `TestWithRubyAliasesDoesNotWriteIntoTheCallersRows` | | E2 | перенос `note` при промоушене | `membank` | `TestPromotionCarriesTheOwnersNote` +1 | | E3 | рантайм-половина гарда ОБЪЯВЛЕННОГО пути | `pipeline` | `TestADeclaredDecisionFileThatVANISHESIsStillAnError` | | F | абсолютность путей в `artifacts{}` | `pipeline` | `TestTheReportedPathsAreAbsoluteEvenWhenTheConfigIsNOT` | | G | байтовый гейт `changedDoc` | `pipeline` | **ВЫЖИВАЕТ — см. §17** | | H | решения едут С КНИГОЙ, а не с `project_db` | `config` | `TestTheDecisionFilesTravelWithTheBookAndNotWithTheDatabase` | | I | нормализация ДОКУМЕНТА (значение уже в файле) | `membank` | `TestAHandAuthoredNoteTheEngineCannotWriteIsRepairedNotRefused` | | J | равенство в `renderProved` (а не «просто разобралось») | `membank` | `TestTheRenderGateRefusesBytesThatReadBackAsAnotherDocument` | | K | нормализация ЗАПРОСА (идемпотентность повтора) | `membank` | `TestARepeatOfADecisionWhoseTextIsPaddedIsStillAlreadyApplied` | | L | `book_id` — один элемент пути | `config` | `TestBookIDIsRefusedWhenItIsNotAPathElement` | | M | четыре движковых пути попарно различны | `config` | `TestTwoKeysOnOnePathAreRefused` | | N | гард флага — на ПРИСУТСТВИЕ, не на значение | `cmd/tmctl` | `TestAFlagIsRefusedForBEINGTHERE_NotForCarryingAValue` | | O | перевёрнутое окно глав | `membank` | `TestAReversedChapterWindowIsRefused` | | P | инертный отказ судится по НАБОРУ | `membank` | `TestApprovingATermAndDecliningItsAliasInOneCallIsRefused` | | Q | `note` не стирается умолчанием | `membank` | `TestANoteIsNotErasedByAnOmission` | | R | кап документа решений | `pipeline` | `TestTheDecisionDocumentIsCapped` | | S | стоп между решением и записью не пишет | `pipeline` | `TestAStopRequestWritesNothing/cancelled_while_the_decision_is_being_computed` | | T | пред-существующая поломка — по СУБЪЕКТУ | `membank` | `TestRepairingOneOfTwoBrokenTermsIsAccepted` | | U | честность отчёта о непогасшем стопе | `pipeline` | `TestTheStopClearsWhenTheOwnerDecidesTheWholeSignatureMap` | | V | конверт путей на поверхности потребителя | `pipeline` | `TestTheManifestCarriesTheArtifactEnvelope` | | W | кап на КОЛИЧЕСТВЕ решений (§29 п.1) | `pipeline` | `TestTheDecisionCOUNTIsCappedBecauseTheBYTESCannotBound` | | Y | субъект поломки без рендеринга (§29 п.2) | `membank` | `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt` +1 | | Z | нечитаемая карта подписи ≠ «всё решено» (§29 п.6) | `pipeline` | `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided` | | AB | побег «это был терм сам по себе» (§30 п.2) | `membank` | `TestADeclineIsRefusedWhenTheSurfaceGOESONFiringAsAnotherTermsAlias` | | AC | `aliasOwner` игнорирует статус (§30 п.3) | `membank` | `TestDecliningTheAliasOfAnUNSIGNEDRowIsAccepted` | | AD | загрузчик отдаёт построенные записи при отказе (§30 п.1) | `membank` | `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS` | | AE | позиционный субъект (§30, «плюс два») | `membank` | `TestTwoRecordsWithNoIdentityDoNotBlockEachOthersRepair` | | AF | довод отказа описывает всех, кого отвергает (§30 п.4) | `cmd/tmctl` | `TestTheKeysFileRefusalDescribesEveryoneItTurnsAway` | | AG | остановленный вызов не печатает, что применил (§34) | `pipeline` | `TestAStoppedCallDoesNotPRINTThatItApplied` | | AH | нечитаемая карта подписи ≠ «всё решено» (§34) | `pipeline` | `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided` | | AI | отказ не предупреждает о перезаписи, которой не будет (§34) | `pipeline` | `TestARefusedCallDoesNotWarnAboutARewriteThatWillNotHappen` | | AJ | отказ по алиасу сида сужен до случая, где он инертен (§34) | `membank` | `TestDecliningASeedAliasIsAcceptedWhenTheDeltaHoldsThatRow` | ## 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(документа))`; ⚠ первая редакция называла здесь `ownsSurface`, которой в коде нет — она была снята вместе с побегом §30 п.2), поэтому проход по 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 } ``` `undecided > 0` — **факт**, что стоп не погаснет. `undecided == 0` — **не обещание**, и это написано в комментарии типа явно. Пустой `map` значит «прогон до границы банка ещё не доходил». В режиме проекции число — то, что БУДЕТ после применения (единственное чтение, которое может иметь превью). Запинено там, где оно осмысленно, — в тесте цепи §4.12: частичный документ на карте из двух сурфейсов даёт `undecided=1`, полный — `undecided=0`, и после него стоп действительно гаснет. ## 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-самопроверка сравнила его с пропатченным же хешем и **прошла**. Дерево копии осталось с `rep.CanonicalRewrite = false`, и **два полных раунда измерений после этого шли по мутированному коду**: три пина отчитались как поймавшие мутации, к которым не имели отношения (E3, F, R, S, U, V показывали ложный «RED» на `TestTheReport…`). Поймано ровно тем, чему учит §11 п.3 отчёта пака: одиночный прогон — не замер. Я пере-проверил один подозрительный «RED» изолированным прогоном, он не воспроизвёлся, и это вскрыло дефект инструмента. **Починено дважды:** одно чтение и одна запись на файл (порядок объявлен контрактом в комментарии), плюс второй независимый рубеж — отпечаток ВСЕХ файлов каталога, снятый на заведомо чистом дереве и пере-проверяемый ПЕРЕД каждой мутацией и после последней. Дерево копии после итогового прогона побайтно равно рабочему (`diff -rq --exclude=bin <рабочее> <копия>` пусто, и `find . \( -name '*.go' -o -name '*.json' \) | sort | xargs sha256sum | sha256sum` даёт на обеих сторонах одно значение при 265 файлах. ⚠ Сам дайджест здесь НЕ приводится: дерево с тех пор менялось, и записанное число воспроизвести уже нельзя — повторяемо равенство двух сторон, а не константа). **Общая форма правила, шире этого инструмента:** самопроверка, чей эталон снят ПОСЛЕ первого изменения, не проверяет ничего. Эталон снимается один раз, на состоянии, которое известно как чистое. ## 26. Попунктный чек §4 — все шестнадцать Комплектность сверена механически: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` → **16**. | пункт | статус | чем закрыт | |---|---|---| | **4.1** ⛔ блокер: round-trip на ОБОИХ документах | ✅ **сделано ШИРЕ заказа** | нормализация документа + запроса, рендер ДОКАЗЫВАЕТ равенство (не только разбираемость — заказанная форма пропустила бы две тихие потери), байты проверены один раз и один раз записаны, фаззер пути ДВЕРИ, характеризационный тест на библиотеку. §15, §20; мутации I/J/K; живая проба §19 | | **4.2** пред-существующие чинятся по одной | ✅ сделано; **дважды углублено по ревью** | вердикт загрузчика разложен по СУБЪЕКТАМ (`membank/problems.go`), ключ сравнения — субъект. Первая редакция останавливалась на два шага раньше: субъект нёс РЕНДЕРИНГ, и правка `dst` делала чужую поломку «новой» (§29 п.2); и поломки ПОД загрузчиком были не видны, пока документ не грузился (§29 п.3). Мутации T, Y, AD (⚠ X снята вместе с правилом маски — §29 п.3, каталог её больше не несёт); пины `TestRepairingOneOfTwoBrokenTermsIsAccepted`, `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt`, `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS`; живая проба §19 | | **4.3** два ключа на один путь | ✅ **сделано ШИРЕ на одну проверку** | попарно различны четыре движковых пути (`project_db`, `glossary_seed`, `mined_delta`, `mined_rejects`) — `project_db == mined_delta` означало бы YAML поверх sqlite. Проверено на всех `book.yaml` репозитория и стенда (`find . -name book.yaml \| wc -l` → **22**): нарушителей ноль. Мутация M | | **4.4** гард флага на ПРИСУТСТВИЕ | ✅ сделано | `fs.Visit` (единственное, что отвечает на этот вопрос), оба гарда, плюс пустое значение у присутствующего флага — тоже отказ. Мутация N | | **4.5** кап документа + строка отказа полосой | ✅ сделано, ПЕРЕСМОТРЕНО по ревью | ДВА капа: 1 МиБ на чтение + **5 000 решений** на применение. Первая редакция капила только байты и утверждала, что этим ограничено ВРЕМЯ — опровергнуто замером (§29). Класс 14 у обоих. Хвост про `exit 2` — пингом. Мутации R, W | | **4.6** конверт путей потребителю | ✅ сделано, компромисс назван | `manifest --json` несёт тот же `artifacts{}`; ВЫВОДИТСЯ на каждом чтении, поэтому версия документа не двигалась. Компромисс вслух: манифест семантически про дерево глав, путь артефакта там гость. Мутация V | | **4.7** алиас судится по НАБОРУ | ✅ сделано; **углублено по §30 п.2** | проверка перенесена ПОСЛЕ свёртки и судит РЕЗУЛЬТАТ — именно поэтому два отказа (терм + его алиас) остаются законными: к концу вызова алиасом никто не владеет. ⚠ Первая редакция этой строки добавляла «своя поверхность проверяется по входному документу» — такого побега в коде НЕТ, он был заведён и снят как вредный (§30 п.2). Мутации P, AB | | **4.8** перевёрнутое окно глав | ✅ сделано | отказ с цитатой окна; открытая верхняя граница законна. Мутация O | | **4.9** честность про непогасший стоп | ✅ **исполнено фактом, не предсказанием** | `signature{map,surfaces,undecided}` по правилу самого прогона; `undecided>0` — факт, `undecided==0` — явно не обещание. Аргумент в §22. Мутация U | | **4.10** SIGTERM | ✅ сделано; сомнение записки СНЯТО замером; **довод в комментарии исправлен по ревью** | ctx проверяется дважды, стоп не пишет ничего, exit 5. Несущий довод — НЕ длина непрерываемого участка (первая редакция назвала её «≈20 с, два порядка внутри stop grace», и это было неверно в 15 раз), а то, что записи идут последними и атомарно: убитый где угодно до них процесс не записал ничего. Мутация S (пин целится именно во вторую проверку) | | **4.11** десять посадок, пин на каждую | ✅ 9 из 10 + доказательство по десятой | A–H, E1–E3 краснеют своими пинами; **G недостижима за первым гейтом — доказано, не предположено** (§17), вместо поведенческого пина запинен контракт функции | | **4.12** цепь в репозитории | ✅ сделано | `internal/pipeline/bankchain_test.go`, $0 на фейковом провайдере, ОДИН проект от стопа до погасшего стопа; форма фикстуры (2 терма = 1 approve + 1 decline) проверяется как ПРЕМИСА; ограничение синтетики названо в комментарии; карта подписи после погасшего стопа зафиксирована явно | | **4.13** девять мелочей | ✅ по каждой решение и довод | §21: починено 3, отклонено с аргументом 5, пингом 1 | | **4.14** довод отказа `--keys-file` | ✅ сделано | текст называет `redrive` (платный, свой `.env`), `backup`/`migrate` (деплой-шаги) и $0-читающие поимённо. Развилку про канал ключей `redrive` НЕ решал | | **4.15** три тавтологичных пина | ✅ сделано | вердикт `Preexisting` исполняется всеми четырьмя проверками (`TestAPreExistingFaultOfEveryKindStaysReportedAndUnblocking`); четвёртый оракул фаззера снят с тавтологии и стал НЕПОДВИЖНОЙ ТОЧКОЙ; `TestDecisionByBankID` сверяется с НЕЗАВИСИМО посчитанными константами + отдельный пин на инъективность, ради которой в `TermID` сделана длина-префикс | | **4.16** платных вызовов ноль | ✅ | `sha256` базы стенда не сдвинулся; рантайм по платному пути — PLAUSIBLE, как и ожидалось | | §5 батарея после каждой правки · посадки СВЕРХ списка · посадка называет ПАКЕТ · живая проба в КОПИИ · артефакт-находок | ✅ | 36 посадок (26 сверх списка §4.11), пакет пина ≠ пакет правки записан в каталоге, проба в копии, эта секция | | §5.1 последний абзац — план или обещание? · дифф `^func Test` исполнением · интервальная самоверификация субагентом · каждый клейм — с результатом инструмента ЭТОЙ сессии | ✅ | обещаний в отчёте нет: всё найденное закрыто в дереве; §27; ревью гонялось в СЕРЕДИНЕ работы против явных критериев; каждое число выше — с командой | | §6 три оси | ✅ | round-trip формата (§15, §20) · слепота гейта (§16, §17) · общность: в Go, которую я тронул, нет ни одного литерала пары/письменности/орфографии (проверено грепом по девяти файлам); нормализация — `strings.TrimSpace`, определённый Юникодом, а не скриптом | | §8 заявление = команда · §9 эхо до работы · §10 «что НЕ удалось» · §11 развилки | ✅ | §14–§27 · выдано до кода · §24 · §21–§23 | ## 27. Дифф `^func Test` — исполнением ``` $ git diff -U0 -- backend | grep -cE "^-func (Test|Fuzz)" → 0 # удалено: НОЛЬ $ git ls-files --others --exclude-standard -- backend | grep '_test.go$' \ | xargs grep -h '^func Test' | wc -l → 97 # пак оставил 59 $ ... | xargs grep -h '^func Fuzz' | wc -l → 3 # пак оставил 2 ``` **Добавлено дофиксом:** 38 тест-функций и 1 фаззер. ⚠ Первая редакция писала 94/35 — цифра была снята до правок §34, которые добавили ещё три пина. **Удалено: ноль** — и это проверено дважды: грепом по диффу (0 удалённых `func Test`) и поимённо по семнадцати пинам, которые отчёт пака называет по имени (каждый существует ровно один раз). **Тронуто (тело изменено, сигнатура — нет):** `cmd/tmctl/exitcontract_test.go` — `TestTheRefusalTableIsTotal` перестал держать ручной список и читает классы из ИСХОДНИКА обходом `GenDecl`/`ValueSpec` (⚠ не по `*ast.BasicLit` в `case`, как соседний гейт диспетча: значения `RefusalClass` объявлены КОНСТАНТАМИ, и копирование той техники воспроизвело бы её собственную слепоту — это её же пинг) · `internal/membank/decisions_test.go` — `TestDecisionByBankID` сверяется с независимо посчитанными константами · `internal/membank/decisions_fuzz_test.go` — оракул 4 переписан с тавтологии на неподвижную точку · `internal/pipeline/bankdecisions_test.go` и `statusartifacts_test.go` — сигнатура `ApplyBankDecisions` получила `context.Context`. **Новый файл:** `internal/pipeline/bankchain_test.go` (цепь §4.12). ## 28. Состав дофикса **Новое:** `internal/seed/normalize.go` (канонический вид документа) · `internal/membank/problems.go` (вердикт загрузчика по субъектам) · `cmd/tmmutate/` (харнесс мутаций + каталог посадок, `python3 -c "import json;print(len(json.load(open('cmd/tmmutate/mutations.json'))))"` → **36**) · `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, это единственная правка в самой батарее и она ничего не ослабляет) + семь тест-файлов. **Едут вместе с деревом и без них его нельзя принять:** `backend/docs/SEAM_FIX_PLAN.md` (записка-план §7 промта) · `backend/docs/SEAM_PACK_FINDINGS.md` (эта доказательная база) · `backend/docs/SEAM_PACK_PLAN.md` (план пака, не мой, лежит незакоммиченным с 24.08) · секция «Бэкенд» в `docs/PROGRESS.md` · `docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (в нём дописана записка предыдущей сессии, §32). ## 29. Ревью старшей моделью: три дефекта, каждый воспроизведён МНОЙ и починен Ревью запрашивалось по семи решениям, где заказ оставлял форму на мне. Оно согласилось с пятью, не согласилось с одним и нашло три вещи, которых я не видел. **Каждую я воспроизвёл сам прежде, чем чинить** — не потому что не доверяю, а потому что чужой результат, взятый на веру, ровно так же становится нормой. ### 1. Кап в БАЙТАХ не ограничивает ВРЕМЯ — мой довод был неверен Мой расчёт «1 МиБ ≈ 12 000 решений ≈ 20 с» брал ≈85 байт на решение из документа с заметками. Минимальное законное решение — `{"action":"decline","src":"t1"}`, ≈35 байт. **Пере-мерено мной, тот же метод:** | N минимальных `decline` | размер | 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 rejected: ['方源→Фан Юань II: gender "badvalue" is not one of male|female|neuter|hidden'] preexisting: ['方源→Фан Юань: gender "badvalue" is not one of male|female|neuter|hidden'] ``` Одна и та же поломка по обе стороны, под двумя разными рендерингами, и починить её через дверь нельзя вовсе. Это §4.2 на уровень ниже — то есть мой фикс §4.2 остановился на шаг раньше. **Починено:** `subjectOf(class, src, sense, since, until)` — субъект строится ТОЛЬКО из класса поломки и идентичности терма, никогда из поля, которое дверь переписывает. Четыре проверки получили субъект-несущие внутренние варианты, экспортированные `[]string`-обёртки не изменились (ни один чужой вызов не тронут). После фикса тот же вход: `exit=0`, `dst` записан, поломка в `preexisting`. Пин `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt`, мутация Y. ### 3. И то же самое ещё уровнем выше: поломки, которые ЗАГРУЗЧИК прячет Найдено мной при проверке п.2. Три проверки живут ПОД загрузчиком банка и на документе, который не грузится, не выполняются вовсе. Значит, как только решение чинит загружаемость, всё, что они впервые увидели, читается как новое — и владельца отвергают за починку того, что мешало всему. **Воспроизведено:** дельта с одной строкой, у которой и `dst: ""` (не грузится), и `gender: alsobad` (замаскировано) → починка `dst` даёт `exit=14` по gender. **Первая починка была ПРАВИЛОМ МАСКИ** — «если до вызова документ не грузился, а после грузится, эти поломки сообщаются и не отвергают ничего» — и она оказалась ХУЖЕ дефекта; как именно, см. §30 п.1. **Итоговая починка другая и без исключений:** загрузчик отдаёт построенные им записи ДАЖЕ когда отказывает, поэтому три проверки выполняются по обе стороны, и обычное сравнение по субъекту отвечает на оба случая само. Пины `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS` (обе половины), мутации X (снята вместе с правилом), AD. ### 4. Латентная потеря данных на пути nil-байтов Ревью нашло, я перепроверил чтением: если рендер ТЕКУЩЕГО документа падает и это пред-существующая поломка, она сообщается, а не отвергает (и это правильно) — но тогда `res.DeltaBytes == nil`, а обвязка считала `DeltaTouched && changedDoc(raw, nil, …)` → true и **записала бы файл нулевой длины поверх всех одобрений владельца**. Ревьюер не сумел до этого дойти (130+ подобранных значений после нормализации прошли round-trip), и я тоже; но `renderProved` существует именно потому, что множество ломающих значений не известно целиком, и незащищённый `nil` противоречит его собственной предпосылке. **Починено одной строкой:** nil-байты не пишутся никогда. Поведенческого пина нет по той же причине, по которой его нет у G, — вход недостижим; инвариант чистого слоя (`accepted ⇒ DeltaBytes != nil`) держит `FuzzDecisionFieldsThroughTheDoor`. ### 5. Три комментария несли неверные доводы — исправлены В этом проекте комментарий — контракт, по которому следующая сессия действует, поэтому неверный ДОВОД при верном коде опаснее опечатки. (а) `normalizedDecisions` объяснял себя тем, что «держит `note` записываемым» — проверяемо неверно: документ нормализуется ещё раз на выходе. Настоящая цена — ПРАВДИВОСТЬ ОТЧЁТА: без него повтор решения с пробельным текстом отвечает `applied`, пока байтовый гейт отвечает `changed: false`. (б) Довод про SIGTERM опирался на длину непрерываемого участка с числом, неверным в 15 раз; несущий довод — атомарность и порядок записей. (в) «Путь `yaml.Node` закрыт» преувеличивало: закрыт `Encode`, а РУЧНАЯ сборка узла с явным стилем работает (`encode.go:557-570`) — она отвергнута по цене (второй рендерер всей схемы), а не по невозможности. ### 6. Ещё две правки по ревью **Нечитаемая карта подписи** возвращалась как `{surfaces:0, undecided:0}` — побайтно то же, что «всё решено», единственный ответ, который это поле не должно давать по случайности. Заведено поле `unreadable`; пин `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided`, мутация Z. **У `tmmutate` опасное значение было ДЕФОЛТНЫМ** (`-root .` правит живое дерево). Флаг стал обязательным; в шапке названо и то, что отпечаток снимается на старте и потому сертифицирует только дерево, которое было чистым в тот момент. ### С чем ревью НЕ согласилось и что я оставил как есть Ничего: единственное «DISAGREE» — кап (п.1) — исполнено. Пять решений подтверждены, у двух из них ревью дало довод СИЛЬНЕЕ моего, и я заменил довод в комментарии: у конверта путей на манифесте — что бамп версии выбросил бы все сохранённые сайдкары и пере-нарезал каждую книгу ради аддитивного поля; у SIGTERM — атомарность вместо длины. ## 30. Интервальная самоверификация субагентом: ещё пять дефектов, все воспроизведены и починены Ревью гонялось В СЕРЕДИНЕ работы против семи ЯВНЫХ критериев (§5.1 промта), read-only, в копии. Дерево двигалось под ним трижды, поэтому его вердикты пере-исполнены на финальном снимке. Каждый дефект я воспроизвёл сам, своим прогоном, прежде чем чинить. ### 1. ⛔ Правило маски проглатывало ливлок, который вносил САМ вызов Моё же исправление §29 п.3 оказалось хуже дефекта. **Воспроизведено мной:** дельта = битая строка (`dst: ""`) + `Hero→Odin` с алиасом `Shared`; решения = починить битую строку + одобрить `Shared→Dva`. ``` маска снята: rejected=0 accepted=2 bytes=true preexisting=[firing key "shared" is shared by different terms "Hero"→"Odin" и "Shared"→"Dva" … (livelock…)] контроль (документ грузится): rejected=1, та же строка — ОТКАЗ ``` То есть дверь диагностировала детерминированный ливлок матчера, **записала документ** и отчиталась `exit 0`, назвав СВОЮ поломку чужой. Комментарий правила («состояние после такого вызова никогда не хуже, чем до») был ложью ровно здесь: до — банк не грузился (громкий стоп), после — грузится и инжектит ливлок. И два опубликованных поля (`Preexisting`, `preexisting_problems`, оба обещающие «этот вызов их не вызывал») становились ложными утверждениями. **Починено иначе и без исключений:** правило маски снято целиком; вместо него `ParseBankSeed` возвращает записи, которые он ПОСТРОИЛ, даже когда отказывает. Тогда три проверки под загрузчиком выполняются и на «до», и на «после», сравнение по субъекту работает как везде, и оба случая решаются одним правилом: починка загружаемости больше не отвергается (поломка видна с обеих сторон), а внесённый ливлок отвергается (его субъекта на «до» нет). После фикса тот же вход: `rejected=1`, байты не выданы. Пины — обе половины `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS`, мутация AD. ### 2. Побег §4.7 принимал инертный отказ `ownsSurface(in.Delta, …)` пропускал проверку, когда поверхность была термом дельты сама по себе. **Воспроизведено:** дельта `Nick→Nik` и `Hero→Nik2 (alias Nick)`, `decline Nick` → `accepted=1`, строка `Nick` снята, а `Hero` ПРОДОЛЖАЕТ фарить `Nick`. Ровно тот вред, ради которого §4.7 и заказан. Замерено дополнительно: во всех остальных входах побег — no-op (после `dropTerms` алиасом никто не владеет), то есть он менял ответ только во вредную сторону, и зеркальный случай, ради которого он якобы существовал (отказ терму + отказ его алиасу), работает без него. **Починено:** побег снят. Пин `TestADeclineIsRefusedWhenTheSurfaceGOESONFiringAsAnotherTermsAlias`, мутация AB. ### 3. И то же правило ОТВЕРГАЛО законный отказ, с доводом, который неверен `aliasOwner` не смотрел на СТАТУС. **Воспроизведено:** дельта `Hero (status: auto, alias Nick)`, `decline Nick` → отказ: «"Nick" is an ALIAS of the approved term "Hero"→"" … already excluded from proposals». Обе половины довода ложны: строка не approved, а `unsignedEngineSurfaces` выбрасывает mined строки со статусом ≠ `approved`, то есть её поверхности из предложений НЕ исключены и отказ как раз осмыслен. `status: auto` в дельте — не угол: это ровно состояние, о котором первая мина формата строки 199. **Починено:** `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. **Фаззер двери возвращался рано на отказе** и потому не видел бы возврата блокер-класса. Оракул отказа усилен: отказ обязан не выдать байтов, не отчитаться о принятых решениях и не изменить ни один документ — то есть та же слабость, за которую §4.15 критикует старый оракул 4, снята и здесь. ### Что ревью подтвердило исполнением (не переделывал) Нормализация идемпотентна на 50-строчном адверсариальном корпусе и КОПИРУЕТ (проверено мутацией выхода при неподвижном входе) · ложного провала `renderProved` найти не удалось (400 000 случайных строк по алфавиту управляющих/пунктуации/CJK, невалидный UTF-8, NUL, C0/C1, одиночные суррогаты) · оба капа не off-by-one · после второй проверки контекста ничего не пишется, `defer lock.Release()` стоит сразу за захватом · общности не нарушено ни одной новой строкой · `TestDecisionByBankID` и пин на инъективность действительно независимы (все три константы пере-считаны в Python по спецификации из комментария `TermID`) · `SeedProblems.Error()` побайтно равен прежнему склеенному сообщению. ⚠ **Унаследованное замечание, не дефект пака:** всё, на чём эта дверь ключуется, проходит через `internal/text/norm.go`, где таблица `trad2simp` и ветка катакана→хирагана живут В GO, а не в langpack'е. Пара, которой в репо нет, деградирует до NFKC+lowercase — то есть не блокирована, но фолд Go-резидентный. Пингом, не правкой: это шире двери. ## 31. Батареи после всех правок — и одна правка в самой батарее | команда | результат | |---|---| | `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 по построению, строка **123**. Больше ничего | ⚠ **Обе строки сняты ПОСЛЕ рефакторинга §33 и после всех правок по воркфлоу-ревью §34** — то есть описывают дерево, которое передаётся. **ОДНО утверждение о детерминизме, чтобы приёмка не гадала.** Два store-теста (`TestKillMinus9LosesAtMostOneCall`, `TestTheSeamIsNotChargedToTheStoreOperationBudget`) ведут себя так: в ПОЛНОЙ батарее на ненагруженной машине они проходят (строка выше, `EXIT=0`); под нагрузкой параллельных сессий падают; а запущенные ИЗОЛИРОВАННО через `-run` падают **детерминированно и на обеих сторонах A/B** — они сознательно сжимают `opTimeout`, и в одиночку сжатого времени не хватает. То есть: красное в этих двух — ожидаемо и НЕ пак (доказано A/B ниже); зелёное в них — тоже нормальный исход; красное в любом ДРУГОМ тесте — регрессия. **Корпус под строгим загрузчиком — 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 ``` Изолированный прогон обеих сторон даёт БУКВАЛЬНО одинаковый вывод — тот же тест, то же сообщение, тот же номер миграции: ``` --- FAIL: TestTheSeamIsNotChargedToTheStoreOperationBudget (0.85s) migrate_test.go:41: store: apply migration 9: context deadline exceeded ``` То есть на дереве, которого пак не касался, этот тест в изоляции падает ДЕТЕРМИНИРОВАННО: он сознательно сжимает `opTimeout`, и в одиночку сжатого времени не хватает. Атрибуция закрыта — ни пак, ни дофикс тут ни при чём, и это МОЙ замер, а не унаследованное утверждение. Тесты не тронуты (D39.121). `TestManifestOfAnOlderDocumentVersionIsRefused` в стенд-батарее упал ЧИСТО от нагрузки: 5 из 5 зелёных на обеих сторонах. ### ⚠ Одна правка В САМОЙ батарее, и вот замер, из-за которого она сделана `Makefile`: `go test ./... -race -count=1` → `… -timeout=20m` (в обеих целях). **Почему.** Go по умолчанию даёт пакету 10 минут. Замерено: `internal/pipeline` под `-race` идёт **557.7 с** (`/usr/bin/time`, машина с ТРЕМЯ параллельными агент-сессиями, load average 3–12 в моменты замера) — ⚠ первая редакция называла это «на незагруженной машине», и слово было НЕВЕРНО: нагрузка была, что и вскрыла приёмка, намерив 92.8/97.6 с на действительно свободной машине. Оба замера настоящие — разброс ×6 даёт сама нагрузка, и под ней два прогона батареи подряд закончились `panic: test timed out after 10m0s` по всему пакету. Это читается как падение и падением не является. Довод в `Makefile` переписан на диапазон с условиями, без слова idle и без привязки к машине (заказ §8 промта FIX2 закрыт этой правкой — оркестратор уведомлён). **Это НЕ подгонка гейта под зелень:** таймаут — не утверждение. Ни один тест не ослаблен, ни один не удалён; те же самые тесты просто доходят до конца. Пакет, который действительно ЗАВИС, всё так же падает — на двадцатой минуте вместо десятой. **Чего я НЕ доказал:** сколько из 557.7 с добавил дофикс. База в начале сессии на нетронутом дереве пака дала 474.2 с, но внешняя нагрузка между двумя замерами не контролировалась, поэтому разницу я НЕ атрибутирую. Что измерено точно: **все одиннадцать моих новых тестов этого пакета стоят 10.5 с** под `-race` из 557.7 с, то есть 1.9 % — не они. ## 32. Записка предыдущей сессии — попунктно, включая одно ОПРОВЕРЖЕНИЕ Записка живёт в хвосте `docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (её дописала бэкенд-сессия пака перед компактом своего контекста). Прошёл по ней целиком; она уехала в работу почти вся. | что говорила записка | что вышло | |---|---| | «§4.1/§4.2/§4.4/§4.7/§4.8 я перепроверил сам, не трать время» | всё равно воспроизвёл сам — три из пяти живьём на копии книги стенда. Совпало | | **«промт ШИРЕ замера: `dst` и `sense` с ведущим `\n` round-trip НЕ ломают»** | **ОПРОВЕРГНУТО моим замером (§15): ломают.** Ломается любое строковое поле обеих схем. Прав был ПРОМТ, а не записка — и если бы я поверил записке, фикс был бы заплаткой на одно поле | | «харнесс мутаций умер вместе с сессией, пиши заново, сразу с отпечатком целей» | написан заново, **положен в репозиторий** с отпечатком до и после. Отпечатка оказалось мало — см. §25 | | «одиночный прогон — не замер» | применено: перепроверка одного подозрительного `RED` изолированным прогоном вскрыла дефект самого харнесса | | «субагенты пишут в твоё дерево, сверяйся `git ls-files --others`» | обоим ревьюерам запрещено писать, сверено в конце; свои два пробных файла удалил и проверил отсутствие | | «красное в `internal/store` — почти наверняка не ты, но ПРОВЕРЬ ЗАМЕРОМ против HEAD» | сделан A/B против `git worktree` на HEAD (§31). Проверка была не лишней: она дала более сильный результат, чем ожидала записка | | сомнение §4.9 «дверь этого не знает» | верно про предсказание — и оказалось, что дверь знает ДРУГОЕ и достаточное (§22) | | сомнение §4.10 «кап может сделать проблему несуществующей — замерь ПОСЛЕ капа» | замерил: ~11 с, механизм с носителем. Но несущий довод другой — атомарность, а не длительность (§29 п.5) | | сомнение §4.6 «сделал бы и назвал компромисс вслух» | так и сделано | | сомнение §4.13 «`note` нельзя очистить — реши осознанно» | решено; попутно нашлась несогласованность, о которой записка не знала: `approve` заметку хранил, `decline` СТИРАЛ | | «не наследуй ни моё, ни приёмочное число памяти, пере-мерь» | пере-мерено; сошлось с приёмкой, не с паком (§18) | | «сначала посади мутацию, потом пиши пин» | применено; дважды поймало ВАКУУМНЫЕ пины моего же авторства (§16) | | онбординг в код: два слоя, карта пинов, ловушки стенда (три файла с мёртвыми путями, схема v11, нет jieba) | всё три ловушки сработали ровно как описано; без записки это стоило бы нескольких заходов вслепую | **Вывод для оркестратора:** записка была полезной и один раз ВРЕДНОЙ. Она не помечена как «проверено исполнением» построчно, а её самый уверенный абзац оказался неверным. Полезен формат, а не авторитет. ## 33. Рефакторинг по итогам вопроса о качестве Go-кода Замер, а не вкус — но замер пришлось делать ДВАЖДЫ, и первый был неверен. ⚠ **Первая редакция этой секции утверждала, что две точки входа были «самыми длинными функциями своих пакетов», а потолок пакета — 75.** И то и другое НЕВЕРНО: правило, которым я считал, брало строки от `^func` до следующего `^func` вместо тела функции, и на методах давало мусор. По правилу, считающему ТЕЛО (от `^func` до первой строки ровно `}`), максимумы пакетов совсем другие: `internal/pipeline` — 218 (`Status`), `internal/membank` — 105 (`loadVoiceSections`). Никакой «нормы репозитория в 75» нет. **Что верно по правильному замеру и чего хватает для довода:** две точки входа были **92 и 90** строк кода при том, что следующая по длине функция в тех же двух файлах — **42**. То есть вход был вдвое длиннее всего остального в самой двери, которую он и представляет. Обе разобраны на именованные фазы — чистое извлечение, без изменения поведения: ``` ApplyDecisions → resolveAll · refuseSeedConflicts · foldAccepted · refuseInertDeclines · refuseNewFaults ApplyBankDecisions → openDecisionRequest · readBookState · decisionReport · changedFiles · writeDecisionFiles ``` Тело `ApplyDecisions` стало последовательностью из пяти судящих фаз, из которых **ровно одна** что-то меняет (`foldAccepted`) — это и есть контракт двери, теперь видимый в самом коде, а не только в комментарии. Самая длинная функция обоих файлов после: **44** строки (`signatureState`), по команде ниже. ⚠ Две прежние редакции писали здесь 45 и 48 — первая устарела вместе с правками §34, вторая считалась неверным правилом. **Команда** (правило §14 «каждое число — с командой»): непустые строки ТЕЛА, от `^func` до первой строки ровно `}`, минус начинающиеся с `//`: ``` python3 - <<'EOF' import pathlib,re,glob def funcs(f): src=pathlib.Path(f).read_text().splitlines(); out=[] for i,l in enumerate(src): if not l.startswith("func "): continue j=i+1 while j.lock`, пишутся `mined_delta`/`mined_rejects`, которые `book.yaml` может направить куда угодно. Две книги с РАЗНЫМИ `project_db` и ОДНИМ delta друг друга не исключают: ревью продемонстрировало гонку на 1500 одобрений с каждой стороны — одна сторона потеряла всё, `exit 0`, отчёт «применено». Внутрикнижную версию этого §4.3 запрещает; межкнижная законна сегодня. **Выбор между вторым арбитром и отказом при загрузке трогает ратифицированный закон п.2 — это не решение одного пака.** В коде теперь стоит явное «арбитр покрывает проект, а не пути», а не молчаливое допущение. ⚠ В корпусе нарушителей нет: все объявленные пути лежат внутри своей книги (мёртвый префикс `/home/ubuntu/` — это переезд машины, D39.157, а не чужой каталог). - **Карта подписи пишется обычным `os.WriteFile`**, мимо атомарной конвенции сайдкаров этого же пакета — пред-существующее, не мой код. ### Что ревью ПОДТВЕРДИЛО (и это тоже результат) **Рефакторинг §33 — эквивалентен**: порядок фаз, мутация `rs[i].ok` через слайс и место отката проверены отдельной полосой, расхождений нет. **Платформа не ломается**: аддитивные поля отчёта, конверт `artifacts{}` на манифесте без бампа версии, код 14 и размерные капы — прослежены до её кода, дефектов нет. Исправлены при этом два комментария, утверждавших про чужую зону неверное: код 5 платформа пишет как `stopped` (и только при зафиксированном намерении остановиться, иначе `failed`) — не `paused`; и отчёт печатается НЕ в каждом исходе — три отказа (10, 14 до чтения книги, 12) не печатают ничего. ### Правки в самих документах Ревью нашло семь мест, где документ нарушал собственное правило «каждое число — с командой»: «≈13 % по памяти» противоречило трём числам рядом (диапазоны перекрываются — снято как НЕ измеренное) · размер каталога назывался и 24, и 32 · §26 ссылалась на снятую мутацию X · §28 не перечисляла три документа, без которых дерево нельзя принять · «20 из 24» не воспроизводится нынешним каталогом (помечено как исторический замер) · дайджест дерева снят как невоспроизводимая константа · строка про `aliasOwner` описывала проверку, которой в коде нет. Все семь исправлены. ## 35. Проверка ЗАКРЫТИЯ — отдельным воркфлоу, и она нашла, что закрыто не всё На вопрос «все замечания исправлены?» я не стал отвечать по памяти: четыре независимых агента прошли по всем 26 пережившим находкам §34 и по живому дереву решали КАЖДУЮ — закрыта / не закрыта / частично / отклонена осознанно. **Из 28 проверенных пунктов закрытыми оказались 16, осознанно отклонёнными 2, частично 7 и НЕ закрытыми 3.** Что доделано по этой проверке: | остаток | что было не так | как закрыто | |---|---|---| | **F4** | остановленный вызов перестал врать в `mode`/`changed`, но `accepted[]` по-прежнему нёс `state:"applied"` — а это ровно то поле, которое потребитель перебирает | список очищается, как и на пути отказа; пин расширен, мутация AG бьёт по всем трём полям | | **F16** | новая формулировка сама сделала исчерпывающее заявление («три отказа») и промахнулась: `schema_mismatch` (13) тоже печатает пустоту | перечислены все четыре плюс два не-отказных случая (5 и 1) | | **F17** | тот же ложный клейм про платформу выжил в ЭТОМ ЖЕ паке, в `bankdecisions_test.go`; плюс ссылка `:755` на строку `:756` | оба исправлены; `grep paused` по движку — ложных клеймов не осталось | | **§33** | ⛔ «две точки входа — самые длинные функции своих пакетов, потолок пакета 75» **НЕВЕРНО**: правило считало не тела функций. Настоящие максимумы — 218 (`Status`) и 105 (`loadVoiceSections`) | утверждение снято, довод пере-сформулирован на том, что верно (92/90 против 42 у следующей функции тех же двух файлов), приведена команда | | **§16** | секция, которая ПРЕДСТАВЛЯЕТ каталог, называла его размер неверно (32/31 при 36 записях) и не несла четырёх строк §34 | пере-считано командой, строки добавлены | | **§18** | в списке «пер-решенческих помощников» стояла `ownsSurface`, которой в коде нет — довод Θ(N²) частично опирался на несуществующую функцию | заменена на реально существующую `deltaHoldsSurface` | | **§26** | строка §4.7 в одном предложении противоречила и себе, и коду, и §21 | пере-написана по коду | | **§31** | два разных утверждения о детерминизме store-флейков в одной секции (`EXIT=0` против `5×FAIL`) | одно: в полной батарее на ненагруженной машине зелены, под нагрузкой падают, ИЗОЛИРОВАННО падают детерминированно на обеих сторонах A/B | | **§15 ↔ §18** | две несогласованные тройки секунд на 10 000 решений | сказано, что абсолютные числа между секциями не складываются, и что ≈5 % выведены только из пары, снятой подряд | | **§27** | 94 тест-функции / «добавлено 35» — снято ДО правок §34 | 97 / 38, командой | | **§31, корпус** | строка «45 из 45» стояла ВНЕ таблицы (сломанный markdown) и её вторая половина не несла команды | вынесена блоком с обеими командами | | **журнал** | пинг пака «1.6с на 5 000 / 9.1с на 20 000» — датум, которым платформенный пак будет мерить таймаут, расходится с моим замером в 3–16 раз; пинг «кап — однострочник при лендинге» уже сделан; четыре пинга жили только здесь | в журнал добавлен блок «что в записи пака устарело» и четыре недостающих пинга | **Что осталось ОСОЗНАННО не сделанным** — два пункта, оба вне полномочий пака: межкнижная гонка за один `mined_delta` (трогает ратифицированный закон п.2) и запись карты подписи обычным `os.WriteFile` (пред-существующее, не мой код). ⚠ **Вывод, который стоит унести дальше самого пака.** Это было ТРЕТЬЕ ревью подряд, и каждое находило настоящие дефекты, которых не видели предыдущие: одиночный старший ревьюер — три, интервальное — пять, воркфлоу — двадцать шесть, проверка закрытия — ещё десять остатков. **Ни одно «все замечания исправлены» в этой сессии не оказалось верным при проверке.** Вывод не «ревью бесполезны», а обратный: закрытие надо ПРОВЕРЯТЬ отдельно от починки, и не тем, кто чинил. --- # ДОФИКС-2 (промт BACKEND_SEAM_FIX2_SESSION_PROMPT.md, 13 пунктов §4) — секции 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) # ⚠ первая редакция этой строки писала 115: снималась ДО реверс-пина §42/4б и якоря §4.4 — поймано # проверкой закрытия, пере-снято после них ``` Переименовано (не удалено): `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 поимённо; ⚠ первая редакция писала «шесть» — сумма по FX1/FX2/FX4 выдана за вывод одной посадки, поймано воркфлоу-ревью, §45). ## 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-резюм в обеих семантиках. ## 42. Интервальная самоверификация субагентом (§5.1): четыре находки, все minor, все закрыты или аргументированы Ревью шло против десяти явных критериев, адверсариально, с исполнением (точечные тесты + посадки в своей копии). Код по всем десяти сошёлся; находки — класс «довод/покрытие», ровно тот, что предыдущая сессия назвала своим худшим. | # | находка | диспозиция | |---|---|---| | 1 | докстрока `renderSignatureStop` учила отменённой механике «промотируй/отклони, потом re-run» при верном печатаемом тексте | закрыта (переписана на модель флажка; печатаемый баннер был верен и не менялся) | | 2 | комментарий v16 в `migrate.go` формулировал предикат как «a surface outside this set» — читается как «любая поверхность вне», тогда как код стреляет только на кластере, чья ВСЯ поверхность вне памяти; реализация «по комментарию» вернула бы ре-стоп при росте кластера | закрыта («whole surface set outside», с named-ценой ошибки) | | 3 | `bankapply.go`: «stop before the lock (5)» — безотчётное stop-окно шире (пост-локовая проверка `ctx.Err()` до `ApplyDecisions` тоже без отчёта) | закрыта («before the VERDICT exists», обе проверки названы; поведение верно — печатать нечего, вердикта ещё нет) | | 4а | грань домена §4.1 «предикат по итоговой карте, не по пулу ДО потолка» не краснится ни одной посадкой: вечный стоп невоспроизводим в фикстурах (2 терма при потолке 200), а одно-строчной мутации не существует — карта, предикат и память читают ОДИН список `mined` после потолков, расхождение требует сознательной перестройки | **аргументированный не-пин** по прецеденту G: сама структура (один список) и есть защита; записано здесь, чтобы следующая сессия не считала свип дырявым | | 4б | новизна из ОБРАТНОЙ секции не была покрыта: в стоп-фикстурах terminology-гейт выключен и reverse пуста | закрыта поведенческим пином `TestNoveltyFromTheReverseSectionAloneTripsTheFlag`: run 1 с пред-отклонённой banknote-only поверхностью (карта без неё), run 2 со снятым отказом — стоп обязан сработать на новизне, пришедшей ТОЛЬКО через обратную дверь | **Что ревью подтвердило исполнением (не переделывал):** предикат/карта/память — один список после обеих секций и потолков (`mining.go`, file:line в его отчёте) · все шесть исходов отчёта проходят через `finishReport`; исходные поля присваиваются ровно в ДВУХ конструкторах — `finishReport` и до-локовом `capRefusalReport` (у капа `bookState` ещё не существует; проверка закрытия поймала, что первая редакция этой строки говорила «нет нигде» — категорика была шире правды) · `TestTheRefusalTableIsTotal` невакуумен для класса 15 (AST-словарь + сверка длины таблицы) · retired-ключи ловятся полями, а не голым strict-decode · конверт карты читают все движковые читатели, `DecodeFile` на карту не наведён · дверь память не читает · в новом Go-коде ветвлений по паре/книге нет. ## 43. Попунктный чек §4 — все тринадцать Комплектность сверена механически: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX2_SESSION_PROMPT.md` → **13**. | пункт | статус | чем закрыт | |---|---|---| | **4.1** память «предъявленного стопом» | ✅ | store v16 `bank_stop_presented` (плоское множество поверхностей кластеров — Src+Aliases, `INSERT OR IGNORE`, без обрезки); предикат `hasUnpresentedCluster` — пересечение кластера с памятью, домен — ИТОГОВАЯ карта после потолков (обе секции); двусторонний остаток записан пином. Цена миграции названа (§37). Пины: цепь Б, `stopflag_test.go`, `stopmemory_test.go`, реверс-новизна; мутации FX1–FX3 | | **4.2** порядок и момент записи | ✅ | карта (атомарно) → память, в момент СТОПА; отказ памяти НЕ валит стоп (ERROR-строка, названо §37); «подтверди карту» НЕ заведено; draft-only ветка названа. Развилка карта/SQLite-транзакция решена явно (направление потерь — доброкачественный повторный стоп) | | **4.3** авто-провод при поднятом флаге | ✅ | «останавливаться ли» и «проводить ли» — два решения; авто-провод на ЛЮБОМ неостановившемся прогоне; вторая половина названа: остановившийся прогон авто-банк НЕ пишет (решение с доводом, §37). Мутация FX4; пины в WHATSurvives и цепи Б | | **4.4** хвост за потолком | ✅ не лечился | пере-замер своей командой (2354/209/11/198, §38) — числа промта без первоисточника не цитируются; фильтр карты НЕ заведён; пинг в журнале | | **4.5** единая точка фиксации | ✅ | вердикт (`decisionVerdict`) / исход (`finishReport` — единственный конструктор, свитч по `applyOutcome`, шесть исходов); «записано» — пост-состояние ПЕРЕЧИТАННЫХ файлов; пин `TestEveryOutcomeStampsItsOwnMode` + греп-инвариант (присваиваний вне конструктора нет — подтверждено интервальным ревью). Вердиктная половина сохранена (сильная форма отвергнута по букве промта) | | **4.6** `signature.undecided` | ✅ | пере-документирован: НЕ отвечает на вопрос о стопе (в обе стороны), информационный счётчик для экрана подписи; память дверь НЕ читает (решение с доводом: ответ требовал бы следующего намайненного набора, которого нет ни у кого за $0); поле не удалено | | **4.7** полоса в два яруса | ✅ | пол «нет отката, повтор безопасен»; клауза «ничего не произошло» — у пяти исходных классов; гардрейл «разрушительное — только на классе» буквой; класс `write_incomplete` (exit 15) один на оба исхода, различие — `written_*`; отказ по обоим капам печатает отчёт (контракт сведён к одному носителю). Мутации FX18–FX20 | | **4.8** подготовка обоих файлов | ✅ | stage обоих (write+sync+chmod) → два rename → fsync каталога; заявлено НЕ шире сделанного (атомарность против среды, не против SIGKILL — довод в `writeDecisionFiles`, остаток в §41); отказ синка — класс 15 с оговоркой долговечности. Мутации FX16–FX17, пины staged/unsyncable | | **4.9** Clean + конвенция | ✅ | (а) `filepath.Clean` абсолютных в resolve, репро приёмки закрыто пином; (б) оба ключа СНЯТЫ retired-отказом с лечением в тексте (прецедент `adult:`), пути — чистая функция каталога и book_id: МЕЖКНИЖНАЯ гонка (⛔-пинг прошлого пака) снята; воркфлоу-ревью показало ОСТАТОК той же оси — второй конфиг ТОЙ ЖЕ книги (один каталог, один book_id, другой объявленный project_db) пишет те же файлы под другим локом; остаток — пинг, не решение пака (§45). Мутации FX12–FX13 | | **4.10** карта: атомарность и конверт | ✅ | `writeFileAtomic` + конверт `map_version`/`map_id` (sha256 содержимого), рендер ДОКАЗЫВАЕТ round-trip (`RenderSignatureMap` через `renderProved`); легаси-карта читается; карта НЕ накопитель (слияния нет). Мутации FX14–FX15; живьём — инод-пин | | **4.11** слепые пятна | ✅ | все шесть строк таблицы закрыты посадкой+пином (normText-класс · субъект-класс · субъект-окно · абсолютность путей отчёта двери · границы обоих капов · граница окна); пин семантики §4.1 — цепь Б + два переписанных харнесс-теста (возврат старой формулы краснит три теста, вывод tmmutate по FX2 поимённо; «шесть» первой редакции — ошибка, §45). Вакуумный пин пойман посадкой ДО записи (§38) | | **4.12** наследие — списком | ✅ | шапка mining.go · комментарий у loadMinedRejects · unsignedEngineSurfaces · docstring runBankMiningStop · сообщение стопа · renderSignatureStop + два стейл-хинта render.go · help `--verify-bank` · WaveSignatureStop/waverun · комментарий v16; новое состояние «дельта непустая, нового нет» — явная WARN-строка; цепной тест переписан (замена названа: `TestTheStopIsAFlagNotAGateOnDecisions`); пинги с ПОРЯДКОМ и миграционная нота — в журнале | | **4.13** платных вызовов ноль | ✅ | фейковый провайдер + харнесс + копия стенда; sha256 стендовой базы не сдвинулся; рантайм по платному пути — PLAUSIBLE; «выигрыш по чекпоинтам» НЕ заявлен (механизма нет, §41) | | §5 батарея/каталог/проба/артефакт | ✅ | батарея после каждой правки; `make battery` EXIT=0; стенд-батарея красна ровно на jieba-парити; каталог 36→56, `NOTHING` роняет гейт (свойство не тронуто); живая проба §39; эта база | | §6 три оси | ✅ | семантика флажка — прогонами движка (цепи, харнесс), без кода платформы · точка фиксации — конструктор + греп + пин исходов · слепота гейта — 56 посадок / 0 неожиданных | | §7–§11 | ✅ | план до кода (`SEAM_FIX2_PLAN.md`, механическая сверка 13) · эхо до работы · «что НЕ удалось» §41 · развилки решались по букве промта, интерпретаций против него нет | ## 44. Проверка ЗАКРЫТИЯ — отдельным агентом, не чинившим (§5.1) Прогнана по презумпции «каждое заявленное закрытие ложно, пока не увидено исполнением»: все 13 строк §43 и все 4 диспозиции §42 подтверждены в дереве (13 точечных тестовых прогонов + 3 полных пакета — зелёные; носители по file:line; реверс-пин признан невакуумным с названным механизмом падения). Найдено ПЯТЬ расхождений — традиция «ни одно "всё исправлено" не выдерживает проверки» держится и на этой сессии: | # | находка | диспозиция | |---|---|---| | 1 | счётчик §40 «115» устарел на единицу (реверс-пин добавлен после замера) | пере-снят ПОСЛЕ всех правок: **117** (+ якорь §4.4); поправка с провенансом в §40 | | 2 | untracked-бинарь `backend/tmctl` (20 МБ, след сессии) | удалён | | 3 | клейм §42 «присваиваний вне конструктора нет» буквально ложен: `capRefusalReport` — второй, до-локовый конструктор | формулировка исправлена в §42 (код законен, документирован, запинен — расходилась категорика, не закрытие) | | 4 | пере-замер §4.4 жил только в скратчпаде — тот же класс «числа без первоисточника», за который промт корил 2110/… | проб положен в дерево постоянным якорем-пином `internal/miner/emissioncap_test.go`: утверждает текущее поведение, краснеет в день сдвига потолка/порядка фильтров (прецедент 0f4c556); §38 поправлен | | 5 | untracked вне backend/+docs/: `eval/dovodka/*`, `platform/docs/p8-review/*` | НЕ этого пака (mtime 24.08, параллельные зоны — платформенный P8 назван промтом законной параллелью); чужое не трогаю, оркестратору видно из этого же отчёта | После правок: батарея пакетов miner/pipeline/membank/config/seed/store — зелёная; каталог не менялся (56 записей — финальный прогон «0 неожиданных» шёл на дереве ДО пунктов 1–4, ни один из которых не трогает код вне нового тест-файла miner). ## 45. Воркфлоу-ревью: шесть полос × три скептика — 23 сырых находки, 20 выживших, 16 уникальных, все закрыты Форма: шесть параллельных полос-искателей под антипаттерны проекта (неверные доводы при верном коде · вакуумные пины · семантика флажка глубоким чтением · дверь/отчёт глубоким чтением · общность движка · шов/контракт), каждая находка — через трёх адверсариальных скептиков с разными линзами (корректность трассой · достижимость · «не принятый ли уже остаток»), выживание — большинство; 75 агентов, скептики убили 3, дубли между полосами — 4. Полосы отдельно подтвердили 75 позиций «проверено, сходится». **Кодовые (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 major + 10 minor), все закрыты:** - **«FX2 краснит шесть тестов»** (§40/§43, дважды) — ложь, приписанная инструменту: краснят РОВНО ТРИ (`TestTheStopIsAFlag…`, `TestMiningStopWHATSurvivesResume`, `TestTerminologistResumeIsFree`); «шесть» было суммой по FX1/FX2/FX4, выданной за вывод одной посадки — нарушение §8 в самой острой форме (число рядом с командой, которая его опровергает). Оба места исправлены с провенансом ошибки. - **Якорь-пин §4.4 НЕ краснел при сдвиге потолка** — премиссы не фиксировали значение 200 (150/300/400 проходили зелёными), вопреки трём носителям клейма. Значение потолка теперь запинено премиссой с текстом, ведущим двигающего к пингу; проверено: смена 200 → красный. - **«pre-lock check» в `bankapply.go` не существовал** — обе проверки контекста пост-локовые; переписано на правду (пост-чтение — без отчёта, пред-запись — С отчётом; стоп раньше первой проверки вообще не наблюдается). ⚠ Это была моя правка ПО находке интервального ревью — исправление само внесло ложь; §42 п.3 диспозиция скорректирована этим пунктом. - **Шапка полосы в `cmd/tmctl/main.go` осталась вторым носителем старого пола** («nothing was written»), который класс 15 опровергает — приведена к двух-ярусной букве, «nothing was written» перенесено в клаузы классов. - **Довод арбитра сужен**: «межкнижная гонка недостижима по построению» была шире правды — остался второй конфиг ТОЙ ЖЕ книги (один каталог/book_id, другой объявленный `project_db`): пишет те же файлы под другим локом, мимо flock живого прогона (ревью воспроизвело живьём). Межкнижная версия снята; остаток — пинг в журнале (второй арбитр/отказ при загрузке — закон п.2, решение не паку); комментарий `bankdecisions.go`, §43 и журнальная запись исправлены. - Minor-доводы, все закрыты: клауза «nothing happened at all» классов 10–14 сужена до «состояния книги» (пред-полётный бэкап translate пишется ДО отказа 12/13 — теперь оговорён) · v16-комментарий migrate.go завышал цену контрфактуала («re-stop on every run» → один лишний стоп на рост кластера) · draft-only прогон с флагом печатал «nothing in the delta is new to it», хотя новизна не вычислялась — своя строка · баннер стопа выдавал размер карты за число НОВЫХ термов (`(N term(s) in the map)`, `WaveSignatureStop.Error` переписан; поимённой разметки новизны в карте нет — продуктовая поверхность, не этот пак) · `bankexport.go` учил отменённую механику «promote/decline в файлы» (→ дверь) · стейл `:485` в render.go (→ по имени) · док `addKeyed` «only a record's position» (ложь — статусы и рендеринги тоже) · кап-отчёт: `preexisting_problems: []` ≠ «проверено чисто» — оговорено на поле · фикстура нормализации в `stopflag_test.go` была тождеством под фолдом (клейм про trad→simp был вакуумен) — заменена на 陳博 с премисс-проверкой пересечения фолда. Каталог: 56 → **58** посадок (FX21, FX22). После всех правок этой секции: полный прогон каталога на копии итогового дерева — **58 записей, 0 неожиданных исходов** (FX21/FX22 краснеют своими пинами поимённо; посадка «потолок 200 → 150» отдельно проверена руками — якорь §4.4 красный с текстом, ведущим к пингу); `make battery` — **EXIT=0**, `make lint` — 0 issues. --- # ФИКС3 (промт BACKEND_SEAM_FIX3_SESSION_PROMPT.md, 6 пунктов) — секция 46 ## 46. Фикс-лист приёмки дофикса-2: четыре пина, один снос, зачистка двух контрактов Комплектность: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX3_SESSION_PROMPT.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.2-пина фикс2 и не пере-снят | | **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*` (⚠ первая редакция писала «0, тесты жили в незакоммиченном файле» — ложь, пойманная проверкой закрытия: `contractblockers_test.go` ЗАКОММИЧЕН, дифф их видит; снос легитимен — предмет снесён заказом §4.5, других удалений диффа нет). **Проверка закрытия (не-чинившим):** все шесть пунктов подтверждены исполнением — обе мутации (FX23, FX24) посажены им в своей копии и убиты пинами; греп-оси §6 пере-сняты; три расхождения — два неточных самоотчёта этой секции (исправлены выше с провенансом) и вновь рождённый `backend/tmctl` (артефакт `go build ./cmd/tmctl/` из каталога; удалён, класс закрыт `backend/.gitignore` с `/tmctl`).