textmachine/backend/docs/SEAM_PACK_FINDINGS.md

1157 lines
144 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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