textmachine/backend/docs/SEAM_PACK_FINDINGS.md

1440 lines
192 KiB
Markdown
Raw 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/BACKEND_SEAM_PACK_SESSION_PROMPT.md`, план — `SEAM_PACK_PLAN.md`.
**Каждое число — с командой, которой получено;** приёмка пере-раняет выборочно. Команды из `backend/`.
## 0. Состав пака
**Новое:** `internal/membank/decisions.go` (чистая логика решений) · `internal/pipeline/bankdecisions.go`
(обвязка: лок, чтения, записи, отчёт) · `cmd/tmctl/bankapply.go` (CLI) · `internal/seed/decode.go`
(строгие декодеры схемы) · `internal/standdata/standdata.go` (тест-резолв корпуса) + 6 тест-файлов и
2 фаззера.
**Тронуто:** `internal/config/book.go` (конвенционные дефолты) · `internal/pipeline/mining.go`
(дефолт-осведомлённые чтения) · `internal/pipeline/status.go` (`status_version` + `artifacts{}`) ·
`internal/pipeline/export.go` (`export_version`) · `internal/pipeline/bankexport.go` (`bankTermID`
`membank.TermID`, одно определение на двоих) · `internal/pipeline/refusal.go` + `cmd/tmctl/main.go`
(класс 14) · `internal/store/store.go` (+34 строки: `LockProject`) · `internal/membank/memseed.go`
(строгий разбор) · `internal/seed/seed.go` (устав пакета) · `cmd/tmctl/invocation.go`, `dotenv.go`
(`--keys-file`, `--decisions`).
## 1. Батареи
**Батарея НЕ детерминированно зелена на этой машине, и это НЕ пак** — читай до пере-рана, иначе
красное будет отнесено на меня. Два пред-существующих `-race`-флейка в `internal/store` (§10 п.7)
срабатывают примерно раз на 24 полных прогона: замерено `go test ./... -race -count=1` ×4 → прогон 1
чист, прогон 2 — `TestKillMinus9LosesAtMostOneCall`. **Красное ИМЕННО в этих двух тестах ожидаемо;
любое другое красное — регрессия.**
| что | команда | результат |
|---|---|---|
| хермет. батарея | `make battery` | `EXIT=0` на чистом прогоне; skip: `TestMinerFullBookParity`, `TestHelperEventsRun`, `TestHelperKillLoop`. См. предупреждение выше |
| стенд-батарея, **без единого `TM_*_DIR`** | `make battery-stand` | `EXIT=2`, красен ровно `TestMinerFullBookParity`, причина — отсутствующий `eval/exp16/data/jieba_dict_general_zh.txt` (строка 123) |
| линтер | `make lint` | `0 issues.` |
| vet под оба тег-набора · формат | `go vet ./... && go vet -tags live ./...` · `test -z "$(gofmt -l .)"` | чисто |
Стенд-картина совпала с опорной точкой оркестратора ⇒ **критерий §4.6 выполнен буквально.**
## 2. Адверсариальные посадки: 16/16 краснеют
Харнесс сажает мутацию → гоняет тесты НАЗВАННОГО пакета → откатывает; он же снимает sha256 целей до
прогона и проверяет после (`TREE IS NOT PRISTINE`). ⚠ Пакет пина ≠ пакет правки.
| # | посадка | пакет пина | что покраснело |
|---|---|---|---|
| M1 | дефолт срабатывает на НЕпустом ключе | `config` | `TestDeclaredDecisionPathIsNotDefaulted` |
| M2 | частичный результат переживает отказ (чистый слой) | `membank` | `TestAllOrNothing` +2 |
| M3 | частичная ЗАПИСЬ переживает отказ (обвязка) | `pipeline` | `TestBankApplyRefusesAPartialSet` |
| M4 | повторный вызов пишет файл заново | `pipeline` | `TestBankApplyRepeatIsAByteNoOp` +2 |
| M5 | лок берётся БЛОКИРУЮЩЕ | `pipeline` | `TestBankApplyRefusesABusyProject` (по 10-сек. границе) |
| M6 | читающий глагол принимает ключи | `cmd/tmctl` | `TestOnlyTranslateAcceptsKeys` |
| M7 | занятый лок отказывает классом 10 вместо 12 | `pipeline` | `TestBankApplyRefusesABusyProject` |
| M8 | пишется `status: auto` вместо `approved` (мина №1 строки 199) | `membank` | `TestApproveWritesAnExplicitApprovedStatus` +2 |
| M9 | решение, тронувшее только rejects, пере-рендерит и delta | `pipeline` | `TestADeclineDoesNotRewriteTheDelta` |
| M10 | промоушен теряет алиасы строки банка | `membank` | `TestPromotionKeepsTheBankRowAliases` |
| M11 | voice/address сида не сверяются с результатом | `membank` | `TestDecliningACharacterAVoiceProfileNamesIsRefused` |
| M12 | два отказа по одной поверхности проходят как два терма | `membank` | `TestTwoDeclinesOfOneSurfaceAreIllFormed` |
| M13 | пред-существующая поломка проглатывается молча | `membank` | `TestAPreExistingCollisionDoesNotBlockAnUnrelatedDecision` |
| M14 | документ из одних комментариев снова не грузится | `seed` | `TestDecodeAcceptsAnEmptyDocument` |
| M15 | ruby-чтения выброшены из судимого набора | `membank` | `TestApprovingARubyReadingOfASeedTermIsRefused` |
| M16 | инертный отказ по алиасу дельты принимается | `membank` | `TestDecliningAnAliasOfAnApprovedTermIsRefused` |
## 3. Живые пробы на реальной книге стенда
В КОПИИ вне рабочего дерева (`cp -r books/gu-zhenren/minirun-verify2 <scratch>/live`, пути пере-указаны
на копию). **В `books/` не записано ничего.**
| проба | результат |
|---|---|
| глагол на НЕмигрированном проекте | `exit=13`, `schema_mismatch found=11 expected=15 … run tmctl migrate` |
| проекция (`--dry-run`) | `exit=0`, `mode=projection`, `canonical_rewrite=true`, md5 обоих файлов НЕ изменились |
| применение | `exit=0`, `changed=true`; ручной `陈博` сохранил `gender`/`decl`/`translit_policy` |
| повтор того же решения | `changed=false`, `already_applied`, **inode и md5 не изменились** |
| конфликт с подписанной строкой сида (`春秋蝉`) | `exit=14`, отчёт называет строку сида ПОИМЁННО, файлы не тронуты |
| занятый лок (flock держит сторонний процесс) | `exit=12`, `wall=0.02s` |
| `decline` над рукописной дельтой | md5 дельты не изменился, комментарии владельца целы |
| reject-файл ИЗ ОДНИХ КОММЕНТАРИЕВ | `exit=0` (до фикса R1 было `exit=10`, `seed: EOF`) |
| загружаемость результата | `seed-lint OK: 0 fail-louds, 0 shared-key collisions` |
| конвенционный дефолт (ключи из `book.yaml` удалены) | `artifacts.mined_delta = <dir>/<book_id>.mined-delta.yaml`, файлов на диске нет; `approve`+`decline` одним вызовом создают оба |
| все seed/delta/signature/auto-bank стенда под строгим загрузчиком | 21 из 21 чисто (независимо: 29/29 на более широкой выборке, включая книги dovodka/role-topology/tenant-panel) |
## 4. Замеры
| что | число | вердикт |
|---|---|---|
| **цена глагола** (`bank-apply --dry-run`, `/usr/bin/time`) | 5 000 термов → 1.60с/133МБ · 10 000 → 3.62с/217МБ · 20 000 → 9.08с/350МБ; ×2.32.5 на удвоение | **датум для таймаута платформенного воркера.** Бюджет платформы на ОДИН вызов движка — 60с (`platform/internal/runs/reconcile.go:280`), до него ~80k термов. Реальная книга (кап майнера 200 за акт, сид стенда 58) — под секундой. Цена — двойной прогон проверки документа, и он же не даёт пред-существующей поломке блокировать починку |
| **контеншн** (20 одновременных `bank-apply`) | `2×0`, `18×12`, после гонки `seed-lint OK` | лок арбитрирует чисто; два нуля законны — второй взял лок после первого и оказался no-op |
| **деньги** (sha256 базы до/после) | `7ea61ccecada341b` → идентичен | глагол не сдвинул ни байта хранилища. Сильнее SELECT по ledger: не требует sqlite и покрывает всё хранилище |
| **детерминизм** (тот же документ в ДВУХ процессах, `cmp`) | побайтово идентичны | закрывает единственный реалистичный источник недетерминизма в Go — порядок обхода map |
| **`FuzzDecisionDocument`** | **487 163 исполнения, 0 падений** | оракулы: не паникует · всё-или-ничего · **ни одно решение не теряется молча** (каждый входной индекс ровно один раз в 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` доказывает, что прогон читает именно тот файл), **но цепь
целиком не гонялась ни разу**, а мина №1 строки 199 — livelock именно уровня ЦЕПИ, которого частичные
тесты по построению не видят.
**Почему я его не снял — проверено, не заявлено:** банк-стоп на этой машине не воспроизводится вовсе
(`pipeline.yaml` требует `contrast_path: …/eval/exp16/data/jieba_dict_general_zh.txt`, `ls``No such
file or directory` — та же причина, по которой красен парити), и даже с контрастом до стопа надо дойти
ОПЛАЧЕННОЙ черновой волной. Синтетический контраст = подделать замер.
**Команда:** на КОПИИ книги, припаркованной на exit 3, скормить `bank-apply` документ на всю карту
подписи (id из банк-экспорта), затем возобновить `translate --verify-bank`.
**ДЕФЕКТ** — стоп снова с ТЕМИ ЖЕ поверхностями. **НЕ дефект** — свежая пачка ≤200 термов реверс-секции
на большой книге (мина №3 строки 199: кап режется ПОСЛЕ фильтров).
## 9. Остаточные риски — решение оркестратора, не моё
- **Канонический пере-рендер необратим на уровне ДВИЖКА.** Смягчено трижды: проекция сообщает
`canonical_rewrite` ДО мутации · ДАННЫЕ переживают round-trip полностью (теряются форматирование и
комментарии шапки) · **на стенде восстановление УЖЕ есть** — репозиторий книг ВЕРСИОНИРУЕТ эти файлы
(`git -C books ls-files | grep -c 'mined-delta\|mined-rejects'`**30**, `check-ignore` → не
игнорируется), откат через `git -C books checkout`; на SaaS каталог книги платформенный и снапшот —
её сторона шва. Бэкап в движке не строил: механизм не заказан, носителя нет, существующий покрывает
обе стороны. Владельцу: провенанс из комментариев шапки переносить в поле `note:`.
- **Размер документа решений не ограничен** (`os.ReadFile`), тогда как платформа свои чтения от движка
капает (`maxStatus` 16МБ / `maxBank` 256МБ). Дотянуться могут: на SaaS — воркер самой платформы
(ограничен её API-капами), локально — оператор, роняющий свой процесс. Документ на 10 000 решений
≈2МБ ⇒ кап в диапазоне 116МБ стоит одной константы плюс строки отказа. **Однострочник при лендинге.**
## 10. Пинги — предметы ВНЕ карты пака, правок не делал
1. **`rubyToCandidates` мёртв в проде.** `grep -rn "rubyToCandidates" --include=*.go .` → 4 хита:
определение `internal/membank/memseed.go:301`, два комментария и ЕДИНСТВЕННЫЙ вызов —
`memseed_test.go:204`. `internal/pipeline/seeding.go:36-38` строит `manualSrcs` (аргумент этой
функции) и никуда не передаёт; Go молчит, потому что запись в map считается использованием.
Следствие: строк `Source:"ruby"` в банке не возникает, а платформа держит для них ветку словаря
(`platform/internal/ingest/bank.go` `termOrigin``OriginAnnotated`).
2. **Гейт `TestDispatchCommandsCoversTheSwitch` слеп к `case` с КОНСТАНТОЙ** — считает только
`*ast.BasicLit`, поэтому команда, объявленная константой, невидима гейту, чей смысл в том, что дефект
строки 176 «отрастал трижды». Обнаружено посадкой; я вернулся к литералу (конвенция файла
`if cmd == "seed-lint"`), поэтому дыра не эксплуатируется.
3. **`redrive` — платный путь без канала ключей аргументом.** Промт: «флаг только у `translate`», сделано
ровно так. Канал у него есть (конвенционный `.env`), и платформа его не зовёт
(`runner/engine.go` строит только translate/status/manifest/export). Развилка следующему паку.
4. **Проекция берёт тот же лок**, что и применение ⇒ «поправить термин при живом прогоне» даёт 12 даже
на ПРЕВЬЮ, на всю длину прогона. Платформенный пак обязан это спроектировать, а не обнаружить.
5. **Пути `mined_delta`/`mined_rejects` опубликованы в `status --json`.** Законно по п.1 закона, но эти
файлы версионного конверта не несут, а опубликованный путь читается как приглашение. Единственная
изгородь против их парсинга платформой — комментарий. Самое мягкое место в «формат знает ОДНА сторона».
6. **Схема сида без `omitempty`**`yaml.Marshal(seed.File{})` печатает все ключи, включая пустые. Уже
существовало (`miner_emit.go` `DeltaYAML` рендерит так же), глагол наследует. Смена тегов = правка
формата данных (`internal/seed/seed.go` это оговаривает).
7. **Два флейка в `internal/store`, пред-существующие.** `TestTheSeamIsNotChargedToTheStoreOperationBudget`
и `TestKillMinus9LosesAtMostOneCall`; тесты НЕ трогал (D39.121: правка теста ради зелени недопустима). Оба меряют ВРЕМЯ (первый сознательно сжимает
`opTimeout`, второй гоняет helper против таймера SIGKILL). Чередующийся A/B на `git worktree` HEAD,
3 раунда × 10: `kill9` — 2/30 на HEAD против 0/30 у меня; `seam` (`-race`, 30) — 2/30 против 1/30;
БЕЗ `-race` — 0/30. Покрытие независимо подтверждает: `LockProject`/`Release` этими тестами не
исполняются вовсе. ⚠ **0/30 против 2/30 НЕ значит «пак улучшает»** — на таком числе событий это шум
(Фишер p≈0.5); решающее и достаточное — что флейк живёт на дереве, которого пак не касался.
Воспроизведение: два дерева, `-race -count=10`, чередуя, под параллельным `go test ./...`.
## 11. Три правила протокола, вынесенные инцидентами сессии
1. **Харнесс мутаций обязан фингерпринтить цели.** Прогон `mutate.py` был убит таймаутом инструмента,
`finally` не отработал, и посадка M4 (снятый гейт байтового no-op) просидела в
`internal/pipeline/bankdecisions.go` ~40 минут, выглядя продакшн-кодом; сторонний ревьюер снял с
этого дерева замеры и доложил «дефект гейта идемпотентности». Поймал посадку пин
`TestBankApplyRepeatIsAByteNoOp` на первом прогоне после. Харнесс починен (sha256 целей до и после).
Пин усилен по следу: повтор проверяется над файлом с РУЧНЫМ комментарием в шапке — это то, что
пере-рендер уничтожает и чего не заметила бы проверка содержимого — плюс что `changed:false` не врёт.
2. **Снимай отпечаток дерева ПЕРЕД сборкой бинаря замера.** Симметричная половина того же класса:
ревьюер собрал `tmctl` из незакоммиченного дерева, не сняв отпечаток, и инструмент унаследовал
посадку. За сессию на это наступили обе стороны.
3. **Одиночный прогон — не замер.** Атрибуцию флейков я сперва вывел из одного прогона («зелено в
изоляции ⇒ нагрузка»), затем другой одиночный прогон дал ровно обратное. Решает только чередующийся
A/B (§10 п.7).
## 12. Попунктный чек промта
| пункт | статус |
|---|---|
| §2 зона записи только `backend/` + секция «Бэкенд» в `docs/PROGRESS.md`; не коммитить; чужое не трогать | ✅ `git status` подтверждает |
| §4.1 дефолты пустых ключей; гард ЯВНОГО пути не ослаблен; отсутствующий дефолт = «решений нет» | ✅ M1 + `TestDeclaredAndUnreadableDecisionPathStillRefuses` |
| §4.2 глагол $0; байтовый no-op; отзыв = ЗАМЕНА; всё-или-ничего; два решения по терму = отказ; конфликт с сидом С ИМЕНЕМ; проекция БЕЗ мутации; занятый лок РОВНО 12; flock неблокирующе; версия в ответе; `depth` явным полем | ✅ M2M8 + `TestDeclineWritesTheRejectAndWithdrawsTheApproval`, `TestOneCallDecidesATermOnce`, `TestDecisionOnASignedSeedTermIsRefusedByName`, `TestBankApplyDryRunWritesNothing`, `TestBankApplyDeclineThenApproveConverges`; §3, §4 |
| §4.2 НЕ проектировать «глубину до черновика» | ✅ `depth: edit_wave`; `TestTheRunReadsTheConventionalDelta` доказывает `Source:"mined"` |
| §4.3 `--keys-file` только у `translate`; деплой-файл ПЕРВЫМ; нечитаемый = отказ конфигом; ничего «на вырост» | ✅ M6 + `TestKeysFileWinsOverTheConventionalEnv` + `TestAnUnreadableKeysFileRefusesWithTheConfigClass` |
| §4.4 строгость сид-загрузчика + тест на unknown-key | ✅ 2 кейса в `TestSeedLintCatchesDefects` (`gendr`, `termz`) |
| §4.5 путь артефакта аддитивно + версия документа; НЕ тащить пере-именование банк-экспорта (ЛОМАЮЩЕЕ, окно — строка 161) | ✅ `artifacts{}` + `status_version`; пин `rep.Artifacts.BankExport == r.bankExportPath()` |
| §4.6 дефолт корпуса маркерным резолвом, env-переопределения старше | ✅ §1 |
| §4.7 платных вызовов НОЛЬ; `--keys-file` фейковыми ключами; вердикт PLAUSIBLE | ✅ sha256 базы (§4) + `TestTheKeysFileActuallyReachesTheProvider` (сверяет `Authorization` на fake-провайдере, и БЕЗ флага тот же прогон отказывает конфигом — проверка не вакуумна); §7 |
| §5 батарея после каждой правки; красный тест не подгонять; посадки ВНЕ списка промта; посадка называет ПАКЕТ; живая проба в КОПИИ; артефакт-находок | ✅ 16 посадок, 8 сверх списка; флейки охарактеризованы и НЕ тронуты; §2, §3 |
| §5.1 последний абзац — план или обещание? сделать СЕЙЧАС | ✅ всё найденное ревью закрыто в этом дереве; единственное отложенное — §8, вне моих возможностей по деньгам |
| §5.1 дифф `^func Test` исполнением; интервальная самоверификация субагентом | ✅ §13; ревью гонялось в СЕРЕДИНЕ пака против явных критериев |
| §6 оси: шов (инвентарь ЧУЖОГО кода) · общность · деньги | ✅ читал `artifacts.go`/`bank.go`/`engine.go`/`spawn.go`/`resync.go`/`exit.go` сам; проверил, что ни один декодер платформы не строгий ⇒ аддитивные поля безопасны; Go не ветвится по паре/книге; деньги — sha256 базы |
| §8 число с командой · §9 эхо до работы · §10 «что НЕ удалось» · §11 развилки пингом | ✅ §1§4 · выдано · §7 · §10 |
## 13. Дифф `^func Test` — исполнением
```
$ git diff -U0 -- backend | grep -E "^[+-]func Test"
+func TestTheRefusalTableIsTotal(t *testing.T) {
$ git ls-files --others --exclude-standard -- backend | grep '_test.go$' | xargs grep -h '^func Test' | wc -l
59
$ ... | xargs grep -h '^func Fuzz' | wc -l
2
```
**Добавлено:** 59 тест-функций в шести НОВЫХ файлах + `TestTheRefusalTableIsTotal` в существующем
`cmd/tmctl/exitcontract_test.go` + 2 фаззера. **Удалено:** ноль.
**Тронуто в существующих файлах — шесть, ни у одной сигнатура не изменилась:**
- `exitcontract_test.go` — в список полосы добавлены `exitSchemaMismatch` и `exitDecisionsRejected`
(иначе гейт молча перестал бы покрывать новый код) + тест тотальности;
- `invocation_test.go` — frozen-строка usage расширена именем `bank-apply`. Объявленное расширение
контракта тем же механизмом, которым строка росла пять раз до меня (её собственный комментарий их
перечисляет), и второй половине того же теста («usage обязана называть каждую dispatch-команду»)
иначе не удовлетворить;
- `checks/labelharness_test.go`, `membank/labelharness_test.go`, `miner/miner_parity_test.go` — §4.6:
три копии резолвера заменены на `standdata` (120 строк, +62);
- `membank/seedlint_test.go` — два кейса unknown-key (§4.4).
---
# Дофикс той же двери (сессия 25.08.2026)
Промт — `docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (16 пунктов §4), план — `SEAM_FIX_PLAN.md`. Секции 013
выше — улика приёмки, не тронуты. **Каждое число ниже — с командой, которой получено;** команды из
`backend/`. Дерево не коммичено, лендит оркестратор.
## 14. Батареи
| что | команда | результат |
|---|---|---|
| **база ДО правок** (дерево пака) | `make battery` | `EXIT=2`, красен ровно `TestKillMinus9LosesAtMostOneCall` — один из двух названных промтом пред-существующих `-race`-флейков. Флейк воспроизвёлся на дереве, которого я ещё не касался: атрибуция моя, не наследованная |
| батарея после правок | `make battery` | см. §31 |
| стенд-батарея | `make battery-stand` | см. §31 |
| линтер | `make lint` | `0 issues.` |
## 15. §4.1 — корень формата: замер, решение, цена
**Триггер шире клейма промта и НЕ совпадает с поправкой записки прошлой сессии.** Прогнал матрицу
«значение поля → рендер → разбор» на обеих схемах (проба вне рабочего дерева, `gopkg.in/yaml.v3 v3.0.1`):
| значение | что рендерится | round-trip |
|---|---|---|
| `"\nтекст"`, `"\n\nтекст"`, `"\n\tx"` | `\|4-` + строки | **не разбирается** (`did not find expected key`) |
| `"\n"` | `\|4+` | разбирается, значение становится `""`**тихая потеря** |
| `"\n x"` | `\|4-` + 10 пробелов | разбирается, значение становится `"x"`**тихая потеря** |
| `"текст\n"`, `"a\nb"`, `" x"`, `"\tx"`, `"\r\nx"` | скаляр/блок | ок |
Два следствия, оба несущие. (1) Клейм промта «бьёт по ЛЮБОМУ свободному текстовому полю» **ВЕРЕН**;
поправка записки пака («`dst` и `sense` не ломаются») — **НЕТ**, ломается любое строковое поле обеих схем
(проверено на `src`/`dst`/`sense`/`note` и на `rejects[].note`). (2) Заказанная проверка
`render → parse` две тихие потери **пропустила бы**: документ разбирается, значение другое, дверь
отчитывается `applied`. Инвариант обязан быть «прочиталось **И совпало**».
**Механика — сверена с исходником библиотеки, а не выведена.** Внутри элемента блочной
последовательности эмиттер увеличивает отступ на жёсткую двойку, чтобы «пропустить индикатор `- `»
(`emitterc.go:239-241`, комментарий там же), а индикатор отступа блочного скаляра печатает `best_indent`
(`emitterc.go:1848-1852`). Расхождение — ровно ширина `- `. Апстрим починил это в линии-преемнике
(yaml/go-yaml PR #150, ишью #65); `v3.0.1` — то, что в `go.mod`, и оно не поддерживается.
**Путь «починить рендер стилями `yaml.Node`» ЗАКРЫТ:** `(*yaml.Node).Encode` внутри сам гоняет
marshal→unmarshal и падает на том же входе — узел не построить (замерено).
**Популярный обход `enc.SetIndent(2)` НЕ применён и применять его нельзя:** на этом пине он делает
документ разбираемым и **молча теряет ведущий перевод строки** (замерено: `"\nне термин"``"не термин"`,
`"\n\necho a\necho b\n"``"\necho a\necho b\n"`). Это обмен громкой ошибки на тихую правку слов
владельца — ровно тот класс, который канон запрещает абсорбировать.
**Что сделано.** (а) `seed.File.Normalize()`/`RejectFile.Normalize()` — ОДНО правило на все текстовые
поля обеих схем (`TrimSpace`, пустые коллекции → nil), применяется на РЕНДЕРЕ, не на чтении;
(б) дверь так же нормализует свободный текст входящих решений — то же правило, которое она и загрузчик
уже применяли к `src`/`sense`/`dst`; (в) `RenderSeedFile`/`RenderRejectFile` ДОКАЗЫВАЮТ байты:
marshal → разбор настоящим строгим декодером → `reflect.DeepEqual`; провал — ошибка, никогда не байты;
(г) байты, которые чистый слой проверил, он же и отдаёт наверх (`ApplyResult.DeltaBytes`) — второго
рендерера в обвязке больше нет; (д) **характеризационный тест на саму библиотеку**
(`TestYamlV3CannotReadBackWhatItWritesForALeadingNewline`) краснеет в день, когда дефект починят: обход
перестаёт быть молчаливым.
**(а) и (б) взаимно прикрывают друг друга** — это замерено, а не предположено: посадка, снимающая одну из
них, выживала, пока пин не был написан на случай, до которого другая не достаёт (I → пред-существующее
значение В ФАЙЛЕ; K → идемпотентность повторного решения с пробельным текстом). Оба теперь краснеют.
**Цена инварианта — замерена, не оценена.** Два бинаря из одного дерева, второй с выключенной
проверкой равенства, один документ на 10 000 решений, по 3 прогона (`/usr/bin/time -f '%e %M'`):
**с проверкой 16.52 / 18.10 / 18.30 с, 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 МБ) выше любого прогона с ней.** Первая редакция публиковала
здесь «≈13 %», сравнивая минимум с минимумом, — снято. Основная цена глагола — НЕ проверка (см. §18).
## 16. §4.11 + свои посадки: 36 мутаций, 35 краснеют, 1 выживает осознанно
Харнесс — **в репозитории**: `cmd/tmmutate` + каталог `cmd/tmmutate/mutations.json` (36 записей —
`python3 -c "import json;print(len(json.load(open('cmd/tmmutate/mutations.json'))))"`, у каждой
названо СВОЙСТВО, за которое она отвечает). Три сессии подряд писали такой инструмент в скретчпад и
теряли его вместе с сессией; каталог — это и есть память о том, что кто-то счёл достойным охраны.
Прогон — на КОПИИ дерева (`cp -a`, равенство копии проверено побайтно), сам инструмент снимает
отпечаток каждой цели и каталога целиком и краснеет на «TREE IS NOT PRISTINE».
**База ДО пинов: 20 из 24 выжили** (⚠ снято на РАННЕЙ редакции каталога из 24 записей; нынешние 36 её
не воспроизводят — это исторический замер, а не команда, которую можно повторить) — то есть все десять
слепых пятен §4.11 подтверждены исполнением,
плюс мой собственный новый код был не запинен. **Итог после всех итераций: 36 посадок, 35 краснеют на
СВОЁМ пине, одна (G) записана как аргументированный выживший (§17).** Каталог поэтому не отчёт, а ГЕЙТ:
исход, не совпавший с записанным, роняет прогон — в том числе неожиданно ПОЙМАННАЯ посадка, что значит
«запись устарела, обнови её».
**Две мои собственные посадки поймали два моих же ВАКУУМНЫХ пина** (последняя итерация): пин на
позиционные субъекты держал строки без идентичности ПЕРВЫМИ, поэтому их позиции никогда не двигались; а
пин на полноту довода отказа искал имя команды в тексте, где оно и так стоит — в обвинении «not by %q»,
а не в объяснении. Оба переписаны и оба теперь краснеют. Это второе за сессию подтверждение правила
«сначала посадка, потом пин»: пин, написанный первым, проверяет то, что и так верно.
```
$ cd <копия backend> && go run ./cmd/tmmutate -root . -logs <dir>
```
| # | посадка (свойство) | пакет пина | что покраснело |
|---|---|---|---|
| A | лок двери — тот же flock, что держит ПРОГОН | `pipeline` | `TestBankApplyRefusesAProjectAN_RUNIsHolding` |
| B | `canonical_rewrite` + `preexisting_problems` (2 правки в 1 файле) | `pipeline` | `TestTheReportCarriesTheTwoFieldsThePackMitigatesItsRisksWith` |
| C | класс отказа без номера уезжает в catch-all 19 | `cmd/tmctl` | `TestTheRefusalTableIsTotal` |
| D | нормализация в REPLACEMENT-половине отказа | `membank` | `TestADeclineWithdrawsAnApprovalWrittenInAnotherOrthography` |
| E1 | глубокая копия алиасов в `withRubyAliases` | `membank` | `TestWithRubyAliasesDoesNotWriteIntoTheCallersRows` |
| E2 | перенос `note` при промоушене | `membank` | `TestPromotionCarriesTheOwnersNote` +1 |
| E3 | рантайм-половина гарда ОБЪЯВЛЕННОГО пути | `pipeline` | `TestADeclaredDecisionFileThatVANISHESIsStillAnError` |
| F | абсолютность путей в `artifacts{}` | `pipeline` | `TestTheReportedPathsAreAbsoluteEvenWhenTheConfigIsNOT` |
| G | байтовый гейт `changedDoc` | `pipeline` | **ВЫЖИВАЕТ — см. §17** |
| H | решения едут С КНИГОЙ, а не с `project_db` | `config` | `TestTheDecisionFilesTravelWithTheBookAndNotWithTheDatabase` |
| I | нормализация ДОКУМЕНТА (значение уже в файле) | `membank` | `TestAHandAuthoredNoteTheEngineCannotWriteIsRepairedNotRefused` |
| J | равенство в `renderProved` (а не «просто разобралось») | `membank` | `TestTheRenderGateRefusesBytesThatReadBackAsAnotherDocument` |
| K | нормализация ЗАПРОСА (идемпотентность повтора) | `membank` | `TestARepeatOfADecisionWhoseTextIsPaddedIsStillAlreadyApplied` |
| L | `book_id` — один элемент пути | `config` | `TestBookIDIsRefusedWhenItIsNotAPathElement` |
| M | четыре движковых пути попарно различны | `config` | `TestTwoKeysOnOnePathAreRefused` |
| N | гард флага — на ПРИСУТСТВИЕ, не на значение | `cmd/tmctl` | `TestAFlagIsRefusedForBEINGTHERE_NotForCarryingAValue` |
| O | перевёрнутое окно глав | `membank` | `TestAReversedChapterWindowIsRefused` |
| P | инертный отказ судится по НАБОРУ | `membank` | `TestApprovingATermAndDecliningItsAliasInOneCallIsRefused` |
| Q | `note` не стирается умолчанием | `membank` | `TestANoteIsNotErasedByAnOmission` |
| R | кап документа решений | `pipeline` | `TestTheDecisionDocumentIsCapped` |
| S | стоп между решением и записью не пишет | `pipeline` | `TestAStopRequestWritesNothing/cancelled_while_the_decision_is_being_computed` |
| T | пред-существующая поломка — по СУБЪЕКТУ | `membank` | `TestRepairingOneOfTwoBrokenTermsIsAccepted` |
| U | честность отчёта о непогасшем стопе | `pipeline` | `TestTheStopClearsWhenTheOwnerDecidesTheWholeSignatureMap` |
| V | конверт путей на поверхности потребителя | `pipeline` | `TestTheManifestCarriesTheArtifactEnvelope` |
| W | кап на КОЛИЧЕСТВЕ решений (§29 п.1) | `pipeline` | `TestTheDecisionCOUNTIsCappedBecauseTheBYTESCannotBound` |
| Y | субъект поломки без рендеринга (§29 п.2) | `membank` | `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt` +1 |
| Z | нечитаемая карта подписи ≠ «всё решено» (§29 п.6) | `pipeline` | `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided` |
| AB | побег «это был терм сам по себе» (§30 п.2) | `membank` | `TestADeclineIsRefusedWhenTheSurfaceGOESONFiringAsAnotherTermsAlias` |
| AC | `aliasOwner` игнорирует статус (§30 п.3) | `membank` | `TestDecliningTheAliasOfAnUNSIGNEDRowIsAccepted` |
| AD | загрузчик отдаёт построенные записи при отказе (§30 п.1) | `membank` | `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS` |
| AE | позиционный субъект (§30, «плюс два») | `membank` | `TestTwoRecordsWithNoIdentityDoNotBlockEachOthersRepair` |
| AF | довод отказа описывает всех, кого отвергает (§30 п.4) | `cmd/tmctl` | `TestTheKeysFileRefusalDescribesEveryoneItTurnsAway` |
| AG | остановленный вызов не печатает, что применил (§34) | `pipeline` | `TestAStoppedCallDoesNotPRINTThatItApplied` |
| AH | нечитаемая карта подписи ≠ «всё решено» (§34) | `pipeline` | `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided` |
| AI | отказ не предупреждает о перезаписи, которой не будет (§34) | `pipeline` | `TestARefusedCallDoesNotWarnAboutARewriteThatWillNotHappen` |
| AJ | отказ по алиасу сида сужен до случая, где он инертен (§34) | `membank` | `TestDecliningASeedAliasIsAcceptedWhenTheDeltaHoldsThatRow` |
## 17. Единственная выжившая мутация — и почему пин на неё невозможен
**G (байтовый гейт).** Заказ §4.11(ж) просит поведенческий пин. Его нет и быть не может: гейт
**недостижим за первым**, и это доказывается, а не предполагается.
`deltaChanged = DeltaTouched && changedDoc(диск, next, пусто)`. `DeltaTouched` ставится только когда
решение реально изменило документ, а `next` — канонический рендер УЖЕ ИЗМЕНЁННОГО документа. Чтобы
`changedDoc` вернул false, байты на диске должны БЫТЬ этим рендером — но тогда документ, разобранный из
них, уже равен результату, никакое решение ничего не меняло, и `DeltaTouched` был бы false. Нормализация
щели не открывает: файл, чьи значения несут окружающие пробелы, в себя же не рендерится.
**Диспозиция:** гейт ОСТАВЛЕН как второй рубеж на случай, когда `*Touched` станет грубее сегодняшнего, и
запинен как КОНТРАКТ ФУНКЦИИ (`TestChangedDocIsTheSecondFenceAndItsContractIsPinnedDirectly`), включая
правило «отсутствующий файл + пустой документ — НЕ создавать». Снести его я не стал: изгородь, за которую
я вижу не до конца, дешевле оставить, чем убрать.
## 18. Замеры — своей названной методикой
**Методика:** `/usr/bin/time -f '%M %e'`, 3 прогона на размер, медиана, машина без нагрузки,
`bank-apply --dry-run` (записи нет), документы из N `approve` с короткой заметкой.
| N решений | размер документа | wall (мин/мед/макс) | maxRSS (мин/макс) |
|---|---|---|---|
| 1 000 | 82.9 КБ | 0.57 / 0.58 / 0.74 с | 51.9 / 52.1 МБ |
| 5 000 | 423 КБ | 4.91 / 5.18 / 5.36 с | 171 / 215 МБ |
| 10 000 | 848 КБ | 14.53 / 15.73 / 16.54 с | 391.7 / 391.9 МБ |
| 20 000 | 1.72 МБ | 46.72 / 52.16 / 52.20 с | 740 / 762 МБ |
**Разночтение по памяти между паком и приёмкой закрыто в пользу приёмки.** Пак дал 133/217/350 МБ на
5k/10k/20k, приёмка 199/395/798 МБ, мой замер — 203/392/745 МБ. Числа приёмки воспроизводятся, числа пака
занижены примерно вдвое. **По времени сойтись не удалось:** пак дал 1.60/3.62/9.08 с, у меня 5.2/15.7/50 с
— в 35 раз медленнее. Проверкой раунд-трипа это НЕ объясняется (§15: ≈5 %), бинарь пака я не
пере-собирал, и объяснения у меня нет. **Число, которое поедет в платформенный пак как датум для
таймаута, — моё или приёмочное, не паковское.**
**Почему цена сверхлинейная — факт исходника, а не гипотеза:** каждый пер-решенческий помощник
сканирует весь документ (`findTerm`, `dropTerms`, `addReject`, `aliasOwner`, `deltaHoldsSurface` — все
`O(len(документа))`; ⚠ первая редакция называла здесь `ownsSurface`, которой в коде нет — она была снята
вместе с побегом §30 п.2), поэтому проход по N решениям Θ(N²) по построению. **Чего я НЕ доказал:** что именно
это доминирует в замеренном времени. Единственная проба-индекс, которую я на это потратил, была написана
неверно (пере-строение индекса на каждом добавлении) и дала обратный результат; дальше я не пошёл — это
вне заказа. Ускорение — пингом (§23), не правкой.
**⚠ Эта таблица снята на документах из `approve` с заметкой (≈85 байт на решение) и потому НЕ описывает
худший случай, который допускает кап в БАЙТАХ.** См. §29 п.1: минимальный `decline` — ≈35 байт, и 1 МиБ
пускает 28 900 таких решений, то есть 308 секунд. Вывод «1 МиБ ≈ 12 000 решений ≈ 20 с» из первой
редакции этого отчёта **неверен**, и кап пришлось делать двойным.
**Деньги:** `sha256sum <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 }
```
`undecided > 0`**факт**, что стоп не погаснет. `undecided == 0`**не обещание**, и это написано в
комментарии типа явно. Пустой `map` значит «прогон до границы банка ещё не доходил». В режиме проекции
число — то, что БУДЕТ после применения (единственное чтение, которое может иметь превью).
Запинено там, где оно осмысленно, — в тесте цепи §4.12: частичный документ на карте из двух сурфейсов
даёт `undecided=1`, полный — `undecided=0`, и после него стоп действительно гаснет.
## 23. Пинги — предметы ВНЕ карты дофикса, правок не делал
1. **`exit 2` при OOM читается платформой как успех** (заказ §4.5 прямо называет это пингом). Движок при
нехватке памяти выходит `fatal error: out of memory` + код 2, а `platform/internal/ingest/exit.go`
читает 2 как «команда СДЕЛАЛА работу». Класс общедвижковый. Кап §4.5 не даёт `bank-apply` стать первым,
кто до него доедет, но самого класса не снимает.
2. **Каталог книги без права записи даёт `exit 1`**, и платформа спишет проблему прав ХОСТА в бюджет
попыток КНИГИ. Класс движка корректен (см. §21); чинить надо чтение кода 1 на платформе.
3. **Проекция отвечает «что применится», а не «почём».** Рекомендация: **не строить**. Закон п.7 про
«почём ДО подтверждения» уже исполнен там, где есть деньги (rebill-смета, `status --json`), а этот
глагол $0 по построению — единственная его «цена» это пере-редактура, которая квотируется и гейтится в
другом месте и другим согласием. Поле «$0» в отчёте было бы шумом. Решать оркестратору.
4. **Карта подписи не удаляется после погасшего стопа** (`os.Remove` в `mining.go` не встречается).
Пред-существующее. Потребитель, читающий файл как ответ на «есть ли что подписать», читает уже
подписанную карту. Зафиксировано ЯВНО тестом цепи как текущее поведение; честный ответ на тот же
вопрос — `signature.undecided` (§22).
5. **Цена глагола Θ(N²) по построению** (§18) — это то, почему кап на КОЛИЧЕСТВЕ стоит на 5 000, а не
выше. Лечится индексом по ключу терма в `applyOne`/`dropTerms` и по нормализованной поверхности в
`addReject`/`dropReject`; вне заказа, не делал. Поднимать кап без этого нельзя.
6. **Апстрим-дефект `yaml.v3` (§15) лечится по-настоящему двумя способами,** и оба вне пака: (а) тип
скаляра схемы с `MarshalYAML`, возвращающим узел с явным стилем (проверено по исходнику: явный стиль
проверяется РАНЬШЕ ветки блочного скаляра, `encode.go:557-570`); (б) переезд на `go.yaml.in/yaml/v4`
— там этот случай починен, но это релиз-кандидат и смена module path сразу в двух зонах. Мой обход
несёт ссылку и характеризационный тест, а не «похоже, баг yaml».
7. **Цена доказывающего рендера** ≈5 % времени (§15); по памяти разница внутри разброса прогонов и НЕ
измерена. Плюс `notCanonical` теперь тоже разбирает документ. Названо, чтобы следующий замер не
приписал это чему-то другому.
## 24. Что НЕ доказано и что НЕ удалось
- **Рантайм-вердикт по ПЛАТНОМУ пути остаётся PLAUSIBLE.** Санкции на платные вызовы у дофикса нет, и
ни одного платного вызова не сделано (`sha256` базы стенда не сдвинулся). Это ожидаемый исход, а не
дефект отчёта.
- **Цепь §4.12 доказана на СИНТЕТИЧЕСКОМ контрасте**, а не на книге стенда: банк-стоп на этой машине не
воспроизводится вовсе (`pipeline.yaml` требует jieba-контраста, которого клон не получает по
построению — строка 123). Ограничение названо в комментарии самого теста: синтетика меняет, КАКИЕ
кандидаты предложены, но не то, проходит ли подписанная строка фильтр подписи. Для вопроса «гаснет ли
стоп» это корректно; для качества майнера — нет.
- **Расхождение по ВРЕМЕНИ с замером пака (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-самопроверка
сравнила его с пропатченным же хешем и **прошла**. Дерево копии осталось с `rep.CanonicalRewrite = false`,
и **два полных раунда измерений после этого шли по мутированному коду**: три пина отчитались как поймавшие
мутации, к которым не имели отношения (E3, F, R, S, U, V показывали ложный «RED» на `TestTheReport…`).
Поймано ровно тем, чему учит §11 п.3 отчёта пака: одиночный прогон — не замер. Я пере-проверил один
подозрительный «RED» изолированным прогоном, он не воспроизвёлся, и это вскрыло дефект инструмента.
**Починено дважды:** одно чтение и одна запись на файл (порядок объявлен контрактом в комментарии), плюс
второй независимый рубеж — отпечаток ВСЕХ файлов каталога, снятый на заведомо чистом дереве и
пере-проверяемый ПЕРЕД каждой мутацией и после последней. Дерево копии после итогового прогона побайтно
равно рабочему (`diff -rq --exclude=bin <рабочее> <копия>` пусто, и
`find . \( -name '*.go' -o -name '*.json' \) | sort | xargs sha256sum | sha256sum` даёт на обеих сторонах
одно значение при 265 файлах. ⚠ Сам дайджест здесь НЕ приводится: дерево с тех пор менялось, и
записанное число воспроизвести уже нельзя — повторяемо равенство двух сторон, а не константа).
**Общая форма правила, шире этого инструмента:** самопроверка, чей эталон снят ПОСЛЕ первого изменения,
не проверяет ничего. Эталон снимается один раз, на состоянии, которое известно как чистое.
## 26. Попунктный чек §4 — все шестнадцать
Комплектность сверена механически: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md`**16**.
| пункт | статус | чем закрыт |
|---|---|---|
| **4.1** ⛔ блокер: round-trip на ОБОИХ документах | ✅ **сделано ШИРЕ заказа** | нормализация документа + запроса, рендер ДОКАЗЫВАЕТ равенство (не только разбираемость — заказанная форма пропустила бы две тихие потери), байты проверены один раз и один раз записаны, фаззер пути ДВЕРИ, характеризационный тест на библиотеку. §15, §20; мутации I/J/K; живая проба §19 |
| **4.2** пред-существующие чинятся по одной | ✅ сделано; **дважды углублено по ревью** | вердикт загрузчика разложен по СУБЪЕКТАМ (`membank/problems.go`), ключ сравнения — субъект. Первая редакция останавливалась на два шага раньше: субъект нёс РЕНДЕРИНГ, и правка `dst` делала чужую поломку «новой» (§29 п.2); и поломки ПОД загрузчиком были не видны, пока документ не грузился (§29 п.3). Мутации T, Y, AD (⚠ X снята вместе с правилом маски — §29 п.3, каталог её больше не несёт); пины `TestRepairingOneOfTwoBrokenTermsIsAccepted`, `TestAFaultTheDoorHasNoChannelForCannotDeadlockIt`, `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS`; живая проба §19 |
| **4.3** два ключа на один путь | ✅ **сделано ШИРЕ на одну проверку** | попарно различны четыре движковых пути (`project_db`, `glossary_seed`, `mined_delta`, `mined_rejects`) — `project_db == mined_delta` означало бы YAML поверх sqlite. Проверено на всех `book.yaml` репозитория и стенда (`find . -name book.yaml \| wc -l`**22**): нарушителей ноль. Мутация M |
| **4.4** гард флага на ПРИСУТСТВИЕ | ✅ сделано | `fs.Visit` (единственное, что отвечает на этот вопрос), оба гарда, плюс пустое значение у присутствующего флага — тоже отказ. Мутация N |
| **4.5** кап документа + строка отказа полосой | ✅ сделано, ПЕРЕСМОТРЕНО по ревью | ДВА капа: 1 МиБ на чтение + **5 000 решений** на применение. Первая редакция капила только байты и утверждала, что этим ограничено ВРЕМЯ — опровергнуто замером (§29). Класс 14 у обоих. Хвост про `exit 2` — пингом. Мутации R, W |
| **4.6** конверт путей потребителю | ✅ сделано, компромисс назван | `manifest --json` несёт тот же `artifacts{}`; ВЫВОДИТСЯ на каждом чтении, поэтому версия документа не двигалась. Компромисс вслух: манифест семантически про дерево глав, путь артефакта там гость. Мутация V |
| **4.7** алиас судится по НАБОРУ | ✅ сделано; **углублено по §30 п.2** | проверка перенесена ПОСЛЕ свёртки и судит РЕЗУЛЬТАТ — именно поэтому два отказа (терм + его алиас) остаются законными: к концу вызова алиасом никто не владеет. ⚠ Первая редакция этой строки добавляла «своя поверхность проверяется по входному документу» — такого побега в коде НЕТ, он был заведён и снят как вредный (§30 п.2). Мутации P, AB |
| **4.8** перевёрнутое окно глав | ✅ сделано | отказ с цитатой окна; открытая верхняя граница законна. Мутация O |
| **4.9** честность про непогасший стоп | ✅ **исполнено фактом, не предсказанием** | `signature{map,surfaces,undecided}` по правилу самого прогона; `undecided>0` — факт, `undecided==0` — явно не обещание. Аргумент в §22. Мутация U |
| **4.10** SIGTERM | ✅ сделано; сомнение записки СНЯТО замером; **довод в комментарии исправлен по ревью** | ctx проверяется дважды, стоп не пишет ничего, exit 5. Несущий довод — НЕ длина непрерываемого участка (первая редакция назвала её «≈20 с, два порядка внутри stop grace», и это было неверно в 15 раз), а то, что записи идут последними и атомарно: убитый где угодно до них процесс не записал ничего. Мутация S (пин целится именно во вторую проверку) |
| **4.11** десять посадок, пин на каждую | ✅ 9 из 10 + доказательство по десятой | AH, E1E3 краснеют своими пинами; **G недостижима за первым гейтом — доказано, не предположено** (§17), вместо поведенческого пина запинен контракт функции |
| **4.12** цепь в репозитории | ✅ сделано | `internal/pipeline/bankchain_test.go`, $0 на фейковом провайдере, ОДИН проект от стопа до погасшего стопа; форма фикстуры (2 терма = 1 approve + 1 decline) проверяется как ПРЕМИСА; ограничение синтетики названо в комментарии; карта подписи после погасшего стопа зафиксирована явно |
| **4.13** девять мелочей | ✅ по каждой решение и довод | §21: починено 3, отклонено с аргументом 5, пингом 1 |
| **4.14** довод отказа `--keys-file` | ✅ сделано | текст называет `redrive` (платный, свой `.env`), `backup`/`migrate` (деплой-шаги) и $0-читающие поимённо. Развилку про канал ключей `redrive` НЕ решал |
| **4.15** три тавтологичных пина | ✅ сделано | вердикт `Preexisting` исполняется всеми четырьмя проверками (`TestAPreExistingFaultOfEveryKindStaysReportedAndUnblocking`); четвёртый оракул фаззера снят с тавтологии и стал НЕПОДВИЖНОЙ ТОЧКОЙ; `TestDecisionByBankID` сверяется с НЕЗАВИСИМО посчитанными константами + отдельный пин на инъективность, ради которой в `TermID` сделана длина-префикс |
| **4.16** платных вызовов ноль | ✅ | `sha256` базы стенда не сдвинулся; рантайм по платному пути — PLAUSIBLE, как и ожидалось |
| §5 батарея после каждой правки · посадки СВЕРХ списка · посадка называет ПАКЕТ · живая проба в КОПИИ · артефакт-находок | ✅ | 36 посадок (26 сверх списка §4.11), пакет пина ≠ пакет правки записан в каталоге, проба в копии, эта секция |
| §5.1 последний абзац — план или обещание? · дифф `^func Test` исполнением · интервальная самоверификация субагентом · каждый клейм — с результатом инструмента ЭТОЙ сессии | ✅ | обещаний в отчёте нет: всё найденное закрыто в дереве; §27; ревью гонялось в СЕРЕДИНЕ работы против явных критериев; каждое число выше — с командой |
| §6 три оси | ✅ | round-trip формата (§15, §20) · слепота гейта (§16, §17) · общность: в Go, которую я тронул, нет ни одного литерала пары/письменности/орфографии (проверено грепом по девяти файлам); нормализация — `strings.TrimSpace`, определённый Юникодом, а не скриптом |
| §8 заявление = команда · §9 эхо до работы · §10 «что НЕ удалось» · §11 развилки | ✅ | §14§27 · выдано до кода · §24 · §21§23 |
## 27. Дифф `^func Test` — исполнением
```
$ git diff -U0 -- backend | grep -cE "^-func (Test|Fuzz)" → 0 # удалено: НОЛЬ
$ git ls-files --others --exclude-standard -- backend | grep '_test.go$' \
| xargs grep -h '^func Test' | wc -l → 97 # пак оставил 59
$ ... | xargs grep -h '^func Fuzz' | wc -l → 3 # пак оставил 2
```
**Добавлено дофиксом:** 38 тест-функций и 1 фаззер. ⚠ Первая редакция писала 94/35 — цифра была снята до
правок §34, которые добавили ещё три пина. **Удалено: ноль** — и это проверено дважды: грепом
по диффу (0 удалённых `func Test`) и поимённо по семнадцати пинам, которые отчёт пака называет по имени
(каждый существует ровно один раз).
**Тронуто (тело изменено, сигнатура — нет):** `cmd/tmctl/exitcontract_test.go``TestTheRefusalTableIsTotal`
перестал держать ручной список и читает классы из ИСХОДНИКА обходом `GenDecl`/`ValueSpec` (⚠ не по
`*ast.BasicLit` в `case`, как соседний гейт диспетча: значения `RefusalClass` объявлены КОНСТАНТАМИ, и
копирование той техники воспроизвело бы её собственную слепоту — это её же пинг) ·
`internal/membank/decisions_test.go``TestDecisionByBankID` сверяется с независимо посчитанными
константами · `internal/membank/decisions_fuzz_test.go` — оракул 4 переписан с тавтологии на неподвижную
точку · `internal/pipeline/bankdecisions_test.go` и `statusartifacts_test.go` — сигнатура
`ApplyBankDecisions` получила `context.Context`.
**Новый файл:** `internal/pipeline/bankchain_test.go` (цепь §4.12).
## 28. Состав дофикса
**Новое:** `internal/seed/normalize.go` (канонический вид документа) · `internal/membank/problems.go`
(вердикт загрузчика по субъектам) · `cmd/tmmutate/` (харнесс мутаций + каталог посадок, `python3 -c "import json;print(len(json.load(open('cmd/tmmutate/mutations.json'))))"`**36**) ·
`internal/pipeline/bankchain_test.go`.
**Тронуто:** `internal/membank/decisions.go` (доказывающий рендер, нормализация запроса, набор-осознанный
алиас, перевёрнутое окно, байты результата, `note` не стирается) · `memseed.go`, `memvoice.go` (вердикт
как список субъектов; `Error()` побайтно прежний) · `internal/pipeline/bankdecisions.go` (кап, контекст,
пер-файловый `canonical_rewrite`, `signature{}`, байты из чистого слоя) · `mining.go` (`signatureMapPath`
и `rejectSurfaces` — одно определение на двоих) · `status.go`, `manifest.go` (единственный производитель
конверта путей; конверт на манифесте) · `internal/config/book.go` (попарно различные пути, `book_id` как
элемент пути) · `cmd/tmctl/invocation.go` (гарды на присутствие, довод отказа), `bankapply.go`, `main.go`
(контекст) · **`Makefile`** (пер-пакетный таймаут назван явно — §31, это единственная правка в самой
батарее и она ничего не ослабляет) + семь тест-файлов.
**Едут вместе с деревом и без них его нельзя принять:** `backend/docs/SEAM_FIX_PLAN.md` (записка-план
§7 промта) · `backend/docs/SEAM_PACK_FINDINGS.md` (эта доказательная база) · `backend/docs/SEAM_PACK_PLAN.md`
(план пака, не мой, лежит незакоммиченным с 24.08) · секция «Бэкенд» в `docs/PROGRESS.md` ·
`docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (в нём дописана записка предыдущей сессии, §32).
## 29. Ревью старшей моделью: три дефекта, каждый воспроизведён МНОЙ и починен
Ревью запрашивалось по семи решениям, где заказ оставлял форму на мне. Оно согласилось с пятью, не
согласилось с одним и нашло три вещи, которых я не видел. **Каждую я воспроизвёл сам прежде, чем чинить**
— не потому что не доверяю, а потому что чужой результат, взятый на веру, ровно так же становится нормой.
### 1. Кап в БАЙТАХ не ограничивает ВРЕМЯ — мой довод был неверен
Мой расчёт «1 МиБ ≈ 12 000 решений ≈ 20 с» брал ≈85 байт на решение из документа с заметками. Минимальное
законное решение — `{"action":"decline","src":"t1"}`, ≈35 байт. **Пере-мерено мной, тот же метод:**
| N минимальных `decline` | размер | wall | maxRSS |
|---|---|---|---|
| 5 000 | 174 КБ | **11.32 с** | 40 МБ |
| 12 000 | 421 КБ | **57.96 с** | 70 МБ |
| 20 000 | 709 КБ | **153.5 с** | 112 МБ |
| 28 900 | **1 029 369 Б — ПОД капом** | **308.2 с** | 133 МБ |
То есть кап пускал документ, который платформа убивает по своему 60-секундному бюджету на КАЖДОЙ попытке,
навсегда, — а строка отказа «split it», ради которой класс 14 и заводился, не печаталась никогда. Это тот
самый вечный ретрай, который полоса отказов существует чтобы превращать в действие.
**Починено:** второй кап — на КОЛИЧЕСТВЕ решений, 5 000, тот же класс 14. 11 с в худшей замеренной форме,
пятикратный запас внутри бюджета платформы. Байтовый кап остался пред-разборным сторожем и больше ничего
про время не утверждает. Пин `TestTheDecisionCOUNTIsCappedBecauseTheBYTESCannotBound`, мутация W.
### 2. Субъект поломки нёс РЕНДЕРИНГ — тупик на оси, у которой нет двери
Три проверки из четырёх называют `dst` в тексте сообщения (человеку он нужен), а `dst` — ровно то, что
решение меняет. Ключом по тексту поломка про терм выглядела НОВОЙ в тот момент, когда дверь меняла
рендеринг этого терма. **Воспроизведено мной на живом бинаре:** строка дельты с `gender: badvalue` (ось,
у которой в `Decision` НЕТ поля вообще) + одно решение, меняющее `dst` этого же терма →
```
exit=14
rejected: ['方源→Фан Юань II: gender "badvalue" is not one of male|female|neuter|hidden']
preexisting: ['方源→Фан Юань: gender "badvalue" is not one of male|female|neuter|hidden']
```
Одна и та же поломка по обе стороны, под двумя разными рендерингами, и починить её через дверь нельзя
вовсе. Это §4.2 на уровень ниже — то есть мой фикс §4.2 остановился на шаг раньше.
**Починено:** `subjectOf(class, src, sense, since, until)` — субъект строится ТОЛЬКО из класса поломки и
идентичности терма, никогда из поля, которое дверь переписывает. Четыре проверки получили
субъект-несущие внутренние варианты, экспортированные `[]string`-обёртки не изменились (ни один чужой
вызов не тронут). После фикса тот же вход: `exit=0`, `dst` записан, поломка в `preexisting`. Пин
`TestAFaultTheDoorHasNoChannelForCannotDeadlockIt`, мутация Y.
### 3. И то же самое ещё уровнем выше: поломки, которые ЗАГРУЗЧИК прячет
Найдено мной при проверке п.2. Три проверки живут ПОД загрузчиком банка и на документе, который не
грузится, не выполняются вовсе. Значит, как только решение чинит загружаемость, всё, что они впервые
увидели, читается как новое — и владельца отвергают за починку того, что мешало всему.
**Воспроизведено:** дельта с одной строкой, у которой и `dst: ""` (не грузится), и `gender: alsobad`
(замаскировано) → починка `dst` даёт `exit=14` по gender.
**Первая починка была ПРАВИЛОМ МАСКИ** — «если до вызова документ не грузился, а после грузится, эти
поломки сообщаются и не отвергают ничего» — и она оказалась ХУЖЕ дефекта; как именно, см. §30 п.1.
**Итоговая починка другая и без исключений:** загрузчик отдаёт построенные им записи ДАЖЕ когда
отказывает, поэтому три проверки выполняются по обе стороны, и обычное сравнение по субъекту отвечает на
оба случая само. Пины `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS` (обе половины),
мутации X (снята вместе с правилом), AD.
### 4. Латентная потеря данных на пути nil-байтов
Ревью нашло, я перепроверил чтением: если рендер ТЕКУЩЕГО документа падает и это пред-существующая
поломка, она сообщается, а не отвергает (и это правильно) — но тогда `res.DeltaBytes == nil`, а обвязка
считала `DeltaTouched && changedDoc(raw, nil, …)` → true и **записала бы файл нулевой длины поверх всех
одобрений владельца**. Ревьюер не сумел до этого дойти (130+ подобранных значений после нормализации
прошли round-trip), и я тоже; но `renderProved` существует именно потому, что множество ломающих значений
не известно целиком, и незащищённый `nil` противоречит его собственной предпосылке.
**Починено одной строкой:** nil-байты не пишутся никогда. Поведенческого пина нет по той же причине, по
которой его нет у G, — вход недостижим; инвариант чистого слоя (`accepted ⇒ DeltaBytes != nil`) держит
`FuzzDecisionFieldsThroughTheDoor`.
### 5. Три комментария несли неверные доводы — исправлены
В этом проекте комментарий — контракт, по которому следующая сессия действует, поэтому неверный ДОВОД при
верном коде опаснее опечатки. (а) `normalizedDecisions` объяснял себя тем, что «держит `note`
записываемым» — проверяемо неверно: документ нормализуется ещё раз на выходе. Настоящая цена —
ПРАВДИВОСТЬ ОТЧЁТА: без него повтор решения с пробельным текстом отвечает `applied`, пока байтовый гейт
отвечает `changed: false`. (б) Довод про SIGTERM опирался на длину непрерываемого участка с числом,
неверным в 15 раз; несущий довод — атомарность и порядок записей. (в) «Путь `yaml.Node` закрыт»
преувеличивало: закрыт `Encode`, а РУЧНАЯ сборка узла с явным стилем работает (`encode.go:557-570`) — она
отвергнута по цене (второй рендерер всей схемы), а не по невозможности.
### 6. Ещё две правки по ревью
**Нечитаемая карта подписи** возвращалась как `{surfaces:0, undecided:0}` — побайтно то же, что «всё
решено», единственный ответ, который это поле не должно давать по случайности. Заведено поле
`unreadable`; пин `TestASignatureMapThatCannotBeReadDoesNotReportAsAllDecided`, мутация Z.
**У `tmmutate` опасное значение было ДЕФОЛТНЫМ** (`-root .` правит живое дерево). Флаг стал обязательным;
в шапке названо и то, что отпечаток снимается на старте и потому сертифицирует только дерево, которое
было чистым в тот момент.
### С чем ревью НЕ согласилось и что я оставил как есть
Ничего: единственное «DISAGREE» — кап (п.1) — исполнено. Пять решений подтверждены, у двух из них ревью
дало довод СИЛЬНЕЕ моего, и я заменил довод в комментарии: у конверта путей на манифесте — что бамп
версии выбросил бы все сохранённые сайдкары и пере-нарезал каждую книгу ради аддитивного поля; у SIGTERM
— атомарность вместо длины.
## 30. Интервальная самоверификация субагентом: ещё пять дефектов, все воспроизведены и починены
Ревью гонялось В СЕРЕДИНЕ работы против семи ЯВНЫХ критериев (§5.1 промта), read-only, в копии.
Дерево двигалось под ним трижды, поэтому его вердикты пере-исполнены на финальном снимке. Каждый дефект
я воспроизвёл сам, своим прогоном, прежде чем чинить.
### 1. ⛔ Правило маски проглатывало ливлок, который вносил САМ вызов
Моё же исправление §29 п.3 оказалось хуже дефекта. **Воспроизведено мной:** дельта = битая строка
(`dst: ""`) + `Hero→Odin` с алиасом `Shared`; решения = починить битую строку + одобрить `Shared→Dva`.
```
маска снята: rejected=0 accepted=2 bytes=true
preexisting=[firing key "shared" is shared by different terms "Hero"→"Odin" и "Shared"→"Dva" … (livelock…)]
контроль (документ грузится): rejected=1, та же строка — ОТКАЗ
```
То есть дверь диагностировала детерминированный ливлок матчера, **записала документ** и отчиталась
`exit 0`, назвав СВОЮ поломку чужой. Комментарий правила («состояние после такого вызова никогда не хуже,
чем до») был ложью ровно здесь: до — банк не грузился (громкий стоп), после — грузится и инжектит ливлок.
И два опубликованных поля (`Preexisting`, `preexisting_problems`, оба обещающие «этот вызов их не
вызывал») становились ложными утверждениями.
**Починено иначе и без исключений:** правило маски снято целиком; вместо него `ParseBankSeed` возвращает
записи, которые он ПОСТРОИЛ, даже когда отказывает. Тогда три проверки под загрузчиком выполняются и на
«до», и на «после», сравнение по субъекту работает как везде, и оба случая решаются одним правилом:
починка загружаемости больше не отвергается (поломка видна с обеих сторон), а внесённый ливлок
отвергается (его субъекта на «до» нет). После фикса тот же вход: `rejected=1`, байты не выданы.
Пины — обе половины `TestRepairingAnUnloadableDocumentIsNotRefusedOverWhatItUNMASKS`, мутация AD.
### 2. Побег §4.7 принимал инертный отказ
`ownsSurface(in.Delta, …)` пропускал проверку, когда поверхность была термом дельты сама по себе.
**Воспроизведено:** дельта `Nick→Nik` и `Hero→Nik2 (alias Nick)`, `decline Nick``accepted=1`, строка
`Nick` снята, а `Hero` ПРОДОЛЖАЕТ фарить `Nick`. Ровно тот вред, ради которого §4.7 и заказан.
Замерено дополнительно: во всех остальных входах побег — no-op (после `dropTerms` алиасом никто не
владеет), то есть он менял ответ только во вредную сторону, и зеркальный случай, ради которого он якобы
существовал (отказ терму + отказ его алиасу), работает без него.
**Починено:** побег снят. Пин `TestADeclineIsRefusedWhenTheSurfaceGOESONFiringAsAnotherTermsAlias`,
мутация AB.
### 3. И то же правило ОТВЕРГАЛО законный отказ, с доводом, который неверен
`aliasOwner` не смотрел на СТАТУС. **Воспроизведено:** дельта `Hero (status: auto, alias Nick)`,
`decline Nick` → отказ: «"Nick" is an ALIAS of the approved term "Hero"→"" … already excluded from
proposals». Обе половины довода ложны: строка не approved, а `unsignedEngineSurfaces` выбрасывает mined
строки со статусом ≠ `approved`, то есть её поверхности из предложений НЕ исключены и отказ как раз
осмыслен. `status: auto` в дельте — не угол: это ровно состояние, о котором первая мина формата строки
199. **Починено:** `aliasOwner` судит только подписанные строки (пустой статус = approved, как дефолтит
загрузчик). Пин `TestDecliningTheAliasOfAnUNSIGNEDRowIsAccepted`, мутация AC.
### 4. §4.14 был исполнен неполно — и тем же способом, против которого заказан
Довод отказа `--keys-file` не описывал `bank-apply` — глагол, который этот пак и добавил. А тест,
написанный ЗАКРЫТЬ §4.14, перечислял четыре команды ручным списком — ровно слабость, против которой
поднят §4.11(в), воспроизведённая в тесте про неё же. **Починено:** довод называет все девять; тест
перечисляет из `dispatchCommands` и требует, чтобы имя каждой команды встречалось в её собственном
отказе. Мутация AF.
### 5. Гейт тотальности читал ОДИН файл и одну форму объявления
`refusalClassesFromSource` разбирал `refusal.go` и требовал явного `vs.Type == RefusalClass`. Класс,
объявленный в соседнем файле пакета или конверсией `RefusalX = RefusalClass("x")`, был ему невидим —
та же слепота, ради снятия которой гейт и переписан. **Починено:** `parser.ParseDir` по всему пакету и
обе формы объявления.
### Плюс два, которые ревью нашло, а я закрыл превентивно
**Позиционные субъекты** («record N has no identity») не были запинены вовсе — добавлен
`TestTwoRecordsWithNoIdentityDoNotBlockEachOthersRepair` и мутация AE.
**Фаззер двери возвращался рано на отказе** и потому не видел бы возврата блокер-класса. Оракул отказа
усилен: отказ обязан не выдать байтов, не отчитаться о принятых решениях и не изменить ни один документ —
то есть та же слабость, за которую §4.15 критикует старый оракул 4, снята и здесь.
### Что ревью подтвердило исполнением (не переделывал)
Нормализация идемпотентна на 50-строчном адверсариальном корпусе и КОПИРУЕТ (проверено мутацией выхода
при неподвижном входе) · ложного провала `renderProved` найти не удалось (400 000 случайных строк по
алфавиту управляющих/пунктуации/CJK, невалидный UTF-8, NUL, C0/C1, одиночные суррогаты) · оба капа не
off-by-one · после второй проверки контекста ничего не пишется, `defer lock.Release()` стоит сразу за
захватом · общности не нарушено ни одной новой строкой · `TestDecisionByBankID` и пин на инъективность
действительно независимы (все три константы пере-считаны в Python по спецификации из комментария
`TermID`) · `SeedProblems.Error()` побайтно равен прежнему склеенному сообщению.
**Унаследованное замечание, не дефект пака:** всё, на чём эта дверь ключуется, проходит через
`internal/text/norm.go`, где таблица `trad2simp` и ветка катакана→хирагана живут В GO, а не в langpack'е.
Пара, которой в репо нет, деградирует до NFKC+lowercase — то есть не блокирована, но фолд Go-резидентный.
Пингом, не правкой: это шире двери.
## 31. Батареи после всех правок — и одна правка в самой батарее
| команда | результат |
|---|---|
| `make lint` | `0 issues.` |
| `go vet ./...` + `go vet -tags live ./...` + оба прохода `-vettool` | чисто |
| `gofmt -l .` | пусто |
| **`make battery`** | **`EXIT=0` — ПОЛНОСТЬЮ зелено**, включая оба названных промтом `-race`-флейка |
| **`make battery-stand`** | `EXIT=2`, красен **ровно `TestMinerFullBookParity`** — ожидаемая картина промта: нет jieba-контраста (`eval/exp16/data/jieba_dict_general_zh.txt`), он лежит по внутрирепозиторному пути, но вне git по построению, строка **123**. Больше ничего |
**Обе строки сняты ПОСЛЕ рефакторинга §33 и после всех правок по воркфлоу-ревью §34** — то есть
описывают дерево, которое передаётся.
**ОДНО утверждение о детерминизме, чтобы приёмка не гадала.** Два store-теста
(`TestKillMinus9LosesAtMostOneCall`, `TestTheSeamIsNotChargedToTheStoreOperationBudget`) ведут себя так:
в ПОЛНОЙ батарее на ненагруженной машине они проходят (строка выше, `EXIT=0`); под нагрузкой
параллельных сессий падают; а запущенные ИЗОЛИРОВАННО через `-run` падают **детерминированно и на обеих
сторонах A/B** — они сознательно сжимают `opTimeout`, и в одиночку сжатого времени не хватает. То есть:
красное в этих двух — ожидаемо и НЕ пак (доказано A/B ниже); зелёное в них — тоже нормальный исход;
красное в любом ДРУГОМ тесте — регрессия.
**Корпус под строгим загрузчиком — 45 из 45 чисто**, двумя командами (строка вынесена из таблицы: она
несёт команды, а не одно число):
```
$ for f in $(find books backend -name "*seed*.yaml" -o -name "*mined-delta*.yaml" \
-o -name "*auto-bank*.yaml" -o -name "*mined-signature*.yaml" | grep -v '\.bak'); do
tmctl seed-lint --seed "$f" >/dev/null || echo "FAIL $f"; done # → 30 OK / 0 FAIL
$ find books backend -name "*mined-rejects*.yaml" | wc -l # → 15
$ … | while read f; do python3 -c 'import sys,yaml;d=yaml.safe_load(open(sys.argv[1]))or{};
assert set(d)<={"rejects"};
[__import__("sys").exit(1) for r in (d.get("rejects") or []) if set(r)-{"src","note"}]' "$f" \
|| echo "FAIL $f"; done # → 15 OK / 0 FAIL
```
### Атрибуция красного — ЗАМЕРОМ против HEAD, а не «промт сказал»
`git worktree add --detach <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
```
Изолированный прогон обеих сторон даёт БУКВАЛЬНО одинаковый вывод — тот же тест, то же сообщение, тот же
номер миграции:
```
--- FAIL: TestTheSeamIsNotChargedToTheStoreOperationBudget (0.85s)
migrate_test.go:41: store: apply migration 9: context deadline exceeded
```
То есть на дереве, которого пак не касался, этот тест в изоляции падает ДЕТЕРМИНИРОВАННО: он сознательно
сжимает `opTimeout`, и в одиночку сжатого времени не хватает. Атрибуция закрыта — ни пак, ни дофикс тут
ни при чём, и это МОЙ замер, а не унаследованное утверждение. Тесты не тронуты (D39.121).
`TestManifestOfAnOlderDocumentVersionIsRefused` в стенд-батарее упал ЧИСТО от нагрузки: 5 из 5 зелёных
на обеих сторонах.
### ⚠ Одна правка В САМОЙ батарее, и вот замер, из-за которого она сделана
`Makefile`: `go test ./... -race -count=1``… -timeout=20m` (в обеих целях).
**Почему.** Go по умолчанию даёт пакету 10 минут. Замерено: `internal/pipeline` под `-race` идёт
**557.7 с** (`/usr/bin/time`, машина с ТРЕМЯ параллельными агент-сессиями, load average 312 в моменты
замера) — ⚠ первая редакция называла это «на незагруженной машине», и слово было НЕВЕРНО: нагрузка была,
что и вскрыла приёмка, намерив 92.8/97.6 с на действительно свободной машине. Оба замера настоящие —
разброс ×6 даёт сама нагрузка, и под ней два прогона батареи подряд закончились
`panic: test timed out after 10m0s` по всему пакету. Это читается как падение и падением не является.
Довод в `Makefile` переписан на диапазон с условиями, без слова idle и без привязки к машине (заказ §8
промта FIX2 закрыт этой правкой — оркестратор уведомлён).
**Это НЕ подгонка гейта под зелень:** таймаут — не утверждение. Ни один тест не ослаблен, ни один не
удалён; те же самые тесты просто доходят до конца. Пакет, который действительно ЗАВИС, всё так же
падает — на двадцатой минуте вместо десятой.
**Чего я НЕ доказал:** сколько из 557.7 с добавил дофикс. База в начале сессии на нетронутом дереве пака
дала 474.2 с, но внешняя нагрузка между двумя замерами не контролировалась, поэтому разницу я НЕ
атрибутирую. Что измерено точно: **все одиннадцать моих новых тестов этого пакета стоят 10.5 с** под
`-race` из 557.7 с, то есть 1.9 % — не они.
## 32. Записка предыдущей сессии — попунктно, включая одно ОПРОВЕРЖЕНИЕ
Записка живёт в хвосте `docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md` (её дописала бэкенд-сессия пака перед
компактом своего контекста). Прошёл по ней целиком; она уехала в работу почти вся.
| что говорила записка | что вышло |
|---|---|
| «§4.1/§4.2/§4.4/§4.7/§4.8 я перепроверил сам, не трать время» | всё равно воспроизвёл сам — три из пяти живьём на копии книги стенда. Совпало |
| **«промт ШИРЕ замера: `dst` и `sense` с ведущим `\n` round-trip НЕ ломают»** | **ОПРОВЕРГНУТО моим замером (§15): ломают.** Ломается любое строковое поле обеих схем. Прав был ПРОМТ, а не записка — и если бы я поверил записке, фикс был бы заплаткой на одно поле |
| «харнесс мутаций умер вместе с сессией, пиши заново, сразу с отпечатком целей» | написан заново, **положен в репозиторий** с отпечатком до и после. Отпечатка оказалось мало — см. §25 |
| «одиночный прогон — не замер» | применено: перепроверка одного подозрительного `RED` изолированным прогоном вскрыла дефект самого харнесса |
| «субагенты пишут в твоё дерево, сверяйся `git ls-files --others`» | обоим ревьюерам запрещено писать, сверено в конце; свои два пробных файла удалил и проверил отсутствие |
| «красное в `internal/store` — почти наверняка не ты, но ПРОВЕРЬ ЗАМЕРОМ против HEAD» | сделан A/B против `git worktree` на HEAD (§31). Проверка была не лишней: она дала более сильный результат, чем ожидала записка |
| сомнение §4.9 «дверь этого не знает» | верно про предсказание — и оказалось, что дверь знает ДРУГОЕ и достаточное (§22) |
| сомнение §4.10 «кап может сделать проблему несуществующей — замерь ПОСЛЕ капа» | замерил: ~11 с, механизм с носителем. Но несущий довод другой — атомарность, а не длительность (§29 п.5) |
| сомнение §4.6 «сделал бы и назвал компромисс вслух» | так и сделано |
| сомнение §4.13 «`note` нельзя очистить — реши осознанно» | решено; попутно нашлась несогласованность, о которой записка не знала: `approve` заметку хранил, `decline` СТИРАЛ |
| «не наследуй ни моё, ни приёмочное число памяти, пере-мерь» | пере-мерено; сошлось с приёмкой, не с паком (§18) |
| «сначала посади мутацию, потом пиши пин» | применено; дважды поймало ВАКУУМНЫЕ пины моего же авторства (§16) |
| онбординг в код: два слоя, карта пинов, ловушки стенда (три файла с мёртвыми путями, схема v11, нет jieba) | всё три ловушки сработали ровно как описано; без записки это стоило бы нескольких заходов вслепую |
**Вывод для оркестратора:** записка была полезной и один раз ВРЕДНОЙ. Она не помечена как «проверено
исполнением» построчно, а её самый уверенный абзац оказался неверным. Полезен формат, а не авторитет.
## 33. Рефакторинг по итогам вопроса о качестве Go-кода
Замер, а не вкус — но замер пришлось делать ДВАЖДЫ, и первый был неверен.
⚠ **Первая редакция этой секции утверждала, что две точки входа были «самыми длинными функциями своих
пакетов», а потолок пакета — 75.** И то и другое НЕВЕРНО: правило, которым я считал, брало строки от
`^func` до следующего `^func` вместо тела функции, и на методах давало мусор. По правилу, считающему
ТЕЛО (от `^func` до первой строки ровно `}`), максимумы пакетов совсем другие: `internal/pipeline` — 218
(`Status`), `internal/membank` — 105 (`loadVoiceSections`). Никакой «нормы репозитория в 75» нет.
**Что верно по правильному замеру и чего хватает для довода:** две точки входа были **92 и 90** строк
кода при том, что следующая по длине функция в тех же двух файлах — **42**. То есть вход был вдвое
длиннее всего остального в самой двери, которую он и представляет.
Обе разобраны на именованные фазы — чистое извлечение, без изменения поведения:
```
ApplyDecisions → resolveAll · refuseSeedConflicts · foldAccepted · refuseInertDeclines · refuseNewFaults
ApplyBankDecisions → openDecisionRequest · readBookState · decisionReport · changedFiles · writeDecisionFiles
```
Тело `ApplyDecisions` стало последовательностью из пяти судящих фаз, из которых **ровно одна** что-то
меняет (`foldAccepted`) — это и есть контракт двери, теперь видимый в самом коде, а не только в
комментарии. Самая длинная функция обоих файлов после: **44** строки (`signatureState`), по команде
ниже. ⚠ Две прежние редакции писали здесь 45 и 48 — первая устарела вместе с правками §34, вторая
считалась неверным правилом.
**Команда** (правило §14 «каждое число — с командой»): непустые строки ТЕЛА, от `^func` до первой
строки ровно `}`, минус начинающиеся с `//`:
```
python3 - <<'EOF'
import pathlib,re,glob
def funcs(f):
src=pathlib.Path(f).read_text().splitlines(); out=[]
for i,l in enumerate(src):
if not l.startswith("func "): continue
j=i+1
while j<len(src) and src[j]!="}": j+=1
n=re.sub(r'^func\s+(\([^)]*\)\s*)?','',l).split("(")[0]
out.append((len([x for x in src[i:j+1] if x.strip() and not x.strip().startswith("//")]),n))
return out
for f in ("internal/membank/decisions.go","internal/pipeline/bankdecisions.go"):
for c,n in sorted(funcs(f),reverse=True)[:3]: print(c,n,f)
EOF
```
**Чем доказано, что поведение не изменилось:** весь каталог пере-прогнан после рефакторинга и дал ту же
картину, плюс зелёные пакеты и `make lint` — 0 issues.
**И вот чем это доказательство ОКАЗАЛОСЬ СЛАБЕЕ, чем здесь было написано.** Первая редакция этой
секции утверждала «32/32 как записано». На деле рефакторинг сломал ДВЕ записи каталога — они ссылались на
переменную, которую он убрал из области видимости, — и инструмент считал провал СБОРКИ красным, то есть
две записи не проверяли ничего, а отчёт называл тесты, которые не могли отработать. Нашло воркфлоу-ревью,
я воспроизвёл. Обе записи пере-выражены, у инструмента появился третий вердикт, и после этого прогон
даёт **36 посадок, 0 неожиданных исходов** — см. §34.
## 34. Воркфлоу-ревью: шесть осей, 50 находок, 26 переживших скептиков
Третье и последнее ревью — **воркфлоу**, а не одиночный агент: шесть независимых полос читают дерево
параллельно, каждая находка отдаётся отдельному скептику с заданием ЕЁ ОПРОВЕРГНУТЬ, выживает только то,
что он не смог. Полосы выбраны по тому, чего не касались два предыдущих ревью: гонка двух `bank-apply` ·
последствия новых полей для платформы (read-only) · эквивалентность только что сделанного рефакторинга ·
сам `cmd/tmmutate` (он теперь в репозитории, значит продакшн-код) · честность хендофф-документов ·
общность и безопасность данных. **50 сырых находок, 26 пережили скептиков** (2 мажора, 12 миноров,
12 в ранге «пинг» — из которых два настоящих пинга ниже, два «дефекта нет, проверено», а остальные восемь
по существу мелкие правки комментариев и документов и сделаны здесь же). Каждую я перепроверил своим
прогоном прежде, чем чинить.
### ⛔ Главное: две посадки каталога НЕ КОМПИЛИРОВАЛИСЬ — и это внёс мой же рефакторинг
`P-alias-judged-on-the-set` и `AB-alias-escape` патчат тело `refuseInertDeclines`, ссылаясь на `in.Delta`.
Рефакторинг §33 убрал `in` из области видимости этой функции. **Проверено мной:** прогон всех 36 посадок
с `go build` после каждой — ровно эти две не собираются (`undefined: in`).
А инструмент считал их КРАСНЫМИ, потому что `red` был буквально `terr != nil`, и провал СБОРКИ выглядел
так же, как поймавший пин. То есть §33 «весь каталог пере-прогнан, 32/32 как записано» было **слабее, чем
я написал**: две записи не проверяли ничего, а отчёт называл конкретные тесты, которые не могли отработать.
**Починено на двух уровнях.**
(1) Обе записи пере-выражены так, чтобы компилироваться против новой сигнатуры, и теперь краснеют на
своих пинах поимённо.
(2) У инструмента появился ТРЕТИЙ вердикт. `RED` теперь требует, чтобы хоть один тест назвал себя
(`--- FAIL:`); прогон, в котором не отчитался ни один тест, — это `NOTHING`, и он **роняет гейт**. Плюс
перед первой посадкой каждый пакет прогоняется НЕТРОНУТЫМ и обязан быть зелёным: без базовой линии
«этот пин ловит эту мутацию» опирается на код возврата, который выдают также сборка, флейк, таймаут и
опечатка в имени пакета. **Негативная проба:** посадка, которая заведомо не компилируется, теперь даёт
`NOTHING … 1 unexpected outcome(s)` и exit 1.
### Продуктовые дефекты — все воспроизведены мной
| что | как это выглядело | починка |
|---|---|---|
| **остановленный вызов ПЕЧАТАЛ, что применил** | на отмене контекста отчёт уходил в stdout с `mode:"apply"`, `changed:true` и `state:"applied"` — при нуле записанных байт. Отличал их только код выхода | `mode:"stopped"`, `changed:false`. Пин `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 |
### Пинги, а не правки
- **⛔ Арбитр покрывает ПРОЕКТ, а не пути.** Flock берётся на `<project_db>.lock`, пишутся
`mined_delta`/`mined_rejects`, которые `book.yaml` может направить куда угодно. Две книги с РАЗНЫМИ
`project_db` и ОДНИМ delta друг друга не исключают: ревью продемонстрировало гонку на 1500 одобрений с
каждой стороны — одна сторона потеряла всё, `exit 0`, отчёт «применено». Внутрикнижную версию этого
§4.3 запрещает; межкнижная законна сегодня. **Выбор между вторым арбитром и отказом при загрузке
трогает ратифицированный закон п.2 — это не решение одного пака.** В коде теперь стоит явное «арбитр
покрывает проект, а не пути», а не молчаливое допущение. ⚠ В корпусе нарушителей нет: все объявленные
пути лежат внутри своей книги (мёртвый префикс `/home/ubuntu/` — это переезд машины, D39.157, а не
чужой каталог).
- **Карта подписи пишется обычным `os.WriteFile`**, мимо атомарной конвенции сайдкаров этого же пакета —
пред-существующее, не мой код.
### Что ревью ПОДТВЕРДИЛО (и это тоже результат)
**Рефакторинг §33 — эквивалентен**: порядок фаз, мутация `rs[i].ok` через слайс и место отката проверены
отдельной полосой, расхождений нет. **Платформа не ломается**: аддитивные поля отчёта, конверт
`artifacts{}` на манифесте без бампа версии, код 14 и размерные капы — прослежены до её кода, дефектов
нет. Исправлены при этом два комментария, утверждавших про чужую зону неверное: код 5 платформа пишет как
`stopped` (и только при зафиксированном намерении остановиться, иначе `failed`) — не `paused`; и отчёт
печатается НЕ в каждом исходе — три отказа (10, 14 до чтения книги, 12) не печатают ничего.
### Правки в самих документах
Ревью нашло семь мест, где документ нарушал собственное правило «каждое число — с командой»: «≈13 % по
памяти» противоречило трём числам рядом (диапазоны перекрываются — снято как НЕ измеренное) · размер
каталога назывался и 24, и 32 · §26 ссылалась на снятую мутацию X · §28 не перечисляла три документа,
без которых дерево нельзя принять · «20 из 24» не воспроизводится нынешним каталогом (помечено как
исторический замер) · дайджест дерева снят как невоспроизводимая константа · строка про `aliasOwner`
описывала проверку, которой в коде нет. Все семь исправлены.
## 35. Проверка ЗАКРЫТИЯ — отдельным воркфлоу, и она нашла, что закрыто не всё
На вопрос «все замечания исправлены?» я не стал отвечать по памяти: четыре независимых агента прошли по
всем 26 пережившим находкам §34 и по живому дереву решали КАЖДУЮ — закрыта / не закрыта / частично /
отклонена осознанно. **Из 28 проверенных пунктов закрытыми оказались 16, осознанно отклонёнными 2,
частично 7 и НЕ закрытыми 3.**
Что доделано по этой проверке:
| остаток | что было не так | как закрыто |
|---|---|---|
| **F4** | остановленный вызов перестал врать в `mode`/`changed`, но `accepted[]` по-прежнему нёс `state:"applied"`а это ровно то поле, которое потребитель перебирает | список очищается, как и на пути отказа; пин расширен, мутация AG бьёт по всем трём полям |
| **F16** | новая формулировка сама сделала исчерпывающее заявление («три отказа») и промахнулась: `schema_mismatch` (13) тоже печатает пустоту | перечислены все четыре плюс два не-отказных случая (5 и 1) |
| **F17** | тот же ложный клейм про платформу выжил в ЭТОМ ЖЕ паке, в `bankdecisions_test.go`; плюс ссылка `:755` на строку `:756` | оба исправлены; `grep paused` по движку — ложных клеймов не осталось |
| **§33** | ⛔ «две точки входа — самые длинные функции своих пакетов, потолок пакета 75» **НЕВЕРНО**: правило считало не тела функций. Настоящие максимумы — 218 (`Status`) и 105 (`loadVoiceSections`) | утверждение снято, довод пере-сформулирован на том, что верно (92/90 против 42 у следующей функции тех же двух файлов), приведена команда |
| **§16** | секция, которая ПРЕДСТАВЛЯЕТ каталог, называла его размер неверно (32/31 при 36 записях) и не несла четырёх строк §34 | пере-считано командой, строки добавлены |
| **§18** | в списке «пер-решенческих помощников» стояла `ownsSurface`, которой в коде нет — довод Θ(N²) частично опирался на несуществующую функцию | заменена на реально существующую `deltaHoldsSurface` |
| **§26** | строка §4.7 в одном предложении противоречила и себе, и коду, и §21 | пере-написана по коду |
| **§31** | два разных утверждения о детерминизме store-флейков в одной секции (`EXIT=0` против `5×FAIL`) | одно: в полной батарее на ненагруженной машине зелены, под нагрузкой падают, ИЗОЛИРОВАННО падают детерминированно на обеих сторонах A/B |
| **§15 ↔ §18** | две несогласованные тройки секунд на 10 000 решений | сказано, что абсолютные числа между секциями не складываются, и что ≈5 % выведены только из пары, снятой подряд |
| **§27** | 94 тест-функции / «добавлено 35» — снято ДО правок §34 | 97 / 38, командой |
| **§31, корпус** | строка «45 из 45» стояла ВНЕ таблицы (сломанный markdown) и её вторая половина не несла команды | вынесена блоком с обеими командами |
| **журнал** | пинг пака «1.6с на 5 000 / 9.1с на 20 000» — датум, которым платформенный пак будет мерить таймаут, расходится с моим замером в 316 раз; пинг «кап — однострочник при лендинге» уже сделан; четыре пинга жили только здесь | в журнал добавлен блок «что в записи пака устарело» и четыре недостающих пинга |
**Что осталось ОСОЗНАННО не сделанным** — два пункта, оба вне полномочий пака: межкнижная гонка за один
`mined_delta` (трогает ратифицированный закон п.2) и запись карты подписи обычным `os.WriteFile`
(пред-существующее, не мой код).
**Вывод, который стоит унести дальше самого пака.** Это было ТРЕТЬЕ ревью подряд, и каждое находило
настоящие дефекты, которых не видели предыдущие: одиночный старший ревьюер — три, интервальное — пять,
воркфлоу — двадцать шесть, проверка закрытия — ещё десять остатков. **Ни одно «все замечания исправлены»
в этой сессии не оказалось верным при проверке.** Вывод не «ревью бесполезны», а обратный: закрытие надо
ПРОВЕРЯТЬ отдельно от починки, и не тем, кто чинил.
---
# ДОФИКС-2 (промт BACKEND_SEAM_FIX2_SESSION_PROMPT.md, 13 пунктов §4) — секции 36+
## 36. Состав фикс2-пака
**Новое:** `internal/store/stopmemory.go` + миграция v16 (`bank_stop_presented`) · `internal/seed/signaturemap.go`
(конверт карты: `map_version` + `map_id` от содержимого) · `internal/pipeline/bankreport_test.go` ·
`internal/pipeline/stopflag_test.go` · `internal/store/stopmemory_test.go` · `internal/seed/normalize_test.go`.
**Тронуто:** `internal/pipeline/mining.go` (предикат новизны · разделение «стоп»/«авто-провод» · запись памяти ·
атомарная запись карты с конвертом · снос формул старой модели) · `internal/pipeline/bankdecisions.go`
(вердикт/исход, `finishReport` — единственная точка фиксации; staged-запись обоих файлов + fsync каталога;
кап-отчёты; `written_delta`/`written_rejects`; пере-док `SignatureState`) · `internal/pipeline/refusal.go`
(два яруса полосы + класс `write_incomplete`) · `internal/pipeline/artifact.go` (staged-вариант + `syncDir`) ·
`internal/config/book.go` (`filepath.Clean` абсолютных + retired-отказ ключей `mined_delta`/`mined_rejects`) ·
`internal/membank/decisions.go` (`RenderSignatureMap` с доказательством; версия отчёта → **v2**) ·
`internal/miner/miner_emit.go` (`DeltaFile`) · `cmd/tmctl/main.go` (exit **15**), `bankapply.go`, `render.go` ·
`internal/pipeline/waverun.go` (док стопа) · тесты: `bankchain_test.go` (переписан под модель флажка, две цепи),
`miningstop_join_test.go` (два теста переписаны с старой семантики), `internal/config/decisionpaths_test.go`
(переписан под retired-модель), `internal/membank/decisions_test.go` (+3 пина) · `cmd/tmmutate/mutations.json`
(36 → **56** записей: 20 новых FX* + 6 ре-якорено после рефакторинга).
## 37. Решения §4.1§4.3, каждое с доводом — и названные остатки
- **Память** — плоское множество нормализованных поверхностей всех предъявленных кластеров (Src + Aliases),
таблица `bank_stop_presented (book_id, surface)`. Кластерность живёт в ПРЕДИКАТЕ (пересечение), не в схеме:
для вопроса «есть ли кластер вне памяти» объединение множеств поверхностей эквивалентно хранению кластеров,
а строк меньше и мигрировать нечего. Рост — только вширь (`INSERT OR IGNORE`), обрезки нет по букве промта.
- **Двусторонний остаток кластерной памяти** записан пином, а не только словами
(`TestAClusterSharingAnAliasWithTheMemoryReadsAsPresented`): мягкая сторона — лишний стоп на новом написании;
вредная — новая сущность, делящая поверхность-алиас с виденным кластером, предъявлена НЕ будет (поедет
редактору авто-строкой). Обе — принятые остатки решения, смена трейда = новое решение.
- **Порядок записи**: карта (АТОМАРНО, §4.10 сделан первым) → память, в момент стопа. **Отказ записи памяти
НЕ валит состоявшийся стоп**: карта на диске, пауза настоящая; цена — один доброкачественный повторный стоп
(ERROR-строка в логе называет это). Валить прогон значило бы превратить законную паузу в аварию из-за
бухгалтерской строки. Между файлом и SQLite общей транзакции нет — принятый остаток, направление потерь
безопасное (повторный стоп, не молчаливая потеря).
- **Остановившийся прогон авто-банк НЕ пишет** — названное решение (§4.3 ⚠): стоп — граница владельца, и
запись банка на ней меняла бы состояние, которое владелец как раз рассматривает; возобновление без решений
доводит авто-провод на следующем прогоне (сходится, проверено цепью Б, прогон 3). Чекпоинты терминолога
делают оба варианта $0-эквивалентными — выбран тот, что сохраняет артефактный набор стопа прежним.
- **Черновая волна (draft-only)**: карта пишется, стопа нет, памяти нет (по букве §4.2); авто-провод теперь
исполняется и при поднятом флаге (раньше — нет; это изменение поведения, названо здесь).
- **Флаг-поднят-нового-нет** — новое состояние: WARN-строка «--verify-bank is raised and the delta is
non-empty, but every cluster... already presented» + авто-провод + полноценное продолжение. Молчание больше
не спутать с «майнинг не сконфигурирован».
- **Миграция**: v15→v16 — новая таблица `CREATE TABLE IF NOT EXISTS` (дешёвая форма из промта; ALTER не
трогали, долг 49а не задет). ⚠ Голова схемы двигается: `OpenReadOnly` отказывает классом 13 всем
непере-мигрированным книгам до `tmctl migrate` — порядок деплоя «migrate первым» это покрывает (D39.134 п.3),
живьём проверено на копии стенда (v11 → v16, пре-миграционный бэкап, integrity green). После апгрейда память
пуста → ОДИН доброкачественный стоп на первом verify-прогоне каждой книги — миграционная нота в журнал.
## 38. Заявление = команда: замеры фикс2
- **§4.4 пере-замер своей командой** (числа промта 2110/442/117/325 первоисточника в дереве не имеют — не
цитирую). Первоисточник ТЕПЕРЬ В ДЕРЕВЕ (упрёк проверки закрытия принят): постоянный якорь-пин
`internal/miner/emissioncap_test.go` (`TestTheEmissionCapPrecedesTheFiltersAndTheTailIsUnreachable`) —
тот же корпус (фамилии langpack × имена, 600 имён по 6 вхождений в 6 разных рамках), утверждает ТЕКУЩЕЕ
поведение и краснеет в день сдвига потолка или порядка фильтров, приводя двигающего к пингу
(прецедент — токенование якоря пинга живым фактом, коммит 0f4c556). Замер той сессии:
`ranked=2354 · годных во всём алфавите=209 · годных в окне потолка=11 · невидимых постоянно=198`;
отклонение ВСЕХ 11 годных окна (9 эмитированных + алиасы) → второй прогон: `emitted=0, fresh_surfaces=0`
ни один из 198 не поднялся. Структурная причина — `miner_emit.go` (`ranked = ranked[:emitRankCap]` ДО
`emissionEligible`, сид- и реджект-скипы тратят слоты ВНУТРИ окна). Картина промта подтверждена на моих
числах; заказ — не лечить, пинг в журнале.
- **Каталог мутаций**: `go run ./cmd/tmmutate -root <копия>`**56 записей, 0 неожиданных исходов**
(55 RED + записанный выживший G-byte-gate). Все 20 новых FX-посадок краснеют СВОИМИ пинами (в выводе
инструмента поимённо); ре-якорены после рефакторинга: E3 (пере-выражена: предмет «declared» умер с §4.9(б),
новая — «unreadable ≠ undecided»), H, S, Z, AH, AG, AI.
- **Пины §4.11, проверенные посадкой ДО записи в каталог** (правило «сначала посадка, потом пин»): посадка
`subjectOf → class` вскрыла, что мой первый «межтермовый» пин держится гардом shared-key (его класс-строка
несёт идентичность) и для этой мутации ВАКУУМЕН — довод пина переписан честно, мутацию ловит оконный пин
(`TestANewFaultIsNotMaskedByTheSameTermsFaultInAnotherWindow`), что и записано в каталоге.
- **Батарея**: `go build ./... && go vet ./... && go test ./... -count=1` — зелёная целиком; `make lint`
0 issues; `make battery` — EXIT=0. Замер времени не публикую: прогоны шли под параллельной нагрузкой
(агент-ревью и мутационный харнесс), а время под нагрузкой этот проект уже дважды обжигало (§31).
## 39. Живые пробы фикс2 — на КОПИИ книги стенда (minirun-verify2), в books/ не записано ничего
Копия вне дерева; мёртвые пути переправлены в трёх файлах (book.yaml, pipeline.yaml, pairs/zh-ru.yaml);
две строки снятых ключей удалены в копии (сам стенд — зона оркестратора, он в курсе). jieba-контраста на
машине нет — стоп/цепь кроются харнессом (`miningstop_join_test.go`, `bankchain_test.go`), как и в прошлый раз.
| проба | результат |
|---|---|
| немигрированный проект (v11 против v16) | `exit=13`, текст называет `tmctl migrate` |
| `tmctl migrate` | `v11 -> v16`, пре-миграционный бэкап, integrity green |
| approve+decline одним документом | `exit=0`, отчёт **v2**: `mode=apply`, `written_delta=true`, `written_rejects=true`, оба файла по конвенции, note с ведущим `\n` записана и читается |
| повтор того же документа | `exit=0`, `changed=false`, оба `already_applied`, `written_*=false` |
| проекция | `exit=0`, `mode=projection`, `written_*=false` |
| решение о ПОДПИСАННОМ сид-терме | `exit=14`, `mode=refused`, причина называет сид и цену (base snapshot) |
| снятый ключ `mined_delta:` в book.yaml | `exit=10`, отказ называет retired + конвенцию + лечение |
| документ на 5001 решение | `exit=14` И отчёт на stdout: `mode=refused`, `rejected[0].index=-1`, `signature.unreadable=true` |
| запись в несписываемый каталог (книга без базы, лок пред-создан) | **`exit=15`**, `mode=write_incomplete`, `written_*={false,false}`, `accepted=[]`; после chmod повтор ТОГО ЖЕ документа → `exit=0` |
| абсолютный `project_db` со вставкой `/./` на конвенционный путь дельты | `exit=10`, отказ называет оба ключа |
| sha256 стендовой базы | не сдвинулся; `git -C books status` чист |
## 40. Дифф `^func Test` — исполнением
```
$ git diff -U0 -- . | grep -cE "^-func (Test|Fuzz)" → 0 # удалено: НОЛЬ
$ git ls-files --others --exclude-standard -- . | grep '_test.go$' \
| xargs grep -h '^func Test' | wc -l → 117 # было 97: +20 тест-функций (фаззеров по-прежнему 3)
# ⚠ первая редакция этой строки писала 115: снималась ДО реверс-пина §42/4б и якоря §4.4 — поймано
# проверкой закрытия, пере-снято после них
```
Переименовано (не удалено): `TestTheStopClearsWhenTheOwnerDecidesTheWholeSignatureMap`
`TestTheChainOwnerDecidesTheWholeMapAndTheRunGoesOn` (замена по заказу §4.12, новая семантика; вторая цепь —
`TestTheStopIsAFlagNotAGateOnDecisions` — и есть пин старой формулы) · `TestDecisionPathsDefaultByConventionOnAnEmptyKey`
`TestDecisionPathsAreAPureFunctionOfTheConvention`; сняты вместе с предметом (§4.9(б), снятие заказано промтом,
не подгонка): `TestDeclaredDecisionPathIsNotDefaulted`, `TestDeclaredAndUnreadableDecisionPathStillRefuses`,
`TestADeclaredDecisionFileThatVANISHESIsStillAnError` (→ `TestAnAbsentDecisionFileIsUndecidedAndAnUnreadableOneIsAnError`),
`TestTwoKeysOnOnePathAreRefused` (→ `TestDeclaredKeysCannotLandOnTheDecisionFiles`); переписаны под модель флажка
(старая семантика была их премиссой): `TestMiningStopWHATSurvivesResume`, `TestTerminologistResumeIsFree`.
⚠ Промт §4.11 говорил «семантика §4.1 не краснит НИ ОДИН тест» — на деле три теста краснели, потому что пинили
СТАРУЮ семантику как премиссу; их переписывание — часть заказа §4.12, и теперь возврат старой формулы краснит
ТРИ теста — `TestTheStopIsAFlagNotAGateOnDecisions`, `TestMiningStopWHATSurvivesResume`,
`TestTerminologistResumeIsFree` (вывод tmmutate по FX2 поимённо; ⚠ первая редакция писала «шесть» —
сумма по FX1/FX2/FX4 выдана за вывод одной посадки, поймано воркфлоу-ревью, §45).
## 41. Что НЕ удалось и что осталось PLAUSIBLE (§10)
- **Атомарность ПАРЫ против смерти процесса не построена и не заявлена**: окно между двумя rename
остаётся; закрывает его только журнал на два файла — механизм, которого дверь не заслужила (довод в
`writeDecisionFiles`). Покрытие остатка — сходимость повтора (приёмка мерила; жива и в моей пробе:
retry после exit 15 сходится) и то, что убитый процесс отчёта не печатает.
- **Долговечность fsync каталога проверена только на ОТКАЗЕ открытия** (chmod 0333): что rename
переживает реальное падение хоста после syncDir — PLAUSIBLE, живого крэша не было и не будет за $0.
- **exit 15 живьём воспроизведён только формой «не легло ничего»**: половинная посадка (`delta лег,
rejects нет`) на живом CLI требует отказа ВТОРОГО rename при успехе первого — среда одного каталога
такого не даёт без root; половина покрыта юнитом `TestHalfLandedIsNamedPerFile` (rename на непустой
каталог) и конвергентностью.
- **Стоп на живом стенде не воспроизводится** (нет jieba-контраста на машине — то же ограничение, что у
прошлых паков): семантика флажка целиком доказана харнессом `miningstop_join_test.go` и цепями
`bankchain_test.go` на фейковом провайдере; рантайм-вердикт по ПЛАТНОМУ пути — PLAUSIBLE, как и
заказано §4.13.
- **«Один лишний стоп» вредной стороны кластерной памяти** (новая сущность на общем алиасе молча не
предъявляется) в интеграции не воспроизведён — у фикстуры нет двух сущностей с общим прозвищем;
запинен на юнит-уровне предиката (`stopflag_test.go`) как ПРИНЯТЫЙ трейд с направлением потерь.
- **Гипотеза §4.13 о пере-оплате терминолога при подписании** механизмом не подтверждена и выигрышем не
заявляется: память предъявленного не трогает ни состав кандидатов, ни ключ чекпоинта (хеш
отрендеренных сообщений); `TestTerminologistResumeIsFree` держит $0-резюм в обеих семантиках.
## 42. Интервальная самоверификация субагентом (§5.1): четыре находки, все minor, все закрыты или аргументированы
Ревью шло против десяти явных критериев, адверсариально, с исполнением (точечные тесты + посадки в
своей копии). Код по всем десяти сошёлся; находки — класс «довод/покрытие», ровно тот, что предыдущая
сессия назвала своим худшим.
| # | находка | диспозиция |
|---|---|---|
| 1 | докстрока `renderSignatureStop` учила отменённой механике «промотируй/отклони, потом re-run» при верном печатаемом тексте | закрыта (переписана на модель флажка; печатаемый баннер был верен и не менялся) |
| 2 | комментарий v16 в `migrate.go` формулировал предикат как «a surface outside this set» — читается как «любая поверхность вне», тогда как код стреляет только на кластере, чья ВСЯ поверхность вне памяти; реализация «по комментарию» вернула бы ре-стоп при росте кластера | закрыта («whole surface set outside», с named-ценой ошибки) |
| 3 | `bankapply.go`: «stop before the lock (5)» — безотчётное stop-окно шире (пост-локовая проверка `ctx.Err()` до `ApplyDecisions` тоже без отчёта) | закрыта («before the VERDICT exists», обе проверки названы; поведение верно — печатать нечего, вердикта ещё нет) |
| 4а | грань домена §4.1 «предикат по итоговой карте, не по пулу ДО потолка» не краснится ни одной посадкой: вечный стоп невоспроизводим в фикстурах (2 терма при потолке 200), а одно-строчной мутации не существует — карта, предикат и память читают ОДИН список `mined` после потолков, расхождение требует сознательной перестройки | **аргументированный не-пин** по прецеденту G: сама структура (один список) и есть защита; записано здесь, чтобы следующая сессия не считала свип дырявым |
| 4б | новизна из ОБРАТНОЙ секции не была покрыта: в стоп-фикстурах terminology-гейт выключен и reverse пуста | закрыта поведенческим пином `TestNoveltyFromTheReverseSectionAloneTripsTheFlag`: run 1 с пред-отклонённой banknote-only поверхностью (карта без неё), run 2 со снятым отказом — стоп обязан сработать на новизне, пришедшей ТОЛЬКО через обратную дверь |
**Что ревью подтвердило исполнением (не переделывал):** предикат/карта/память — один список после обеих
секций и потолков (`mining.go`, file:line в его отчёте) · все шесть исходов отчёта проходят через
`finishReport`; исходные поля присваиваются ровно в ДВУХ конструкторах — `finishReport` и до-локовом
`capRefusalReport` (у капа `bookState` ещё не существует; проверка закрытия поймала, что первая редакция
этой строки говорила «нет нигде» — категорика была шире правды) · `TestTheRefusalTableIsTotal` невакуумен для
класса 15 (AST-словарь + сверка длины таблицы) · retired-ключи ловятся полями, а не голым strict-decode ·
конверт карты читают все движковые читатели, `DecodeFile` на карту не наведён · дверь память не читает ·
в новом Go-коде ветвлений по паре/книге нет.
## 43. Попунктный чек §4 — все тринадцать
Комплектность сверена механически: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX2_SESSION_PROMPT.md` → **13**.
| пункт | статус | чем закрыт |
|---|---|---|
| **4.1** память «предъявленного стопом» | ✅ | store v16 `bank_stop_presented` (плоское множество поверхностей кластеров — Src+Aliases, `INSERT OR IGNORE`, без обрезки); предикат `hasUnpresentedCluster` — пересечение кластера с памятью, домен — ИТОГОВАЯ карта после потолков (обе секции); двусторонний остаток записан пином. Цена миграции названа (§37). Пины: цепь Б, `stopflag_test.go`, `stopmemory_test.go`, реверс-новизна; мутации FX1FX3 |
| **4.2** порядок и момент записи | ✅ | карта (атомарно) → память, в момент СТОПА; отказ памяти НЕ валит стоп (ERROR-строка, названо §37); «подтверди карту» НЕ заведено; draft-only ветка названа. Развилка карта/SQLite-транзакция решена явно (направление потерь — доброкачественный повторный стоп) |
| **4.3** авто-провод при поднятом флаге | ✅ | «останавливаться ли» и «проводить ли» — два решения; авто-провод на ЛЮБОМ неостановившемся прогоне; вторая половина названа: остановившийся прогон авто-банк НЕ пишет (решение с доводом, §37). Мутация FX4; пины в WHATSurvives и цепи Б |
| **4.4** хвост за потолком | ✅ не лечился | пере-замер своей командой (2354/209/11/198, §38) — числа промта без первоисточника не цитируются; фильтр карты НЕ заведён; пинг в журнале |
| **4.5** единая точка фиксации | ✅ | вердикт (`decisionVerdict`) / исход (`finishReport` — единственный конструктор, свитч по `applyOutcome`, шесть исходов); «записано» — пост-состояние ПЕРЕЧИТАННЫХ файлов; пин `TestEveryOutcomeStampsItsOwnMode` + греп-инвариант (присваиваний вне конструктора нет — подтверждено интервальным ревью). Вердиктная половина сохранена (сильная форма отвергнута по букве промта) |
| **4.6** `signature.undecided` | ✅ | пере-документирован: НЕ отвечает на вопрос о стопе (в обе стороны), информационный счётчик для экрана подписи; память дверь НЕ читает (решение с доводом: ответ требовал бы следующего намайненного набора, которого нет ни у кого за $0); поле не удалено |
| **4.7** полоса в два яруса | ✅ | пол «нет отката, повтор безопасен»; клауза «ничего не произошло» — у пяти исходных классов; гардрейл «разрушительное — только на классе» буквой; класс `write_incomplete` (exit 15) один на оба исхода, различие — `written_*`; отказ по обоим капам печатает отчёт (контракт сведён к одному носителю). Мутации FX18FX20 |
| **4.8** подготовка обоих файлов | ✅ | stage обоих (write+sync+chmod) → два rename → fsync каталога; заявлено НЕ шире сделанного (атомарность против среды, не против SIGKILL — довод в `writeDecisionFiles`, остаток в §41); отказ синка — класс 15 с оговоркой долговечности. Мутации FX16FX17, пины staged/unsyncable |
| **4.9** Clean + конвенция | ✅ | (а) `filepath.Clean` абсолютных в resolve, репро приёмки закрыто пином; (б) оба ключа СНЯТЫ retired-отказом с лечением в тексте (прецедент `adult:`), пути — чистая функция каталога и book_id: МЕЖКНИЖНАЯ гонка (⛔-пинг прошлого пака) снята; воркфлоу-ревью показало ОСТАТОК той же оси — второй конфиг ТОЙ ЖЕ книги (один каталог, один book_id, другой объявленный project_db) пишет те же файлы под другим локом; остаток — пинг, не решение пака (§45). Мутации FX12FX13 |
| **4.10** карта: атомарность и конверт | ✅ | `writeFileAtomic` + конверт `map_version`/`map_id` (sha256 содержимого), рендер ДОКАЗЫВАЕТ round-trip (`RenderSignatureMap` через `renderProved`); легаси-карта читается; карта НЕ накопитель (слияния нет). Мутации FX14FX15; живьём — инод-пин |
| **4.11** слепые пятна | ✅ | все шесть строк таблицы закрыты посадкой+пином (normText-класс · субъект-класс · субъект-окно · абсолютность путей отчёта двери · границы обоих капов · граница окна); пин семантики §4.1 — цепь Б + два переписанных харнесс-теста (возврат старой формулы краснит три теста, вывод tmmutate по FX2 поимённо; «шесть» первой редакции — ошибка, §45). Вакуумный пин пойман посадкой ДО записи (§38) |
| **4.12** наследие — списком | ✅ | шапка mining.go · комментарий у loadMinedRejects · unsignedEngineSurfaces · docstring runBankMiningStop · сообщение стопа · renderSignatureStop + два стейл-хинта render.go · help `--verify-bank` · WaveSignatureStop/waverun · комментарий v16; новое состояние «дельта непустая, нового нет» — явная WARN-строка; цепной тест переписан (замена названа: `TestTheStopIsAFlagNotAGateOnDecisions`); пинги с ПОРЯДКОМ и миграционная нота — в журнале |
| **4.13** платных вызовов ноль | ✅ | фейковый провайдер + харнесс + копия стенда; sha256 стендовой базы не сдвинулся; рантайм по платному пути — PLAUSIBLE; «выигрыш по чекпоинтам» НЕ заявлен (механизма нет, §41) |
| §5 батарея/каталог/проба/артефакт | ✅ | батарея после каждой правки; `make battery` EXIT=0; стенд-батарея красна ровно на jieba-парити; каталог 36→56, `NOTHING` роняет гейт (свойство не тронуто); живая проба §39; эта база |
| §6 три оси | ✅ | семантика флажка — прогонами движка (цепи, харнесс), без кода платформы · точка фиксации — конструктор + греп + пин исходов · слепота гейта — 56 посадок / 0 неожиданных |
| §7§11 | ✅ | план до кода (`SEAM_FIX2_PLAN.md`, механическая сверка 13) · эхо до работы · «что НЕ удалось» §41 · развилки решались по букве промта, интерпретаций против него нет |
## 44. Проверка ЗАКРЫТИЯ — отдельным агентом, не чинившим (§5.1)
Прогнана по презумпции «каждое заявленное закрытие ложно, пока не увидено исполнением»: все 13 строк §43
и все 4 диспозиции §42 подтверждены в дереве (13 точечных тестовых прогонов + 3 полных пакета — зелёные;
носители по file:line; реверс-пин признан невакуумным с названным механизмом падения). Найдено ПЯТЬ
расхождений — традиция «ни одно "всё исправлено" не выдерживает проверки» держится и на этой сессии:
| # | находка | диспозиция |
|---|---|---|
| 1 | счётчик §40 «115» устарел на единицу (реверс-пин добавлен после замера) | пере-снят ПОСЛЕ всех правок: **117** (+ якорь §4.4); поправка с провенансом в §40 |
| 2 | untracked-бинарь `backend/tmctl` (20 МБ, след сессии) | удалён |
| 3 | клейм §42 «присваиваний вне конструктора нет» буквально ложен: `capRefusalReport` — второй, до-локовый конструктор | формулировка исправлена в §42 (код законен, документирован, запинен — расходилась категорика, не закрытие) |
| 4 | пере-замер §4.4 жил только в скратчпаде — тот же класс «числа без первоисточника», за который промт корил 2110/… | проб положен в дерево постоянным якорем-пином `internal/miner/emissioncap_test.go`: утверждает текущее поведение, краснеет в день сдвига потолка/порядка фильтров (прецедент 0f4c556); §38 поправлен |
| 5 | untracked вне backend/+docs/: `eval/dovodka/*`, `platform/docs/p8-review/*` | НЕ этого пака (mtime 24.08, параллельные зоны — платформенный P8 назван промтом законной параллелью); чужое не трогаю, оркестратору видно из этого же отчёта |
После правок: батарея пакетов miner/pipeline/membank/config/seed/store — зелёная; каталог не менялся
(56 записей — финальный прогон «0 неожиданных» шёл на дереве ДО пунктов 14, ни один из которых не
трогает код вне нового тест-файла miner).
## 45. Воркфлоу-ревью: шесть полос × три скептика — 23 сырых находки, 20 выживших, 16 уникальных, все закрыты
Форма: шесть параллельных полос-искателей под антипаттерны проекта (неверные доводы при верном коде ·
вакуумные пины · семантика флажка глубоким чтением · дверь/отчёт глубоким чтением · общность движка ·
шов/контракт), каждая находка — через трёх адверсариальных скептиков с разными линзами (корректность
трассой · достижимость · «не принятый ли уже остаток»), выживание — большинство; 75 агентов, скептики
убили 3, дубли между полосами — 4. Полосы отдельно подтвердили 75 позиций «проверено, сходится».
**Кодовые (2), обе закрыты правкой + пином + посадкой:**
| находка | закрытие |
|---|---|
| **`tmctl translate --dry-run` молча игнорировался и запускал ПЛАТНЫЙ прогон** — третье написание денежной дыры, которую это же дерево уже закрывало для `--decisions=`/`--keys-file=`; пред-существует фикс2, нигде не был записан | гард присутствия: `--dry-run` принимают только `bank-apply` и `redrive`, остальным — громкий отказ с ценой в тексте; пин `TestDryRunIsRefusedWhereNothingProjects`; посадка FX21 |
| **Повтор после exit 15 из-за отказа `syncDir` сходился в exit 0, так и не пере-доказав долговечность** — retry по контракту класса шёл byte-no-op-веткой мимо единственного места с синком | no-op-ветка теперь пере-доказывает долговечность: не открывающийся на синк каталог держит класс 15 (`written_*={false,false}`, текст называет непроверенное) до выздоровления среды; пин `TestARetryAfterAFailedDirectorySyncReProvesDurability`; посадка FX22 |
**Довод-находки (⚠ то самое «неверный довод при верном коде», 4 major + 10 minor), все закрыты:**
- **«FX2 краснит шесть тестов»** (§40/§43, дважды) — ложь, приписанная инструменту: краснят РОВНО ТРИ
(`TestTheStopIsAFlag…`, `TestMiningStopWHATSurvivesResume`, `TestTerminologistResumeIsFree`);
«шесть» было суммой по FX1/FX2/FX4, выданной за вывод одной посадки — нарушение §8 в самой острой
форме (число рядом с командой, которая его опровергает). Оба места исправлены с провенансом ошибки.
- **Якорь-пин §4.4 НЕ краснел при сдвиге потолка** — премиссы не фиксировали значение 200 (150/300/400
проходили зелёными), вопреки трём носителям клейма. Значение потолка теперь запинено премиссой с
текстом, ведущим двигающего к пингу; проверено: смена 200 → красный.
- **«pre-lock check» в `bankapply.go` не существовал** — обе проверки контекста пост-локовые; переписано
на правду (пост-чтение — без отчёта, пред-запись — С отчётом; стоп раньше первой проверки вообще не
наблюдается). ⚠ Это была моя правка ПО находке интервального ревью — исправление само внесло ложь;
§42 п.3 диспозиция скорректирована этим пунктом.
- **Шапка полосы в `cmd/tmctl/main.go` осталась вторым носителем старого пола** («nothing was written»),
который класс 15 опровергает — приведена к двух-ярусной букве, «nothing was written» перенесено в
клаузы классов.
- **Довод арбитра сужен**: «межкнижная гонка недостижима по построению» была шире правды — остался
второй конфиг ТОЙ ЖЕ книги (один каталог/book_id, другой объявленный `project_db`): пишет те же файлы
под другим локом, мимо flock живого прогона (ревью воспроизвело живьём). Межкнижная версия снята;
остаток — пинг в журнале (второй арбитр/отказ при загрузке — закон п.2, решение не паку); комментарий
`bankdecisions.go`, §43 и журнальная запись исправлены.
- Minor-доводы, все закрыты: клауза «nothing happened at all» классов 1014 сужена до «состояния книги»
(пред-полётный бэкап translate пишется ДО отказа 12/13 — теперь оговорён) · v16-комментарий migrate.go
завышал цену контрфактуала («re-stop on every run» → один лишний стоп на рост кластера) · draft-only
прогон с флагом печатал «nothing in the delta is new to it», хотя новизна не вычислялась — своя строка ·
баннер стопа выдавал размер карты за число НОВЫХ термов (`(N term(s) in the map)`, `WaveSignatureStop.Error`
переписан; поимённой разметки новизны в карте нет — продуктовая поверхность, не этот пак) ·
`bankexport.go` учил отменённую механику «promote/decline в файлы» (→ дверь) · стейл `:485` в render.go
(→ по имени) · док `addKeyed` «only a record's position» (ложь — статусы и рендеринги тоже) ·
кап-отчёт: `preexisting_problems: []` ≠ «проверено чисто» — оговорено на поле · фикстура нормализации
в `stopflag_test.go` была тождеством под фолдом (клейм про trad→simp был вакуумен) — заменена на 陳博
с премисс-проверкой пересечения фолда.
Каталог: 56 → **58** посадок (FX21, FX22). После всех правок этой секции: полный прогон каталога на
копии итогового дерева — **58 записей, 0 неожиданных исходов** (FX21/FX22 краснеют своими пинами
поимённо; посадка «потолок 200 → 150» отдельно проверена руками — якорь §4.4 красный с текстом,
ведущим к пингу); `make battery` — **EXIT=0**, `make lint` — 0 issues.
---
# ФИКС3 (промт BACKEND_SEAM_FIX3_SESSION_PROMPT.md, 6 пунктов) — секция 46
## 46. Фикс-лист приёмки дофикса-2: четыре пина, один снос, зачистка двух контрактов
Комплектность: `grep -cE '^### 4\.' docs/BACKEND_SEAM_FIX3_SESSION_PROMPT.md` → **6**.
| пункт | статус | чем закрыт |
|---|---|---|
| **4.1** пин «карта → память» | ✅ | мутация приёмки ВОСПРОИЗВЕДЕНА до пина (отказ записи карты → WarnContext: батарея зелёная во всех трёх кандидатных пакетах — pipeline, tmctl, store). Пин приёмом приёмки: путь карты занят непустым каталогом → `TestTheMemoryIsNotWrittenWhenTheMapCannotBe` требует ошибку прогона (не стоп и не успех) и ПУСТУЮ память книги. Проверен посадкой: красный на мутации, зелёный на дереве. Каталог: **FX23** |
| **4.2** отчёт 15 мерит по диску | ✅ | утверждение панели ВОСПРОИЗВЕДЕНО (docsReRead → docsFromResult на write_incomplete: вся батарея зелёная — существующий пин кормит конструктор пустыми состояниями, источники неразличимы). Пин — брат stopped-пина: карта с нерешённой поверхностью + документ, решающий её + «не легло ничего» → `signature.undecided` обязан быть 1 (диск), не 0 (намерение): `TestAWriteFailedCallMeasuresTheSignatureAgainstTheDiskNotTheIntent`. Проверен посадкой. Каталог: **FX24** |
| **4.3** старый пол в двух носителях | ✅ | `runner.go` (openRunner) и `cmd/tmctl/panic_exit_test.go` приведены к двух-ярусной букве: пол полосы + «nothing was written» как клауза классов, с явной оговоркой про класс 15. Контрольный греп: `grep -rn "nothing was written" --include='*.go' .` → **два** хита, оба — клаузы КЛАССОВ, не пола: `ErrDecisionsRefused` (класс 14, там она истинна целиком) и сообщение отказа синка каталога («nothing was written BY THIS CALL…», класс 15 — пер-вызовная клауза с оговоркой). ⚠ Первая редакция этой строки писала «один хит» — поймано проверкой закрытия: греп был снят до правки §4.2-пина фикс2 и не пере-снят |
| **4.4** отменённая формула | ✅ грепом | греп-формы: `re-?fires? the stop` · `stop clears only` · `clears? once every` · `promoted or rejected` · `stop.*forever` · `livelock` рядом с decline · `переспраш`. Найдено и переписано **восемь** носителей (пять из списка промта + `DecodeRejects`-док + док и оракул `TestMiningStopRejectClearsDelta`): правда из ноты — отказ исключает сущность из ЭМИССИИ (терм не возвращается в карту и в авто-банк; про стоп отказ не говорит ничего — стоп это вопрос памяти). Тест реджектов НЕ вакуумен под памятью (его второй проект СВЕЖИЙ — память пуста, продолжение решенческое), и это теперь сказано его доком + явным оракулом `lastMinedCount == 0`. Контрольный греп после зачистки: остались только носители НОВОЙ модели (память/`re-fire on what it already presented`) и история row-199 в цепях |
| **4.5** снос `.bank-stop.json` | ✅ | свой греп читателей по backend/platform/frontend/eval ДО сноса: в коде читателя нет ни в одной зоне (у платформы — только упоминания в её ЖУРНАЛАХ, пинг ниже). Снесено: писатель `writeBankStopTableJSON`, `clearBankStopTableJSON` (+ его вызов на not-configured-пути), пустая перезапись на empty-delta, `bankStopTableJSONPath`, `bankStopTableVersion`, оба DTO, `emptyIfNil`/`emptyVariantsIfNil`, json-теги `BankStopVariant`, поле `WaveSignatureStop.TableJSONPath`, строка CLI «Machine table:», лог-поле `table_json`, четыре теста таблицы (предмет снесён заказом — не подгонка). `.bank-stop.txt` НЕ тронут (читатель — оператор). Контрольный греп `bank-stop.json|TableJSONPath|bankStopTableJSON|tm-bank-stop` по `*.go` → 0 |
| **4.6** якорь свежести банк-экспорта | ✅ утверждение подтверждено, признак заведён | подтверждение: `at` ехал ТОЛЬКО в лог-строке (`exportBank`), при отказе записи артефакт молча оставался прошлым — в документе отличить нечем. Форма: аддитивные поля `as_of` (метка границы) + `run_id` (тот же id, что стрим `events.jsonl`; эмиттер авторитетен — он может пере-минтить id; пустой = «идентичности нет», не «текущий»). Версия `tm-bank-v1` не двигалась (добавление аддитивно, по букве промта). Пин `TestTheBankExportCarriesItsFreshnessAnchor`: последняя граница прогона · смена границы двигает якорь · без идентичности — пусто. Детерминизм не задет: id приходит из контекста/эмиттера, не из часов |
**Что НЕ удалось / остатки (§10):** якорь §4.6 отличает прогоны по `run_id` и границы по `as_of`, но
две записи ОДНОЙ границы одного прогона (не бывает сегодня: каждая граница пишется однажды) были бы
неразличимы — принято, механизм счётчика был бы новым механизмом против буквы промта. Снос §4.5
оставляет упоминания `.bank-stop.json` в ЖУРНАЛАХ платформы (их зона) — пинг, не правка.
**Пинги этого пака (в журнал):** платформенные доки (`platform-PROGRESS.md`, `STACK_DECISIONS.md`)
называют `.bank-stop.json` атомарно-читаемым сайдкаром — файла больше нет, упоминания устареют при
лендинге · потребители банк-экспорта получают `as_of`/`run_id` — аддитивно, ломать нечего.
**Прогоны итогового дерева (§5):** `make battery` → **EXIT=0** (fmt+vet+тесты+линт) · `make battery-stand`
→ красен ровно `TestMinerFullBookParity` (jieba вне git — ожидаемая картина промта) · каталог мутаций
`go run ./cmd/tmmutate -root <копия>` → **60 записей, 0 неожиданных исходов** (перегнан ПОСЛЕ
форматирования — якоря каталога сверены с финальными байтами). Дифф тестов: снесены четыре теста
машинной таблицы (предмет снесён заказом §4.5), добавлены четыре пина (§4.1, §4.2, §4.6, оракул
`lastMinedCount` в тесте реджектов); `git diff -U0 -- . | grep -cE "^-func (Test|Fuzz)"` → **4** — ровно четыре снесённых
`TestBankStopMachineTable*` (⚠ первая редакция писала «0, тесты жили в незакоммиченном файле» — ложь,
пойманная проверкой закрытия: `contractblockers_test.go` ЗАКОММИЧЕН, дифф их видит; снос легитимен —
предмет снесён заказом §4.5, других удалений диффа нет).
**Проверка закрытия (не-чинившим):** все шесть пунктов подтверждены исполнением — обе мутации (FX23,
FX24) посажены им в своей копии и убиты пинами; греп-оси §6 пере-сняты; три расхождения — два неточных
самоотчёта этой секции (исправлены выше с провенансом) и вновь рождённый `backend/tmctl` (артефакт
`go build ./cmd/tmctl/` из каталога; удалён, класс закрыт `backend/.gitignore` с `/tmctl`).