textmachine/docs/archive/PROGRESS-2026-08-backend.md

1114 lines
250 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.

# АРХИВ: секция «Бэкенд» журнала прогресса, эра 2428.08.2026
> ⚠ **ИСТОРИЧЕСКОЕ. Инструкции отсюда НЕ ИСПОЛНЯЮТСЯ.** Это вынесенная 28.08 прожитая проза зонной
> секции `docs/PROGRESS.md` — пять итогов бэкенд-сессий и их записки-планы. **ВСЕ описанные здесь паки
> ЗАЛЕНДЖЕНЫ**, и каждый «дерево готово, НЕ закоммичено» относится к состоянию, которого больше нет.
>
> **Куда что уехало:** входная дверь шва (2427.08, четыре сессии) — `D39.158`, лендинг `d1eb8a9`;
> пак «тихая порча» (28.08) — `D39.164`, лендинг `7d0c6f2`.
>
> ⚠ **Остатки, которые здесь названы, живут СТРОКАМИ единого бэклога, а не этим файлом:** **228**
> (алиас возвращает отклонённую поверхность), **229** (снапшот не фолдит модель внутренних гейтов),
> **230** (размен сходимости против поучения), **231** (смета пере-прохода), **141** (остаток
> детектора сносок), **131** (кап инъекции не считает маркер). Вендор-вердикт по Gemini живёт
> ПИНГОМ ПОЛИГОНУ в живом журнале — здесь его копия, не носитель.
>
> Вынесено аудитом документации 28.08 (D39.167) по слову владельца «пройденную прозу можно сносить».
**⟶ Состояние бэкенда — CURRENT-STATE (один носитель, D39.80); здесь только пинги живых сессий.**
**✅ ИТОГ БЭКЕНД-СЕССИИ 28.08 — ПАК «ТИХАЯ ПОРЧА» (`docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md`), все три пункта. Дерево готово, НЕ закоммичено.**
**§6 КОМПЛЕКТНОСТЬ — пункт → что сделано → каким ИСПОЛНЕНИЕМ подтверждено.**
| Заказ | Что сделано | Чем подтверждено ИСПОЛНЕНИЕМ |
|---|---|---|
| **§3.1** инъекция банка доезжает на каждом провайдере либо громкий отказ | Новая ось `llm.Capability.SystemMessages` (`multi`\|`single`, `capability.go:87-127,158-165`); под `single` `toOpenAIMessages` СКЛЕИВАЕТ ведущий системный ряд в одно сообщение (`httpllm.go:513-559`) и падает громко на системном посреди диалога — зеркально Anthropic; YAML-ось `capabilities.system_messages` с валидацией enum (`config/models.go:101-106,566-578,646-650`); квирк объявлен у провайдера `gemini` с вендор-ссылкой и датой (`configs/models.yaml:120-137`) | 6 тестов `internal/llm/systemmessages_test.go` + сквозной на ОБОИХ волнах `internal/pipeline/systemmessages_wave_test.go`. Мутация «ось игнорируется» → падает `TestSingleSystemEndpointCarriesTheBankOnBothWaves` обеими под-ветками и `…CarriesTheInjection` с текстом `got 2 (roles [system system user])`. Мутация «ось теряется по дороге из YAML» → тот же тест. Каталог: `SH1`, `SH2` |
| **§3.2** дыра выдачи видна читателю файла | ⚠ **Заказ пере-формулирован по находке** (разрешено оркестратором 28.08): маркер уже существовал и ВРАЛ. `ChunkOutcome`/`ChunkExport` несут `DroppedMembers` (`bookrun.go:69-75`, `export.go:45-51`, `waverun.go:488`, `export.go:227-230`); `cmd/tmctl/render.go` печатает маркер В ПОТОКЕ ТЕКСТА (`gapMarker`, :26-63) в обоих рендерах и честные счётчики `withheld`/`incomplete` | Сквозной прогон БОЕВОГО бинаря `cmd/tmctl/exportgap_cli_test.go`: `tmctl translate``tmctl export --plaintext`, текст ДО и ПОСЛЕ на ОДНОМ состоянии (ниже). Матрица семи состояний. Мутации `SH3` (маркер пуст) и `SH4` (счёт выпавших не ведётся) |
| **§3.3** повтор принятого документа сходится | `refuseSeedConflicts` больше не срабатывает, когда отказ УЖЕ на записи (`membank/decisions.go:365-393`, новый `rejectsHoldSurface` :690-700); порядок записи в `writeDecisionFiles` изменён на REJECTS-FIRST с обоснованием (`bankdecisions.go:427-441,458-489`) | 5 тестов `membank/decisions_converge_test.go` + 3 дверных `pipeline/bankdecisions_converge_test.go` (реальные файлы, реальные классы отказа). Мутации `SH5` (сужение снято) и `SH6` (порядок возвращён) |
**РЕШЕНИЯ ПО ПРОПУСКАМ И РОДНЕ — строкой каждое, как просил оркестратор.**
- **Четвёртая дыра (строка 141) — ВЗЯТА БЕСПЛАТНО, работы не потребовала.** Проверено исполнением
(проба по `internal/checks`, файл удалён после замера): сноска `[1] 蛊真人 — название романа.`
мимо детектора примечаний идёт, но поднимает класс `CJKLeak`, а он `CosmeticOnly`
(`checks/sanitizer.go:161-163`) ⇒ `StripCosmetic` вырезает ханьцзы, юнит уходит
`sanitizer_stripped`, и баннер «leak cleaned, verify» для НЕГО правдив. Механизм этого же заказа
накрывает класс без правки детектора — граница промта соблюдена буквально.
⚠ Смежное, НЕ взятое и названное строкой: сноска БЕЗ ханьцзы (`[1] Название романа.`) не поднимает
ни одного класса (замер: `total=0`), едет как есть — но текста при этом НЕ теряет, это лишняя
добавка, а не дыра. Маркер на ней дезинформировал бы (⛔ анти-скоуп промта). Ловля таких сносок —
работа В ДЕТЕКТОРЕ, промт её прямо запретил.
- **Д1 — НЕ ВЗЯТ, отдельным предметом.** Право §9. Канон определяет decline ПОВЕРХНОСТНО
(«declining a surface removes EVERY window of that surface», `14-api-contract/openapi.yaml:2051`),
и расширять его до энтити значило бы менять ратифицированный контракт из бэкенд-сессии. Вторая
половина Д1 («память глушит стоп») — уже НАЗВАННЫЙ и принятый двусторонний остаток в комментарии
`mining.go:308-313`, а не необнаруженный дефект. ⚠ Но одна траектория Д1 реальна и требует
ЧУЖОГО решения: эмиссия майнера энтити-широка (`miner_emit.go` `clusterTouches`), а фильтр
авто-банка ключуется только по собственному src строки (`mining.go:662`), поэтому ОТКЛОНЁННАЯ
поверхность может вернуться алиасом уцелевшей строки. Это противоречит канону уже сегодня.
**Не чиню в этом паке** (правильная форма — снять алиас со строки, а не снести строку; это
предмет банковой онтологии, а не тихой порчи) — **но это заказ, который стоит выдать.**
- **Д3 — НЕ ВЗЯТ, вне моей зоны.** `LiftBankStop` в `backend/` не существует ни в каком написании;
символ живёт в `platform/internal/runs/spawn.go` и `reconcile.go`, то есть в зоне, запертой паком
P9. Движковая половина при этом здорова: порядок «карта → память» ратифицирован и исполнен
(`mining.go:191` пишет карту, `:274` метит память), отказ памяти оценён в комментарии `:275-276`
как «one extra stop, nothing lost». ⚠ Поправка к моему же раннему пингу: я сказал «гарда не
существует» — точнее «не существует В ДВИЖКЕ».
- **Сосед 131 — НАЗВАН, не чиню.** Кап инъекции считает только `src → dst`
(`membank/memory.go:517-521` `glossaryLineTokens`), а рендер дописывает к КАЖДОЙ строке
`tx.UnverifiedMarker` (`:706`) либо `genderConstraintNote(...)` (`:711`), а редакторский блок ещё и
префикс `- ` с кавычками-ёлочками (`:821`). То есть отгружаемая строка строго длиннее той, за
которую платит бюджет; на коротких именных строках перерасход велик. Тема денег и качества
инъекции — не тихий брак в тексте.
**НАХОДКИ, МЕНЯЮЩИЕ ПРОМТ (все три подписаны, ни одна не абсорбирована молча).**
1. **§3.2: маркер существовал и ВРАЛ.** Ветка `case ce.FinalText != ""` печатала «(leak cleaned,
verify)» ЛЮБОМУ флагнутому юниту с текстом, а c-lite member-drop (`export.go` ставит `DispFlagged`
при непустом тексте) попадал ровно в неё. Существующий тест не ловил: покрывал ветку только
причиной `sanitizer_stripped` (`export_cli_test.go:66`). Предмет заказа изменён с «завести маркер»
на «перестать врать и покрыть непокрытое» — разрешено оркестратором 28.08 при закрытом гейте.
2. **§3.2: подсказка промта про место правки была опасна.** Промт указывал `export.go:237` рядом с
`ApplyHeading`. Туда нельзя: `ChunkExport.FinalText` — поле артефактного контракта, а провод судит
`translated`/`withheld` предикатом «текст непуст» (`waverun.go:203-204` и `events.go:538-554`,
`runevents.go:130` `Shipped`). Маркер в пустом юните перевернул бы `withheld``translated`
ровно ⛔(а) промта. `ApplyHeading` этого не делает лишь потому, что на пустом тексте он no-op.
Маркер живёт в РЕНДЕР-слое. Проверено: `FlagReason`-константы до и после ИДЕНТИЧНЫ, `exportVersion`
не двигался, `dropped_members` аддитивно и `omitempty`.
3. **§3.1: посылка «Gemini принимает ровно одно» отозвана автором** (оркестратор, 28.08) — вендор-
вердикт со ссылками и датами отправлен ему отдельно, для `00-provider-quirks.md`. Кратко:
структурная посылка ПОДТВЕРЖДЕНА (`systemInstruction` — один `object (Content)`, `Content.role`
{user, model}), поведение шима вендором НЕ документировано нигде, наш замер даёт «не склеиваются»
и «инструкция первого не исполнена»; «выживает одно или ноль» остаётся ОТКРЫТЫМ и для решения
безразлично.
**ДЕФЕКТ, КОТОРЫЙ Я ВНЁС САМ И ЗАКРЫЛ (называю, потому что он показателен).** Первая редакция ветки
маркера срабатывала по `DroppedMembers > 0` БЕЗ проверки непустого текста. Юнит, у которого выпали ВСЕ
члены, ship-ит пустоту — и получил бы баннер «(INCOMPLETE)» с фразой «фрагмент отсутствует в тексте
НИЖЕ», указывающей на несуществующий текст. То есть я едва не заменил одну ложь читателю на другую.
Найдено собственным разбором ветвлений до опровергателей, закрыто гардом на обоих сайтах вызова,
покрыто состоянием «выпали все» в матрице и запинено мутацией (снятие гарда → тест падает).
**§5 ОСИ РЕВЮ.**
1. **Читатель книги.** Все четыре класса проверены на ОТГРУЖЕННОМ ТЕКСТЕ, не на структурах:
(а) c-lite — был помечен ложно, теперь помечен верно и в потоке текста; (б) юнит целиком withheld и
(в) `glossary_miss` — помечены верно и раньше, работы не требовали, теперь ещё и не попадают под
маркер по ошибке; (г) сноска с ханьцзы — накрыта существующим верным маркером.
2. **Пара, которой в репо нет.** Правка не ветвится по паре ни в одном месте: ось `system_messages`
свойство ЭНДПОЙНТА, маркер — английский операторский словарь той же поверхности, что и соседние
баннеры, поэтому у книги с любым `target_lang` и БЕЗ langpack вывод байт-идентичен. Специально НЕ
стал класть маркер в langpack: `InjectionTextsFor` для незнакомой цели возвращает нулевое значение
молча (`lang/embedded.go:411-423`) — маркер бы исчез, и дыра снова стала бы тихой ровно для той
пары, ради которой мандат общности и написан.
3. **Детерминизм.** Голден зелёный (`go test ./internal/pipeline/ -run TestGolden -count=1`), и это
сильная улика: голден пинит ПОЛНОЕ тело запроса с обоими системными сообщениями
(`testdata/golden/capture.golden:110`), то есть глобальная свёртка сделала бы его красным, а
квирк-свёртка не трогает никого, кто квирк не объявил. `omitempty` на новом поле запинен тестом
(`TestCapabilityWithoutTheAxisMarshalsUnchanged` сверяет БАЙТЫ). Снапшот фолдит Capability только
праймари и `escalate_to` (`snapshot.go:316-340`), а в `pipeline-c1.yaml` это `deepseek-v4-pro`
(`:60`) — gemini там лишь третий в цепочке при `budget_usd: 0` и SINGLE-HOP раннере ⇒ снапшот
боевого конфига не двигается, перекупки нет.
**§8 OBSTACLE — что НЕ удалось и что НЕ проверено.**
- **Живого вызова к Gemini НЕ было** (пак платных вызовов не требует, и я их не делал). Что склейка
ПОМОГАЕТ, доказано структурно вендор-докой и логикой «при любом чтении, кроме склеивания, два
системных теряют текст» — но НЕ замерено на проводе. Решающая проба стоит денег и принадлежит
полигону: маркер на ПОСЛЕДНЕЕ системное + user-only контроль закрыли бы открытый вопрос
«выживает одно или ноль». Также: проба полигона гонялась на `gemini-3.1-flash-lite`, а в цепочке
стоит `gemini-3.1-pro-preview` — слой тот же, слаг другой, перенос не замерен.
- **Достижимость §3.1 сегодня НУЛЕВАЯ, и это не смягчение дефекта, а его честная рамка.** Раннер
исполняет SINGLE-HOP, `escalate_to` боевого конфига — `deepseek-v4-pro`, `budget_usd: 0`. Gemini не
достигается ни одним шиппинг-конфигом; дефект ждал первого конфига, который назовёт его в
`escalate_to` или в `chain[0]` лейбла. Кривая пост-чек-промахов на эскалационном хопе, которую §4.2
просил как второй замер, СНЯТА БЫТЬ НЕ МОЖЕТ по той же причине — мерить нечего.
- **Ловушка, которую я НЕ чинил и обязан назвать: `capabilities` у провайдера с `kind: anthropic`
принимается валидацией и молча игнорируется** — `clients.go` не передаёт `Cap` в Anthropic-клиент
вовсе. Это ПРЕД-СУЩЕСТВУЮЩИЙ класс (так ведут себя ВСЕ оси: `budget_field`, `temperature`,
`reasoning`, `min_max_tokens`), моя ось лишь наследует его. Чинить правильно — тащить `kind` в
`validateCapabilities`, что трогает каждую ось; в этом паке это чужой предмет. Практический риск
сегодня нулевой: провайдера `anthropic` в `models.yaml` нет.
- **Изменён СУЩЕСТВУЮЩИЙ тест — заявляю прямо, не прячу в дифе.**
`internal/pipeline/bankreport_test.go` `TestHalfLandedIsNamedPerFile` кодировал ПОРЯДОК переименований
в своей фикстуре (занимал путь rejects, чтобы упал второй rename). Порядок изменён заказом §3.3, и
фикстура пере-нацелена на путь delta. Утверждение НЕ ослаблено: по-прежнему требуется, что ровно
одна половина легла, что это ПРОВЕРЕННЫЕ байты и что отчёт называет какая. Сверх того добавлен
`TestTheRenameOrderIsWhatMakesADeclineRecover`, который пинит САМ порядок вместе с его причиной,
чтобы его нельзя было вернуть как «уборку». **Если оркестратор считает это подгонкой — я готов
откатить и вынести §3.3-половину с полусостоянием отдельным заказом.**
- **Каталог мутаций: запись `AJ-seed-alias-decline` пере-заякорена.** Моя правка сдвинула строку, на
которую она указывала, и инструмент падал жёсткой ошибкой «the source moved under the catalogue» —
это был бы хард-блокер для следующей сессии. Атакуемое свойство сохранено дословно (снять
дельта-оговорку, оставить остальное); плюс добавлены шесть СВОИХ посадок `SH1``SH6`, по две на
пункт, чтобы фикс был постоянным гейтом, а не разовым замером.
- **Не проверено:** поведение под реальным параллелизмом двух `bank-apply` (лок это исключает, но я
не гонял); поведение склейки на провайдере, который принимает одно системное И имеет префиксный кеш
(у gemini кеш иной) — довод «склейка кеш не портит» опирается на комментарий самого кода и на то,
что префикс сохраняет байты и позицию, живьём не мерен.
**§4 ОПРОВЕРГАТЕЛЬ — ЗАКАЗ ИСПОЛНЕН, И ОН ОКУПИЛСЯ. Шесть линз, мандат «опровергни».**
Веер шесть, а не «24» из §4: численный потолок снят словом владельца 28.08, соразмеряю предмету.
Линзы: Anthropic-путь и кеш · читатель книги · провод `withheld`/`translated` · безопасность банковых
решений · пара, которой в репо нет + детерминизм · вырожденность собственных тестов. **Пять из шести
вернули вердикт REFUTED или PARTIAL, и это правильный исход: они нашли ШЕСТЬ дефектов, из них ЧЕТЫРЕ
я внёс сам этим паком.** Все воспроизведены ИСПОЛНЕНИЕМ на копиях дерева, не чтением.
| Находка | Кто/чем доказано | Что сделано |
|---|---|---|
| **MAJOR: маркер называл ЧУЖУЮ причину.** Юнит, где редактура флагнулась САМА (косм. чистка) И выпал член, печатал причину ЧИСТКИ как причину дыры: «фрагмент не переведён: sanitizer_stripped». Та же ложь, что убирал пак, перенесённая в поток текста | 2 линзы независимо, боевой бинарь | **ИСПРАВЛЕНО:** заведены `DroppedReason` на `ChunkOutcome`/`ChunkExport`, маркер несёт причину ЧЛЕНА, баннер — вердикт ЮНИТА. Пин `SH7`, состояние в матрице обоих рендеров |
| **MAJOR: анти-скоуп.** В черновично-однофазном конвейере юнит — одиночный чанк, и его СОБСТВЕННЫЙ флаг читался как «выпавший член» ⇒ косметически почищенный чанк, у которого ВЕСЬ текст на месте, объявлялся неполным | боевой бинарь, `tmctl translate` и `tmctl export` расходились на одном юните | **ИСПРАВЛЕНО:** учёт выпадений только когда финальная волна — редакторская (`export.go`). Тест `TestADraftOnlyStrippedChunkIsNotCalledIncomplete`, пин `SH8` |
| **MAJOR: перестановка порядка записи купила сходимость ЦЕНОЙ второго тихого вреда.** Rejects-first оставляет строку дельты на диске, а `seedGlossary` дельту отказами НЕ фильтровал ⇒ в окне между падением и ре-сендом ОПЛАЧЕННЫЙ прогон вливал в глоссарий редактора терм, который владелец отклонил | 2 линзы, исполнение на реальных файлах | **ИСПРАВЛЕНО:** `loadMinedDelta` отбрасывает поверхность, стоящую в списке отказов, и говорит об этом WARN'ом. Тест на реальном полусостоянии, пин `SH12`. Заодно исправлено утверждение в шапке `writeDecisionFiles`, которое после перестановки стало ложным |
| **MAJOR: сходимость останавливалась на ОДНОЙ из двух дверей.** `refuseInertDeclines` я решил не трогать — и ошибся: траектория из ОДНИХ ПРИНЯТЫХ вызовов (decline терма-со-своей-строкой → approve терма, который несёт его алиасом) делала повтор стоячего реестра решений отказным навсегда, а всё-или-ничего уносило с ним законный approve | исполнение, полная трасса | **ИСПРАВЛЕНО:** та же оговорка на второй двери (по `in.Rejects`, не `res.Rejects` — фолд уже отработал). Тест `TestTheStandingLedgerConvergesThroughBothDoors` + направление-гард на живом алиасе, пин `SH13` |
| **MAJOR: у боевого объявления в `configs/models.yaml` не было ГЕЙТА вовсе.** Две строки — единственное, что доводит фикс до реального эндпойнта, а удаление их оставляло ВЕСЬ модуль зелёным | опровергатель посадил свою мутацию и получил SURVIVED | **ИСПРАВЛЕНО:** `TestShippedGeminiDeclaresOneSystemMessage` над боевым конфигом, пин `SH9` (проверено: удаление строк → тест краснеет) |
| **MINOR: гарды в `renderTranslate` не были запинены** (близнецы в `renderExport` были), `case "multi"` не покрыт, счётчики читались как разбиение целого | исполнение | **ИСПРАВЛЕНО:** матрица шести состояний для `renderTranslate`, тест `TestExplicitMultiCostsNothing`, счётчик переписан как `exported=N (of them incomplete=M)`. Пины `SH10`, `SH11` |
**ЧТО ОПРОВЕРГАТЕЛИ АТАКОВАЛИ И НЕ СЛОМАЛИ** (чтобы следующий читатель знал, что уже покрыто):
Anthropic-путь (склейка до него не доходит структурно — `Cap` в него не передаётся; два блока и живой
`cache_control` на месте) · префиксный кеш (стабильный префикс сохраняет байты и позицию) · провод
`translated`/`withheld` (`UnitDone.Shipped` считается по пустоте текста; ни `DroppedMembers`, ни
маркер её не трогают) · отсутствие новых причин флага (константы `FlagReason` до и после ИДЕНТИЧНЫ) ·
`exportVersion` не двигался · снапшоты боевых конфигов не двигались · арм-конфиги грузятся.
**ЧТО НАЙДЕНО, НО СОЗНАТЕЛЬНО НЕ ЧИНИТСЯ — с аргументом.**
1. **Ловушка `capabilities` на `kind: anthropic`** — оказалась чинибельной дёшево, и я её ВЗЯЛ: ось
теперь отказывается на этом виде по образцу `cache_ttl` (`models.go`), с тестом. Пред-существующий
класс для ОСТАЛЬНЫХ осей (`budget_field`, `temperature`, `reasoning`, `min_max_tokens`) остаётся —
его правильная форма тащит `kind` в `validateCapabilities` и трогает каждую ось; это чужой предмет.
2. **Снапшот не покрывает модель ГЕЙТА ТЕРМИНОЛОГИИ** (`config/pipeline.go` — не фолдится сознательно,
ратифицированно), а терминолог шлёт ДВА системных. ⇒ флип оси у провайдера, которым пользуется
только этот гейт, меняет байты под НЕИЗМЕННЫМ `RequestHash`, и чекпойнты ложно попадают. Дыра НЕ
моей оси — по ней ездят все оси `capabilities` с D3.1 — но моя первая, чей смысл именно в смене
этих байтов. **Утверждение в комментарии я ослабил до правды и дыру НАЗВАЛ прямо в коде**
(`capability.go`); закрытие — фолд модели гейта, как это делает `repairSnapshot`, и это правка
снапшот-контракта, принадлежащая владельцу решения «не фолдить», а не провайдерскому квирку.
3. **Сузив отказ по поверхности, я потерял поучение в одном углу:** decline, который И записан, И
по-прежнему инертен против подписанного сид-терма, теперь отвечает `already_applied` вместо
единственной работающей инструкции «убери из glossary_seed». Сходимость сочтена более тяжёлой
обязанностью (у вызывающего, который следует опубликованному ретраю, другого хода нет), но размен
реальный: **закрывается полем отчёта, несущим стоячий факт**, и это форма шва.
**ХАРД-БЛОКЕР, НАЙДЕННЫЙ И СНЯТЫЙ (требование владельца «после тебя не осталось хард-блокеров»).**
`go test ./cmd/tmctl/` строил боевой бинарь в `os.MkdirTemp` и НИКОГДА не убирал каталог — по 20 МБ за
прогон. За этот пак накопилось **249 брошенных каталогов `tmctl-bin*`, ~5 ГБ**, и `/tmp` заполнился до
99%: батарея под `-race` начала падать с `link: mapping output file failed: no space left on device`,
что читается как сломанная сборка, а не как кончившееся место. Я на это и купился на один прогон —
сообщать «батарея красная» было бы ложью. **Причина найдена, место освобождено, харнесс починен:**
добавлен `TestMain`, снимающий каталог (`cmd/tmctl/exitcontract_test.go`); проверено исполнением —
два прогона подряд, прирост каталогов 0. Дефект пред-существующий, пак его лишь довёл до отказа.
**ЧИСЛА, С КОМАНДАМИ (§4.1).**
- `go build ./...` · `gofmt -l .` (пусто) · `go vet ./...` · `go vet -tags live ./...`**все 0**.
- `go test ./... -count=1 -timeout=20m`**EXIT 0**.
- `go test ./... -race -count=1 -timeout=20m` (= строка `make test`) → **EXIT 0, real 2m09s**.
- `golangci-lint run --timeout=15m ./...` (= `make lint`) → **0 issues**.
- `go test ./internal/pipeline/ -run TestGolden -count=1`**ok** (голден пинит ПОЛНОЕ тело запроса).
- `go run ./cmd/tmmutate -root <КОПИЯ>`**73 посадки, 0 неожиданных исходов, EXIT 0** (72 RED + один каталогизированный выживший `G-byte-gate`; все 13 моих красны); каталог вырос 60 → 73 (13 новых посадок:
`SH1``SH13`), у каждой из трёх заказанных тем ≥2 своих.
**Каталог гонять ТОЛЬКО на копии** — сам инструмент это требует, а я один раз запустил его на рабочем
дереве и словил таймаут; дерево спас обработчик сигнала самого инструмента (проверено: мутационного
текста не осталось, батарея зелена). Записываю как урок, а не как мелочь.
**ФАЙЛЫ (19 правленых + 5 новых; всё в `backend/`, плюс эта запись в журнале).**
`cmd/tmctl/render.go` · `cmd/tmctl/exitcontract_test.go` · `cmd/tmmutate/mutations.json` ·
`configs/models.yaml` · `internal/config/models.go` · `internal/config/models_catalog_test.go` ·
`internal/llm/capability.go` · `internal/llm/httpllm.go` · `internal/llm/provider_openai.go` ·
`internal/llm/provider_local.go` · `internal/membank/decisions.go` ·
`internal/membank/decisions_fuzz_test.go` · `internal/pipeline/bankdecisions.go` ·
`internal/pipeline/bankreport_test.go` · `internal/pipeline/bookrun.go` ·
`internal/pipeline/export.go` · `internal/pipeline/mining.go` · `internal/pipeline/runner_test.go` ·
`internal/pipeline/waverun.go`.
Новые: `cmd/tmctl/exportgap_cli_test.go` · `internal/llm/systemmessages_test.go` ·
`internal/membank/decisions_converge_test.go` · `internal/pipeline/bankdecisions_converge_test.go` ·
`internal/pipeline/systemmessages_wave_test.go`.
**⛔ ВОПРОСЫ И ПИНГИ ОРКЕСТРАТОРУ — четыре, каждый требует ЧУЖОГО решения.**
1. **Изменён СУЩЕСТВУЮЩИЙ тест, и я это заявляю, а не прячу.** `bankreport_test.go`
`TestHalfLandedIsNamedPerFile` кодировал в фикстуре ПОРЯДОК переименований, который заказ §3.3
изменил; фикстура пере-нацелена на второй rename, утверждение сохранено дословно и усилено новым
`TestTheRenameOrderIsWhatMakesADeclineRecover`, пинящим сам порядок с его причиной. **Считаешь
подгонкой — откачу, и тогда половину §3.3 с полусостоянием надо выносить отдельным заказом.**
2. **Вендор-вердикт по Gemini** — прислан отдельным сообщением, с URL и датами; три поправки к
`00-provider-quirks.md:157` (структурная посылка ≠ поведенческий вывод · `Content.parts[]`
повторяемо · «форум Google» — это два не-сотрудника из 2025). Пишешь ты, зона твоя.
3. **Дыра снапшота у модели гейта терминологии** (см. выше, п.2 «не чинится»). Закрытие — правка
снапшот-контракта; принадлежит владельцу решения «гейт не фолдится», не мне.
4. **Д1 достоин собственного заказа.** Не «расширить decline до энтити» (это против канона), а узко:
эмиссия майнера энтити-широка (`miner_emit.go` `clusterTouches`), а фильтр авто-банка ключуется
только по собственному src строки (`mining.go:662`), поэтому ОТКЛОНЁННАЯ поверхность возвращается
алиасом уцелевшей строки. Канон уже сегодня обещает «declining a surface removes EVERY window of
that surface» — то есть движок не держит обещание. Правильная форма — снять алиас со строки, а не
снести строку; это банковая онтология, и в «тихую порчу» её тянуть не следует.
**ПРАВО §9 ПРИМЕНЕНО ТРИЖДЫ, каждый раз с грунтом:** Д3 отклонён (символ живёт в `platform/`, зона
заперта; движковая половина здорова и порядок «карта → память» исполнен) · Д1 отклонён как отдельный
предмет (выше) · подсказка промта про место маркера отклонена (она перевернула бы `withheld`
`translated`). ⚠ И один раз право §9 я применил ОШИБОЧНО: решил не трогать вторую дверь
`refuseInertDeclines`, рассудив, что её случай — «устаревший документ, а не ретрай». Опровергатель
показал траекторию из ОДНИХ ПРИНЯТЫХ вызовов. Рассуждение было неверным, исполнение это доказало,
дверь починена.
**⚠ ЗАПИСКА НИЖЕ — ПЛАН ДО ПРАВОК (§6), она СОХРАНЕНА как след дисциплины, но её выводы местами ОПРОВЕРГНУТЫ итогом выше. Не наследовать из неё: «сломан только класс (а)» (сломан ещё и стык «редактура флагнулась сама + выпал член»), «фиксация порядка записи безопасна» (она купила второй тихий вред, закрытый фильтром дельты) и «Д3 не существует» (существует, в `platform/`).**
**📝 ЗАПИСКА-ПЛАН БЭКЕНД-СЕССИИ 28.08 — ПАК «ТИХАЯ ПОРЧА» (`docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md`), §6. Правок кода на момент записи НЕТ.**
Три заказа: §3.1 инъекция банка не теряется · §3.2 дыра выдачи видна читателю файла · §3.3 повтор
принятого документа сходится. Ниже — что установлено ЧТЕНИЕМ до правок и как из этого следует план.
⚠ Всё, что помечено «проверить исполнением», в план входит как обязательство, а не как факт.
**Находка 1 (меняет §3.2). Маркер уже есть — и он ВРЁТ.** `cmd/tmctl/render.go:499` печатает для
флагнутого юнита с НЕПУСТЫМ текстом баннер «(leak cleaned, verify)», а `:38` — «[FLAG … — leak
cleaned, exported cleaned, verify]». Ветка выбирается по `ce.FinalText != ""`, и c-lite member-drop
(`export.go:226-230` ставит `DispFlagged` при непустом тексте) попадает ровно в неё. То есть юнит, у
которого ВЫПАЛ ЦЕЛЫЙ ЧЛЕН, объявляется читателю косметической чисткой санитайзера, которой не было.
Существующий тест это не ловит: `cmd/tmctl/export_cli_test.go:66` покрывает ветку только причиной
`sanitizer_stripped`. ⇒ Предмет §3.2 не «завести маркер», а «перестать врать и покрыть непокрытое»
(пере-формулировка разрешена оркестратором 28.08 при закрытом гейте «что читает человек»).
**Гейт §3.2 закрыт чтением: движок НЕ ПИШЕТ ФАЙЛОВ ВООБЩЕ.** Все команды рендерят в stdout
(`cmd/tmctl/main.go:227-228`, `:338-349`). Читаемых человеком поверхностей у движка две, обе
текстовые и обе уже несут пер-юнитный баннер: `tmctl translate` (`render.go:27-72`) и
`tmctl export --plaintext` (`render.go:478-511`). Дефолт `export` — JSON
(`export_cli_test.go:24-28`), и он же артефакт шва к платформе (`research/23` строка 76 —
«артефактный контракт (export-JSON · 101 · 100)»). Читательский файл (epub и пр.) собирает
ПЛАТФОРМА по `POST /books/{bookId}/exports` (`14-api-contract/openapi.yaml:824-870`) — чужая зона.
⇒ Адресат моего маркера — человек, читающий plaintext-выгрузку движка; платформенная половина
остаётся за платформой, и провод различение несёт (`14-api-contract/README.md:300`).
**Следствие для места правки, и оно расходится с подсказкой промта.** Промт указывал на
`export.go:237` (прецедент `ApplyHeading`). Туда маркер класть НЕЛЬЗЯ: `ChunkExport.FinalText` — это
поле `final_text` артефактного контракта, а `waverun.go:203-204` кормит провод предикатом
`oc.FinalText != ""` (`shipped`). Любой маркер, вклеенный в текст ПУСТОГО юнита, перевернул бы
`withheld``translated` — ровно ⛔(а) промта. `ApplyHeading` этого не делает только потому, что на
пустом тексте он no-op (`chunk/chunker.go:186-191`). Различение: титул — часть КНИГИ и обязан ехать
на провод; маркер дыры — метаданные О тексте, и в поле, по которому провод судит, ему нельзя. ⇒
маркер живёт в РЕНДЕР-слое (`cmd/tmctl/render.go`), на слой дальше проекции. Это ответ на требование
промта «либо тем же механизмом, либо объясни, почему другое».
**Классы дыр — ревизия по коду.** (а) c-lite member-drop — помечен, но ЛОЖНО (находка 1); (б) юнит
целиком withheld (`waverun.go:496-509`) и (в) gate-override `glossary_miss` (`export.go:319-323`)
дают пустой текст и попадают в ветку `render.go:502-504` «(not translated, flagged for a human)» —
помечены ВЕРНО, работы не требуют; (г) четвёртая, сноска «[1] …» мимо детектора примечаний: детектор
требует заголовочного слова (`lang/data/target-ru.txt`, `sanitizer_trailing_note`), но ханьцзы в
сноске поднимают класс `CJKLeak`, а он `CosmeticOnly` (`checks/sanitizer.go:161-163`) ⇒ юнит уходит
как `sanitizer_stripped`, и баннер «leak cleaned» для НЕГО правдив. То есть класс (г) накрыт
существующим механизмом БЕСПЛАТНО и работы в детекторе не требует — граница промта соблюдена.
**Проверить исполнением обязательно:** что сноска с ханьцзы действительно даёт `CosmeticOnly`, а не
подтягивает субстантивный класс.
**§3.1 — план и его цена.** Свёртки в `toOpenAIMessages` нет (`llm/httpllm.go:513-521`, копия
один-в-один); её комментарий `:511-513` говорит, что `CacheBoundary` на этом пути бессмысленна ⇒
свёртка кеш не портит. У Anthropic путь другой и трогать его нельзя
(`llm/provider_anthropic.go:169-179`: ведущий system-префикс → блоки, `CacheBoundary` → реальный
`cache_control`). Ось лечения — `llm.Capability` + блок `capabilities:` (провайдерный ИЛИ модельный,
`config/models.go:85`,`:146`,`:442-450`), прецедент безопасного расширения — `MinMaxTokens` с
`json:",omitempty"`. **Детерминизм:** снапшот фолдит РАЗРЕШЁННУЮ Capability только для праймари и
единственного `escalate_to` (`pipeline/snapshot.go:316-340`,`:479`,`:495`); в `pipeline-c1.yaml`
`escalate_to: deepseek-v4-pro` (`:60`), а gemini стоит лишь третьим в цепочке `default` (`:129`) при
`budget_usd: 0` (`:136`) и раннере SINGLE-HOP ⇒ объявление квирка у gemini снапшот боевого конфига
НЕ двигает. Проверить исполнением (голдены + снапшот-тесты).
**Честная граница достижимости:** при SINGLE-HOP и текущих конфигах gemini не достигается ВООБЩЕ;
дефект латентный и ждёт конфига, который назовёт gemini в `escalate_to` или в `chain[0]` лейбла.
**Развилка, которую выношу на опровергателя (§4 заказ):** дефолт оси. «Квирк объявляет тот, у кого
он есть» (нулевое значение = сегодняшнее поведение) не двигает ничего, но НОВЫЙ провайдер с той же
ловушкой снова теряет инъекцию молча — тот же класс тихого вреда. «Свёртка по умолчанию, исключение
объявляет проверенный» безопасна для незнакомого провайдера, но меняет провод боевому переводчику без
замера (запрет «эмпирика решает») и двигает снапшот. Решение — после опровергателя, с обоснованием.
**Посылка промта «Gemini принимает ровно одно системное» ОТОЗВАНА автором** (оркестратор, 28.08):
это было рассуждение; собственный замер полигона (`00-provider-quirks.md:157`) даёт 535 токенов ≈ не
выжило НИ ОДНО. Заказу это безразлично, тесту — нет: утверждается пост-фиксный инвариант «системное
РОВНО ОДНО и инъекция ВНУТРИ него», он верен при любом чтении. Вендор-вердикт со ссылкой уходит
оркестратору для `00-provider-quirks.md` (его зона).
**§3.3 — трасса контрактной сессии ПОДТВЕРЖДЕНА моим чтением, и подтверждено то, что она сама не
читала.** Отказ живёт в `membank/decisions.go:374-377`, бежит ДО фолда (`:296-300`), и
всё-или-ничего вытирает документ целиком (`:302-307`). **Порядок записи я прочитал сам:
`bankdecisions.go:458-473` коммитит ДЕЛЬТУ ПЕРВОЙ**, а собственный комментарий функции `:436-438`
говорит дословно: «delta-first is safe for an approve and NOT for a decline … convergence is what
actually carries both». ⇒ окно застревания реально, и код сам называет допущение, которое ложно.
Идемпотентная ветка уже есть (`already_applied`, `:221-224`,`:269-272`) — вводить понятие не надо.
План: сузить условие отказа так, чтобы оно не срабатывало, когда отказ УЖЕ записан в rejects, плюс
направление-гард (настояще-инертный decline обязан отказывать по-прежнему тем же текстом).
Идентичность документа НЕ вводить — сходимость по СОСТОЯНИЮ.
**Д1 и Д3 — беру право §9 и НЕ чиню, оба с грунтом.**
· **Д3 ОТКЛОНЁН как несуществующий:** гард `LiftBankStop` не найден в коде НИ В КАКОМ написании
(греп по всему `backend/`); реальный носитель — `mining.go:266-277`, и он уже реализует
ратифицированный порядок «карта → память»: `writeFileAtomic` карты на `:191`, `MarkStopPresented`
на `:274`, с комментарием `:270-273` «strictly after the map … the reverse order can mark surfaces
presented whose map never reached disk» и с явной оценкой отказа памяти как «one extra stop, nothing
lost» (`:275-276`). То есть механизм написан ВМЕСТЕ с памятью v16, а не до неё.
· **Д1 ОТКЛОНЁН как ратифицированное поведение + названный остаток:** канон определяет decline
ПОВЕРХНОСТНО («declining a surface removes EVERY window of that surface» —
`14-api-contract/openapi.yaml:2051`), и расширение до энтити противоречило бы контракту; а «память
глушит стоп» — уже НАЗВАННЫЙ и принятый двусторонний остаток в комментарии
`mining.go:308-313`. Оба — строка с причиной, не правка.
**Сосед 131 (называю, не чиню, как велит §3.4):** кап инъекции считает только «src → dst» и не
считает маркер `⟨проверить⟩` и родовые пометки (`lang/data/injection.txt` несёт их как отдельные
значения) ⇒ реальная инъекция шире бюджета. Проверю точный носитель при работе в этом коде и назову
`file:line` в итоге; чинить не буду — тема денег и качества инъекции, а не тихого брака в тексте.
**Порядок работы:** §3.1 → §3.2 → §3.3, каждый с тестом на ПОСТ-фиксный инвариант; батарея под
`-race` + линтер; посадки мутаций ≥1 на пункт с вердиктом по ДЕЛЬТЕ и ТОПИЧНОСТИ; опровергатель на
выбор места §3.1 (заказ §4). Веер агентов соразмеряю предмету: численный потолок «24» из §4 промта
снят словом владельца 28.08.
**✅ ИТОГ БЭКЕНД-СЕССИИ 27.08 (2) — ФИКС3 ПО ФИКС-ЛИСТУ ПРИЁМКИ (`docs/BACKEND_SEAM_FIX3_SESSION_PROMPT.md`), все 6 пунктов. Дерево готово, НЕ закоммичено; продолжает дерево трёх паков.**
Четыре пина, один снос, ноль новых механизмов: пин порядка «карта → память» (мутация приёмки
воспроизведена в трёх пакетах ДО пина; занятый путь карты → ошибка прогона + пустая память) · пин
«отчёт класса 15 мерит подпись по ДИСКУ, не по намерению» (утверждение панели воспроизведено) · оба
носителя старого пола полосы приведены к двух-ярусной букве · отменённая формула «стоп гаснет, когда
решён каждый терм» зачищена ГРЕПОМ — восемь носителей, включая два файла самого пака · снесена
машинная таблица `.bank-stop.json` вместе с писателем и четырьмя тестами (читателя нет ни в одной
зоне — свой греп; человеческая `.bank-stop.txt` не тронута) · банк-экспорт получил якорь свежести
`as_of`+`run_id` (аддитивно на `tm-bank-v1`; «может быть устаревшим» больше не живёт только в логе).
**Доказательная база — `FINDINGS` §46; каталог мутаций 58 → 60 (FX23, FX24).**
**⛔ Пинги:**
1. **Платформе/оркестратору:** `.bank-stop.json` СНЕСЁН (мёртвая публикация упразднённого D39.144
пер-термного экрана) — платформенные журналы (`platform-PROGRESS.md`, `STACK_DECISIONS.md`)
называют его атомарно-читаемым сайдкаром, упоминания устареют при лендинге.
2. **Потребителям банк-экспорта:** документ несёт `as_of` (граница прогона) и `run_id` (тот же id,
что стрим `events.jsonl`); аддитивно, старые читатели не ломаются; пустой `run_id` = «идентичности
нет», не «текущий».
**✅ ИТОГ БЭКЕНД-СЕССИИ 27.08 — ДОФИКС-2 «ФЛАЖОК СТАНОВИТСЯ ФЛАЖКОМ» (`docs/BACKEND_SEAM_FIX2_SESSION_PROMPT.md`). Дерево готово, НЕ закоммичено; продолжает дерево паков 2425.08.**
Все 13 пунктов §4 закрыты: движок сам реализует модель D39.144 — стоп банка срабатывает только на
кластер, которого ни один прежний стоп не предъявлял (персистентная память `bank_stop_presented`,
схема v16); авто-провод неподписанных строк работает и при ПОДНЯТОМ флаге; отчёт двери собирается
ОДНИМ конструктором по явному исходу (проекция · отказ · no-op · прерывание · записано · запись не
удалась), версия отчёта → v2 (`written_delta`/`written_rejects`, mode `refused`/`write_incomplete`);
полоса отказов получила два яруса и класс `write_incomplete` (exit 15); оба файла решений готовятся
целиком до первого rename + fsync каталога; ключи `mined_delta`/`mined_rejects` СНЯТЫ (пути только
конвенцией — МЕЖКНИЖНАЯ гонка недостижима, ⛔-пинг прошлого пака закрыт; воркфлоу-ревью нашло остаток
той же оси: второй конфиг ТОЙ ЖЕ книги с другим объявленным project_db пишет те же файлы под другим
локом, мимо flock живого прогона — пинг ниже, решение не паку); карта подписи
пишется атомарно и несёт конверт `map_version`+`map_id`; шесть слепых пятен гейта закрыты пинами,
проверенными посадкой. Поверх — воркфлоу-ревью (шесть полос × три скептика, `FINDINGS` §45): его две
кодовые находки закрыты правками с пинами — `--dry-run` теперь отказ для всех глаголов без проекции
(раньше `translate --dry-run` МОЛЧА запускал платный прогон), а повтор после отказа синка каталога
держит класс 15, пока долговечность не пере-доказана. **Доказательная база —
`backend/docs/SEAM_PACK_FINDINGS.md` §§36§45; каталог мутаций 36 → 58 записей, 0 неожиданных
исходов.** Здесь — только требующее действия.
**⛔ ПОРЯДОК ДЕПЛОЯ/ЛЕНДИНГА — зависимость НАПИСАНА, а не выведена (заказ §4.12):**
1. **Движок первым, платформа второй.** Сброс флага `--verify-bank` платформой (её персистентная
колонка + особый случай в спавне, `platform/internal/runs/spawn.go`) может умереть ТОЛЬКО ПОСЛЕ
лендинга этого пака: в обратном порядке каждая книга с поднятым флажком в день лендинга перестаёт
писать авто-банк и пере-сидироваться ровно в режиме, ради которого флажок существует.
2. **`tmctl migrate` до прогонов** (голова схемы v15 → v16): `OpenReadOnly` отказывает классом 13 всем
непере-мигрированным книгам — ратифицированный порядок D39.134 п.3 это покрывает, но назван явно.
3. **Миграционная нота:** после апгрейда память пуста → ОДИН доброкачественный стоп на первом
verify-прогоне каждой книги.
**⛔ ПИНГИ ЧУЖИМ ЗОНАМ (правок не делал):**
1. **Платформе:** у неё умирает сброс флага при resume (после лендинга движка — п.1 выше) · отчёт двери
теперь v2 (`tm-bank-decisions-report-v2`: mode `refused`/`stopped`/`write_incomplete`, пофайловые
`written_*`) — её `readEngine` выбрасывает stdout при ненулевых кодах, прежний пинг об этом стал
острее: класс 15 БЕЗ отчёта неотличим от «ничего не легло» · новый exit 15 в полосе 1019 читается
её ридером как refused по построению полосы — ратификация значения на оркестраторе при лендинге.
2. **Полигону:** `eval/bank_autonomy/parse_common.py` парсит `mined-signature.yaml` — карта теперь
несёт верхние ключи `map_version`/`map_id` (лежащие артефакты coldrun не тронуты; новые прогоны
будут в новом жанре).
3. **Фронту:** экран подписи, читающий `signature.undecided` из отчёта двери, обязан перестать трактовать
его как «стоп не погаснет» — поле информационное (D39.144), контракт поля пере-описан в коде.
4. **Майнерский хвост за потолком структурно недостижим** (§4.4, НЕ чинилось по заказу): потолок эмиссии
(200) режет ранжированный пул ДО фильтров пригодности/сида/отказов (`miner_emit.go`,
`ranked[:emitRankCap]` до `emissionEligible`). Пере-замерено своей командой (числа прежнего
сплошного читателя в дерево не попали): 2354 ранжированных · 209 годных во всём алфавите · 11 в окне
потолка · 198 невидимы постоянно; отклонение всех годных окна не подняло ни одного из хвоста
(`FINDINGS` §38). Первоисточник замера — постоянный якорь-пин `internal/miner/emissioncap_test.go`:
он утверждает ТЕКУЩЕЕ поведение и покраснеет в день сдвига потолка или порядка фильтров, приведя
двигающего к этому пингу. Двигать потолок — трогать фрозен-паритет; отдельное решение.
5. **Гипотеза «подписание промахивается мимо чекпоинтов терминолога и пере-оплачивает роль» механизмом
НЕ подтверждена** (§4.13): память предъявленного входов чекпоинта не трогает; если перерасход
существует — он существовал и до пака, отдельный предмет.
6. **Остаток оси арбитра (нашло воркфлоу-ревью, воспроизведено живьём):** второй конфиг ТОЙ ЖЕ книги
(один каталог, один book_id, другой объявленный `project_db`) пишет ту же пару файлов решений под
ДРУГИМ локом — в т.ч. мимо flock живого прогона. Межкнижная версия снята конвенцией; эта — второй
конфиг одной книги, т.е. рукотворная конфигурация оператора. Выбор лечения (второй арбитр на
идентичности файлов решений или отказ при загрузке) трогает закон п.2 — решение не паку, пинг.
Довод в коде сужен честно (`bankdecisions.go`, THE ARBITER).
**✅ ИТОГ БЭКЕНД-СЕССИИ 25.08 — ДОФИКС ТОЙ ЖЕ ДВЕРИ по приёмке (`docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md`). Дерево готово, НЕ закоммичено; продолжает дерево пака 24.08, а не заменяет его.**
Все 16 пунктов §4 закрыты: блокер формата (дверь писала байты, которых её же парсер не читает, и любой
следующий вызов по книге — включая проекцию — падал) · пред-существующие поломки чинятся по одной ·
два ключа на один путь · гарды флагов на ПРИСУТСТВИЕ (`--decisions=` уходил на ПЛАТНЫЙ путь) · кап
документа решений · конверт путей на `manifest --json` · алиасы судятся по НАБОРУ · перевёрнутое окно
глав · честность отчёта о стопе · SIGTERM · десять слепых пятен гейта · **цепь целиком в репозитории**.
**Доказательная база, замеры, попунктный чек и обе итерации ревью — `backend/docs/SEAM_PACK_FINDINGS.md`
§§1430;** здесь только то, что требует ДЕЙСТВИЯ.
**✅ ⛔-ЗАКАЗ ПРИЁМКЕ ИЗ ПАКА СНЯТ.** Цепь «стоп → `bank-apply` → резюм → стоп ГАСНЕТ» теперь живёт в
репозитории как $0-тест на фейковом провайдере (`internal/pipeline/bankchain_test.go`), на ОДНОМ проекте
от стопа до погасшего стопа. Довод пака «своими силами невозможно, надо дойти оплаченной черновой волной»
был шире правды: харнесс для этого уже был в репозитории. Мина №1 строки **199** (ливлок на `status:
auto`) закрыта исполнением, а не рассуждением.
**⚠ РАТИФИКАЦИЯ КЛАССА 14 — прежнее требование в силе, объём немного шире.** Класс `decisions_rejected`
теперь несёт и отказ по КАПУ (документ больше, чем движок применяет за один акт): это факт о ЗАПРОСЕ, а
не о деплое, поэтому 10 был бы неверен. Носители полосы поднимает оркестратор при лендинге, как и
планировалось.
**⛔ ПИНГИ ДОФИКСА (правок не делал):**
1. **`exit 2` при OOM платформа читает как успех** — общедвижковый класс; кап удерживает `bank-apply` от
того, чтобы стать первым, кто до него доедет, но самого класса не снимает.
2. **Каталог книги без права записи даёт `exit 1`** (вне полосы). Класс движка корректен и одинаков у
всех глаголов — чинить надо чтение кода 1 на платформе, которая списывает проблему прав ХОСТА в
бюджет попыток КНИГИ.
3. **Проекция отвечает «что применится», а не «почём».** Рекомендация — НЕ строить: глагол $0 по
построению, поле «$0» было бы шумом. Решать оркестратору.
4. **Карта подписи не удаляется после погасшего стопа** — пред-существующее; зафиксировано тестом как
текущее поведение. Честный ответ на «есть ли что подписать» — новое поле отчёта `signature.undecided`.
5. **Цена глагола Θ(N²) по построению** — каждый пер-решенческий помощник сканирует весь документ.
Именно поэтому кап на КОЛИЧЕСТВЕ стоит на 5 000. Поднимать его без индексации этих сканов нельзя.
6. **Апстрим-дефект `gopkg.in/yaml.v3 v3.0.1`** (эмиттер пишет блочный скаляр, который его же парсер не
читает) вылечивается по-настоящему только скаляр-типом схемы с явным стилем или переездом на
`go.yaml.in/yaml/v4` (релиз-кандидат, смена module path в ДВУХ зонах). Мой обход несёт ссылку на
исходник библиотеки и характеризационный тест, который покраснеет в день починки апстрима.
7. **Фолд поверхностей Go-резидентный:** `internal/text/norm.go` держит `trad2simp` и катакана→хирагана
в Go, а не в langpack'е. Пара, которой в репо нет, деградирует до NFKC+lowercase — не блокирована, но
это ровно тот класс, о котором ревью-вопрос общности. Шире двери, поэтому пингом.
**⚠ ЧТО В ЗАПИСИ ПАКА 24.08 (ниже) УСТАРЕЛО — читать её через эту поправку.**
1. **Пинг 2, «таймаут: 1.6с на 5 000 термов, 9.1с на 20 000» — ЧИСЛА НЕВЕРНЫ, и это ровно тот датум,
которым платформенный пак будет мерить свой таймаут.** Мой замер той же командой: 5 000 `approve`
**5.2 с**, 20 000 → **50 с**; на минимальных `decline` (самая дешёвая форма, и потому та, которой
байтовый кап пускает больше всего) 5 000 → **11.3 с**, 12 000 → **58 с**, 28 900 → **308 с**. Брать
надо `FINDINGS` §18 и §29 п.1, не эти.
2. **Пинг 6 «однострочник при лендинге: документ решений не капирован» — СДЕЛАН.** Капов теперь два
(1 МиБ на чтение + 5 000 решений на применение), потому что байтовый не ограничивает время.
3. **Пинг 3 про флейки** пере-снят сильнее: изолированный A/B против дерева HEAD даёт побайтно
одинаковое падение с обеих сторон — `FINDINGS` §31.
**⛔ ПИНГИ, КОТОРЫЕ ЖИЛИ ТОЛЬКО В `FINDINGS` И НЕ ДОЕЗЖАЛИ ДО ЖУРНАЛА** (найдено проверкой согласованности):
карта подписи пишется обычным `os.WriteFile` мимо атомарной конвенции сайдкаров того же пакета
(пред-существующее) · цена доказывающего рендера ≈5 % времени, плюс `notCanonical` теперь тоже разбирает
документ · опубликованные пути двух файлов решений не несут версионного конверта (пинг пака §10.5) ·
схема сида без `omitempty` (пинг пака §10.6).
**⚠ Два пред-существующих `-race`-флейка в `internal/store` не тронуты** (`TestKillMinus9LosesAtMostOneCall`,
`TestTheSeamIsNotChargedToTheStoreOperationBudget`). База ДО правок, снятая на нетронутом дереве пака,
уже была красной по первому — это моя собственная атрибуция, а не наследованная.
**⛔ ПИНГ, КОТОРЫЙ ТРОГАЕТ РАТИФИЦИРОВАННЫЙ ЗАКОН (п.2) — решать не паку.** Арбитр двери — flock на
`<project_db>.lock`, а пишутся `mined_delta`/`mined_rejects`, которые `book.yaml` может направить куда
угодно. **Две книги с РАЗНЫМИ `project_db` и ОДНИМ delta друг друга не исключают:** воркфлоу-ревью
продемонстрировало гонку на 1500 одобрений с каждой стороны — одна сторона потеряла всё, `exit 0`, отчёт
«применено». Внутрикнижную версию этого §4.3 уже запрещает; межкнижная законна сегодня. Выбор — второй
арбитр или отказ при загрузке на путь вне каталога книги. В корпусе нарушителей нет. В коде стоит явное
«арбитр покрывает проект, а не пути», а не молчаливое допущение.
**🔧 В репозиторий добавлен инструмент: `backend/cmd/tmmutate`** — харнесс мутаций с каталогом из 36
посадок, у каждой названо СВОЙСТВО, за которое она отвечает, и записан ожидаемый исход. Три сессии подряд
писали такой инструмент в скретчпад и теряли его; приёмка при этом нашла восемь мест, где гейт был слеп.
**Он дважды подводил и оба раза был починен по следу:** сначала подсадил мутацию и прошёл собственную
sha256-самопроверку (эталон второй правки снимался с уже пропатченного файла), затем считал КРАСНЫМ провал
СБОРКИ — из-за чего две записи каталога, сломанные рефакторингом, целую итерацию числились пойманными
тестами, которые не могли отработать. Теперь: одно чтение и запись на файл, отпечаток всего каталога перед
каждой посадкой, зелёная базовая линия пакета ДО первой посадки и третий вердикт `NOTHING`, роняющий гейт.
`FINDINGS` §25 и §34.
**✅ ИТОГ БЭКЕНД-СЕССИИ 24.08 — ПАК «ВХОДНАЯ ДВЕРЬ ШВА», движковая половина D39.156. Дерево готово, НЕ закоммичено, передаётся на лендинг.**
Заказ §4.1§4.6 выполнен: конвенционные дефолты `mined_delta`/`mined_rejects` (ключей не объявляет и
файлов заранее не создаёт никто) · глагол `tmctl bank-apply --config <book.yaml> --decisions <doc.json>
[--dry-run]` ($0, идемпотентен побайтово, всё-или-ничего, занятый flock — ровно 12 неблокирующе, ответ
несёт версию и явное `depth: edit_wave`) · `--keys-file` у `translate` (строка **211**) · строгость
сид-загрузчика (**212**) · `status_version` + `artifacts{}` с абсолютными путями (**213**) · дефолт
стендового корпуса маркерным резолвом (**218**, критерий оркестратора выполнен буквально).
**Вся доказательная база, замеры и попунктный чек промта — `backend/docs/SEAM_PACK_FINDINGS.md`;**
здесь только то, что требует ДЕЙСТВИЯ.
**⚠ ТРЕБУЕТ РАТИФИКАЦИИ: заведён класс полосы отказов 14 `decisions_rejected.`** Отвергнутый по существу
набор решений ≠ сломанный деплой: `platform/internal/ingest/exit.go` `DeploymentFault` относит 10 к
«условию, общему для всех книг хоста», и по 10 набор решений одного пользователя списался бы на
оператора. Старый читатель деградирует безопасно (`Refused(14)``OutcomeRefused`). 10 сохранён для
документа, не прочитавшегося КАК документ решений. Гейт тотальности таблицы добавлен.
**ЗАКАЗ ПРИЁМКЕ — единственный незакрытый замер (согласован со сторонним архитектором).** Цепь целиком
не гонялась ни разу: стоп красный → `bank-apply` → резюм → **стоп гаснет**. Части доказаны порознь, но
мина №1 строки **199** — livelock уровня ЦЕПИ, которого частичные тесты по построению не видят. Снять не
могу, и это проверено: банк-стоп на машине не воспроизводится (`contrast_path` → отсутствующий
`eval/exp16/data/jieba_dict_general_zh.txt`, строка **123**), а до стопа надо дойти оплаченной черновой
волной. **Команда:** на КОПИИ книги на exit 3 скормить `bank-apply` документ на всю карту подписи, затем
`translate --verify-bank`. **ДЕФЕКТ** — стоп снова с ТЕМИ ЖЕ поверхностями; **не дефект** — свежая пачка
≤200 термов реверс-секции (мина №3 строки 199). До этого прогона вердикт по циклу — PLAUSIBLE.
**⛔ ПИНГИ ЗОНАМ И СЛЕДУЮЩИМ ПАКАМ:**
1. **`rubyToCandidates` мёртв в проде** — единственный вызов из теста (`memseed_test.go:204`);
`seeding.go:36-38` строит `manualSrcs` и никуда не передаёт (Go молчит: запись в map = использование).
Строк `Source:"ruby"` в банке не возникает, а платформа держит для них ветку словаря
(`ingest/bank.go` `termOrigin``OriginAnnotated`).
2. **Платформенному паку:** первый контакт с непере-мигрированной книгой даёт **exit 13** — воркер решений
обязан планировать `13 → migrate → повтор` · **проекция берёт тот же лок**, поэтому правка термина при
живом прогоне даёт 12 даже на превью, на всю длину прогона · **таймаут**: 1.6с на 5 000 термов банка,
9.1с на 20 000, бюджет платформы на вызов движка — 60с (`runs/reconcile.go:280`) · `bank-apply`
создаёт `<project_db>.lock` при первом касании никогда не запускавшегося проекта.
3. **⚠ БАТАРЕЯ НЕ ДЕТЕРМИНИРОВАННО ЗЕЛЕНА, и это НЕ пак — прочесть ДО пере-рана.** Два
пред-существующих `-race`-флейка в `internal/store` срабатывают примерно раз на 24 полных прогона
(`go test ./... -race -count=1` ×4 → 1 чист, 2-й красный). **Красное именно в этих двух тестах
ожидаемо; любое другое красное — регрессия.** Тесты НЕ трогал (D39.121).
`TestTheSeamIsNotChargedToTheStoreOperationBudget`, `TestKillMinus9LosesAtMostOneCall`. Чередующийся
A/B на `git worktree` HEAD: оба падают на HEAD **не реже** (`kill9` 2/30 против 0/30; `seam` 2/30
против 1/30; без `-race` 0/30). Покрытие: `LockProject`/`Release` этими тестами не исполняются вовсе.
⚠ 0/30 против 2/30 НЕ значит «пак улучшает» — на таком числе это шум (Фишер p≈0.5); достаточно того,
что флейк живёт на дереве, которого пак не касался.
4. **Гейт `TestDispatchCommandsCoversTheSwitch` слеп к `case` с константой** (считает только
`*ast.BasicLit`) — команда, объявленная константой, невидима гейту, чей смысл в дефекте строки **176**.
Я вернулся к литералу (конвенция файла), дыра не эксплуатируется.
5. **`redrive` — платный путь без канала ключей аргументом.** Промт: «флаг только у `translate`», сделано
ровно так; канал у него есть (конвенционный `.env`), платформа его не зовёт. Развилка следующему паку.
6. **Однострочник при лендинге (не блокер):** документ решений читается неограниченным `os.ReadFile`,
тогда как платформа свои чтения от движка капает. Документ на 10 000 решений ≈2МБ ⇒ кап 116МБ.
**⚠ ПРАВИЛА, ВЫНЕСЕННЫЕ ИНЦИДЕНТАМИ — для следующих сессий, не про этот пак.** Прогон харнесса мутаций
был убит таймаутом, `finally` не отработал, и посадка просидела в дереве ~40 минут, выглядя
продакшн-кодом; сторонний ревьюер снял с него замеры и доложил несуществующий дефект. Поймал посадку
собственный пин пака. Отсюда три правила: **харнесс мутаций обязан фингерпринтить цели** (починено) ·
**снимай отпечаток дерева перед сборкой бинаря замера** (на этом наступили обе стороны) · **одиночный
прогон — не замер** (атрибуцию флейков я дважды вывел неверно из одиночных прогонов).
**Что НЕ доказано:** рантайм-вердикт по платному пути — **PLAUSIBLE**. Доказано, что ключ из
`--keys-file` доезжает до заголовка и что без флага прогон отказывает конфигом; не доказано, что
реальный провайдер примет ключ и что SaaS положит файл по этому пути — платформенная половина строки
**211**, чужая зона. Живой `translate` не гонялся: санкции на платные вызовы у пака нет. (24.08)
**⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ БЭКЕНДА. Четыре движковых дефекта, все сверены моими командами.**
1. **ОТВЕТ НА ПИНГ ПОЛИГОНА: дефект ПОДТВЕРЖДЁН, он ваш.** Эскалация на Gemini теряет инъекцию
банка памяти (строка **208**). Механизм пере-проверен: `pipeline/render.go:256-258`=`Role: "system", Content: injection, CacheBoundary: false`
штатно строит ВТОРОЕ системное сообщение, а `llm/httpllm.go` (до-паковые строки 517-523, якорь ИСТОРИЧЕСКИЙ — сигнатура сменилась лендингом D39.164: функция приняла ось `mode SystemMessagesMode` и под квирком СКЛЕИВАЕТ системный ряд, так что описанного здесь «один-в-один» больше нет)
копирует сообщения дословно — склейки нет нигде. ⚠ Лекарство того же класса в проекте УЖЕ
написано и работает для другого провайдера: `llm/provider_anthropic.go:169-179`=`inSystemPrefix := true`
сворачивает ведущий system-префикс в одно поле. Не обобщено на Gemini — это и есть работа.
Полигон ждёт ответа с 22.08 и починил у себя по вендор-доке; их находка верна.
2. **Род не производится авто-путём вовсе** (строка **210**, дополнена 23.08): замка ДВА и они
независимы, а поле `Gender` в схеме сида УЖЕ есть — ось мертва в ПИСАТЕЛЯХ, не в формате.
3. **Выпавший член юнита исчезает из отгружаемых байт БЕЗ внутритекстового маркера**
`pipeline/waverun.go:487-490`=`the flagged member is DROPPED from the edit`. Родня строки **193**;
для читателя это молчаливая дыра в тексте.
4. **Загрузчик сида НЕстрогий** (строка **212**): голый разбор YAML без проверки имён ключей, и
`seed-lint` этого не видит. Опечатка в имени поля молча теряется — в файлах, которые владелец
правит руками чаще всего.
⚠ Плюс строки **211** (движок на SaaS не получает провайдерских ключей — блокер живого прогона) и
**213** (платформа дублирует вашу конвенцию пути): обе половинные, движковая часть ваша.
**⛔ ПИНГ ОРКЕСТРАТОРА №20 — 22.08, ЗОНЕ БЭКЕНДА: три ваших дока разошлись с деревом (аудит доков, сверено кодом; рукой не лезу — ваша зона).**
1. **`backend/README.md`, карта пакетов** объявлена сверенной по коду, а в ней НЕТ `internal/runevents` — эмиттера журнала прогона, то есть ВСЕГО шва движок↔платформа (D39.131), — и нет второго бинаря `cmd/tmvet`. Греп: `runevents` и `tmvet` в файле по нулю вхождений, при том что оба каталога в дереве.
2. **`backend/README.md`, «Известный техдолг»** держит живыми Cyrillic-хардкоды DC-чекеров, которые сам код называет ПОГАШЕННЫМ долгом (фаза 2 общности, D39.64: `isRuTarget` удалён, `TargetActive`/`TargetScriptNonLatin`), и оба его `file:line`-якоря указывают мимо.
4. **(дописано 24.08) `backend/README.md:47`=`/home/ubuntu/books/gu-zhenren/` зовёт книгу-стенд по МЁРТВОМУ пути** `/home/ubuntu/books/gu-zhenren/` — каталога нет ни там, ни в `$HOME`: книги переехали в `<репозиторий>/books` и версионируются своим git (D39.157 п.2). Там же фраза «тексты и производные ВНЕ git» стала неточной: вне ЭТОГО git. Тем же касанием — три тест-хелпера, резолвящие корпус от `$HOME/books` (строка 218); они уже в заказе движкового пака шва §4.6, так что дублировать правку не надо, но док обязан сойтись с ними.
3. **`backend/docs/components.puml`** — диаграмма, которую карта доков продаёт как правимую «одним коммитом с кодом», отстала примерно на месяц: старый exit-контракт (без 4/5 и полосы 1019), неполный список глаголов CLI, удалённый паком-17 `failover.go` и ни следа эмиттера. ⚠ Рендерить её в svg/png НЕ надо (гардрейл) — править исходник.
*(Пак «честные числа и статусы» — ИСПОЛНЕН, ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН `f840867` (17.08; D39.149, ФЧ-5 — D39.150). Отчёт, две волны адверсариального ревью, мутационная приёмка и тело фикс-листа ФЧ-1…ФЧ-8 — в [archive/PROGRESS-2026-08-17.md](archive/PROGRESS-2026-08-17.md); живой остаток — строка бэклога 197.)*
*(Пинги закрытых паков — фикс-пак банка D39.118, блокеры контракта D39.122 — в [archive/PROGRESS-2026-08-04-09.md](archive/PROGRESS-2026-08-04-09.md), D39.125.)*
*(Закрытые бэкенд-записи 0102.08 — общность фаза 2 · качество банка (оба этапа) · фикс-пак банка · finding-1 · пакет-чекеров · мелкая пачка · coldrun-b · ручка эффорта — в [archive/PROGRESS-2026-08-01-02.md](archive/PROGRESS-2026-08-01-02.md).)*
*(Закрытые бэкенд-записи 1415.08 — эмиттер шва (D39.131) · `tmctl migrate` (D39.134) · пере-пин цен DeepSeek (D39.137) — в [archive/PROGRESS-2026-08-14-15.md](archive/PROGRESS-2026-08-14-15.md); вынесено D39.139.)*
*(Закрытая хроника пакетов №0№4 (0410.07) перенесена в [archive/PROGRESS-2026-07-04-10.md](archive/PROGRESS-2026-07-04-10.md) — ревизия D31.)*
*(Хроника пакетов 1112.07 (пакет-5, D15.2-этап-А, санитайзер-фикс D33) — в [archive/PROGRESS-2026-07-10-13.md](archive/PROGRESS-2026-07-10-13.md).)*
*(Трек-A пак-1/1.5 (D39.2/D39.5) — см. записи оркестратора №6 выше.)*
*(Хроника бэкенд-паков 1116 (1925.07, вкл. отчёт-запись пака-15) — в [archive/PROGRESS-2026-07-13-25.md](archive/PROGRESS-2026-07-13-25.md); отчёты паков — `archive/reports/`.)*
*(Закрытые записи 2531.07 — пак-18 · мини-прогон · фикс-пак · сквозная проверка · хендофф 26.07 · ресёрч общности фаза 1 — в [archive/PROGRESS-2026-07-25-31.md](archive/PROGRESS-2026-07-25-31.md).)*
## ⟶ Вынесено 30.08 оркестратором №20: прожитые дампы паков «писатель книги» (D39.175) и «деньги» (D39.170)
> Оба пака ПРИНЯТЫ и ЗАЛЕНДЖЕНЫ, их решения и числа живут телами нот. Здесь — исходные записки-планы и
> итоги сессий целиком, как доказательная база. ⚠ Инструкции отсюда НЕ исполнять.
### 📝 ЗАПИСКА-ПЛАН — пак «писатель книги» (промт `docs/archive/prompts/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT_2026-08-30.md`, строка 236), сессия `textmachine-main-fc`, 30.08
**Что беру (зона `backend/`, HEAD на старте `67c05be`, дерево чисто).**
1. Прод-пакет `backend/internal/bookfile` (только stdlib — `archive/zip`, `encoding/xml`; без `import "testing"`):
`container.go` — OCF-контейнер (`mimetype` первым и `zip.Store`, `container.xml`, запись entry в порядке вызовов,
нулевые таймметки) — `chunktest/epub.go` переводится на него, его OPF-заглушка и XHTML-декой остаются тестовыми;
`epub.go` — EPUB 3 из нейтральной модели `Book{Identifier, Title, Language, Modified, Description,
Chapters[]{Title, Paragraphs[]}}`: `dc:identifier`/`dc:title`/`dc:language`/`dcterms:modified`, nav
(`properties="nav"`, ⛔ НЕ в spine), один XHTML на главу, `<h1>` = заголовок главы, `<p>` = абзац;
`txt.go` — чистый текст из той же модели (заголовок строкой, абзацы через пустую строку, главы через
пустую строку; ни баннеров, ни флагов, ни `gapMarker`).
2. Сборка из `BookExport``backend/internal/pipeline/bookbuild.go`: `Runner.BuildBook(formats, partial)`
`Export(false)` → дыры → отказ либо файл. Запись `writeFileAtomic` рядом с ProjectDB
(`<project_db>.book.<fmt>`, как `.manifest.json`/`.bank.json`), путь публикуется в
`StatusArtifacts.BookFiles` (`book_files`: map формат→путь, аддитивно) — приор промта принят: платформа
пути не выводит сама (17 §1). Существующий файл — перезапись: производный детерминированный артефакт, как
манифест; бэкап отказывает потому, что он точка восстановления, а файл книги пересобирается из стора.
3. Аддитивные поля `tm-export-v1` (форма не меняется, все `omitempty`): `ChunkExport.Heading` (`heading`) —
литерал `u.Members[0].Heading` в момент сборки записи (и у pending-юнита тоже, он из манифеста);
`BookExport.GhostUnits` (`ghost_units`: `[{chapter,chunk_idx}]`) — ключи ghost-строк, чтобы маркер стоял
в главе, а не только в счётчике; `BookExport.TextModified` (`text_modified`, `CCYY-MM-DDThh:mm:ssZ`) —
max(`chunk_status.updated_at`) по финальным строкам, вошедшим в файл (колонка `NOT NULL DEFAULT
datetime('now')`, `store/migrate.go:37`; в `ChunkStatus` дочитывается доп. полем). Это и есть
`dcterms:modified`: последнее изменение отгруженного текста — факт стора, не часов процесса.
4. Глагол `tmctl build --config b.yaml [--format epub,txt] [--out path] [--partial]` (по умолчанию оба формата;
`--out` — только при одном формате). Stdout — JSON-конверт `tm-build-v1` (пути, счётчики, `complete`).
Новый глагол, а не флаг `export`: `export` — чистое чтение в stdout, писать файлы из него — смешать
аудит-проекцию с артефактом; `manifest` — прецедент глагола-писателя сайдкара. `dispatchCommands` и
usage-строка расширяются — контрактное расширение того же рода, что `bank-apply` (тест
`TestParseNoArgsUsage` сам перечисляет такие расширения); чужим глаголам новые флаги ОТКАЗЫВАЮТСЯ
(дисциплина `--keys-file`/`--max-units`).
5. Отказ — новый `pipeline.RefusalBookIncomplete = "book_incomplete"` → exit **16** (полоса 1019; таблица
тотальна по `TestTheRefusalTableIsTotal`); сообщение перечисляет дыры `глава/юнит/причина`.
6. Читательские слова — ДАННЫЕ: `<langpack_root>/<target_lang>/reader.txt` (`key<TAB>value`, плейсхолдеры
`{chapter} {unit} {dropped} {holes} {total} {ghost}`), читает ОТДЕЛЬНЫЙ загрузчик `lang.LoadReaderWords`;
в `Pack.Version()` НЕ фолдится (пин тестом «наличие reader.txt не меняет Version()» + `current_snapshot`
minirun до/после). Ключ — язык ЦЕЛИ, не пара: слова читает читатель целевого языка, и они не зависят от
исходного. Поставляю `backend/configs/langpacks/ru/reader.txt`. Каталог `<tgt>/` загрузчик пака не
читает (`Load` — только `<src>/` и `<pair>/`, `langpack.go:215-330`), так что канал вне фолда по
построению, а не по соглашению.
**Политика §3.2 — по умолчанию ОТКАЗ, `--partial` — файл с пометкой; довод:**
- Дыры: pending · withheld (`Disposition!="pending" && FinalText==""`) · incomplete (`DroppedMembers>0` при
непустом тексте) — арифметика `render.go:573-587` выносится в общий хелпер `pipeline`, которым пользуется и
`renderExport` (вывод `--plaintext` побайтово прежний) · **ghost — тоже дыра**: при укрупнении нарезки
строка-лидер несёт текст ПРЕЖНЕГО, более короткого юнита, а остаток источника лежит в ghost-строках и в
файл не попадает (acceptance: 57 строк → 37 юнитов, 20 ghost при `pending 0` — ровно «молча полная»
книга). Позиция ghost внутри главы неизвестна → маркер в конце главы с числом потерянных строк.
- Default = отказ: D29.1(б) — reader-facing выдача fail-closed; дверь платформы получит `--partial` для
контрактного «may be exported, finished or not» (14 §createExport).
- `--partial`: пометка неполноты — первый абзац первого документа spine (то, что читатель видит, открыв
книгу) + `dc:description`; маркер — отдельный абзац на месте КАЖДОЙ дыры (pending/withheld — вместо текста
юнита; incomplete — перед текстом юнита, как `gapMarker`; ghost — в конце главы). Форма — из `reader.txt`;
без пака (acceptance, `runner.go:346`) — несловесная: `⚠ {chapter}.{unit}` у дыры, `⚠ {holes}/{total}` в
пометке (символ + числа, никакого словаря в Go). В `FinalText`/`export.go` маркер не попадает.
- drift — НЕ дыра: текст файла побайтово текст прогона (`export.go`, комментарий к `exportConfigDrift`);
попадает в JSON-конверт `build` и в WARN, в файл — нет («config drift» — операторский словарь).
Следствие §4 заявляю сразу: чистого бездрейфового файла на реальной книге на этом HEAD НЕТ — чистый
путь доказывается синтетикой, реальные книги — посадки на дыры.
- Оглавление и `<h1>` при `Heading==""` — номер главы цифрами (пустая ссылка nav невалидна; цифры —
единственный лейбл, который не решает открытый К-3 «какой лейбл видит читатель у главы без заголовка»;
словесный шаблон в данных — 1 строка после ратификации, вопрос владельцу в отчёте). При `Heading!=""`
срез известного префикса `heading+"\n\n"` из `FinalText` (`chunker.go:186-192`), `<h1>` = Heading.
- Абзац = непустая строка `FinalText`. Замерено на `minirun/export.json`: юнит 1/0 — 14 разделителей `\n\n`
и 7 одиночных `\n`, юнит 2/0 — 56 и 0: редактор чередует их без семантики, различать = выдумать
различие; строка — единица абзаца исходника-вебновеллы. Круг §4 сравнивает абзацы ингеста с абзацами,
которые положил писатель.
- Метаданные: `dc:title` = `config.Book.Title` как есть (на стенде `蛊真人` — ограничение в отчёт), `dc:language`
= `TargetLang`, `dc:identifier` = `urn:textmachine:book:<book_id>`, `dc:creator` нет (источника нет).
- 0 юнитов — отказ `sourceHasNoContent` (spine без элементов невалиден); 1 юнит — обычная книга.
**Что НЕ беру:** платформу и дверь `createExport`/`getExport`; лестницу D25.1 в коде и в `BookExport`;
строки 160/201 (`ApplyHeading` и три точки склейки не трогаю; модельные заголовки в прозе не извлекаю и не
правлю); `--resnapshot` и любые платные вызовы; `langpack_root` в копию acceptance; направление письма
(`dir`/`page-progression-direction`) — в `internal/lang` данных о направлении нет (`lang-script.txt`
только скрипты), RTL-цели в репо нет → вопрос в отчёт, не литерал; CSS/обложку; новые зависимости.
**Чем докажу:** герметично — `bookfile` (mimetype первым/stored, nav не в spine, детерминизм двух сборок,
экранирование, 0/1 глава), `pipeline` (круг `IngestEncoded` равенством абзацев на прогоне fake-провайдера;
pending через потолок; withheld через санитайзер; **ghost — усадка исходника после прогона (посадки в
тестах не было, строю)**; drift — бамп `prompt_version`; синтетический `BookExport` на incomplete), `cmd/tmctl`
(incomplete бинарём через `setupGapProject`; парс флагов; exit 16 через totality-тест), `lang` (парс
`reader.txt`; `Version()` не меняется от его наличия). На копиях книг в песочнице с переписанными путями:
minirun — оракул июля 14/14 по `final_text/snapshot_id/disposition`, `current_snapshot` до/после
`reader.txt`, `cmp` двух ПРОЦЕССОВ, epubcheck 5.3.0 `0 fatal / 0 error`, круг ингестом; acceptance — отказ с
перечнем и `--partial`-файл с несловесными маркерами; coldrun-a — отказ 14/14 pending;
`git -C books status --short -- gu-zhenren` до/после одинаков (снят до старта: 6 строк `D …-wal/-shm`).
`make battery` + `-race`; в середине — субагент-ревью готовой части против критериев; в конце —
адверсариальный проход по направлениям §4.
### ✅ ИТОГ — пак «писатель книги» (промт `docs/archive/prompts/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT_2026-08-30.md`, строка 236), сессия `textmachine-main-fc`, 30.08
**Что построено (зона `backend/`, поверх `67c05be`; НЕ закоммичено — дерево на лендинг оркестратору; `git status`: 10 изменённых + 8 новых путей в `backend/`, плюс этот журнал).**
- `internal/bookfile/` (новый прод-пакет, только stdlib, без `testing`): `container.go` — OCF-контейнер (`mimetype` первым, `Store`, через `CreateRaw` — без data-descriptor, флаги `0x0000`; `container.xml`; entry в порядке вызовов; нулевые zip-таймметки); `epub.go` — EPUB 3 (`dc:identifier`+`unique-identifier`, `dc:title`, `dc:language`, `dcterms:modified`, nav `properties="nav"` ⛔ не в spine, XHTML на главу: `<h1>` + `<p>`); `txt.go` — чистый текст; `model.go` — модель `Book/Chapter`, `Blocks(i)` (единственное определение содержимого документа главы), `CleanText` (общая чистка XML-недопустимых символов для обоих форматов), экранирование.
- `internal/chunk/chunktest/epub.go` переведён на `bookfile.NewContainer`; тест-декои (OPF-заглушка, `<title>c</title><style>`) остались тестовыми; 9 импортёров зелёные без правки ассертов.
- `internal/lang/reader.go``LoadReaderWords(root, targetLang)`: `<langpack_root>/<target>/reader.txt`, читается ОТДЕЛЬНО от `Load``Pack.Version()` не фолдится по построению — `Load` читает только `<src>/` и `<pair>/`); `DefaultReaderWords()` — несловесная форма (символ + числа; тест пинит отсутствие букв). Данные: `configs/langpacks/ru/reader.txt` (7 ключей: `hole.{pending,withheld,incomplete,stale,ghost}`, `notice.{holes,ghost}`).
- `internal/pipeline/holes.go` — ОДИН предикат дыры `UnitHole`/`HoleCounts` (порядок = `renderExport`); `render.go` считает шапку `--plaintext` через него (вывод побайтово прежний).
- `internal/pipeline/export.go` — аддитивно (`omitempty`): `ChunkExport.Heading` (`heading` — литерал `u.Members[0].Heading`, у pending тоже), `BookExport.GhostUnits` (`ghost_units`, ключи ghost-строк, сортированы), `BookExport.TextModified` (`text_modified` — max `chunk_status.updated_at` по ФИНАЛЬНЫМ строкам, вошедшим в экспорт); `exportConfigDrift`: строка со стадией, которой нет в текущем конфиге, — drift (находка 10 ниже). `store/chunkstatus.go` дочитывает `updated_at` в `ChunkStatus.UpdatedAt` (колонка `NOT NULL DEFAULT datetime('now')`, `migrate.go:37`; миграции — только `ADD COLUMN`: у minirun `text_modified` = `2026-07-25T19:17:53Z`, время боевого прогона, не миграции 30.08).
- `internal/pipeline/bookbuild.go``Runner.BuildBook(BuildOptions{Formats, Out, Partial})`: `Export(false)``staleUnits``assembleBook` (чистая функция, возвращает список дыр) → отказ `RefusalBookIncomplete` (`refusal.go`, exit **16**) либо стейдж всех форматов + общий коммит в `<project_db>.book.<fmt>` (не запрошенные соседи удаляются — рядом с БД лежит ОДНА сборка); `StatusArtifacts.BookFiles` (`book_files`, map формат→абс. путь; в `status --json` и `manifest --json`). `--out` — только НОВЫЙ файл (существующий путь любого рода, несуществующий каталог → `config_invalid`, exit 10). Сбой между двумя `rename``write_incomplete` (exit 15) с перечнем landed/not landed.
- `cmd/tmctl`: глагол `build --config b.yaml [--format epub,txt] [--out path] [--partial]`; stdout — JSON `tm-build-v1` (`files`, `total/pending/withheld/incomplete/stale_units`, `stale_unknown`, `ghost_rows`, `config_drift`, `complete`, `text_modified`); чужим глаголам новые флаги отказываются, `build` отказывает `--plaintext/--pairs/--json`; usage-строка расширена (`invocation_test.go` — контрактное расширение по собственному комментарию теста «Every one is a deliberate contract extension»; это ЕДИНСТВЕННАЯ правка существующего теста). `backend/README.md` — строка `cmd/tmctl` дополнена `build`.
**Политика (§3.2) как реализована — ШЕСТЬ состояний, не пять:** дыры — pending · withheld (`Disposition!="pending" && FinalText==""`, плюс юнит, чей текст после чистки не даёт ни одного абзаца) · incomplete (`DroppedMembers>0`) · **stale** (финальная строка сделана под исходник, которого в книге уже нет: `content_hash` строки ≠ `cachedRenderedContentHashes` — тот же факт, по которому резюм перекупает юнит; считается ТОЛЬКО без дрейфа, под дрейфом `stale_unknown: true`, никогда не «нет») · **ghost** (при укрупнении нарезки строка-лидер несёт текст прежнего, более короткого юнита; остаток лежит в ghost-строках и в файл не попадает: acceptance 57→37, 20 строк при `pending 0`). По умолчанию — ОТКАЗ при любой дыре с перечнем `chapter N unit M: <вид> (<причина>)` и ghost-ключами (≤50 строк); `--partial` — файл с пометкой (первые абзацы первого документа spine + `dc:description`; в txt — после заголовка книги) и маркером на месте КАЖДОЙ дыры (pending/withheld/stale — вместо текста; incomplete — перед текстом; ghost — в конце главы, счётчиком). drift — НЕ дыра (текст файла = текст прогона): в JSON-конверте и WARN, в файле нет. Оглавление/`<h1>` при `Heading==""` — номер главы цифрами. Абзац — непустая строка `FinalText` (`\r\n`/`\r`/`\n`). `dc:title``book.yaml` как есть (на стенде `蛊真人`), `dc:language` = `target_lang` (форма тега BCP 47 проверяется, иначе `config_invalid`), `dc:identifier` = `urn:textmachine:book:<book_id>`, автора нет.
**Сверка состава против §3 (пункт → сделано → чем доказано; все команды — на копиях в песочнице `<SB>/stand`, бинарь `go build -o <SB>/stand/tmctl ./cmd/tmctl`).**
| §3 | Сделано | Доказательство |
|---|---|---|
| 3.1 EPUB + чистый txt из `BookExport`, ОДИН файл на формат, из `tmctl` | да: `tmctl build``<project_db>.book.epub` / `.book.txt` | `build-minirun.json`: 2 пути, `complete: true`; `java -jar epubcheck-5.3.0/epubcheck.jar <book>.epub``0 fatals / 0 errors / 0 warnings / 0 infos` на всех трёх EPUB (minirun, acceptance `--partial`, coldrun-a `--partial`) |
| 3.1 без аудит-баннеров/флагов/`gapMarker` в txt | да | `TestTXTShape`, `TestBuildBookRoundTrips…` (нет `=== CHAPTER`, `TEXT MISSING`, `pending`, `flagged`); `TestBuildRefusesTheGapAndPartialMarksIt` — реальный c-lite прогон бинарём: `⚠ 1.0 1` перед уцелевшим текстом, ни `INCOMPLETE`, ни `cjk_artifact` |
| 3.1 `--plaintext` не тронут | да | `cmp` вывода бинаря из `git archive HEAD` и нового: IDENTICAL на minirun (171932 б), acceptance (354821 б), coldrun-a (1077 б) |
| 3.1 основа — `chunktest/epub.go`, OCF в прод без `testing` | да | `TestEPUBFixtureMimetypeIsOCFConformant` и 9 импортёров зелёные без правки; `TestContainerIsOCFConformant` (флаги `0`, extra `0`, `mimetype` на фиксированных смещениях) |
| 3.1 детерминизм двумя ПРОЦЕССАМИ | да | `cmp first/<f> <copy>/<f>` — IDENTICAL для 6 файлов (две сборки в двух процессах); zip-таймметки `1980-00-00 00:00`; sha256 EPUB minirun `084ff0a9…`, совпал с независимой сборкой ревьюера |
| 3.2 пять (шесть) состояний, отказ по умолчанию / `--partial` | да | acceptance: `build` → exit 16 «pending=0 withheld=1 incomplete=1 stale=0 ghost_rows=20 of total_units=37 (stale: UNKNOWN …) · chapter 1 unit 0: withheld (cjk_artifact) · chapter 10 unit 0: incomplete — 1 member(s)… (length) · ghost rows: 1/1 1/3 …»; `--partial``⚠ 2/37`, `⚠ +20`, `⚠ 1.0`, `⚠ 10.0 1`, `⚠ 10 +1`…; coldrun-a: exit 16 (14 pending) / `--partial` со словесными маркерами из `reader.txt`; minirun (только drift): записан по умолчанию, `config_drift: true`, `stale_unknown: true`, в файле `⚠` нет; тесты `TestBuildBookRefusesAPending…`, `…WithheldUnitIsAHole`, `…GhostRowsAreAHole` (посадка построена: усадка исходника после прогона), `…StaleUnitsAreAHole`, `…DriftIsReportedNotMarked`, `…RemovedStageIsDrift`, `TestAssembleBookMarksEveryHoleKind` |
| 3.2 не в снапшот | да | `current_snapshot` minirun с `ru/reader.txt` и без — `bb0b3b2aaf0d…` оба раза (копия корня пака в песочнице); `TestReaderWordsDoNotMoveThePackVersion` |
| 3.2 книга без langpack — несловесно | да | acceptance (`langpack_root` не задан): `⚠ 1.0`, `⚠ 10.0 1`, `⚠ 2/37`; `TestReaderWordsAbsentIsTheNonVerbalForm` |
| 3.2 маркер не в `FinalText`/`export.go` | да | JSON HEAD vs new: `existing equal True`, добавлены только `text_modified`, `heading`, `ghost_units` |
| 3.3 `ApplyHeading` и три точки не тронуты; срез только известного префикса | да | `git diff HEAD -- backend/internal/chunk/chunker.go backend/internal/chunk/ingest.go backend/internal/pipeline/waverun.go` пуст; вызов `ApplyHeading` в `export.go` — контекстная строка; minirun: 10 заголовков срезаны, круг 402/402; `TestAssembleBookMarksEveryHoleKind` (упоминание заголовка в тексте не срезается) |
| 3.3 `Heading==""` → номер цифрами в nav и `<h1>` | да | acceptance nav `1``25`; minirun/coldrun-a — `Глава N` из `heading` |
| 3.3-бис пять требований EPUB 3; nav не в spine; `dc:language` из `TargetLang` | да | `TestEPUBCarriesTheFiveRequiredThingsAndKeepsNavOutOfTheSpine`; OPF minirun: `urn:textmachine:book:guzhenren-minirun`, `蛊真人`, `ru`, `2026-07-25T19:17:53Z`; epubcheck |
| 3.4 место/атомарность/конверт/глагол/коды | да | `status --json`/`manifest --json` копии minirun: `book_files: {epub, txt}`; `TestTheRefusalTableIsTotal`, `TestDispatchCommandsCoversTheSwitch`, `TestParseNoArgsUsage`; `TestBuildBookLeavesOneBuildBesideTheDatabase`; `TestBuildBookOutNamesANewFile` |
| 3.5 платформа не тронута; `tm-export-v1` аддитивно; пары — данные; без зависимостей | да | `git status` — только `backend/` и `docs/PROGRESS.md`; `go.mod` не менялся; греп читательских слов по новому Go-коду — только комментарии |
| §4 `make battery` (`-race`) | да, 2 настоящих прогона | `battery-1.log` (до правок ревью) и `battery-3.log` (после всех правок): `BATTERY_EXIT=0`, 18 пакетов, скипы — три прежних стендовых (`TestMinerFullBookParity`, `TestHelperEventsRun`, `TestHelperKillLoop`). ⚠ «Батарея №2» была ложным прогоном: `make` из корня репо (`No rule to make target`), мой `echo` замаскировал код — поймано по логу, перезапущено из `backend/` |
| §4 дифф `^func Test` | ни один не исчез | `git diff HEAD -- '*_test.go' \| grep '^-func Test'` пуст; `func Test` в `backend/`: 1017 → 1044 (+27) |
| §4 круг ингестом равенством | да | `tmroundtrip` (собран `go build -overlay`, файла в дереве нет): minirun 10/10 глав, 402 абзаца; acceptance `--partial` 25/25, 1568; coldrun-a `--partial` 10/10, 25 |
| §4 `git -C books status` до/после | см. obstacle | 6 строк `D …-wal/-shm` до; после — 4: у acceptance `-wal`/`-shm` ПОЯВИЛИСЬ (идентичны трекнутым; БД не изменена) |
| §4 хирургия конфигов в копиях | список | minirun/coldrun-a `book.yaml`: удалены `mined_delta:`/`mined_rejects:`; acceptance `pipeline-acceptance.yaml`: удалены `stm_depth: 2`, `overlap_tokens: 200`, оба `prompt:`; добавлен `acceptance/pairs/zh-ru.yaml` (копия minirun/pairs с `prompts_root` на `backend/prompts` этого дерева); все абсолютные пути переписаны на песочницу/этот ворктри, греп по копиям пуст; миграции v10/v7/v14 → v16, `backups/` — в песочнице |
| §4 промежуточное и финальное ревью субагентом | да, Fable 5 ×2 | ниже |
**Промежуточное ревью (Fable 5, исполнением) — 8 находок, все закрыты:** (1) MAJOR: `--format`-подмножество оставляло соседний формат прежней сборки рядом с БД при равноправном `book_files` → стейдж всех форматов + общий коммит, не запрошенные соседи удаляются, пин `TestBuildBookLeavesOneBuildBesideTheDatabase`; (2) `text_modified` считался по всем строкам всех стадий → только по финальным строкам экспорта (acceptance `20:41:47Z``20:40:00Z`, coldrun-a — эпоха: ничего не отгружено); (3) пустой `title` → exit 1 → `config_invalid` с именем ключа, то же для `target_lang` не в форме BCP 47; (4) `mimetype` с data-descriptor → `CreateRaw`; (5) control-символы срезались только в EPUB → `CleanText` один раз для обоих; (6) `--format epub,epub` → дедуп; (7) `backend/README.md` без `build` → дополнено; таблицы exit-кодов в `docs/` — пинг; (8) подтверждено верным: `UnitHole` = арифметика `renderExport`, `DispOK` с пустым текстом недостижим, нулевые таймметки, `reader.txt` вне `Load`.
**Финальный адверсариальный проход (Fable 5; overlay-тесты, свой бинарь, epubcheck на adversarial-книгах: краевые символы, RTL, 2000 глав) — 10 находок:**
1. MAJOR — правка исходника НА МЕСТЕ (ключи `(chapter, chunk_idx)` уцелели, текст под ними сменился) давала файл, побайтово равный прежнему, под `complete: true`. **Исправлено — шестое состояние `stale`** (см. политику; пин `TestBuildBookStaleUnitsAreAHole`: правка главы 2 → `chapter 2 unit 0: stale`, глава 1 не задета; после `translate` — снова whole). ⚠ Побочный факт: `projectRebill` сверяет content-hash ТОЛЬКО при сдвинутом снапшоте (bank-only ветка), при том же снапшоте строка идёт как «$0 resume» без сверки — проекция денег `status` слепа к правке исходника на месте, `translate` при этом перекупает (ревьюер воспроизвёл: «2 units re-bought»). Не моя строка — пинг.
2. MAJOR — `--out` перезаписывал что угодно, включая БД проекта («file is not a database») и исходник. **Исправлено:** `--out` только для НОВОГО файла; существующий путь / отсутствующий каталог → `config_invalid`; место рядом с БД по-прежнему перезаписывается. Пин `TestBuildBookOutNamesANewFile` (БД, исходник, чужой файл, несуществующий каталог; ни одного `.tmp-` после). Стенд: `--out <project_db>` → exit 10, БД цела.
3. MINOR — сбой на фазе коммита второго формата оставлял смешанную сборку и exit 1. **Исправлено:** `write_incomplete` (exit 15) с перечнем landed/not landed; комментарий файла говорит правду. Тестом не покрыто («не проверено»: сбой `rename` в только что записанном каталоге без инъекции не воспроизводится).
4. MINOR — юнит с непустым `FinalText`, дающий ноль абзацев (control-символы/`U+FFFF`), не был дырой. **Исправлено:** сборщик читает юнит по тому, что тот реально положил (ноль абзацев → withheld); счётчики отчёта — из списка дыр сборщика; `HoleCounts` остался ровно арифметикой `renderExport`.
5. MINOR — голый `\r`: txt нёс его, XML нормализует → форматы расходились. **Исправлено:** `\r\n`/`\r`/`\n` — конец строки.
6. MINOR — `--out` в несуществующий каталог → exit 1. **Исправлено:** `config_invalid`.
7. NIT — `build` молча принимал `--plaintext/--pairs/--json`. **Исправлено:** отказ.
8. NIT — `dc:title` с паддингом vs обрезанный txt. **Исправлено:** `TrimSpace` в модели и EPUB.
9. NIT — ghost-маркер по номеру главы СТАРОЙ нарезки может встать в соседнюю главу при усадке. **Не исправлено, задокументировано** (`assembleBook`): книга всё равно помечена неполной, ложна лишь координата; иного источника координаты у ghost-строки нет.
10. MINOR (унаследовано `Export`) — удаление стадии `edit` делало черновики «полной» книгой без дрейфа. **Исправлено в `exportConfigDrift`:** строка со стадией, которой нет в конфиге, — drift; под ним stale = unknown; пин `TestBuildBookRemovedStageIsDrift`. ⚠ `Status` (`checkWave`) несёт то же слепое пятно — пинг.
Подтверждено проходом как верное: все посадки A (withheld при `pending 0`, ghost-only, исчезнувшая глава, все юниты withheld, 0 юнитов, `HoleCounts` = шапка `renderExport` на всей матрице состояний); детерминизм (нет `time.Now`/`rand`/`os.Stat`/map-итерации в байты; 2 процесса под `TZ=Asia/Tokyo LANG=C` — идентично); заголовки (срез только известного префикса; pending-первый юнит с заголовком получает заголовок; пустой заголовок недостижим — `parseHeading`); nav вне spine; круг на трёх стендовых книгах воспроизведён независимо + epubcheck чист на adversarial-книгах; RTL — валиден, без `dir`, честно неподдержан; 2000 глав — 47 мс/707 КБ, ingest 38 мс; ids/hrefs по индексу; словесных литералов в Go нет; пин `Version()` подлинный; exit 16 в полосе, таблица тотальна; JSON аддитивен; `ApplyHeading` и три точки не тронуты; ни один тест не удалён; линтер 0.
**Obstacle reporting — что НЕ удалось и что осталось непроверенным.**
- **Не проверено — открывается ли в читалке.** Читалки на машине нет; «валиден» ≠ «открылся». Проверено ТОЛЬКО валидатором W3C (epubcheck 5.3.0, `0/0/0` ×3) и кругом через собственный ингест движка.
- **Не проверено — чистый бездрейфовый файл на реальной книге.** Недостижим на этом HEAD без платного перепрогона (все три копии под `config_drift: true`); чистый путь доказан синтетикой (`TestBuildBookRoundTripsThroughIngestAndPublishesItsPaths`: `complete: true`, `stale_unknown: false`, без пометок). Следствие: **stale-проверка на реальных книгах стенда не сделана** (`stale_unknown: true` у всех трёх — под дрейфом факт неразличим), доказана только харнесом.
- **Не проверено — сбой между двумя `rename`** (находка 3): ветка `write_incomplete` написана, не исполнена.
- **Не проверено — посадка на incomplete в `pipeline`-харнесе:** реальный member-drop только через бинарь (gap-харнесс `cmd/tmctl`) и acceptance (10/0, `length`); в `internal/pipeline` — синтетический `BookExport`.
- **Не проверено — направление письма.** `dir`/`page-progression-direction` не выставляются; данных о направлении в `internal/lang` нет; RTL-файл валиден, но выровнен слева — честно неподдержано (вопрос 4).
- **Не объяснено — read-only открытие ОРИГИНАЛЬНОЙ acceptance-БД.** `git -C books status --short -- gu-zhenren` до: 6 строк `D …-wal/-shm`; после: 4 — у `acceptance/guzhenren-acc-a.db` `-wal` (0 байт, 13:47:36 +03) и `-shm` (32 КБ, 13:47:49 +03) появились, идентичны трекнутым (`git diff` пуст), сама БД (mtime 24.08) не изменена. Мои команды все шли по копиям (`project_db`/`source_file` копии — в песочнице, проверено грепом; `.lock` копии — от миграции 12:56); тесты дерева acceptance-БД не открывают (греп); субагент-ревьюер завершился раньше этого времени. В `ListAgents` живы ещё три сессии (оркестратор и две, вероятно, полигонные) — кто-то из них открыл оригинал read-only. Файлы НЕ трогал (чужой репозиторий).
- **Опровергнуто (промтом):** «глава 1 minirun остаётся `""` даже с правилом» — на HEAD у minirun заголовок у всех 10 глав (`headings 10`, nav `Глава 1…Глава 10`); `""` у всех 25 глав acceptance — подтверждено.
- **Подтверждено:** числа промта §4 (minirun 14/14 ok · drift · оракул июля 14/14 по `final_text/snapshot_id/disposition`; acceptance 37 юнитов · ghost 20 · withheld 1/0 · incomplete 10/0 · drift; coldrun-a 14/14 pending · drift); миграции v10/v7/v14 → v16; epubcheck приносится и работает.
- **Ограничения формы (по промту, не исправлялись):** `dc:title` = `蛊真人` (перевод не выдумывается); автора нет; `text_modified` — «последняя ЗАПИСЬ» финальной строки ($0-резюм, переписавший строку без изменений, двигает штамп; между сборками детерминизм сохраняется); побайтовый детерминизм гарантирован для ОДНОГО бинаря (другой toolchain Go — иной deflate-поток при тех же документах); ghost-координата — старой нарезки (находка 9); `stale` под дрейфом — «неизвестно».
- **Самопойманная ошибка:** ложный прогон «батареи №2» (см. таблицу).
- **Эхо §7:** правок задания релеем владельца не поступало; единственное сообщение оркестратора (подтверждение адресата + напоминания) заказом не является; приор «артефакт рядом с ProjectDB + `StatusArtifacts`» принят с доводом.
**Вопросы владельцу (гейт между этим паком и дверью платформы, D25.1):**
1. **Лестница `run_complete / structurally_complete / quality_reviewed / publishable`:** писатель различает ровно «без дыр» (`complete: true`) и «с дырами» (отказ / `--partial` с пометкой). Что из лестницы печатать на титуле и в имени файла и какая ступень даёт копию БЕЗ пометки — решение владельца; в код не внесено намеренно.
2. **Лейбл главы без заголовка (К-3):** сейчас — номер цифрами. Словесный шаблон — одна строка в `reader.txt`, если ратифицируется; не сделано, чтобы не решать К-3 данными.
3. **Ghost и stale как дыры по умолчанию:** довод — оба дают «молча полную» книгу (acceptance: 20 строк не в файле при `pending 0`; правка исходника на месте: старый перевод под `complete: true`). Если владелец хочет «предупреждение, не отказ», — по строке в `BuildBook`.
4. **Направление письма для RTL-цели:** поле данных цели (в `reader.txt` или рядом), не Go — когда появится RTL-пара.
**Пинги оркестратору (чужие зоны, не правил):** (а) exit-код **16 `book_incomplete`** в таблицы `docs/glossary.md:64` и `docs/architecture/14-api-contract/README.md:731,758`; (б) `docs/architecture/09-target-architecture.md`: слой «писатель книги» построен; следующий шаг — дверь платформы `createExport`/`getExport` + `export_formats` из `bookfile.Formats`; дверь обязана СТРОИТЬ (`tmctl build --format <f> [--partial]`) и отдавать то, что построила, — не читать файл, лежащий рядом с БД (копия прежней сборки); (в) `projectRebill`/`status` слепы к правке исходника на месте при том же снапшоте (перекупка без предупреждения о деньгах) — строка бэклога; (г) `Status.checkWave` не видит исчезнувшую стадию (то же, что находка 10 у `Export`) — строка бэклога; (д) строки 160/201 не тронуты, `Heading` в `tm-export-v1` — тот же временный рендер (D39.122 п.2д), помечено в комментарии поля.
### 📝 ЗАПИСКА-ПЛАН — пак «деньги» (промт `docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md`, D39.165 §1б + строка 231), сессия `textmachine-e4`, 28.08
**Скоуп:** `backend/` — §3.1 потолок ОБЪЁМА рядом с денежным · §3.2 смета пере-прохода без покупки. `platform/` не трогаю (проводка `CeilingChapters` — не мой заказ, честная граница §0 промта принята).
**§3.1 — В ЧЁМ МЕРИТЬ: выходной юнит (`ManifestUnit`), кандидат (а). Обоснование.**
Ровно эта гранулярность и есть то, что движок отгружает и что публикует манифест: `ManifestUnit` строится итерацией по `r.outputUnits(chunks)` (`backend/internal/pipeline/manifest.go:330-357`), то есть по тем же `editUnit`, которыми идёт волна редактуры; `units_total` манифеста — то самое поле, из которого интейк платформы пишет `chapters.units_total` (`platform/internal/pgstore/readmodel.go:113,166-175`, читал не правил). Значит перевод «N глав → M юнитов» у платформы ТОЧНЫЙ, а не оценочный: манифест снимается `tmctl manifest` за $0 и без ключей (`runner.go:239-244``models.CheckKeys` только `if forWrite`) ДО первой оплаты.
**Кандидат (в), `chunk×stage`, отвергнут именно по ловушке, названной в промте.** Это биллинговая единица (`rebill.go:322`), и в ней «10 единиц» — разный объём книги при разной форме конвейера: на юнит приходится `len(Members)*nDraftStages + nEditStages` позиций (`rebill.go:207`). Продавая (а) и каппя (в), мы воспроизвели бы дефект §0 слоем ниже. Кандидат (б), чанк, — внутренний артефакт нарезки, покупателю не виден и манифестом не считается.
**Честная оговорка, которую обязан назвать:** число юнитов книги зависит от ФОРМЫ конвейера — редакторский конвейер даёт крупные `editUnit`, черновой-только даёт по юниту на чанк (`waverun.go:67-76`). Это свойство конфигурации, а не ветвление по паре; но манифест и потолок обязаны сниматься с ОДНОГО деплоя, иначе «10 юнитов» значат разное.
**§3.1 — КАК СЧИТАЮТСЯ РЕПИНЫ И РЕТРАИ (прямой ответ на вопрос промта).**
- **Ретрай и эскалационный хоп — НЕ считаются.** Они внутри юнита: ретрай — итерация цикла попыток (`stagerun.go:158-194`), эскалация — отдельный `runAttempt` (`escalation.go`), но `chunk_status` на `chunk×stage` пишется ОДНА (`stagerun.go:297-308`). Довод по существу: объёмный потолок ограничивает ДОСТАВКУ, денежный — ТРАТУ. Хвост качества (1.5%→23% по D39.165 §1) — факт денежный, и у него уже есть свой потолок; если бы ретраи жгли объём, купивший 10 глав получал бы 8, потому что две главы заикнулись, — то есть платил бы КНИГОЙ за качественный хвост движка. Это ровно та подмена, которую пак и лечит.
- **Репины и $0-резюмы — НЕ считаются.** Иначе пере-проход, пере-привязывающий 500 юнитов за $0 (`rebill.go:315-320`), сжёг бы потолок, не доставив ничего.
-**Правило одной строкой: потолок считает выходные юниты, по которым ЭТОТ прогон сделает хотя бы один провайдерский вызов.**
**§3.1 — МЕХАНИЗМ: пре-флайт скоуп, а не счётчик в воркере.**
Допуск считается ОДИН раз ДО волн, по состоянию стора, и даёт множество юнитов-в-работе; волны просто пропускают то, что вне скоупа. Предикат «$0-обслуживаемый» берётся из УЖЕ существующего — того же, которым считает `projectRebill`/`repin`, — чтобы объёмный стоп и денежная смета не могли разойтись двумя определениями.
**Почему не счётчик со слотами в воркере (рассмотрено и отвергнуто):** волна параллельна (`runWave`, `waverun.go:236-309`), и при отказе в слоте конкурентно розданные элементы пропадали бы НАВСЕГДА — при `workers=8` и потолке 1 семь юнитов оказались бы пропущены, а освободившийся после $0-репина слот уже некому было бы отдать. Пре-флайт эту гонку растворяет.
**Грабля LiteLLM исполнена конструктивно:** ни один элемент вне скоупа не НАЧИНАЕТСЯ, поэтому пере-бой на один вызов невозможен в принципе, а не по проверке.
**Грабля OpenHands исполнена:** остановка называет свой потолок — в `BookResult`, в строке лога и в выводе CLI.
**§3.1 — ФОРМА ОТВЕТА ЧЕРЕЗ ШОВ: беру решение оркестратора и ПОДТВЕРЖДАЮ его исполнением.**
Код выхода 0, замороженный словарь (`main.go:115-140`) не трогается. **Проверил то, о чём он просил пингнуть, — не ломает:** exit 0 у платформы = статус ПРОГОНА `ready` (`platform/internal/runs/reconcile.go:703-706`, читал не правил), а не «книга дочитана»; полоса книги едет событиями юнитов, поэтому дочитанной книга не объявляется и следующую покупку не блокирует.
**Словарь потока НЕ трогаю в этом паке.** Новое значение `Finished.Outcome` было бы минором по правилу `runevents.go:29-40`, но у платформы неизвестный ВЫХОДНОЙ код читается как отказ (`ingest/exit.go:168-169`), и множить развилки в чужом пакете без контрактного минора — не мой заказ (§3.3). Различение живёт в отчёте и логе, как и решил оркестратор.
**Ортогональность цела:** `--ceiling-usd` остаётся кумулятивным книжным капом и судит в `Reserve` (`ledger.go:56-72`); объёмный потолок леджера не касается вовсе и живёт до волн. Денег не считает, холдов платформы не двигает.
**§3.2 — ФОРМА: чинится СВЁРТКА, а не заводится глагол. Это дешевле всех трёх кандидатов промта.**
Слепое окно — не в том, что `status` пишущий, а в том, что он сворачивает НЕ ТО: `projectStoredMemory` (`status.go:815-826`) материализует СОХРАНЁННЫЙ глоссарий, то есть фолд ПРОШЛОГО прогона, тогда как ближайший `translate` пере-сеет банк из ФАЙЛОВ (`seedGlossary`) и сделает это ДО гейта согласия (`bookrun.go`: `seedGlossary` → … → `checkRebillConsent`). **Собственное оправдание комментария — «exactly as it does for `translate` itself» — неверно: `translate` правку файла как раз ВИДИТ.** Значит сегодня `status` НЕДО-считывает то, что `translate` реально выставит, и это денежный дефект, а не отсутствующая функция.
**Лечение:** фолд тех же входов, что берёт `seedGlossary` (сид + рубя + мined-дельта + авто-банк), — **В ПАМЯТИ, без единой записи**. `status` при этом остаётся ЧИСТО ЧИТАЮЩИМ: он читает файлы вместо стора, а фолд не пишет никуда. Возражение оркестратора против «флага у `status`» снимается тем, что оно стояло на посылке «свёртка идёт через стор» — она не обязана.
**Записи не будет и не может быть:** `status` открывает стор через `NewReadOnlyRunner``OpenReadOnly` с `PRAGMA query_only=1` (`store/store.go:117-166`), и на состоянии §4.3 (прогон СОСТОЯЛСЯ ⇒ база есть) фолбэк на `Open` не срабатывает вовсе — записей НОЛЬ, даже first-touch.
**Родня 124(в) закрывается тем же:** `status` уже считает ре-билл при ЛЮБОМ дрейфе, не только банковом, — то есть после фикса он и есть «оценить пере-проход, ничего не покупая», ради которого поименован `resnapshot --dry-run`.
**Третий кандидат (`bank-apply --dry-run`) рассмотрен ПЕРВЫМ, как заказано, и отклонён с доводом:** он требует `--decisions`, а в продуктовом потоке D39.165 §3 решения к моменту продажи УЖЕ на диске — документа больше нет. Плюс он Runner-free и не держит ни статусов, ни манифеста, ни конфига (`bankdecisions.go:294-314`), то есть под смету пришлось бы втащить в него весь Runner. Ту же проекцию он сможет позвать позже — она будет одной функцией.
**§3.2 — ТОЖДЕСТВО БЕСПЛАТНОЙ СМЕТЫ И ПЛАТНОГО ПРОГОНА: доказываю, а не предполагаю.**
Канонический фолд идёт через стор и там ПЕРЕ-СОРТИРУЕТСЯ: `ORDER BY src, sense, since_ch, until_ch, status, dst` (`store/glossary.go:223`). Порядок несущий дважды — `MaterializeBank` порядок входа НЕ трогает (`membank/memory.go:283-346`), а дальше он определяет и хеш версии (`ComputeVersionScopedIn` стримит SHA-256 по строкам в порядке итерации), и ЛИНИИ отрендеренного блока через стабильную сортировку бюджета (`memory.go:608-619`), которую режет токенный бюджет.
**Почему тождество ДОКАЗУЕМО, а не правдоподобно:** таблица несёт `UNIQUE (book_id, src, sense, since_ch, until_ch)` (`store/migrate.go:203`), поэтому префикс из четырёх колонок НЕ МОЖЕТ дать ничью — порядок ТОТАЛЬНЫЙ и полностью определён значениями, а не rowid и не порядком вставки. `COLLATE` в схеме нет нигде ⇒ сравнение текста побайтное, как и `<` в Go. Алиасы читаются `ORDER BY term_id, alias`, внутри записи это сортировка по `alias` (`glossary.go:247-249`). `glossary.id` в хеш не входит намеренно (`memory.go:360-363`).
⇒ Фолд в памяти обязан: собрать тот же набор значений + отсортировать по (Src, Sense, SinceCh, UntilCh, Status, Dst) + отсортировать алиасы + то же для voices/pairs по их собственным ключам.
**Проверка — ДИФФЕРЕНЦИАЛЬНАЯ, исполнением:** тест гоняет оба пути на одном состоянии и требует совпадения построчно (кроме `ID`), совпадения `Version()`/`BaseVersion()`, совпадения отрендеренных байтов и совпадения самого числа `projectRebill`. Ассерт равенства, а не «похоже».
**Пишущий путь НЕ трогаю:** порядок ВСТАВКИ в `seedGlossary` остаётся как есть, иначе поехали бы `glossary.id` и телеметрия `injected_ids`. Добавляется чистая функция упорядочения для читающей стороны.
**Находки по дороге (в блате пака, называю сразу):**
1.**`projectRebill` в `status` рендерит по НУЛЕВОМУ базовому банку.** `projectStoredMemory` ставит только `r.memory`, `r.baseMemory` не ставит НИКОГДА (единственные присваивания — `seeding.go:157,159`), а ветка репина берёт именно его (`rebill.go:167`) и молча пропускает позиции при `nil` (`repin.go:130`). Следствие: при движении БАЗОВЫХ строк банка `status` считает всю черновую волну пере-оплатой, то есть расходится с `translate` — вопреки собственному обещанию `status.go:206` «status can never quote a different number».
2.**При `snapshot_drift` у `status` память вообще не материализуется:** `projectStoredMemory` зовётся под `if !rep.SnapshotDrift` (`status.go:635`), а `projectRebill` — безусловно при наличии строк (`status.go:673`), и `memoryVersion()` при `nil` отдаёт хеш ПУСТОГО банка (`snapshot.go:190-195`) ⇒ книга объявляется пере-оплаченной целиком.
3.**Мина `omitempty` подтверждена исполнением чтения:** три РАЗНЫХ состояния — «не запускалось», «посчитано и ноль», «расчёт упал» (`status.go:668-681` логирует и оставляет нули) — на проводе неотличимы. Это ровно цена D39.166 п.2.
Первые две мой фикс §3.2 снимает по построению (фолд считается безусловно и ставит ОБА банка); третью закрываю формой поля.
**НЕ делаю:** калибровку $0.03 (строка 202) · цену от объёма исходника · строки 228/230/141/131 · словарь кодов выхода · словарь потока событий · `platform/` · коммиты. Незакоммиченную работу полигона не касаюсь.
### ✅ ИТОГ — пак «деньги» (промт `docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md`, D39.165 §1б + строка 231), сессия `textmachine-e4`, 29.08
**Батарея зоны:** `make battery`**21 пакет из 21, 0 FAIL**, линтер **0 issues**, `test -race` зелёный целиком.
**Проверяю ПОЛНОТОЙ СПИСКА, а не отсутствием `FAIL`, и это правило стоило мне двух ложных выводов.** Первый «зелёный» прогон был на 20 пакетов из 21 с `FAIL` в хвосте, который обёртка подала как `exit code 0`. Сверка: `comm -23 <(go list ./...) <(grep -E "^(ok|\?)" <лог>)` — пусто.
**И про `internal/store`.** Он дважды ронял батарею (`TestTheSeamIsNotChargedToTheStoreOperationBudget`, `apply migration 14: context deadline exceeded`), и я сначала списала это на память — **неверно.** Проверила как надо: развернула ЧИСТЫЙ HEAD через `git archive` и погоняла тот же тест в обоих деревьях вперемежку — **моё 4/5 падений, HEAD 4/5**, то есть от моих правок не зависит. Тест ужимает `opTimeout` до 150 мс (`migrate_test.go:408-429`) и под параллельной нагрузкой не укладывается; на разгруженной машине проходит 3/3, и итоговая батарея выше — с ним. Каталог `internal/store` я не трогала ни строкой. Оркестратору отправлено строкой.
Пропущенные тесты, как требует Makefile, называю: `TestMinerFullBookParity` (нет стендовых данных), `TestHelperEventsRun`, `TestHelperKillLoop` (точки входа подпроцессов, вне обычного прогона).
Дерево ПОСЛЕ трёх кругов: **20 отслеживаемых файлов правлено (+761/204) и 5 заведено (3106 строк)**; новых тестов **36** (30 в `internal/pipeline`, 6 в `cmd/tmctl`). Числа взяты командами: `git diff --stat -- backend/` для правленых (новые в него не входят — они untracked), `wc -l` для заведённых, `grep -c '^func Test'` для тестов. **Не коммичу — на лендинг.** ⚠ Числа выше по тексту, где они расходятся с этими, относятся к ПЕРВОМУ кругу и оставлены как след, а не как состояние.
**Мутационных посадок 26** (11 на первом круге, 15 на втором и третьем), каждая — на свой пункт, вердикт по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи: снятие сортировки фолда · потолок на единицу больше · снятие допуска в черновой волне · снятие допуска в редакторской · «свободные юниты тратят грант» · возврат к прежней терминальности (воспроизводит перерасход) · `!ok``ok &&` (воспроизводит перерасход) · допуск только лидера (воспроизводит усечённую главу) · фильтр согласия в тождество (воспроизводит отказ законной работе) · перестановка лога замечаний (воспроизводит потерю диагностики) · снятие отказа несогласованной форме конвейера. Все одиннадцать падают, и падает ИМЕННО адресный тест.
#### Таблица комплектности против §3
| Пункт промта | Что сделано | Каким ИСПОЛНЕНИЕМ подтверждено |
|---|---|---|
| **§3.1** потолок ОБЪЁМА рядом с денежным | Флаг `--max-units N` у `translate`; единица — **выходной юнит** (`ManifestUnit`=`editUnit`, `units_total` манифеста); допуск считается ДО волн (`planVolume`, `backend/internal/pipeline/volume.go`), волны не начинают элемент вне скоупа | `TestTheVolumeCeilingStopsAtWhatWasBought` — 5 глав, потолок 2: отгружено ровно 2, `res.Volume` заполнен, у глав 35 **нет ни одной строки `chunk_status`**, вызовов провайдера ровно 4 (2 юнита × 2 стадии) |
| §3.1 — «проверять ПЕРЕД началом единицы» (грабля LiteLLM) | Свойство конструктивное: элемент вне скоупа не начинается вовсе | Тот же тест, клауза (3): отсутствие строк у N+1 и точный счёт вызовов; мутация «потолок на единицу больше» (`Granted <= MaxUnits`) — падают 3 теста |
| §3.1 — «остановка НАЗЫВАЕТ свой потолок» (грабля OpenHands) | `BookResult.Volume` (`VolumeStop{MaxUnits, Delivered, Reworked, Free, LeftFresh, LeftRework}` — ⚠ поля ПЕРЕСТРОЕНЫ на втором круге, см. находку 2), строка лога, строка вывода CLI | `TestAVolumeStopIsNotAMoneyStop` — денежный стоп остаётся типизированным `*CeilingHalt` (exit 4), объёмный не ошибка вовсе и несёт `res.Volume` |
| §3.1 — счёт РЕПИНОВ | Не считаются: считаются только юниты, по которым прогон сделает ≥1 вызов | `TestAResumeDoesNotSpendItsVolumeOnFreeUnits` — вторая покупка 2 юнитов на книге, где 2 уже сделаны: `Free=2`, `Delivered=2`, `LeftFresh=1`, свежих вызовов ровно 4. Мутация «свободные тратят грант» — тест падает |
| §3.1 — счёт РЕТРАЕВ | Не считаются: ретрай/эскалация внутри юнита | `TestRetriesDoNotBurnTheVolumeCeiling` — провайдер отдаёт `finish_reason: length` на первом черновике: юнитов всё равно 2, вызовов >4 |
| §3.1 — форма ответа через шов | Решение оркестратора исполнено: код выхода **0**, словарь `main.go:115-140` не тронут, словарь `runevents` не тронут | `res.ExitCode()==0` в тесте; сверено чтением `platform/internal/runs/reconcile.go:703-706` — exit 0 = статус ПРОГОНА `ready`, книга дочитанной не объявляется |
| §3.1 — ортогональность | `--max-units` не касается леджера, резерваций и кумулятивной семантики `--ceiling-usd` | `TestAVolumeStopIsNotAMoneyStop`; `MaxUnits` не входит ни в `BriefHash`, ни в снапшот-пейлоад |
| **§3.2** смета БЕЗ покупки | Читающие поверхности сворачивают банк **в памяти** из тех же входов, что `seedGlossary` (`projectFoldedMemory`), вместо СОХРАНЁННОГО глоссария | `TestABankEditIsVisibleToTheEstimateBeforeAnythingIsBought` — прогон состоялся → `bank-apply`**старая проекция 0, новая ≠0** на ОДНОМ состоянии |
| §3.2 — $0 абсолютный | Ни одного провайдерского вызова | Тот же тест: `rec.count()` не изменился; леджер (`committed`/`reserved`) не двинулся |
| §3.2 — read-only сверх first-touch | Ни одной записи | Тот же тест: **sha256 файла `.db` до и после совпадает**; `status` идёт через `NewReadOnlyRunner``OpenReadOnly` с `PRAGMA query_only=1` |
| §3.2 — ключей не требует | Путь не строит ни одного LLM-клиента (`models.CheckKeys` только `if forWrite`) | Пин `cmd/tmctl/keysfile_test.go` не тронут и зелёный |
| §3.2 — **тождество** сметы и прогона | `storeOrder` воспроизводит `ORDER BY` стора; фолд один на оба пути | `TestTheInMemoryFoldIsIdenticalToTheStoreRoundTrip`оба пути на одном состоянии: строки, voices, pairs, `Version()`, `BaseVersion()` и **карта отрендеренных content-hash** совпадают. Мутация «убрать сортировку» — падает с топичным «row 0 differs» |
| §3.2 — мина `omitempty` | `rebill_units`/`rebill_usd` больше не `omitempty`; добавлено `rebill_basis` (`pending`/`stored`/`none`/`failed`) | `TestTheWireTellsFreeFromUnknown` — «нечего пере-оплачивать» и «посчитано, и это ноль» на проводе различимы |
| §3.2 — книга остаётся инспектируемой | Отказ фолда не валит чтение: фолбэк на сохранённый глоссарий + честный `basis` + WARN | `TestTheEstimateSurvivesABookWhoseFoldRefuses` — сид-коллизия: `status` отвечает, `basis=stored` |
| §3.1 — потолок не отказывает законной работе И не даёт обойти порог дроблением | ⚠ **Строка ПЕРЕПИСАНА после отзыва первой редакции — см. второй круг, находка 1.** Порог судится по ВСЕЙ книге (дробить бесполезно); именованный кап `--accept-rebill=X` судится по тому, что заплатит ЭТОТ прогон (законная работа не отклоняется); прогон, который не пере-оплачивает ничего, гейта не встречает вовсе | `TestABoundedRunCannotOutrunTheConsentThreshold` (обе стороны на одном состоянии) + `TestARunThatRePaysNothingIsNotAskedForConsent` (третья ветка). Мутации: порог по прогону · кап по книге · снятие ранней ветки — все три падают адресно |
| §3.1 — потолок не даёт перерасхода на неполном юните | Терминальность = ПОКРЫТИЕ всех ожидаемых позиций (`unitFullyRecorded`), а не вердикт `resolveChunkState` | `TestAFlaggedMemberWithNoEditRowIsNotFree` — флагнутый член + ноль edit-строк; без фикса юнит признаётся свободным и оплачивается сверх потолка |
| §3.1 — неопределённость всегда в сторону «платно» | `!ok \|\| h != ContentHash` ⇒ невоспроизводимый хеш = платно | `TestAnUnreproducibleHashIsNeverFree` (6 состояний, включая `skipped` и снятую стадию) |
| §3.1 — допускается ВЕСЬ юнит, а не его лидер | Допуск помечает каждого члена | `TestTheCeilingAdmitsEveryMemberOfAUnit` — мелкая нарезка ⇒ 3 чанка в главе; мутация «только лидер» даёт усечённую главу |
| §3.1 — форма конвейера, где «юнит» двусмыслен | `planVolume` ОТКАЗЫВАЕТ (переводчик последней стадией при живых редакторских) | `TestTheCeilingRefusesAPipelineWhoseUnitsAreTwoThings`; без флага — поведение не меняется |
| §3.1 — черновой-только конвейер | Та же единица, другая форма | `TestTheVolumeCeilingHoldsOnADraftOnlyPipeline` — 1 вызов на юнит, 2 гранта = 2 вызова |
| §3.1 — стоп назван в выводе CLI | Строка с тремя действенными числами + «это ЗАВЕРШЕНИЕ, не пауза» | `TestRenderTranslateNamesTheVolumeCeiling`; необрезанный прогон о потолке молчит |
| §3.2 — «неизвестно» никогда не несёт числа | Решение вынесено в чистую `rebillOutcome`/`projectable` | `TestAFailedBasisNeverCarriesAFigure` — таблица на 5 состояний |
| §3.2 — падающий фолд не уносит свою диагностику | Замечания в лог ДО обработки ошибки | `TestAFailedFoldStillReportsWhatItSkipped` |
| §3.1 — отчёт различает ДОСТАВКУ и ПЕРЕ-ДЕЛКУ (добавлено 2-м кругом) | `VolumeStop` раздвоен на `Delivered`/`Reworked` и `LeftFresh`/`LeftRework`; грант идёт сперва в новое; приглашение купить — только про никогда-не-доставленные | `TestAFullyDeliveredBookOffersNothingLeftToBuy` (0 доставлено / 1 переделан / 0 к покупке) · `TestAPurchaseBuysNewBookBeforeReMakingOldBook` (грант уходит в новое, а не в переделку) · `TestRenderTranslateNeverSellsWhatIsAlreadyOwned`. Мутация «схлопнуть два прохода в один» падает |
| §3.1 — движок говорит ЮНИТАМИ, не главами (добавлено 2-м кругом) | Все строки отчёта и CLI переведены на «units»; главы — слово платформы, конвертирует манифест | `TestRenderTranslateNamesTheVolumeCeiling` + `TestAFullyDeliveredBookOffersNothingLeftToBuy` пинят формулировку |
| §3.2 — смета дана и в той единице, которой сайзится покупка (3-й круг) | Аддитивное `rebill_output_units` рядом с `rebill_units` (chunk×stage); имя `rebill_units` не тронуто | `TestTheEstimateIsAlsoGivenInTheUnitAPurchaseIsSizedIn`; фикстура специально недегенеративна; мутация «выдать Rows за юниты» падает |
| §3.2 — один документ = один банк (2-й круг) | `unsigned_bank_terms` считается по ТОМУ ЖЕ банку, что и денежные поля | Материализация помнит свои строки (`r.bankRows`); проверено батареей |
| §5 ось 3 — детерминизм | `MaxUnits` не входит ни в снапшот, ни в `BriefHash`; байты не двигаются | `TestTheVolumeCeilingMovesNoRequestByte` — снапшоты обеих волн не сдвинулись, каждое тело обрезанного прогона есть среди тел безпотолочного; мутация «подмешать MaxUnits в снапшот» падает |
| §3.3 — чего в паке нет | Константу $0.03, цену от объёма исходника, строки 228/230/141/131, `platform/` — не трогал | — |
#### Адверсариальный проход по СВОЕЙ готовой работе — что он нашёл
Веер: шесть линз (деньги · тождество фолда · шов · регрессии пака · параллелизм/детерминизм · качество моих же тестов), тяжёлая модель — РОВНО ОДИН агент (денежная линза), остальным попроще; затем по одному независимому опровергателю на каждую находку. Сырых находок 10, после опровержения осталось 7 настоящих. **Все семь внёс сам этот пак — приёмке они не видны, поэтому называю первыми.**
**1. ⛔ ПЕРЕРАСХОД: юнит с флагнутым членом и без edit-строки считался БЕСПЛАТНЫМ.** `unitsServedFree` спрашивал терминальность у `resolveChunkState`, а тот отдаёт `ChunkFlagged`, как только флагнута ЛЮБАЯ строка, — ДО проверки, что все ожидаемые позиции на месте. Юнит, у которого черновик одного члена флагнут, а edit-строки ещё нет (состояние после подписного стопа банка, Ctrl-C или любого обрыва в редакторской волне), признавался терминальным и свободным, проходил БЕЗ списания гранта и потом оплачивался редактором. Прогон платил за юнитов больше, чем куплено, и, поскольку остаток оставался нулём, **даже не сообщал об объёмном стопе.**
**Лечение:** предикат заменён на `unitFullyRecorded` — покрытие ВСЕХ ожидаемых позиций строками; полнота и есть терминальность, и именно она предсказывает «вызова не будет». **Доказано:** `TestAFlaggedMemberWithNoEditRowIsNotFree` (обрыв редактора на прогоне 1 → флагнутый член + ноль edit-строк; прогон 2 с потолком 1). Возврат к прежнему предикату мутацией — тест падает с «the other unit was judged free and paid for anyway».
**2. ⛔ ОТКАЗ ЗАКОННОЙ РАБОТЕ: гейт согласия судил ВСЮ книгу при обрезанном прогоне.** Нашёл сам, до линз. `checkRebillConsent` стоял ПЕРЕД `planVolume` и проецировал пере-оплату по всем строкам, тогда как прогон с `--max-units 2` пере-оплатит максимум два юнита. Р6 требует согласия на КОНКРЕТНУЮ трату — а тут отказ (или запрос согласия) шёл на деньги, которые никто не потратит. Это ровно то, о чём предупреждает сам `rebill.go`: «число, которое одобрили и не списали, — это то, из-за чего такие числа перестают читать».
**Лечение:** скоуп считается ДО гейта; СУММА сужена до допущенных юнитов, ПОРОГ оставлен книжным (5% книги — свойство книги, не покупки). **Инертно без флага** (`scope==nil` ⇒ фильтр тождественный ⇒ число байт-в-байт прежнее) — это и делает правку ратифицированного гейта безопасной. **Доказано на первом круге** мутацией «фильтр в тождество». ⚠ **Это решение ОТОЗВАНО на втором круге** (находка 1 ниже) — читать вместе с ним, а не отдельно.
**3. Отказ `redrive` учил ЛОЖНОМУ факту об движке.** Текст говорил «`redrive` не гоняет книжную волну, которую это могло бы ограничить» — неправда: `redrive` зовёт `TranslateBook` (`status.go:984`). Мой же комментарий четырьмя строками выше говорил обратное и верное. **Лечение:** у `redrive` теперь свой текст, называющий НАСТОЯЩУЮ причину (деструктивный сброс ограничен своим селектором, и потолок в юнитах ограничил бы вторую половину команды, читаясь как ограничение целого). Тест дотянут: он проверяет не только имя флага, но и что причина не врёт про адресата.
**4. Рефактор `seedGlossary` терял диагностику падающего прогона.** Замечания (пропуск ruby-алиаса по омонимии, дроп строк авто-банка) собирались в `gatherBankInputs`, но логировались ПОСЛЕ проверки ошибки — то есть на прогоне, который умер на более поздней проверке, они пропадали. До рефактора они писались по месту и доезжали. **Лечение:** замечания уходят в лог ДО обработки ошибки; `gatherBankInputs` для этого возвращает частичные входы вместе с ошибкой. **Доказано:** `TestAFailedFoldStillReportsWhatItSkipped`; перестановка двух блоков мутацией — тест падает.
**5. `rebill_basis: failed` мог ехать рядом с большим ПОСЧИТАННЫМ числом.** Если банк не материализовался вовсе, `projectRebill` всё равно запускался — против ПУСТОГО банка, поэтому все строки выглядели протухшими и на провод уезжала пере-оплата всей книги, а CLI печатал рядом «цифры ниже нули, потому что их не удалось посчитать». Базис, противоречащий собственным числам, хуже отсутствия базиса. **Лечение:** решение вынесено в чистую `rebillOutcome` (+ `projectable`) — проекция не запускается без банка, «неизвестно» никогда не несёт числа. Табличный тест на все пять состояний; **инвариант недостижим из фикстуры** (нужен одновременный отказ и фолда, и стора), поэтому он и вынесен в функцию — иначе его нечем было бы держать.
**6. Потолок отказывается от формы конвейера, в которой «юнит» — два разных объекта.** Если ПОСЛЕДНЯЯ стадия — переводчик, а редакторские стадии есть (конфиг такой порядок не запрещает), `outputUnits` отдаёт синглтон на ЧАНК, а редакторская волна всё ещё группирует их в юниты: потолок, выданный в синглтонах, допустил бы чанк одного члена, и редактор пошёл бы по юниту с недоделанным черновиком за полную цену. **Лечение:** `planVolume` на такой форме ОТКАЗЫВАЕТ громко (без флага — ничего не меняется). Тест + мутация.
**7. Мои собственные тесты доказывали меньше, чем обещали — три дыры, все закрыты.**
(а) **Мульти-членный юнит не проверялся ни одной фикстурой** (все главы были однопредложенные ⇒ у каждого юнита ровно один член), поэтому цикл по `u.Members` в допуске был непокрыт: мутация «помечать только лидера» проходила ВСЮ батарею. Закрыто `TestTheCeilingAdmitsEveryMemberOfAUnit` (мелкая нарезка черновика под крупным редакторским потолком ⇒ 3 чанка в главе); мутация теперь падает с «buyer gets a truncated chapter they paid for in full».
(б) **Ветка «хеш не воспроизведён ВООБЩЕ» была непокрыта**а это направляющий инвариант всего потолка («любая неопределённость — в сторону „платно“»). Мутация `!ok || …``ok && …` проходила батарею. Закрыто `TestAnUnreproducibleHashIsNeverFree`.
(в) **Тест тождества фолда сравнивал ТОЛЬКО черновую волну.** Путь A не гонял `TranslateBook`, поэтому строк `chunk_status` не было, и `renderedContentHashes` не давал ни одной edit-позиции — а edit-волна ровно та, куда попадают `mined`-строки фикстуры (базовый банк их исключает). То есть половина, ради которой фикстура и заводилась, не проверялась. Закрыто: перед сравнением идёт настоящий прогон, и **покрытие обеих волн теперь АССЕРТИТСЯ**, а не предполагается (`len(hashes)!=0` от этого не защищал: две черновые карты тоже равны).
**ОТДЕЛЬНОЙ СТРОКОЙ, потому что это правка РАТИФИЦИРОВАННОГО контура и она обязана быть видна в акте, а не выводиться из диффа: я тронула гейт согласия D20.2-Q2.**
**ЧИТАТЬ ВМЕСТЕ С НАХОДКОЙ 1 ВТОРОГО КРУГА — здесь описана ПЕРВАЯ, ОТОЗВАННАЯ редакция.** Она сужала СУММУ до допущенных юнитов при книжном ПОРОГЕ и защищалась доводом «инертно без флага». Довод верный и неважный: с флагом размер прогона выбирает каллер, и дробление обходило порог. **Действующая редакция:** порог — по всей книге (дробить бесполезно), именованный кап — по тому, что заплатит этот прогон (законная работа не отклоняется), прогон без пере-оплаты гейта не встречает. Инертность без флага сохранена и в ней: при `scope == nil` обе проекции — один объект, и число байт-в-байт прежнее.
**Опровергнуто и НЕ чинилось (3 из 10):** три находки опровергатель снял как уже вылеченные тем же проходом (я чинил по мере поступления, и опровергатели читали уже исправленный код) — они и есть пункты 3, 4 и 7(а/б) выше.
#### Второй круг приёмки — что нашли и что я с этим сделала
Оркестратор прогнал по паку ещё два аппарата: слепой верификатор (читал ТОЛЬКО заказ и дифф, отчёта не видел) и искателя вне карты отчёта. Плюс я прогнала свой второй код-ревью — шесть линз, тяжёлая модель один агент, по опровергателю на находку. Итог круга: **двенадцать настоящих находок, все внесены паком, все вылечены** — плюс одна чужая, проверенная и переданная строкой.
**1. ⛔ ОТЗЫВ МОЕГО ЖЕ РЕШЕНИЯ: сужение суммы гейта D20.2-Q2 открывало обход порога дроблением.** Первый круг я закрыла тем, что сумму пере-оплаты считаю по допущенным юнитам, а порог оставляю книжным, и защитила это доводом «правка инертна без флага». Довод верный и НЕважный: с флагом каллер САМ выбирает, насколько мал каждый прогон, поэтому `--resnapshot --max-units N` в цикле пере-оплачивает книгу целиком, ни разу не перейдя порог и ни разу не спросив согласия. Измерено искателем на реальных числах: 30 глав за $0.109200, порог $0.005460, безпотолочный прогон ОТКАЗЫВАЕТ, а `--max-units 1` проходит четыре раза подряд по $0.003640. Порог обхода арифметический: покупка ≤5% книги — гейт молчит; на 500-главной вебновелле «10 глав» это 2%.
**Лечение — разделены два РАЗНЫХ вопроса, а не один компромисс.** «Надо ли вообще спрашивать человека» — политика КНИГИ, судится по всей её пере-оплате, которая от размера покупки не сжимается. «`--accept-rebill=X`» — инструкция каллера о ТРАТЕ, судится по тому, что этот прогон реально заплатит (платформа фондирует этот кап из холда прогона, то есть он размерен покупкой, а не книгой). Отказ печатает ОБА числа: собрать их в одно — и есть способ соврать в любую сторону.
**Доказано:** `TestABoundedRunCannotOutrunTheConsentThreshold`обе стороны на одном состоянии: потолок 1 без согласия ОТКАЗЫВАЕТ (обход закрыт), а именованный кап $0.002, покрывающий собственный счёт прогона ~$0.00182, ЧЕСТЁН (законная работа не отклонена).
**2. ⛔ `VolumeStop` не различал ДОСТАВКУ и ПЕРЕ-ДЕЛКУ — и это была нехватка модели, а не сбой счётчика.** Один счётчик на оба вида оплаченного юнита давал ложь, на которую оператор действует: книга переведена на 100%, банк подвинут, покупка №1 печатает «29 остались», №2 — «28», ни одной новой главы не создано, exit 0 и `ready` подтверждают успех. А CLI подавал остаток как «что осталось КУПИТЬ».
**Лечение — переделан тип, а не подлатан счёт.** Юнит классифицируется тремя состояниями (`unitFresh` / `unitFree` / `unitRework`), и обе половины отчёта раздвоены: `Delivered`/`Reworked` и `LeftFresh`/`LeftRework`. Приглашение купить произносится ТОЛЬКО про никогда-не-доставленные юниты; покупка, целиком ушедшая в переделку, обязана это сказать. Раздвоение бесплатно: оно падает из того же теста полноты позиций, которым считается «бесплатно».
**Доказано:** `TestAFullyDeliveredBookOffersNothingLeftToBuy` (0 доставлено, 1 переделан, 0 никогда-не-доставленных, остаток назван пере-проходом) + `TestRenderTranslateNeverSellsWhatIsAlreadyOwned` (строка CLI не предлагает купить то, чем уже владеют).
**3. ⛔ `status`/`export` начали читать авто-банк, который писался НЕ атомарно.** Дыра, которую пак открыл в чужом файле: до него авто-банк читал только тот прогон, который его и писал, поэтому `os.WriteFile` с усечением был безопасен. Как только читающие поверхности стали сворачивать банк сами, файл попал в класс, ради которого существует `artifact.go`, — и окно усечения даёт либо ошибку разбора (покупателю устаревшие цифры), либо валидный ПРЕФИКС списка термов: фолд банка, которого не существовало, и `rebill_units`, который ни один прогон не выставит.
**Лечение:** `writeFileAtomic`, как у трёх соседей; авто-банк вписан в перечень сайдкаров в шапке `artifact.go`. **Доказано:** `TestTheAutoBankIsWrittenAtomically` — инструмент inode (переименование меняет его ВСЕГДА, запись на месте — никогда; сильнее сверки байтов).
**4. Пин согласия `status`↔`translate` покрывал только беспотолочный случай.** Механическая правка сигнатуры протащила туда `nil`, и расхождение под потолком пин бы не поймал. Дописан второй ассерт: под объёмным потолком отказ гейта обязан по-прежнему цитировать КНИЖНОЕ число — то самое, которое печатает `status`, — потому что докстринг `status.go` обещает это дословно.
**5. Отказ денежного потолка съедал объёмный отчёт.** Когда прогон встаёт по деньгам, результата нет, и оператор не узнаёт, что ему было выдано и сколько успело доехать. Добавлена строка WARN на этом пути: стоп ниже — НЕ объёмный, вот что было выдано и что осталось недоставленным.
**6. Инструмент «ничего не записано» был слеп к WAL — и, дотянутый, сразу поймал факт.** Дайджест мерил только основной файл, а SQLite в WAL кладёт коммит в `-wal`. Дотянула — и увидела изменение. **Разобрала, а не подогнала:** данные не двигались (основной файл байт-в-байт), а появились ПУСТОЙ `-wal` и `-shm`: так устроен любой WAL-читатель. Поэтому ассерт теперь точный: основной файл неизменен И журнал не несёт ни одного кадра; `-shm` не проверяется намеренно, требовать его отсутствия значило бы требовать от SQLite невозможного. Гарантию держит `query_only`, дайджест — подтверждение.
**Косметика того же круга:** висячие ссылки на `bankfold.go`/`bankfold_identity_test.go` исправлены на реальные `bankmaterialize*` · `backend/README.md` (позиция №2 онбординга зоны) теперь знает про ВТОРОЙ потолок, его ортогональность денежному, «стоп = завершение» и предупреждение про майнящие книги · в `volume.go` явно записано, что нулевое значение `unitClass` обязано быть `unitFresh` (пустая карта = книга, которую не гоняли).
**7. ⛔ ПОТОЛОК ТРАТИЛ ПОКУПКУ НА ПЕРЕ-ДЕЛКУ ВПЕРЁД ДОСТАВКИ.** Допуск шёл одним проходом в порядке книги, а после правки банка уже доставленные юниты — это как раз РАННИЕ. Воспроизведено ревью: покупка 2 юнитов на пятиглавой книге с двумя доставленными пере-делала главы 12 и не начинала главу 3 — `Delivered=0, Reworked=2, LeftFresh=3`. Деньги списаны, книга не сдвинулась ни на главу, а три никогда не переводившиеся главы остались нетронутыми.
**Лечение:** грант идёт СНАЧАЛА в новое, и только потом в переделку. Безопасно потому, что байты юнита не зависят от того, какие ДРУГИЕ юниты шли: липкая выборка считается по ВСЕЙ книге до обеих волн (`precomputeSticky`), поэтому юнит 3 раньше юнита 1 рендерится ровно так же. **Размен назван, а не спрятан:** покупка, ЦЕЛЬЮ которой был пере-проход, теперь потратит грант на недоставленные главы, если они есть. Это правильное умолчание — непрочитанная глава ценнее пере-сделанной, а пере-оплату и так ограничивает гейт согласия, — но это УМОЛЧАНИЕ, и я выношу его оркестратору как выбор, а не как факт.
**8. ⛔ МОЯ ЖЕ СТРОКА СТОПА СООБЩАЛА ЮНИТЫ КАК ГЛАВЫ.** `«%d NEW chapter(s) delivered»` печаталось поверх счётчика, который инкрементится на `editUnit`, — а глава это больше одного юнита всякий раз, когда нарезка закрывает юнит по редакторскому потолку, и много юнитов в черновом-только конвейере. **То есть подмену «юнит↔глава», ради устранения которой пак и заведён, совершал отчёт самого пака** — и мой же тест пинил эту формулировку как инвариант. Воспроизведено: одна глава, три юнита, потолок 2 → «2 NEW chapter(s) delivered» на книге из ОДНОЙ незавершённой главы.
**Лечение:** движок говорит только «units», везде. Главы — слово ПЛАТФОРМЫ, и конвертирует их она, манифестом. Тесты пере-пинены на верную формулировку.
**9. Один документ `status` описывал ДВА банка.** Денежные поля (`rebill_*`) считались от свёртки файлов решений, а `unsigned_bank_terms` — по-прежнему от СОХРАНЁННОГО глоссария. Оператор, спрашивающий «есть ли что подписать, прежде чем платить дальше», получал ноль, тогда как ближайший `translate` уже собирался инжектить неподписанную строку из авто-банка и оплатить изменённый ею юнит. **Лечение:** материализация запоминает строки, из которых собрана, и счёт неподписанных идёт по ТОМУ ЖЕ банку. Один документ — один банк.
**10. Цифры в отказе гейта были СМЕШАННОГО происхождения.** В книжную фразу вклеивался скоупный `Repinned`, то есть в предложении, где все остальные числа книжные, стояло меньшее число из другой проекции. Две линзы поймали независимо — что смешанные провенансы в одном предложении и производят. **Лечение:** каждое число берётся из той же проекции, что и фраза; скоупная оговорка несёт скоупный ре-пин.
**11. `export` ОТКАЧЕН — и это отказ от собственной правки, а не находка чужого.** Я перевела его `ConfigDrift` на свёртку «за компанию» со `status`. Но это поле читает полигонная экстракция, и её вопрос ДРУГОЙ: «согласован ли ЭТОТ документ с прогоном, который его произвёл». Экспорт, байты которого — ровно то, что отгрузил прогон, измерим и при лежащем рядом неприменённом файле решений; свёртка помечала бы такой экспорт дрейфующим и тихо дисквалифицировала бы здоровые замеры в чужой зоне. **Пере-определять чужое потребляемое поле как побочный эффект своего заказа — не моё право.** Расхождение теперь ЯВНОЕ и запинено `TestExportAndStatusAnswerDifferentDriftQuestions`, чтобы следующий не «починил» его обратно.
**12. Дыры в моих же тестах, найденные второй линзой качества:**
(а) **ни один тест не звал CLI-`translate` с `--max-units`** — то есть единственная строка, которая проводит флаг в движок (`r.MaxUnits = maxUnits`), была не проверена вовсе, и её удаление оставило бы всю батарею зелёной, пока `tmctl translate --max-units 2` переводит книгу целиком. Закрыто `TestTranslateWiresMaxUnits` по образцу соседнего `TestTranslateWiresAcceptRebill`; мутация «убрать проводку» падает с точным сообщением.
(б) **ветка «прогон не пере-оплачивает ничего» не была покрыта** — её удаление проходило всю батарею, а она ровно то, что не даёт гейту отказать законной работе. Закрыто `TestARunThatRePaysNothingIsNotAskedForConsent` (состояние строится через `ResetChunkStages`, как делает redrive).
(в) **`export` после правки банка не был покрыт ничем** — теперь покрыт, вместе с решением о расхождении.
**13. И находка НЕ из моей зоны, проверенная как положено.** `internal/store` дважды уронил мою батарею (`TestTheSeamIsNotChargedToTheStoreOperationBudget`, `apply migration 14: context deadline exceeded`). Я этот каталог не трогала ни строкой. Развернула ЧИСТЫЙ HEAD (`git archive`) в отдельный каталог и погоняла тот же тест в обоих деревьях вперемежку по пять раз: **моё падает 4/5, HEAD падает 4/5** — от моих правок не зависит. Причина в самом тесте: он ужимает `opTimeout` до 150 мс (`migrate_test.go:408-429`), и под параллельной нагрузкой одиночный ALTER в него не укладывается. Оркестратору отправлено строкой; сама не чиню — чужая зона.
**И правило, которое из этого следует, шире случая: ЗЕЛЁНАЯ БАТАРЕЯ — ЭТО ПОЛНЫЙ СПИСОК ПАКЕТОВ ПЛЮС ОТСУТСТВИЕ `FAIL`, а не отсутствие `FAIL`.** Мой первый «зелёный» прогон был на 20 пакетов из 21, с `FAIL` в хвосте, который обёртка подала как `exit code 0`. Сверяю против `go list ./...` теперь всегда.
**И одна поправка к моему же первому отчёту.** В записке-плане я написала, что предикат «бесплатно» — «тот же, которым считает `projectRebill`, чтобы они не разошлись двумя определениями». Это НЕВЕРНО и я поправляю: `projectRebill` намеренно игнорирует ось СОДЕРЖИМОГО (его собственный докблок это и говорит — правка исходника двигает `content_hash`, но не снапшот, и там она невидима), а предикат потолка сверяет отрендеренный хеш на КАЖДОЙ строке. То есть они расходятся, и расходятся в безопасную сторону: потолок считает платным то, что смета считает бесплатным. Предикат написан против ИСПОЛНИТЕЛЯ (`runStage`), а не заимствован у проекции, и в `volume.go` это теперь записано явно.
#### Третий круг — ратификация ОПУСАМИ трёх развилок, где я сама не была уверена
Владелец снял потолок на веер и назвал главное: **ратификаторы — опусы, особенно на важном.** Поэтому три развилки, которые я не имела права закрыть своим словом, ушли трём опус-судьям с мандатом «настаивай на своём, а не соглашайся». Плюс второй код-ревью в шесть линз с опровергателями (15 сырых находок, 4 пережили). Модель каждому агенту задавалась ЯВНО.
**Развилка 1 — «капнутое согласие: дыра или согласие?» → ВЕРДИКТ: ЗДОРОВО, менять нечего.** Ревьюер показал цикл: `--resnapshot --max-units 1 --accept-rebill=0.002` четыре раза пере-оплачивает книгу целиком, ни разу не перейдя порог. Ратификатор воспроизвёл цикл СВОИМ прогоном и разобрал по существу: (а) каждый прогон явно называет сумму и списывает ровно её (замерено: заявлено $0.001820 — списано $0.001820); (б) каждый прогон ПИШЕТ рядом книжную цифру и порог, то есть каллеру прямо сказано, что книга выкупается по кусочку; (в) суммарно цикл не достигает ничего, чего одиночный голый `--accept-rebill` не достигает за один вызов; (г) кумулятивный денежный потолок дроблением не обходится. **Это Р6 в чистом виде — согласие на КОНКРЕТНУЮ трату, данное явно и многократно.**
**Его же рекомендация принята:** свойство, которое делает это согласием, а не дырой, — раскрытие — раньше не было запинено ничем. Теперь есть `TestAConsentedSliceIsAlwaysToldWhatTheBookCarries`; мутация «убрать книжные цифры из строки согласия» падает.
**Развилка 2 — «денominация двух половин пака» → ВЕРДИКТ: ДЕФЕКТ, ЧИНИТЬ.** И он измерил, а не вывел: на трёхглавой фикстуре один документ `status --json` нёс `rebill_units: 15` двумя строками ниже `total_units: 6`. Все остальные «units» в документе — ВЫХОДНЫЕ юниты (`total_units`, прогресс, `chapters[].units_total`), а `rebill_units` единственный считает `chunk×stage`, и коллизия сидит в самом слове. **Поделить нельзя:** отношение `len(Members)·nDraftStages + nEditStages` меняется от юнита к юниту ВНУТРИ книги, а его множители на провод не ходят вовсе. Траектория: платформа берёт смету, которую пак сам просит вернуть в аллоулист, и передаёт это число в `--max-units` — покупает втрое больше книги, чем собиралась. **Дефект пака, воспроизведённый слоем ниже, ровно тот, ради устранения которого пак заведён.**
**Лечение — его форма, аддитивная:** `rebill_units` НЕ переименован (устоявшееся имя, смысл не менялся), рядом заведено `rebill_output_units` — та же пере-оплата в той единице, которой сайзится покупка. Человеческий рендер печатает обе с подписями. `RebillProjection.OutputUnits` считается по лидерам затронутых юнитов там же, где инкрементится `Rows`. **Доказано:** `TestTheEstimateIsAlsoGivenInTheUnitAPurchaseIsSizedIn` — фикстура специально сделана недегенеративной (правка КОНФИГА, а не банка: банковая правка двигает только редакторскую волну и обе денominации совпали бы, и тест ничего бы не доказал); мутация «выдать `Rows` за выходные юниты» падает.
**Развилка 3 — «безопасно ли ставить доставку впереди переделки» → ВЕРДИКТ: ЗДОРОВО, и обосновано замером, а не рассуждением.** Ратификатор проверил три вещи исполнением. (а) **Порядок безопасен:** `precomputeSticky` — чистая функция от (чанки, банк, бюджет), считается ДО допуска и потребляется позиционно, поэтому инъекция чанка i не зависит от того, шёл ли чанк i1; редакторская волна берёт только СВОИ члены и делает свежий Select с nil-sticky. Он прогнал книгу, где глава 4 переведена и отредактирована в прогоне, где глава 1 не запускалась вовсе, — **тела запросов дословно совпали с телами прямого сплошного прогона.** Проверил и остальные кандидаты на кросс-юнитную связь: стоп майнинга строит кандидатов по ВСЕМ чанкам (от скоупа не зависит), свёртка банкнот — множественная (порядок не важен), счётчики событий засеиваются книжно и инкрементятся под once-key. (б) **A/B на одной книге, четыре покупки по 2 юнита:** доставка-вперёд отдаёт 6/6, порядок книги — 4/6, причём при порядке книги две покупки подряд печатали «Still in the book: 4 unit(s) NEVER delivered» не сдвинувшись. (в) **Дверь пере-прохода не сломана:** на полностью доставленной книге доставка-вперёд ЕСТЬ порядок книги (свежих юнитов нет), то есть чистый пере-проход работает как прежде.
**Его требование выполнено — и оно было существенным:** это не живой дефект, а ТРОПА ГНИЕНИЯ. Схлопывание двух проходов обратно в один оставляло батарею ЗЕЛЁНОЙ, то есть следующая сессия, «прибирающая» цикл или добавляющая четвёртый класс юнита, тихо вернула бы дефект. Закрыто `TestAPurchaseBuysNewBookBeforeReMakingOldBook`; мутация «схлопнуть в один проход по порядку книги» падает — и падает ровно на вторичном следствии, которое ратификатор и предсказал: при порядке книги та же покупка не просто перераспределяется, а ПОЛУЧАЕТ ОТКАЗ гейта согласия.
**Дополнительно из второго ревью (пережили опровержение и вылечены):**
**тройное повторение самого дорогого расчёта.** Обрезанный прогон пере-рендеривал всю книгу до трёх раз: в `planVolume` и в каждой из двух проекций гейта, при том что между ними ничего не менялось. Заведён мемо, **срок жизни которого — жизнь БАНКА**: `materializeBanks` его сбрасывает, потому что переживший свой банк мемо отдал бы хеши банка, которого у прогона больше нет, — ровно тот класс тихой неверности, ради устранения которого пак и существует. Мемо — скорость, а его инвалидация — корректность, поэтому запинена именно она (`TestTheWireHashMemoDiesWithItsBank`).
**предупреждение про майнящие книги не было покрыто ничем**а незакрытое тестом предупреждение молча перестаёт эмититься. Закрыто, вместе с обратной стороной: на книге БЕЗ потолка оно не должно возникать.
#### Четвёртый круг — денежный охотник приёмки: 6 находок, 3 вылечены, 3 переданы
**1. ⛔ БЛОКИРУЮЩАЯ, и она пробивала саму цель пака — ВЫЛЕЧЕНА.** `planVolume` классифицирует ДО черновой волны, но между волнами стоит стоп майнинга, который **пере-сеивает банк прямо посреди прогона** (`mining.go`, ветка авто-продолжения), после чего `waverun` пере-считывает edit-снапшот. А free-юниты допускаются ВНЕ гранта — потому что бесплатная работа ничего не стоит. Значит каждый из них судился по снапшоту, который прогон сам же и заменил, и любой, чьи инжектируемые байты новый банк изменил, получал свежий ПЛАТНЫЙ вызов редактора, не разрешённый никаким грантом, — а строка отчёта называла его «rode along at $0». Охотник замерил: 4 вызова при гранте 1, $0.007280 объявлены бесплатными.
**Лечение — не гард, а ПЕРЕ-ПЛАН:** вопрос «бесплатен ли юнит» задаётся заново, против снапшота, который стал реальным, в последний момент перед редакторской волной — то есть «проверять перед единицей» по-прежнему держится. Ставший платным берёт слот, если он есть; если нет — его редактура НЕ идёт, и он честно отчитывается как доставленный-но-не-пере-сделанный.
**И сделано СТРУКТУРНО, а не вызовом:** волна ОТКАЗЫВАЕТСЯ работать со скоупом, спланированным против другого снапшота. Без этого удаление вызова пере-плана оставляло всю батарею зелёной, а потолок молча переставал держать — я это проверила мутацией. Теперь удаление вызова роняет прогон с адресным сообщением.
**Доказано:** `TestFreeUnitsAreReJudgedWhenTheBankMovesMidRun` (снапшот не двигался → ничего не меняется; двигался → ни одного «free», ровно один слот занят, остальные удержаны и отказаны `allowsUnit`) + `TestTheEditWaveRefusesAStalePlan`. Две мутации.
**Своей фикстурой конечное следствие я не воспроизвела — ПЯТЬ попыток, — и разбор этого провала оказался ценнее самих попыток.** Две причины, обе названы приёмкой и обе проверены мной на своём дереве.
**Причина 1: я искала не на той стороне дельты.** У mined-дельты две стороны. **WHICH** (список термов) майнится из исходника ВСЕЙ книги — он полон после первой покупки и больше не двигается; отсюда мой неверный вывод «однородный источник ничего не даст». А байты банка двигает **WHAT**`dst` каждого терма, и она **draft-side**: складывается из банкнот, которые модель уже выдала (`mining.go:109`), **то есть растёт с каждой покупкой ДАЖЕ при полностью однородном источнике**. Я меняла ТЕКСТ глав, а надо было менять то, что ПРЕДЛАГАЮТ ЧЕРНОВИКИ.
**Причина 2, из-за которой попытка (5) не могла сработать в принципе, — спойлер-гейт `since_ch`** (`membank/memory.go:640-647`, жёсткое `chapter < since_ch`). Вводя терм поздними главами, я выталкивала его `since_ch` за пределы уже доставленных — и он не попал бы в их инъекцию НИКОГДА, какой бы `dst` потом ни получил. ⚠ **Условия тянут в РАЗНЫЕ стороны:** естественный способ заставить дельту расти (поздние главы вводят новый терм) ровно этим и убивает достижимость. Дефекту нужна ОБРАТНАЯ асимметрия: терм присутствует в РАННЕМ исходнике, а его рендеринг приходит поздно — то есть асимметрия ПОВЕДЕНИЯ МОДЕЛИ, а не текста.
**Пере-проверила зондом приёмки у себя:** `方源 dst="" since_ch=3``dst=Фан Юань since_ch=3`, банк двинулся, а `content_hash` главы 1 `MOVED=false`, вызовов ровно два. То есть мой прогон был верен, а гипотеза о причине — нет.
**Диагностика на будущее, две строки:** дампить авто-банк между покупками и смотреть у терма ДВА поля — что `dst` перешёл из пустого в непустой **и** что `since_ch` ≤ номера уже доставленной главы. Второго я не проверяла.
**Сценарный тест теперь есть**`TestAMidRunBankMoveNeverBillsBeyondTheGrant` (рецепт приёмки: четыре главы, побайтово одинаковые, различаются только банкнотами). **Я пере-проверила его двумя своими посадками, а не приняла на слово:** снятие вызова пере-плана падает структурным отказом; снятие вызова И гарда воспроизводит дефект целиком — **4 вызова вместо 2, $0.007280 вместо $0.003640, обе уже купленные главы пере-отредактированы, и отчёт зовёт их «2 rode along at $0»**.
**Оба пина стоят рядом и не заменяют друг друга** (различение оркестратора, точнее моего): инвариантный утверждает «пере-план вызывается», сценарный — «покупатель не платит второй раз», а платит покупатель именно за второе.
**Жёсткое равенство `== 2×грант` оставлено намеренно:** гейт терминолога в фикстуре выключен явно, счёт детерминирован, и равенство ловит не только перерасход, но и молчаливую ПОТЕРЮ оплаченной работы.
**Условие достижимости УЗКОЕ, и это записано с обеих сторон:** нужна конъюнкция трёх условий (терм на WHICH-списке · его `dst` приходит поздно · `since_ch` покрывает уже доставленную главу). В фикстуре приёмки два терма из трёх её не выполняют и остаются инертными. Оценку «покупка №50 переоплачивает 490 юнитов» автор находки признал ПОТОЛКОМ ТЯЖЕСТИ, когда сработало, а не ожидаемым случаем; частоту на реальной книге никто не мерил. На диспозицию не влияет — лечение структурное и покрывает класс независимо от узости входа.
**2. ⛔ Флагнутый юнит объявлялся доставленным — ВЫЛЕЧЕНА.** Допуск решается ДО работы (этим и ограничиваются деньги), но решение ЗАПЛАТИТЬ за юнит — не факт существования главы: юнит может вернуться флагнутым и не отгрузить ничего. Отчёт печатал «2 NEW unit(s) delivered» поверх одного читаемого юнита. **Лечение: счётчики выравниваются ПОСЛЕ волн, по фактическим исходам** (`reconcile`), и заведён отдельный счётчик «оплачено, но флагнуто» с прямой формулировкой: деньги потрачены, текста нет, покупкой не чинится — это `redrive`. Тест + мутация.
**6. Подсказка обещала то, чего грант не сделает — ВЫЛЕЧЕНА.** `rebill_output_units` подавался как «та единица, в которой считает `--max-units`» — правда, и в одиночку вводит в заблуждение: грант идёт СПЕРВА в недоставленное, поэтому на книге, где оно есть, `--max-units N` доставит новое и не пере-сделает ничего. Оговорка была в коде, но не там, где читает оператор. Теперь в подсказке. Тест.
**3 и 4 — ПОДТВЕРЖДАЮ, НЕ вылечила, и говорю почему.** Обе об одном: ось «свежий/пере-делка» выведена из ПОЛНОТЫ СТРОК, а не из факта отгрузки. Добавление стадии делает доставленную книгу «никогда не доставлявшейся» (#3); юнит, прерванный между волнами, второй раз считается свежим и тратит слот повторно (#4). **Правильный носитель у движка уже есть — реестр анонсов (`events_outbox.once_key`, `unitOnceKey` = книга+волна+юнит), то есть собственное определение «этот юнит был отгружен читателю», монотонное на всю жизнь книги.** Но читающего метода у стора нет (есть только `EnqueueOnce`, который ПИШЕТ), а `internal/store` я не трогала ни строкой за весь пак и не считаю правильным открывать его в хвосте четвёртого круга. **Это отдельный заказ на одну функцию чтения + перевод оси на неё.**
**ПОПРАВКА к моей же формулировке, внесена оркестратором при приёмке и принята.** Я написала «деньги целы: ошибается слово, а не потолок». Слишком мягко — **слово тут ПРОДАЁТ.** Отчёт, называющий дочитанную книгу «3 unit(s) NEVER delivered», через строку CLI приглашает их купить, и покупатель платит за уже полученное. Деньги целы В ДВИЖКЕ — потолок держит, счёт вызовов верен, — но не в том, что увидит платформа и предъявит покупателю. Диспозиция не меняется (предмет отдельный, `internal/store` в хвосте пака не открываю), но цена ошибки — покупательская, а не редакторская, и в строке заказа стоит именно так.
**5 — цифру принимаю, договорённость в силе, но она теперь важнее.** Терминолог переигрывается каждой покупкой: три покупки по 1 юниту дали три полнокнижных консолидации по `usd=0.005460` каждая, при том что сам юнит дешевле. **Накладные масштабируются КНИГОЙ, а не грантом** — книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. Мы согласились, что место этой траты — в цене; при мелкой нарезке продажи она перестаёт быть накладной и становится основной статьёй, и цена обязана это знать.
#### Сверка по буллетам промта — что закрыто и чем
**§3.2, «родня 124(в)» — заказано сказать ПРЯМО, говорю:** да, моя форма закрывает и `resnapshot --dry-run`. `status` считает ре-билл при ЛЮБОМ дрейфе, не только банковом (`status.go`: проекция идёт при наличии строк, а не под `ConfigDrift`), поэтому после починки свёртки он И ЕСТЬ «оценить пере-проход, ничего не покупая» — ровно та способность, ради которой строка 124(в) поименована будущей дверью. **Отдельного глагола `resnapshot --dry-run` заводить не нужно.** Это удешевляет проект на один глагол и один документ.
**§3.2, «ключей не требует» — закрыто структурно, а не тестом, и вот почему это правильно:** `models.CheckKeys` вызывается ТОЛЬКО под `if forWrite` (`runner.go:260-266`), а `status`/`export` идут через `NewReadOnlyRunner`. Своего глагола пак не завёл, поэтому пин `keysfile_test.go`, перечисляющий читающие глаголы, покрывает мой путь без изменений — и он зелёный (6 тестов). Новый тест здесь ничего бы не доказал сверх пина.
**§5 ось 2 («пара, которой в репо нет») — проверено:** в `volume.go` и `bankmaterialize.go` ветвлений по паре/книге нет. Единственное упоминание языка — `lang.TargetChecksFor(r.Book.TargetLang)`, и это ДАННЫЕ: лукап по встроенным файлам `target-<lang>.txt` (`lang/embedded.go:106-118`), а не свитч. Строка перенесена из `seedGlossary` дословно, новой пар-логики пак не вносит. Пара, которой в репо нет, работает добавлением файла данных.
**§5 ось 3 (детерминизм) — была заявлена без доказательства, теперь доказана:** `TestTheVolumeCeilingMovesNoRequestByte` — снапшоты обеих волн не двигаются от установки потолка, и КАЖДОЕ тело запроса обрезанного прогона встречается среди тел безпотолочного (в том числе когда потолок отдаёт юниты ВНЕ порядка книги). Мутация «подмешать `MaxUnits` в снапшот» падает с точным сообщением. Это несущее: сдвинь потолок снапшот — и передача флага пере-покупала бы всю уже оплаченную книгу.
**§3.3, соседи — называю строкой, не чиню:**
**`redrive` зовёт `TranslateBook` по ВСЕЙ книге** (`status.go:926`): своя денежная неожиданность, была до пака. Флаг `--max-units` я ему сознательно не дал, отказ несёт настоящую причину.
**мёртвая переменная `manualSrcs`** в прежнем `seedGlossary` (писалась, не читалась никем) — исчезла при выносе гathera; ничего с ней не потерялось, проверено грепом по старой версии.
**джоб-гард не отличает банк-онли-движение от смены конфига** (`stagerun.go:47-59`): он и есть корень хвоста про майнящие книги. Ратифицированный Р6-контур, чужой предмет — не трогала.
#### Диспозиция ВСЕХ находок ревью — 25 сырых, ни одна не потеряна
Сведено из журналов двух ревью-воркфлоу (10 + 15 сырых находок) плюс трёх опус-ратификаций. Каждая строка — либо ВЫЛЕЧЕНА (с тестом и мутацией), либо ОТКЛОНЕНА с доводом, либо НАЗВАНА и НЕ вылечена с указанием, почему и чей это предмет. Пустых клеток нет намеренно: находка без диспозиции — это находка, которую следующая сессия найдёт заново.
| # | Находка | Диспозиция |
|---|---|---|
| 1 | Отказ `--max-units` учил ЛОЖНОМУ факту про `redrive` | ✅ свой текст с настоящей причиной + тест, проверяющий, что причина не врёт про адресата |
| 2 | Рефактор `seedGlossary` терял замечания падающего прогона | ✅ лог ДО обработки ошибки; тест + мутация |
| 3 | Мульти-членный юнит не покрыт ни одной фикстурой | ✅ мелкая нарезка (3 чанка в главе); мутация «допускать лидера» падает |
| 4 | Ветка «хеш не воспроизведён ВООБЩЕ» не покрыта | ✅ `TestAnUnreproducibleHashIsNeverFree`, 6 состояний |
| 5 | Тест тождества сравнивал только черновую волну | ✅ прогон ПЕРЕД сравнением + ассерт покрытия ОБЕИХ волн |
| 6 | ⛔ Перерасход: флагнутый член без edit-строки считался бесплатным | ✅ `unitFullyRecorded` вместо вердикта `resolveChunkState`; репро-тест + мутация |
| 7 | ⛔ Вторая покупка на МАЙНЯЩЕЙ книге падает `exit 1` | ⚠ **НЕ вылечено, названо.** Корень — джоб-гард (`stagerun.go:47-59`), ратифицированный Р6-контур, чужой предмет. Мною добавлен громкий WARN + тест на него; проводка гейчена оркестратором |
| 8 | Трата стопа майнинга/терминолога вне потолка и переигрывается | ⚠ **НЕ вылечено, названо.** Резать её объёмом семантически неверно (не единица доставки); место — в ЦЕНЕ, платформенная сторона |
| 9 | Форма конвейера, где «юнит» — два объекта | ✅ `planVolume` отказывает громко; тест + мутация |
| 10 | `rebill_basis: failed` рядом с посчитанной цифрой | ✅ решение вынесено в чистую `rebillOutcome`; табличный тест на 5 состояний + мутация |
| 11 | ⛔ Потолок тратил покупку на переделку вперёд доставки | ✅ доставка-вперёд; ратифицировано опусом (порядок безопасен, A/B 6/6 против 4/6); тест + мутация на ТРОПУ ГНИЕНИЯ |
| 12 | Объёмный стоп не доезжает машинным каналом | ⚠ **НЕ вылечено, и это РАТИФИЦИРОВАННАЯ отсрочка:** промт §3.1 сам поместил различение в отчёт/лог, а не в поток; словарь потока — зона оркестратора, он сказал «не сейчас, строкой». Названо вопросом B |
| 13 | Ветка `proj.Rows == 0` не покрыта | ✅ `TestARunThatRePaysNothingIsNotAskedForConsent`; мутация падает |
| 14 | `export` не покрыт после правки банка | ✅ решено ОТКАТОМ export + пином осознанного расхождения |
| 15 | Нет теста на проводку `--max-units` через CLI | ✅ `TestTranslateWiresMaxUnits`; мутация «убрать проводку» падает |
| 16 | Предупреждение про майнящие книги не покрыто | ✅ тест на обе стороны (есть потолок / нет потолка) + мутация |
| 17,20 | Смешанное происхождение цифр в отказе гейта | ✅ каждое число из своей проекции; скоупная оговорка несёт скоупный ре-пин |
| 18 | Тройное повторение самого дорогого расчёта | ✅ мемо со сроком жизни БАНКА; запинена ИНВАЛИДАЦИЯ (корректность), не скорость |
| 19 | Капнутое согласие — дыра? | ✅ **РАТИФИЦИРОВАНО ЗДОРОВЫМ** опусом (согласие, не обход); по его же рекомендации запинено РАСКРЫТИЕ, которое этим согласием и делает |
| 21 | ⛔ Строка стопа сообщала ЮНИТЫ как ГЛАВЫ | ✅ движок говорит только «units»; тесты пере-пинены на верную формулировку |
| 22 | Отчёт называл несуществующий тест и снятые поля | ✅ таблица и §9 исправлены; отозванный пункт помечен ОТОЗВАННЫМ, а не переписан молча |
| 23 | Денominация: `rebill_units` (chunk×stage) против `--max-units` (юниты) | ✅ **РАТИФИЦИРОВАНО ДЕФЕКТОМ** опусом (замерено 15 против 6); аддитивное `rebill_output_units`, `rebill_units` НЕ переименован; тест + мутация |
| 24 | Семантика `export.ConfigDrift` изменена «за компанию» | ✅ ОТКАЧЕНО: чужое потребляемое поле не пере-определяется побочным эффектом; расхождение запинено, чтобы не «починили» обратно |
| 25 | Один документ `status` описывал ДВА банка | ✅ материализация помнит свои строки; счёт неподписанных — по тому же банку |
**Итого: 22 вылечены с тестом и мутацией, 3 названы и не вылечены** (7, 8, 12) — у каждой указан предмет и чей он.
#### ⚠ Мой рефактор сломал ШЕСТЬ упоминаний в чужом доке — я насчитала два и ошиблась в адресе
**ИСПРАВЛЕНО ПО ПРИЁМКЕ 29.08: первая редакция этого раздела была неверна дважды, и обе ошибки — метода, а не арифметики.** Оставляю разбор, а не переписываю молча.
**Ошибка 1 — счёт. Я насчитала ДВА сломанных якоря; сломано было ШЕСТЬ упоминаний** (те же два плюс четыре про `AttachRubyAliasesToManual`). Причина: я взяла число из `counts.py --lint`, **который по собственному объявлению content-сверяет только якоря С ТОКЕНОМ** («без токена — несверяемы: 728»). Эту его слепоту я в этой же сессии сама и процитировала — и всё равно приняла его счёт за полный. Правильный инструмент был на расстоянии одного грепа: `grep -n "seeding\.go"` по доку.
**Ошибка 2 — адрес. Я разрешала якорь по НОМЕРУ СТРОКИ, а надо по СОДЕРЖИМОМУ.** В старом файле на `:157` действительно лежала материализация базового банка, и я записала переезд туда. Но док в строке 76 говорил не о ней, а о `InjectivityCollisions` — warning-only диагностике, которая **никуда из `seeding.go` не уезжала**, а просто съехала со `157` на `64`. На `bankmaterialize.go:326`, куда я её отправила, лежит `r.baseMemory = membank.MaterializeBank(baseIn, …)` — к этому якорю отношения не имеющее. Номер совпал, предмет — нет.
**Верная таблица (сверено содержимым, каждая строка проверена грепом в дереве):**
| Что говорил док | Было | Стало |
|---|---|---|
| `InjectVoice is FALSE…` — условный фолд voice/address | `seeding.go:130-133` | `bankmaterialize.go:291-294` (текст перенесён ДОСЛОВНО) |
| `InjectivityCollisions` — warning-only диагностика dst-коллизий | `seeding.go:157` | **`seeding.go:64`** — из файла НЕ уезжала |
| `AttachRubyAliasesToManual`×4 упоминания | `seeding.go:43-50`, `:47` | `bankmaterialize.go:88-94`, сам вызов на `:92` |
Правку в доке делал оркестратор — `docs/architecture/` не моя зона. Мой вклад здесь: сломала я, а нашла и починила приёмка, потому что я доверилась инструменту вместо грепа и номеру вместо содержимого. **Урок годен шире этого случая: якорь разрешается тем, ЧТО он утверждает, а не тем, на какой строке лежал.**
#### §8. Obstacle — что НЕ удалось и что НЕ проверено
**0. ГЛАВНОЕ, и это НЕ вылечено: объёмный потолок на книге, которая МАЙНИТ банк, ломает вторую покупку.** Найдено денежной линзой, **воспроизведено дважды независимо** (линзой и опровергателем, каждым на своей копии репозитория, с контролем).
Механика: обрезанная покупка черновит часть книги, а стоп майнинга получает НЕобрезанный список чанков (`waverun.go:154` передаёт весь `chunks`) и досыпает `dst` по мере появления новых банкнот. Значит авто-банк РАСТЁТ от покупки к покупке; следующая покупка сворачивает его в обогащённый банк, edit-снапшот съезжает, а edit-джобы ПРЕДЫДУЩЕЙ покупки к нему пришпилены. Джоб-гард (`stagerun.go:47-59`) банк-онли-движение не отличает от смены конфига и падает **нетипизированной** ошибкой ⇒ **exit 1, платформа пишет `failed`** — вместо задуманного «завершение, код 0». Контроль (без `--max-units`, книга целиком за один прогон, потом резюм) проходит чисто — то есть путь открывает именно обрезание.
**Поправка опровергателя, важная и снижающая цену:** `--resnapshot` пере-оплату НЕ вызывает в общем случае — после подъёма снапшота джоба работает обычный ре-пин по `content_hash`, и уже отредактированные главы возвращаются **за $0**; платит только та глава, в тексте которой новый терм реально встречается.
**ПОПРАВКА к моей же оценке, найдена оркестратором при приёмке и проверена мной по коду — я ЗАНИЗИЛА область.** Я написала «платформа `--resnapshot` уже передаёт, флагманский путь цел, бьёт по ручному вызову». Неверно: платформа передаёт его НЕ всегда, а под условием `book.BankMoved` (`platform/internal/runs/runs.go:288`, читал не правил), и этот флаг ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит (`platform/internal/pgstore/books.go:985-993`: «a correction landed»). Значит бьёт именно по ФЛАГМАНСКОМУ пути: майнящая книга, вторая покупка, правок не было ⇒ флага нет ⇒ гард ⇒ `failed`. **Решение оркестратора: механизм лендится, но ПРОВОДКА `--max-units` на платформе гейчена до лечения.** **Смягчение, которое я сделал:** `planVolume` при живом майнинге пишет громкий WARN, называя и эффект, и `--resnapshot`. **Чего я НЕ делал:** не трогал джоб-гард — это ратифицированный Р6-контур и та самая мина D39.165 §3 / эрраты 28.08-и, её лечение назначено платформенной половине. **Заказывай отдельно, если хочешь, чтобы банк-онли-движение джоб-гард пропускал сам.**
**1. Стоп майнинга и терминолог тратят ВНЕ объёмного потолка — и переплачиваются каждой покупкой.** Тоже подтверждено. Мой пункт был мягче («потолок режет юниты, книго-уровневые вызовы — только деньги»); линза дожала: кандидатное пространство терминолога зависит от набора уже отчерновленных чанков, поэтому между покупками оно МЕНЯЕТСЯ, request_hash другой, и чекпойнты не переигрываются за $0. Книга, проданная десятью покупками, платит **десять** книжных консолидаций там, где цельный прогон платит одну. Ограничивают это только `gates.terminology.budget_usd` и `--ceiling-usd`. **Не чинил:** резать терминолога объёмом семантически неверно (он не единица доставки), а переносить его за пределы обрезанного прогона — это пере-проектирование майнинга (родня строки 203(з)). **Обязан быть в цене продажи, если книга майнит.**
2. **Живого прогона с провайдером НЕТ.** Всё исполнение — против фейкового провайдера батареи. Ни одного платного вызова я не делал и не был на это уполномочен. Значит: цифры «2 вызова на юнит» и «$0.00182 за юнит» — свойства фикстуры, а не рынка; поведение потолка на реальном материале не мерено.
3. **Прогон, обрезанный по объёму и упёршийся в подписной стоп банка, уходит с кодом 3**, и `res.Volume` тогда не докладывается: остановка принадлежит банку, а не потолку. Поведение корректное, но пара «стоп банка + объёмный потолок» отдельным тестом НЕ покрыта.
4. **Платформенная половина не сделана и не заказана мне** (§0 промта): `--max-units` в argv никто не передаёт. Пока платформа не научится считать `главы → юниты` по манифесту и передавать флаг, обещание «купил N глав — получил N глав» НЕ выполнено. Механизм есть, трубы нет.
5. **`storeOrder` доказан на моей фикстуре, а не на всех входах.** Тождество опирается на: `UNIQUE (book_id, src, sense, since_ch, until_ch)` (`store/migrate.go:203`) ⇒ префикс ключа не даёт ничьей, и на отсутствие `COLLATE` в схеме ⇒ побайтное сравнение TEXT. Оба факта проверены грепом и чтением DDL. Чего я НЕ проверял: поведение при значениях с невалидным UTF-8 и при типовой аффинности SQLite, если в TEXT-колонку когда-нибудь ляжет число.
6. **Порог согласия (`rebill_consent_usd`) я НЕ трогал** — сужена только СУММА. Если владелец считает, что при объёмной покупке должен ужиматься и порог, это отдельное решение.
7. Батарея прогнана на `make battery`; **`make battery-stand` (корпусные тесты) НЕ гонял** — нет стендовых данных на этой машине.
#### §9. Вопросы оркестратору — пингом, не интерпретацией в свою пользу
1.**Продавать ли объёмом книги, которые МАЙНЯТ банк — до отдельного заказа?** Obstacle-0: вторая покупка на такой книге падает `exit 1`/`failed` без `--resnapshot`, и это открывает именно обрезание (контроль без потолка проходит чисто). Платформа `--resnapshot` уже передаёт (D39.166), значит флагманский путь цел, но ручной вызов — нет, и цена покупки на майнящей книге включает лишнюю книжную консолидацию терминолога (Obstacle-1). **Мой совет:** либо не ставить `--max-units` на майнящих книгах до заказа по джоб-гарду, либо принять оба хвоста явно. Решать тебе — я поставил громкий WARN и не стал трогать ратифицированный Р6-контур.
2. **Форма §3.2 отличается от той, что назвала строка 231.** Строка предписывает «движковый глагол ЛИБО флаг». Я не сделал ни того, ни другого: я починил СВЁРТКУ, потому что слепое окно было не в том, что `status` пишущий, а в том, что он сворачивал последний прогон вместо ближайшего. Это дешевле обоих кандидатов и ближе к уже ратифицированной двери D39.165 §3 («платформа читает смету из УЖЕ существующего `status --json`»). **Нужна ратификация формы и правка формулировки строки 231.**
3. **Поля шва.** `rebill_units`/`rebill_usd` потеряли `omitempty`, добавлено `rebill_basis`; `status_version` оставлен `tm-status-v1` (правка АДДИТИВНАЯ, потребителей у полей сейчас нет — они сняты с аллоулиста). Строка 231 обещает их возврат «вместе с глаголом» — глагола нет, значит возвращаются **вместе с этим фиксом**. Нужен контрактный минор и ре-аллоулист трёх полей. ⚠ **Уточнение от платформенной сессии `textmachine-c0`, сверено ею по коду:** пара снята с аллоулиста паком P10 **по ТАЙМИНГУ** (`platform/internal/ingest/resync.go:37-43`) — `status` проецировал СОХРАНЁННУЮ память, поэтому сразу после apply честно отдавал ноль, — а не «за отсутствием потребителя», как я написала сначала. Это ровно та причина, которую мой §3.2 и снимает. Риска для шва при этом нет: `DecodeStatus` (`resync.go:76`) — обычный `json.Unmarshal` без `DisallowUnknownFields`, поэтому новое поле её сторона просто не заметит. Возврат — слово оркестратора, он берёт минор на себя (пойдёт вместе с `session_ended`, канон 0.8.0).
4.**ЭТОТ ПУНКТ ОТОЗВАН МНОЮ ЖЕ — подтверждать его НЕ надо.** Здесь стояла просьба подтвердить сужение СУММЫ гейта D20.2-Q2 объёмным скоупом с доводом «инертно без флага». Довод верный и НЕважный: с флагом каллер сам выбирает размер прогона, поэтому дробление обходило порог и пере-оплачивало книгу целиком, ни разу не спросив согласия (замерено: 30 глав, четыре прохода по $0.003640 при пороге $0.005460). **Действующее решение — второй круг, находка 1:** порог судится по ВСЕЙ книге; именованный кап `--accept-rebill=X` — по тому, что заплатит ЭТОТ прогон; прогон, не пере-оплачивающий ничего, гейта не встречает. Подтверждать надо ЭТО.
5. **`redrive` я флаг НЕ дал** (отказ по правилу «команда, которая на флаг не действует, его отвергает»). Соседняя правда, которую называю строкой, а не чиню: `redrive` внутри зовёт `TranslateBook` по ВСЕЙ книге (`status.go:926`), то есть у него своя денежная неожиданность — она была и до пака.
6. **Сигнал объёмного стопа в ПОТОК событий я не завёл.** По правилу `runevents.go:29-40` новый тип события — это минор, и читатель незнакомые типы игнорирует; но это твоя зона и контрактный минор, а §3.3 его в пак не клал. Если он нужен платформе — заказывай отдельно.
#### Вопросы оркестратору после второго круга — новые и изменившиеся
**A. Разблокировка майнящих книг — есть дешёвый вариант, но решение продуктовое, поэтому НЕ беру.** Причина поломки второй покупки в том, что стоп майнинга досыпает авто-банк между покупками. Если на прогоне с `--max-units` стоп майнинга НЕ запускать вовсе, авто-банк не растёт, edit-снапшот не двигается, вторая покупка идёт чисто, и хвост №1 исчезает без правки ратифицированного джоб-гарда. Цена ровно одна и она честная: **книга, продаваемая по частям, не майнит банк, пока её продают** — майнинг случается на цельном прогоне. Это размен «продавать по главам ИЛИ майнить», а не техническая деталь, поэтому его выбираешь ты. Если скажешь «да» — это несколько строк и тест.
**B. Различение доставки и переделки сейчас ЧИТАЕМО ЧЕЛОВЕКОМ, но не машиной.** `VolumeStop` несёт `Delivered`/`Reworked`/`LeftFresh`/`LeftRework`, и CLI это печатает — но платформа читает поток событий и код выхода, а туда различение не идёт. Значит покупка десяти глав, целиком ушедшая в переделку, для платформы по-прежнему выглядит как успешная доставка десяти. Полное лечение — поле в терминальном кадре потока, то есть твой контрактный минор (ты уже сказал «не сейчас, строкой»). Пишу это ещё раз потому, что после второго круга это перестало быть косметикой: именно здесь пере-оплата всё ещё может продаться как доставка, просто теперь молча на проводе, а не громко в отчёте.
**C. Порог согласия остаётся КНИЖНЫМ, и это теперь несущее решение, а не умолчание.** После отзыва сужения порог судит всю пере-оплату книги. Следствие, которое надо принять явно: **любая** покупка через `--max-units` на сдвинутом банке встречает гейт и требует `--accept-rebill`. Платформа его передаёт (при `BankMoved`), так что путь рабочий, но «тихих» пере-проходов больше не будет ни одного — и это ровно то, чего требует Р6. Если владелец захочет, чтобы мелкие покупки проходили без вопроса, это НЕ порог надо двигать, а заводить учёт уже-данного согласия — отдельный механизм, отдельный заказ.
**D. Проводка `--max-units` гейчена тобой — я это поддерживаю и после второго круга поддерживаю сильнее.** Пока различение доставки и переделки не доезжает до платформы машинным каналом (пункт B), платформа не может отличить «купил 10 глав и получил 10» от «купил 10 глав и получил 10 переделок». Механизм готов, труба — нет, и открывать её раньше пункта B я бы не стала.
#### Дополнение к вопросам после третьего круга
**E. Три развилки закрыты РАТИФИКАЦИЕЙ, а не моим словом — фиксирую, чтобы ты не пере-решал их заново.**
— «капнутое согласие» — ЗДОРОВО (согласие, а не обход; раскрытие теперь запинено);
— «денominация» — был ДЕФЕКТ, вылечен аддитивным `rebill_output_units`;
— «доставка вперёд переделки» — ЗДОРОВО, порядок безопасен (байты вне-очередного юнита дословно совпали с прямым прогоном), выигрыш замерен A/B (6/6 против 4/6).
Если пере-решаешь — пере-решай явно: под каждым лежит исполнение, а не рассуждение.
**F. Аллоулист: полей теперь ЧЕТЫРЕ, и четвёртое обязательно.** К `rebill_units`/`rebill_usd`/`rebill_basis` добавилось **`rebill_output_units`**. ⚠ **Возвращать `rebill_units` БЕЗ него нельзя**: `rebill_units` считает `chunk×stage`, все прочие «units» документа — выходные юниты, отношение внутри одной книги непостоянно, и платформа, отмерившая пере-проход по `rebill_units` и передавшая это число в `--max-units`, купит кратно больше книги. Ратификатор измерил: `rebill_units: 15` двумя строками под `total_units: 6`.
**G. Про умолчание «доставка вперёд переделки» — прошу зафиксировать словом, а не молчанием.** Покупка, ЦЕЛЬЮ которой был пере-проход, теперь потратит грант на недоставленные главы, если они есть. Ратификатор показал, что дверь пере-прохода этим не ломается (на дочитанной книге доставка-вперёд И ЕСТЬ порядок книги), но на ЧАСТИЧНО доставленной книге разница видна. Если продукту нужен режим «только переделывать» — это флаг, и это твой заказ, не мой.