textmachine/docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md

644 lines
72 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)
> Выдан оркестратором №19, 25.08.2026. Активный промт зоны бэкенда — один; этот заменяет
> `BACKEND_SEAM_PACK_SESSION_PROMPT.md` (отработан, уезжает в архив при лендинге).
> Параллельно законен только `platform/docs/archive/PLATFORM_P8_REVIEW_SESSION_PROMPT_2026-08-22.md (ОТРАБОТАН 27.08)` (read-only, пересечений нет).
## 1. Какая проблема и что решит твой результат
Продукт — издательский перевод больших книг мультиагентным пайплайном. Его центральная ценность —
**банк памяти**: сквозной глоссарий имён и терминов, держащий консистентность на всю книгу.
Предыдущий пак построил **входную дверь**: `tmctl bank-apply` принимает решения владельца о банке
ДАННЫМИ, а не правкой YAML руками. Пак **принят не был**. Приёмка прогнала цепь целиком и
подтвердила, что дверь работает по назначению — но нашла **один блокер и восемь мажоров**, и главный
из них превращает дверь в свою противоположность: **один законный отказ пользователя необратимо
ломает книгу так, что чинить её можно только тем самым текстовым редактором, ради отмены которого
дверь и строилась.**
Твой результат — дверь, которую можно отдать пользователю. Пак не переделывается: он хороший,
дыры узкие и адресные. Всё ниже — правки в уже написанном коде плюс пины к местам, где гейт
оказался слепым.
**Дерево пака НЕ закоммичено и лежит в рабочем каталоге.** Ты продолжаешь его, а не начинаешь заново.
## 2. Зона записи и git
Пишешь ТОЛЬКО в `backend/`. Читать можешь всё; `platform/`, `frontend/`, `eval/`, `docs/` — чужие зоны,
правки в них запрещены. **Единственное исключение:** пинги и итог сессии — в `docs/PROGRESS.md`, секция
«Бэкенд». **Ты не коммитишь** — дерево готовишь и передаёшь на лендинг оркестратору (канон git-норм —
`CLAUDE.md`). `git add -A`, `git add .`, `git commit -a` запрещены в любом случае. В дереве живёт
незакоммиченная работа параллельных сессий — не трогай её и не «прибирай».
## 3. Карта чтения (пять позиций, больше не нужно)
1. **`backend/docs/SEAM_PACK_FINDINGS.md`** — доказательная база твоего же пака. Читать §0 (состав),
§5 (закрытые дефекты — чтобы не чинить дважды), **§6 (класс полосы 14 — статус ратификации, см. ниже)**,
§7 (что НЕ доказано), §9 (два остаточных риска), §10 (семь пингов).
**Что из них стало заказом, точно:** §9 — ВТОРОЙ риск (кап размера документа) стал §4.5; ПЕРВЫЙ
(необратимость канонического пере-рендера) заказом НЕ стал и остаётся риском, он получает только
пин на поле-смягчение (§4.11 б) и уточнение точности этого поля (§4.13). §10 — заказом стала
ПОЛОВИНА п.3 (текст отказа, §4.14); остальные шесть перечислены в приложении «пинги, которые
дофикс НЕ исполняет». Не ищи в промте заказов, которых там нет.
**Статус класса 14:** заводить его законно и это уже сделано — ратифицированный закон обещает,
что двери встают паками, а прецедент кода 13 прошёл ровно так (направление → финализация приёмкой).
Ноту и подъём обоих ратифицированных носителей полосы ставит ОРКЕСТРАТОР при лендинге; ты работаешь
на классе 14 как на действующем и §4.11 (в) строишь на нём.
2. **`docs/architecture/17-seam-inbound-law.md`** — ратифицированный закон входной двери, семь пунктов.
Твой контракт. Пункты 1, 2, 3 и 7 ты трогаешь напрямую.
3. **`docs/architecture/05-decisions-log.md`, нота D39.156** — почему закон такой.
4. **`docs/PROGRESS.md` — строка БЭКЛОГА 199** ⚠ это ID строки ТАБЛИЦЫ, брать грепом `^| 199 |`;
буквальный `sed -n '199p'` даст чужой текст. В её теле — ТРИ мины формата; мины 1 и 2 пак
обезвредил, **мина 3 (кап реверс-секции 200 ПОСЛЕ фильтров) не обезврежена и попадает в §4.9**.
5. **`docs/architecture/12-go-style-notes.md`** — норматив общности. Ревью-вопрос по умолчанию:
«заработает ли на паре, которой в репо ещё НЕТ, без правки Go?»
**Код первичен.** Все `file:line` ниже — отправные точки, снятые приёмкой на дереве пака; открывай и
проверяй сам (дисклеймер о происхождении доков — шапка `CLAUDE.md`).
## 4. Состав дофикса
Каждый пункт несёт **как приёмка это показала** — воспроизведи сам прежде, чем чинить: правка без
воспроизведённого репро лечит не то.
### 4.1 ⛔ БЛОКЕР: reject-документ не проверяется на читаемость вообще — ДЕЛАЙ РОВНО ТАК инвариант, РЕШИ САМ форму
`documentProblems` (`internal/membank/decisions.go`) рендерит и парсит обратно **только дельту**.
`res.Rejects` не участвует ни в одной проверке.
**Репро (воспроизвести первым делом):** решение `{"action":"decline","src":"方小子","note":"\nне термин"}`
`note` начинается с перевода строки, что для свободного текста пользователя абсолютно законно.
Вызов принимается: `exit 0`, `changed=true`, `state: applied`, `rejected: []`. Записанные байты:
```
rejects:
- src: 方小子
note: |4-
не термин
```
`seed.DecodeRejects` их не читает: `seed: yaml: line 1: did not find expected key`. После этого **любой**
следующий `bank-apply` по книге — включая `--dry-run`, который закон п.7 объявляет безопасной
проекцией, — падает с `exit 10`. Тот же `note` на `approve` дверь **отвергает** (`exit 14`, ничего не
записано): механизм существует и просто не применён ко второму файлу.
**Заказ:** результат, который дверь СОБИРАЕТСЯ записать, обязан пройти round-trip `render → parse`
**по обоим документам**, и провал этой проверки — отказ, а не запись. Форму (расширить
`documentProblems` вторым аргументом, отдельная функция, что угодно) решаешь сам и аргументируешь.
**Корень глубже, и его тоже закрой:** `yaml.Marshal` рендерит строку с ведущим `\n` как блочный
скаляр с индикатором отступа (`|4-`), который его же строгий декодер не принимает. Это бьёт по
ЛЮБОМУ свободному текстовому полю обоих документов, а не только по `note` у `decline`. Решение о том,
нормализовать ли такой вход на входе двери или чинить рендер, — твоё, но оно обязано быть общим
для всех текстовых полей, а не заплаткой на одно поле.
⚠ И прежде чем чинить: **это поведение библиотеки — сходи в её доку и трекер, не гадай** (гардрейл
проекта на аномалии зависимостей). Если это её известный дефект — обход законен и должен нести
ссылку; если наше злоупотребление — чини вызов. Формулировку «похоже, баг yaml» без сверки в
комментарий не абсорбировать.
**Фаззер здесь сильнее примера, и его слепота СТРУКТУРНА — вот она.** Оракул
`FuzzSeedDocumentRoundTrip` («движок не должен рендерить документ, который сам не читает») **исправен**:
приёмка досеяла один вход с ведущим `\n` в `note`, и он падает мгновенно. Не сработал он потому, что
фаззится СЫРОЙ YAML — то есть корпус по построению состоит из строк, УЖЕ переживших YAML-разбор, а
настоящий вход двери это JSON-строка, на которую YAML не накладывает никаких ограничений. Ни один из
семи сидов не несёт многострочного значения вовсе.
**Заказ:** фаззить путь ДВЕРИ — произвольные строки полей `Decision` через `ApplyDecisions` в рендер и
обратно, — и оракул «что движок ЗАПИСЫВАЕТ, движок ЧИТАЕТ ОБРАТНО» распространить на ОБА документа.
### 4.2 Пред-существующие поломки нельзя чинить по одной — ДЕЛАЙ РОВНО ТАК
Гейт «отказывают только НОВЫЕ проблемы» сравнивает **тексты сообщений** (`known[p]`), а вердикт
загружаемости приходит ОДНОЙ строкой, несущей ВСЕ битые термы сразу: `ParseBankSeed` копит
`problems` по каждому терму и склеивает их в одну ошибку, а `documentProblems` сворачивает эту
ошибку в один элемент `[]string`. Поэтому починка одного терма меняет строку ЦЕЛИКОМ, совпадения с
`known` больше нет, и вызов отказывает.
⚠ **Это НЕ «загрузчик падает на первой строке» — прежняя редакция промта утверждала так и была
неверна (испр. по опровергателю промта).** Разница несущая: чинить надо разложение
многосубъектного вердикта на проблемы ПО СУБЪЕКТАМ, а не накопление ошибок в общем загрузчике сида —
он уже накапливает, и он читает `glossary_seed` КАЖДОЙ книги.
**Репро:** дельта с двумя строками `status: approved, dst: ""`. Решение чинит одну → **`exit 14`**,
и текст отказа называет ВТОРУЮ строку, которой решение не касалось. Обе одним документом → `exit 0`.
Это прямо опровергает собственный контракт двери (комментарий поля `Preexisting`: «a book whose
files already contradict each other has to stay repairable through this door»).
**Заказ:** пред-существующая проблема опознаётся по СУБЪЕКТУ (терм/поверхность, которой она
касается), а не по тексту сообщения. Проверено приёмкой: несвязанное решение при одной
пред-существующей поломке проходит корректно, ломается именно инкрементальная починка НЕСКОЛЬКИХ.
### 4.3 Два ключа на один путь теряют решение молча — ДЕЛАЙ РОВНО ТАК
**Репро (воспроизведено приёмкой):** `book.yaml`, объявляющий `mined_delta` и `mined_rejects` на ОДИН
путь, файл при этом пустой — то есть парсится обеими схемами. Результат: `exit 0`, оба решения
`applied`, а на диске остаётся только reject-лист.
Одобрение владельца исчезает молча, и файл после этого не грузится как дельта — следующий прогон
падает на разборе конфига.
**Заказ:** `config.LoadBook` отказывает, если два пути совпадают. Одна проверка плюс строка отказа.
### 4.4 Гард флага ключён на ЗНАЧЕНИЕ, а не на присутствие — ДЕЛАЙ РОВНО ТАК
`invocation.go`: `if *decisions != "" && cmd != "bank-apply"`. Поэтому `--decisions=` с пустым
значением гарда не задевает.
**Репро:** `tmctl translate --config b.yaml --decisions=` идёт по ПЛАТНОМУ пути ровно как без флага
(проверено: обе формы падают на одной и той же поздней стадии). Реальный триггер — `--decisions=$DOC`
с незаданной переменной в юните или скрипте. Это ровно класс R2 твоего же §5, который ты назвал
«стоил бы ДЕНЕГ», уцелевший в одной форме.
**Заказ:** оба гарда (`--decisions`, `--keys-file`) ключить на ПРИСУТСТВИЕ флага, не на непустоту
значения. Пустое значение у присутствующего флага — тоже отказ.
⚠ Присутствие даёт **только `fs.Visit`** — он обходит ТОЛЬКО реально переданные флаги; `fs.Lookup`
на этот вопрос не отвечает вовсе, он возвращает ОБЪЯВЛЕННЫЙ флаг независимо от того, передавали его
или нет. Живой прецедент лежит в том же файле: `ceilingGiven` через `fs.Visit`, с комментарием,
объясняющим ровно эту разницу. ⚠ Комментарий над самими гардами УЖЕ говорит «scoped to the flag
being PRESENT» — то есть код расходится со своим же комментарием, и это дополнительная улика.
### 4.5 Документ решений не капирован, а OOM движка платформа читает как успех — ДЕЛАЙ РОВНО ТАК инвариант, РЕШИ САМ число
`os.ReadFile(decisionsPath)` без ограничения. Замерено приёмкой на документах из одних `approve`:
20 000 решений → 0.730.80 ГБ RSS, 40 000 → 1.42 ГБ при документе всего в 2 МБ, то есть ~36 КБ
памяти на решение. Разброс на 20 000 — два прогона документов слегка разной формы (с `note` и без),
а не две разные оси.
**И вторая половина, которой в §9 не было:** движок при нехватке памяти выходит **кодом 2**
(`fatal error: ... out of memory` — Go так завершается), а платформа читает 2 как
`CompletedWithFlags` — «команда СДЕЛАЛА свою работу, часть юнитов на человека»
(`platform/internal/ingest/exit.go`, читать, не править). То есть смерть движка от памяти
прочитывается как успешный прогон. Класс пред-существующий и общий для всех глаголов — но
`bank-apply` первый, чей размер входа задаёт ПОЛЬЗОВАТЕЛЬ.
**Заказ:** кап на размер документа решений плюс внятная строка отказа полосой. Число выбираешь сам
и аргументируешь; ориентир — платформа капирует свои чтения от движка (16 МБ статус / 256 МБ банк).
Хвост про exit 2 — **пингом**, не тихой правкой: он общедвижковый и не твой скоуп.
### 4.6 `artifacts{}` не доезжает до своего потребителя — РЕШИ САМ форму
Проверено приёмкой грепом: `Artifacts` существует **только** в `internal/pipeline/status.go`, а
единственный код платформы, выводящий движковый путь (`runner.ReadBank``projectDB`
`filepath.Join(workdir, book_id+".db")` + суффикс `.bank.json`), зовётся из её read-model, чей
интерфейс движка — `{Manifest, Export}`, без `Status` (`platform/internal/readmodel/`, читать, не править).
Буква §4.5 прежнего заказа исполнена, **цель строки бэклога 213 — нет**: платформа не может выкинуть
свой `projectDB()`, не заведя третий вызов движка на каждую материализацию.
**Заказ:** тот же версионированный конверт путей отдать на поверхности, которую потребитель уже
читает. `manifest --json` — самый дешёвый кандидат (read-model уже его гоняет), но форму и состав
выбираешь ты и аргументируешь. ⚠ Пере-именование банк-экспорта в фикс-имя по-прежнему ЛОМАЮЩЕЕ,
его окно — строка 161, сюда не тащить.
**Твой же пинг §10 п.5** («опубликованный путь читается как приглашение его парсить; единственная
изгородь — комментарий») этим углубляется, и это осознанно: закон п.1 разрешает платформе читать по
путям, СООБЩЁННЫМ движком, а п.3 требует версионированного конверта на каждом выходе. Изгородью
становится конверт, а не комментарий. Если считаешь, что этого мало, — скажи аргументом, это законный
ответ по §11.
### 4.7 Отказ по алиасу терма, одобренного в ТОМ ЖЕ вызове, принимается инертным — ДЕЛАЙ РОВНО ТАК
Проверка `aliasOwner` смотрит в `res.Delta` ДО цикла применения, поэтому терм, одобряемый решением
`i`, ещё не лежит в дельте, когда судится решение `j`.
**Репро:** один документ = `approve 方源` + `decline 方小子`, где `方小子` — алиас кластера майнера у
`方源`. Оба отчитываются `applied`, `rejected: []`. В дельте `方小子` остаётся ФАЙРЯЩИМ алиасом
одобренного терма, а reject — только фильтр предложений. Прозвище продолжает подставляться в
переводе, владелец уверен, что снял его. Те же два решения ДВУМЯ вызовами — корректно отвергаются
по имени.
**Заказ:** правило судит НАБОР, как и заявлено в шапке `ApplyDecisions`, а не только состояние файла
до вызова.
### 4.8 Перевёрнутое окно глав принимается — ДЕЛАЙ РОВНО ТАК
`resolveDecision` проверяет только `< 0`. **Воспроизведено приёмкой:** решение с
`since_chapter: 90, until_chapter: 3` принимается и записывает в дельту строку, которая не сработает
ни в одной главе. Отказывать.
### 4.9 Мина 3 строки 199: «применено» ≠ «стоп снимется» — РЕШИ САМ форму
`reverseSectionTerms` режет кап 200 ПОСЛЕ фильтров, поэтому на книге с >200 banknote-only
поверхностями подпись всей карты стоп НЕ снимет: приедет свежая двухсотка, и актов подписи
понадобится `ceil(N/200)`.
Механизм против этого паком не заказан и сейчас не заказывается. **Заказывается ЧЕСТНОСТЬ отчёта:**
вызов, после которого стоп заведомо не погаснет, не должен читаться как «готово». Форму (поле в
отчёте, строка предупреждения, что-то ещё) решаешь сам и аргументируешь; если считаешь, что дверь
этого знать не может — скажи это аргументом, и это будет принято.
### 4.10 SIGTERM игнорируется, лок держится — РЕШИ САМ форму
Проверено приёмкой: на длинном документе процесс переживает `SIGTERM` и продолжает держать
эксклюзивный лок проекта; снимается только `SIGKILL`. Платформа останавливает движок именно
`SIGTERM` (`platform/internal/runner/`, читать, не править), а лок блокирует прогон книги.
Закон п.2 отводит `SIGTERM` роль «дочти и отпусти» для ПРОГОНА; для $0-глагола аналог — «брось
работу и отпусти лок, ничего не записав»: у всё-или-ничего нет частичного результата, который стоило
бы дописывать. Форму решаешь сам.
### 4.11 Восемь слепых пятен гейта, десять мутаций — ДЕЛАЙ РОВНО ТАК
Каждое ниже — место, где приёмка посадила мутацию и **батарея осталась зелёной**. Живого дефекта в
них нет; отсутствует пин. ⚠ Считай по МУТАЦИЯМ, а не по строкам таблицы: строка (д) несёт три
независимые посадки, и пин нужен на каждую. Каждый пин обязан краснеть на своей мутации — покажи это
в отчёте прогоном.
| № | что не сторожится | мутация, которую пин обязан поймать |
|---|---|---|
| а | **арбитраж лока против ПРОГОНА.** `TestBankApplyRefusesABusyProject` держит лок тем же `store.LockProject`, поэтому проверяет «LockProject конфликтует с LockProject». Инвариант ратифицирован (закон п.2, «тот же flock проекта») | сменить файл лока в `LockProject` (`dbPath + ".lock"` → любой другой) — сегодня батарея зелёная. Пин обязан держать лок так, как его держит ПРОГОН (`store.Open`) |
| б | **`canonical_rewrite` и `preexisting_problems`** — два опубликованных поля отчёта, на которых пак держит смягчение двух СВОИХ ЖЕ названных рисков (§9 необратимый пере-рендер, R7 проглоченная поломка) | занулить оба (`rep.CanonicalRewrite = false`, `PreexistingProblems: []string{}`) — сегодня батарея зелёная |
| в | **гейт тотальности таблицы отказов слеп к сценарию, который сам называет.** `TestTheRefusalTableIsTotal` держит РУЧНОЙ список классов и сверяет его длину с длиной `refusalExit` | добавить шестой класс в `pipeline.RefusalClass`, не внося его НИ в `refusalExit`, НИ в список теста — сегодня тест ПРОХОДИТ, а класс уехал бы в catch-all 19. Перечисляй классы из ИСХОДНИКА. ⚠ `TestDispatchCommandsCoversTheSwitch` — прецедент ТЕХНИКИ, но не реализации: он ключён на `*ast.BasicLit` и потому слеп к `case` с константой (это его собственный пинг, §10 п.2 отчёта), а значения `RefusalClass` объявлены именно КОНСТАНТАМИ — копирование один-в-один воспроизведёт слепоту. Нужен обход `GenDecl`/`ValueSpec` |
| г | **нормализация в REPLACEMENT-половине отказа.** Тесты используют поверхности, на которых `NormalizeSourceKey` — тождество | `nk := text.NormalizeSourceKey(src)``nk := src` в `dropTerms`. Пин: одобрен 虫, отказ приходит по 蟲 (традиционная форма) — отказ обязан снять одобрение |
| д | **глубокое копирование алиасов в `withRubyAliases`** — ровно класс R4/R5, ради которого поле `Ruby` заведено; **перенос `note` при промоушене**; **рантайм-половина гарда ОБЪЯВЛЕННОГО пути** (`decisionFilePresent(path, declared=true)`) | снять глубокую копию · `if false { t.Note = d.Note }` · `case !declared && ...``case ...` — все три сегодня зелены |
| е | **абсолютность путей в `artifacts{}` запинена ВАКУУМНО.** Проверка `filepath.IsAbs` в `statusartifacts_test.go` есть, но фикстура даёт уже абсолютный `--config`, поэтому `absPath` в ней ничего не делает и его удаление не краснит. Замерено: с ОТНОСИТЕЛЬНЫМ `--config` `absPath` — несущий, и без него потребитель в другом рабочем каталоге прочитает не тот файл | убрать `absPath` у любого из четырёх путей — сегодня батарея зелёная. Пин обязан звать глагол с относительным `--config` |
| ж | **байтовый гейт `changedDoc` не запинен**: мутация `deltaChanged := res.DeltaTouched` (байтовый гейт снят целиком) оставляет и `TestBankApplyRepeatIsAByteNoOp`, и `TestBankApplyWritesTheConventionalFiles` зелёными. Контракт «байтовый no-op» держится сегодня только на `*Touched` из чистого слоя — второй рубеж не защищён | снять байтовое сравнение в `changedDoc` |
| з | **правило «файлы решений едут С КНИГОЙ, а не с `project_db`»** не запинено: конвенционный дефолт можно перевесить на каталог `project_db`, и батарея зелёная — во всех тестах два каталога совпадают. Правило несущее: `project_db` может указывать куда угодно, а решения владельца обязаны ехать с книгой в бэкапе и экспорте каталога | перевесить дефолт на `filepath.Dir(b.ProjectDB)`; пин обязан развести два каталога |
⚠ Уже существующие пины, которые тоже придётся починить, вынесены отдельным номером — **§4.15**.
### 4.12 Цепь §8 — ЗАКРЫТА ПРИЁМКОЙ, забери её в репозиторий — ДЕЛАЙ РОВНО ТАК
Твой §8 просил приёмку прогнать цепь целиком и объяснял, что своими силами это невозможно: «до стопа
надо дойти ОПЛАЧЕННОЙ черновой волной». **Довод оказался шире правды.** В репозитории уже есть
харнесс, гоняющий тот же банк-стоп против ФЕЙКОВОГО провайдера за $0 —
`internal/pipeline/miningstop_join_test.go`: `setupMiningStopProject` + `newJSONProvider` +
`newVerifyRunner` + `runToSignatureStop` + `readSignatureMap`, на них стоит ~30 живых тестов.
Приёмка написала и прогнала цепь целиком, и она **работает**: стоп на 2 термах → `bank-apply`
промотит dst-несущий и отклоняет WHICH-only → резюм → «empty delta, auto-continuing to the edit wave»,
редакторская волна отработала. Мина 1 строки 199 (ливлок на `status: auto`) не сработала.
**Заказ: этот тест обязан жить в репозитории**, потому что он — единственный, кто видит ливлок
уровня ЦЕПИ, которого частичные тесты по построению не видят. Собери его сам по описанию выше:
дойти до стопа → разобрать карту подписи `seed.DecodeFile` → построить документ, где термы с `dst`
идут в `approve`, а WHICH-only в `decline``ApplyBankDecisions` → пере-запустить прогон → убедиться,
что стоп ПОГАС и редакторская волна пошла.
**Как понять, что собрал верно.** На фикстуре без опций стоп приходит на ДВУХ термах — один с
`dst`, один WHICH-only, — то есть документ решений выходит «1 approve + 1 decline», `bank-apply`
отвечает `changed=true`, `accepted=2`, а файлы у него КОНВЕНЦИОННЫЕ (ключей фикстура не объявляет).
На резюме движок пишет в журнал `skipped_as_declined=1 emitted_by_miner=0` и «empty delta,
auto-continuing to the edit wave», после чего редакторская волна отрабатывает. Если у тебя иначе —
собрано не то.
**Ограничение назови в комментарии теста сам:** синтетический контраст меняет, КАКИЕ кандидаты
предложены, но не то, проходит ли подписанная строка фильтр подписи. Для вопроса «гаснет ли стоп»
это корректно; для замера качества майнера — нет.
⚠ Соседний факт, поймай его при написании: `TestMiningStopRejectClearsDelta` заканчивается
проверкой «погасший стоп не оставляет карту подписи», но использует ВТОРОЙ, свежий проект, где карта
никогда не писалась, — проверка почти вакуумна. На ОДНОЙ книге карта остаётся на диске: движок её
нигде не удаляет (`os.Remove` в `mining.go` не встречается). Поведение пред-существующее, паком не
введено — **пинг, не правка**, но пусть твой тест это зафиксирует явно.
### 4.13 Мелочи — РЕШИ САМ, чинить или отклонить с аргументом
- **`canonical_rewrite` — ИЛИ по двум файлам**, поэтому предупреждает про файл, который вызов не
откроет (замерено: на книге, чьи оба файла записал движок, флаг корректно `false`; горит, когда
комментарии оператора несёт reject-лист, а решение трогает только дельту). Потребитель рисует
предупреждение не про то.
- **`note` нельзя ОЧИСТИТЬ**: повторное решение без `note` сохраняет прежнюю заметку. Заказом не
оговорено ни в ту, ни в другую сторону — назови поведение решением, каким бы оно ни было.
- **`book_id` не проверяется на путевую безопасность**, а путь строится ДО валидации: `book_id: ../x`
уводит оба файла решений выше каталога книги. Класс пред-существующий (`<book_id>.db`), пак
расширил его с одного файла до трёх. На SaaS `book_id` генерируется платформой, то есть
пользователю недостижим.
- **Каталог книги без права записи → `exit 1`, ВНЕ полосы отказов** (`store: ... attempt to write a
readonly database`). Платформа спишет проблему прав ХОСТА в бюджет попыток КНИГИ. Класс
пред-существующий (`store.Open` ведёт себя так же), новый глагол его унаследовал.
- **Проекция создаёт `<project_db>.lock`** в каталоге книги. Требование §4.2 «проекция БЕЗ мутации»
держится (файлы решений не тронуты), но «не трогает ничего» буквально неверно.
- **Порядок двух записей пессимален для `decline`.** Комментарий обосновывает «дельта первой» тем,
что крах между записями оставит промоушен без снятого отказа — верно для `approve`. Для `decline`
тот же порядок теряет решение ЦЕЛИКОМ: дельта переписана (одобрение снято), reject не записан.
⚠ **Приёмка это НЕ воспроизвела** — атомарная замена обходит права на файл, а настоящее окно
микросекундное; повтор вызова при этом сходится. Утверждение остаётся рассуждением, не замером:
реши сам, стоит ли оно правки, и аргументируй.
- **`artifacts.bank_export` печатается безусловно**, в том числе для книги, где файла ещё нет
(проверено на копии стенда). Читатель платформы это переживает (обычная ошибка открытия), но поле
именует МЕСТО, а не наличие — реши, достаточно ли этого, и назови решение.
- **`aliasOwner` возвращается на ПЕРВОМ совпадении**, поэтому исход `decline` в теории зависит от
порядка строк дельты, если поверхность одновременно алиас одной строки и `src` другой.
⚠ Такой документ уже ловится `ApprovedSharedKeyCollisions`, и приёмка значимого расхождения
**воспроизвести не смогла** — это гипотеза, а не находка. Отклонить с аргументом законно.
- **Проекция отвечает «что применится», а не «почём».** Закон п.7 звучит как «почём ДО
подтверждения», прежний заказ денег в проекции не требовал, а смета уже едет в `status --json`.
Это вопрос ДИСПОЗИЦИИ, а не дефект исполнения: **не чини, а вынеси пингом** со своей рекомендацией —
решать оркестратору.
### 4.14 Довод отказа `--keys-file` не покрывает всех, кого отвергает — ДЕЛАЙ РОВНО ТАК
Текст отказа (`cmd/tmctl/invocation.go`) звучит так: «`--keys-file` принимает только `translate`, не
%q: **$0-читающие команды (report/status/export/manifest/seed-lint)** не должны требовать ключей вовсе
(D20.4)». В ту же ветку попадают `redrive` (**платный** путь: «only a real redrive re-attacks and
re-bills»), `backup` и `migrate` (деплой-шаги). Довод их не описывает, и оператор, получивший этот
отказ на `redrive`, читает про себя неправду.
**Заказ:** довод обязан покрывать всех, кого ветка отвергает. Одна строка.
⚠ Саму развилку «нужен ли `redrive` канал ключей аргументом» **НЕ решай** — прежний промт сказал
«флаг только у `translate`», и это исполнено верно; развилка остаётся пингом.
### 4.15 Три уже существующих пина тавтологичны или узки — ДЕЛАЙ РОВНО ТАК
Вынесено отдельным номером, чтобы механическая сверка §7 их видела.
- **Вердикт `Preexisting` исполняется одной проверкой из четырёх:**
`TestAPreExistingCollisionDoesNotBlockAnUnrelatedDecision` гоняет только
`ApprovedSharedKeyCollisions`; `MinedDeltaSeedCollisions`, `GenderVocabViolations` и
`UnknownVoiceCharacters` не исполнены ни одним тестом.
- **Четвёртый оракул `FuzzDecisionDocument`** пере-выполняет ровно то, что `documentProblems` уже
выполнила внутри `ApplyDecisions`, и заперт под `len(res.Rejected) == 0` — тавтология.
- **`TestDecisionByBankID`** вычисляет ожидаемый id той же функцией, которую проверяет, поэтому не
видит потери инъективности — свойства, ради которого в `TermID` и сделана преамбула про
length-prefix. Сверяйся с независимо посчитанным значением, а не с самой функцией.
### 4.16 Деньги: платных вызовов в паке НОЛЬ — ДЕЛАЙ РОВНО ТАК
Санкции на платные вызовы нет и не запрашивается. Всё выше проверяется юнит-тестами, фейковым
провайдером и пробой на копии книги стенда. Упёрся в место, где без денег не доказать, — **пинг с
названной суммой, не трата**. Рантайм-вердикт по платному пути остаётся PLAUSIBLE — это ожидаемый
исход, а не дефект отчёта; так и напиши.
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ
Не «перечитал» — **исполнил**. Минимум:
- **Батарея целиком, ИЗ КАТАЛОГА `backend/`**, после каждой содержательной правки: `go build ./...`,
`go vet ./...`, `go test ./... -count=1`, **`make lint`**. ⚠ `go.mod` в корне репозитория НЕТ — модули раздельные.
⚠ **И `make battery-stand` — обязательно, минимум один раз в конце.** Пак владеет `internal/standdata`
(тест-резолв корпуса), а регрессию в резолвере обычная батарея не ловит по построению: без корпуса
эти тесты просто скипаются. Ожидаемая картина — всё зелено, кроме `TestMinerFullBookParity`, красного
из-за отсутствующего jieba-контраста (он лежит по внутрирепозиторному пути, но вне git по построению —
клон его не получает; это строка бэклога 123, не твой дефект).
Красный тест не «подгоняется»: править или удалять тест, голден или гейт ради зелени —
НЕДОПУСТИМО; несогласие с тестом — пинг оркестратору, а не правка.
⚠ **В `internal/store` живут два пред-существующих `-race`-флейка** —
`TestTheSeamIsNotChargedToTheStoreOperationBudget` и `TestKillMinus9LosesAtMostOneCall`. Приёмка
доказала тремя независимыми путями, что они не от пака: правка `store.go` чисто аддитивна, покрытие
новых функций под обоими тестами ноль, и чередующийся A/B против дерева без пака даёт ту же
частоту. Оба меряют ВРЕМЯ и чувствительны к нагрузке. Красное ИМЕННО в них — шум; красное в любом
другом тесте — регрессия.
- **Собственные адверсариальные посадки ВНЕ списка §4.11.** Десять названных там — обязательный
минимум, а не потолок: посади свои в новый код и покажи, что батарея краснеет.
⚠ **Посадка обязана называть ПАКЕТ, в котором ищется пин** — не тот, где лежит правка: ложное
«мутация выжила» опаснее пропущенной, потому что выглядит как результат.
⚠ **Харнесс мутаций обязан снимать отпечаток целей до и после** — твой же §11 п.1 объясняет цену.
- **Живая проба на копии книги стенда** (`<репозиторий>/books/gu-zhenren/*`). Работать в КОПИИ вне
рабочего дерева; правки в `books/` не делать вовсе — это отдельный git-репозиторий и не твоя зона.
- **Субагенты РАЗРЕШЕНЫ явно** — на ревью своего кода, на поиск дефектов вне твоей карты, на
опровержение твоих же выводов. Дефолт-запрет харнесса иначе тихо победит. ⚠ Соразмеряй параллельность с ресурсами:
субагенты, одновременно гоняющие полную батарею, топят друг друга, и упавший от нагрузки агент
выглядит как отсутствие находок.
- **Артефакт-находки обязателен:** дописывай `backend/docs/SEAM_PACK_FINDINGS.md` новой секцией
(не переписывай старые — они улика приёмки), что посадил, что краснело, что выжило.
### 5.1 Обязательное для кодового пака
- **Последний абзац отчёта — это план или обещание? Значит сделай его СЕЙЧАС.**
- **Дифф `^func Test` — исполнением, а не по памяти:** покажи командой, какие тесты добавлены и тронуты.
- **Интервальная самоверификация субагентом** против ЯВНЫХ критериев в середине работы.
- **Перед отчётом сверь каждый клейм с результатом инструмента ЭТОЙ сессии.**
## 6. Оси ревью (три, по характеру работы)
1. **Round-trip формата** — главная ось этого дофикса: всё, что дверь ЗАПИСЫВАЕТ, она обязана
ПРОЧИТАТЬ обратно, на обоих документах и на всех текстовых полях. Фаззер здесь сильнее примера.
2. **Слепота гейта** — для каждой правки: какой мутацией она сторожится и краснеет ли пин НА САМОМ
ДЕЛЕ. Восемь мест §4.11 — это там, где гейт уже оказался слепым; не заведи девятое.
3. **Общность** — заработает ли на паре, которой в репо нет, без правки Go. Приёмка проверила дверь
на `en→de` и `ja→en` и нашла её чистой; твои правки не должны это сломать.
Ты вправе ЗАМЕНИТЬ любую ось своей, если аргументируешь, чем твоя лучше ловит дефекты этой работы.
## 7. Записка-план
До кода — короткая записка `backend/docs/SEAM_FIX_PLAN.md`: что делаешь, в каком порядке, какие
развилки видишь. Комплектность против §4 сверяется МЕХАНИЧЕСКИ (таблицей или грепом), а не глазами:
**пунктов шестнадцать** (`grep -cE '^### 4\.' `), и «кажется, всё» здесь не работает. Заказов ВНЕ §4
нет — всё, что раньше жило в приложениях и в хвостах, вынесено в §4 отдельными номерами
(§4.14, §4.15); приложения несут только справку и пинги.
## 8. Заявление = команда
Каждое число и каждая категорика в отчёте — **с командой, которой получены, прямо рядом**. Не по
памяти о том, как ты это выяснял. Приёмка пере-ранит выборочно; расхождение отчёта с пере-раном
дороже, чем честное «не проверял».
⚠ Прошлый отчёт дал повод: его замер цены глагола (`5000 → 1.60с/133МБ · 20000 → 9.08с/350МБ`) при
пере-ране приёмки сошёлся по времени на 5k/10k и разошёлся вдвое по ПАМЯТИ (199/395/798 МБ), а
выведенный из него потолок «до ~80k термов в 60-секундный бюджет платформы» оказался оптимистичен
примерно вдвое. Числа, которые поедут в чужой пак как датум, стоят отдельной аккуратности.
## 9. Эхо-протокол старта
ДО работы — не больше десяти строк: скоуп как ты его понял · инварианты, которые не тронешь · что
делать НЕ будешь. Расхождение с этим промтом видно сразу и стоит дёшево.
## 10. Что НЕ удалось
Обязательная секция отчёта. «Не проверено» ≠ «работает». Вердикт о рантайме без живого прогона —
максимум PLAUSIBLE, и так и пиши.
## 11. Канал вопросов
Конфликт промта с кодом или доками — **пинг через владельца, не интерпретация**. Ты вправе сказать
«этого делать не надо» с аргументом: пак, принёсший развилку вместо тихой девиации, — хороший пак.
Четыре пункта выше (§4.6, §4.9, §4.10, §4.13) прямо приглашают такой ответ.
---
## Приложение: что приёмка ПОДТВЕРДИЛА — не чини это заново
Чтобы дофикс не тратил время на уже доказанное:
- **Цепь работает** (§4.12) · **мины 1 и 2 строки 199 обезврежены** (явный `status: approved` пишется;
approve без `dst` отвергается).
- **§4.1§4.7 прежнего заказа исполнены полностью**, все девять семантик §4.2 — тоже, семь пунктов
закона — тоже; скоуп-крипа нет. Пять вещей сверх заказа (класс 14, `export_version`, пакет
`standdata`, строгость reject-читателя, два поля отчёта) — аргументированы и безопасны.
- **Ни один платформенный канал не ломается**: аддитивные поля прошли через её собственные декодеры
на реальных документах; строгих декодеров у неё нет ни одного на пути движковых документов;
id термов банка не сдвинулись; `Refused(14)` истинно, `DeploymentFault` 14 не покрывает, а интейк
отправляет нераспознанный код полосы в ветку, которая файл пользователя НЕ удаляет.
- **Строгость не сломала ни один живой документ корпуса** — 44 из 44 (29 сид-схемы + 15 reject-схемы).
- **Критерий §4.6 ПРЕЖНЕГО заказа (дефолт корпуса через `standdata`) выполнен буквально** — не путать
с §4.6 ЭТОГО промта, который про другое; **§13 сходится точно** (59 тест-функций + 2 фаззера,
удалено 0); все 28 пинов, названных отчётом, существуют ровно по одному разу.
- **$0 доказан** структурно и исполнением; ледджер и база не сдвинулись ни на байт.
- **Три заявки ревьюеров приёмка ОПРОВЕРГЛА, не поднимай их заново:** src, нормализующийся в пустой
ключ, ловится дедупом поверхностей · повторный `decline` корректно отчитывается `already_applied` ·
отказ по алиасу подписанного сид-терма отвергается по имени.
## Приложение: пинги, которые дофикс НЕ исполняет
Названы, чтобы ты их не «допилил попутно»:
- **exit 2 при OOM читается платформой как успех** (§4.5) — общедвижковый класс, не твой скоуп.
- **`readEngine` платформы выбрасывает stdout при любом ненулевом коде, кроме 2** — а отчёт
`bank-apply` при `exit 14` и есть продукт вызова. Требование к ПЛАТФОРМЕННОМУ паку.
- **Развилка «нужен ли `redrive` канал ключей аргументом»** — остаётся пингом. Заказом стала только
вторая половина этого пинга — неполный довод в тексте отказа, §4.14.
- **`rubyToCandidates` мёртв в проде** · **гейт `TestDispatchCommandsCoversTheSwitch` слеп к `case` с
константой** (⚠ эта слепота названа в §4.11 (в) как ловушка при копировании техники — не унаследуй её) ·
**схема сида без `omitempty`** — три пинга твоего же §10, остаются пингами.
- **Проекция берёт ТОТ ЖЕ лок, что и применение** (§10 п.4) ⇒ «поправить термин при живом прогоне»
даёт `12` даже на ПРЕВЬЮ, на всю длину прогона. Проектируется ПЛАТФОРМЕННЫМ паком (его окно —
состояние между прогонами, когда движок вышел и лок отпущен), не движковым.
- **Опубликованные пути двух файлов решений читаются как приглашение их парсить** (§10 п.5) —
⚠ см. §4.6: изгородь снимается сознательно и по ратифицированному направлению закона.
- **Карта подписи не удаляется после погасшего стопа** (§4.12) — пред-существующее, пинг.
---
## Записка от бэкенд-сессии ПАКА (написана до компакта её контекста, 25.08)
Я — та сессия, чей пак не приняли. Промт я прочитал целиком и **верифицировал исполнением** ключевые
клеймы, прежде чем писать это. Ниже не инструкции: у тебя есть преимущество, которого нет у меня —
взгляд с нуля, и я не хочу его портить. Здесь только то, что дорого узнавать заново.
### Что я ПЕРЕПРОВЕРИЛ САМ и подтвердил (не трать на это время)
Воспроизведено моими прогонами на этом же дереве: **§4.1 блокер** (`note` с ведущим `\n` у `decline` →
записывается `note: |4-`, `seed.DecodeRejects` не читает) · **§4.2** (починка ОДНОЙ из двух битых строк
→ отказ, обеих сразу → `exit 0`) · **§4.4** (все три формы обходят гард: `--decisions=`,
`--decisions ""`, `--keys-file=`) · **§4.7** (approve `方源` + decline `方小子` одним вызовом → оба
`applied`, `rejected: []`) · **§4.8** (окно `[90,3]` принимается).
### ⚠ Единственное место, где промт ШИРЕ замера — проверь до того, как строить фикс
§4.1 говорит, что корень «бьёт по ЛЮБОМУ свободному текстовому полю обоих документов». **Мой замер
этого не показывает.** Прогнал пять форм значения через рендер и обратно, оба документа:
```
note="\nтекст" дельта=ломается rejects=ломается
note="текст\n" дельта=ок rejects=ок
note="a\nb" дельта=ок rejects=ок
note=" пробел" дельта=ок rejects=ок
note="\tтаб" дельта=ок rejects=ок
```
То есть триггер — **ведущий перевод строки**, а не «любой свободный текст» и не пробельность вообще.
Отдельно: `dst` и `sense` с тем же ведущим `\n` у меня round-trip НЕ сломали, а `note` — сломал, и
я это не объяснил. Практический вывод, и он усиливает заказ, а не отменяет его: **не пытайся
перечислить ломающие входы — их множество я измерил и оно не совпало с интуицией промта.** Чини
ИНВАРИАНТ (round-trip на обоих документах), а не набор полей. Заплатка на «поля с ведущим `\n`» будет
выглядеть рабочей и промахнётся по следующей форме.
### Инструмент, которого у тебя не будет, а он нужен
**Харнесс мутаций жил в скретчпаде сессии, не в репозитории.** Он умер вместе с ней. Тебе придётся
писать его заново, и §4.11 требует десять посадок. Совет: сразу закладывай снятие sha256 целей до
прогона и проверку после — у меня прогон убило таймаутом, `finally` не отработал, посадка просидела в
дереве ~40 минут, выглядя продакшн-кодом, и сторонний ревьюер снял с неё замеры и доложил
несуществующий дефект. Поймал её мой же пин. Подумай, не место ли такому харнессу в репозитории —
приёмка теперь тоже сажает мутации, а инструмент каждый раз пишется с нуля.
### Три вещи, на которых я обжёгся, и они повторятся
1. **Одиночный прогон — не замер.** Атрибуцию флейков я дважды вывел из одного прогона и оба раза
получил противоположный ответ. Решает только чередующийся A/B на двух деревьях (`git worktree` на
HEAD — рабочее дерево при этом не трогается, `checkout` поверх грязного запрещён).
2. **Субагенты пишут в твоё дерево.** Ревью-агенты оставили `zz*`/`*scratch*`/`refuter_*` в
`internal/`, часть удалили за собой, часть нет; один раз это уронило батарею. Сверяйся
`git ls-files --others` перед каждым «финальным» прогоном, и не грепай по `zz` — попадёшь в `Fuzz`.
3. **Красное в `internal/store` — почти наверняка не ты** (два `-race`-флейка, §5 промта). Но проверь
это ЗАМЕРОМ против HEAD, а не поверь мне на слово: я один раз получил обратную картину.
### Мои сомнения по заказу — там, где §11 приглашает спорить
- **§4.9 (честность отчёта про непогасший стоп).** Я не уверен, что дверь это ЗНАЕТ. Она видит, сколько
решений применила, но не сколько поверхностей осталось за капом реверс-секции — это знает майнер на
следующем проходе. Скорее всего честное поле звучит как «этот вызов решил N; погаснет ли стоп,
определит следующий майнинг», а не как предсказание. Если придёшь к тому же — это законный ответ.
- **§4.10 (SIGTERM) и §4.5 (кап) взаимодействуют.** На реальной книге вызов — десятки миллисекунд;
лок держится долго только на патологически большом документе, который §4.5 и запрещает. Возможно,
кап делает половину проблемы несуществующей, и тогда обработчик сигнала — механизм без носителя.
Замерь длительность ПОСЛЕ капа, прежде чем решать.
- **§4.6 (пути в `manifest --json`).** Заказ разумен по потребителю, но семантически манифест — про
дерево глав, и путь артефакта там гость. Я бы это сделал и назвал компромисс вслух, а не молча.
- **§4.13 «`note` нельзя очистить»** — реши осознанно. Сейчас пустой `note` в решении означает «не
трогай», и это было намеренно (иначе каждое решение стирало бы провенанс владельца). Обратное
поведение тоже защитимо; плохо только не назвать.
### Про числа
Мой замер памяти (`133/217/350 МБ`) разошёлся с приёмочным (`199/395/798 МБ`) примерно вдвое. **Я не
знаю, кто прав**, и не выяснил — методику я не зафиксировал (`/usr/bin/time -f %M`, машина под
нагрузкой). Не наследуй ни моё, ни их: пере-мерь со своей названной методикой, потому что это число
поедет в платформенный пак как датум для таймаута.
### Главный урок, если брать один
Я потратил два адверсариальных ревью на **рассуждение** — они нашли настоящие дефекты, но дефект,
который стоил бы ДЕНЕГ (`--decisions` принимался всеми командами), нашёлся, когда я перепроверил
собственный скоуп **исполнением**. А §4.11 этого промта — восемь мест, где мой гейт оказался слепым,
и я узнал об этом от приёмки. Отсюда единственный совет по методу: **сначала посади мутацию, потом
пиши пин.** Пин, написанный первым, почти всегда проверяет то, что и так верно.
— бэкенд-сессия пака, до компакта
### Онбординг в САМ КОД — этого нет ни в промте, ни в `FINDINGS`, а искать заново дорого
**Дверь двухслойная, и порядок чтения не произвольный:**
1. `internal/membank/decisions.go` — **чистая логика**, без I/O, часов и рандома. Здесь вся семантика:
что принимается, что отвергается, чем отказ обоснован. Сюда садятся §4.1, §4.2, §4.7, §4.8 и
бо́льшая часть §4.11.
2. `internal/pipeline/bankdecisions.go` — **обвязка**: лок, чтения, два гейта записи на файл, отчёт.
Сюда садятся §4.5 (кап), §4.10 (сигнал), §4.6 (если решишь трогать `status`/`manifest`).
3. `cmd/tmctl/bankapply.go` — тонкий принт отчёта. `cmd/tmctl/invocation.go` — §4.4 и §4.14.
Обратный порядок даёт ложное впечатление, что дверь про файлы. Она про решения; файлы — следствие.
**Карта «предмет → где пин»** (пригодится, когда будешь чинить и захочешь понять, что сломал):
`membank/decisions_test.go` — семантика решений · `membank/decisions_fuzz_test.go` — round-trip и
оракулы набора · `pipeline/bankdecisions_test.go` — лок, гейты записи, классы отказа, «прогон читает
конвенционный файл» · `pipeline/statusartifacts_test.go` — `artifacts{}`/версии · `config/decisionpaths_test.go` —
конвенционные дефолты · `cmd/tmctl/keysfile_test.go` — гарды флагов и e2e через реальный бинарь ·
`seed/decode_test.go` — строгость и пустые документы.
**Инварианты, которые легко сломать ПОПУТНО, чиня §4** (у каждого сегодня есть пин, кроме отмеченных
в §4.11 как слепые):
- `res.DeltaTouched` / `res.RejectsTouched` — гейты **на файл**, не на вызов. Их смысл: `decline`
не должен пере-рендеривать дельту, которой не касался. Если будешь трогать `applyOne`, следи, чтобы
флаг ставился ровно тем действием, которое реально изменило документ.
- `newDocumentProblems` сравнивает **до/после**, и это единственное, что не даёт пред-существующей
поломке блокировать починку. §4.2 просит поменять КЛЮЧ сравнения (субъект вместо текста), а не
выкинуть сравнение.
- `withRubyAliases` делает **глубокую** копию алиасов: `AttachRubyAliasesToManual` мутирует на месте,
а входные строки сида читаются ещё раз в «до». Мелкая копия ломает сравнение до/после незаметно.
- `mergeTerm` мержит на существующую строку, а не заменяет её — иначе решение о `dst` стирает
рукописный `gender`, которого во входном словаре двери нет вовсе.
### Как заново собрать живую пробу на стенде (я потратил на это несколько заходов вслепую)
Копия ОБЯЗАТЕЛЬНА вне рабочего дерева; в `books/` не писать. Три ловушки, каждая стоила мне прогона:
1. `cp -r books/gu-zhenren/minirun-verify2 <scratch>/live` — внутри **все пути абсолютные и мёртвые**
(`/home/ubuntu/...`, машина переехала). Переписывать надо в **ТРЁХ** файлах: `book.yaml`,
`pipeline.yaml` **и `pairs/zh-ru.yaml`** — в последнем лежит `prompts_root`, и про него забываешь,
потому что промпты «вроде рядом».
2. База стенда — **схема v11**, бинарь ждёт v15. Первый же вызов даёт `exit 13`. Это НЕ дефект: сделай
`tmctl migrate --config <копия>/book.yaml` на копии. (Заодно это единственный способ увидеть
ратифицированный класс 13 живым — юнит-тесты всегда создают базу свежей.)
3. Банк-стоп на этой машине **не воспроизводится** — `pipeline.yaml` требует jieba-контраст, которого
в клоне нет по построению (строка 123). Для цепи §4.12 используй фикстуру
`internal/pipeline/miningstop_join_test.go`, а не стендовую книгу: приёмка прямо говорит, что
харнесс там уже есть и цепь на нём гоняется за $0.
### Чего в моей памяти было, а в документах нет — выгружаю здесь
- **Почему `sameTerm` сравнивает YAML-рендер, а не `reflect.DeepEqual`:** вопрос функции — «изменится
ли ФАЙЛ», а `DeepEqual` различает nil и пустой слайс, чего у формата нет, и дал бы ложное «изменилось».
Это в комментарии кода есть, но если будешь «упрощать» — знай, что это не небрежность.
- **Почему словарь решений именно такой** (`note` принимается и не публикуется, `aliases` публикуются
и не принимаются): развёрнуто в комментарии `Decision`. Короткий слоган «вход = выход» НЕВЕРЕН, и
«починка» кода под слоган сломает дверь в обе стороны.
- **Класс 14 заводился с проверкой чужой стороны:** `DeploymentFault` платформы покрывает 10/12/13
одинаково, а зовут её только пути интейка — поэтому мис-классификация была перспективной, а не живой.
Если будешь трогать полосу, эта проверка делается одним грепом по `platform/internal/ingest/exit.go`.
- **`make battery` не детерминированно зелёная** (~1 падение на 24 полных `-race` прогона, всегда в
одном из двух названных store-тестов). Первый абзац §1 `FINDINGS` про это; прочти ДО первого прогона,
иначе отнесёшь красное на себя.
— бэкенд-сессия пака, до компакта (дополнение)