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

250 KiB
Raw Permalink Blame History

АРХИВ: секция «Бэкенд» журнала прогресса, эра 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 translatetmctl 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). Маркер в пустом юните перевернул бы withheldtranslated — ровно (а) промта. 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» — это был бы хард-блокер для следующей сессии. Атакуемое свойство сохранено дословно (снять дельта-оговорку, оставить остальное); плюс добавлены шесть СВОИХ посадок SH1SH6, по две на пункт, чтобы фикс был постоянным гейтом, а не разовым замером.
  • Не проверено: поведение под реальным параллелизмом двух 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=20mEXIT 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=1ok (голден пинит ПОЛНОЕ тело запроса).
  • go run ./cmd/tmmutate -root <КОПИЯ>73 посадки, 0 неожиданных исходов, EXIT 0 (72 RED + один каталогизированный выживший G-byte-gate; все 13 моих красны); каталог вырос 60 → 73 (13 новых посадок: SH1SH13), у каждой из трёх заказанных тем ≥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 отклонён как отдельный предмет (выше) · подсказка промта про место маркера отклонена (она перевернула бы withheldtranslated). ⚠ И один раз право §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). Любой маркер, вклеенный в текст ПУСТОГО юнита, перевернул бы withheldtranslated — ровно (а) промта. 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 approve5.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 termOriginOriginAnnotated).
  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-якоря указывают мимо.
  3. (дописано 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, так что дублировать правку не надо, но док обязан сойтись с ними.
  4. 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; живой остаток — строка бэклога 197.)

(Пинги закрытых паков — фикс-пак банка D39.118, блокеры контракта D39.122 — в archive/PROGRESS-2026-08-04-09.md, D39.125.)

(Закрытые бэкенд-записи 0102.08 — общность фаза 2 · качество банка (оба этапа) · фикс-пак банка · finding-1 · пакет-чекеров · мелкая пачка · coldrun-b · ручка эффорта — в 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; вынесено D39.139.)

(Закрытая хроника пакетов №0№4 (0410.07) перенесена в archive/PROGRESS-2026-07-04-10.md — ревизия D31.)

(Хроника пакетов 1112.07 (пакет-5, D15.2-этап-А, санитайзер-фикс D33) — в 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/reports/.)

(Закрытые записи 2531.07 — пак-18 · мини-прогон · фикс-пак · сквозная проверка · хендофф 26.07 · ресёрч общности фаза 1 — в 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. Сборка из BookExportbackend/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.goLoadReaderWords(root, targetLang): <langpack_root>/<target>/reader.txt, читается ОТДЕЛЬНО от LoadPack.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.goRunner.BuildBook(BuildOptions{Formats, Out, Partial}): Export(false)staleUnitsassembleBook (чистая функция, возвращает список дыр) → отказ RefusalBookIncomplete (refusal.go, exit 16) либо стейдж всех форматов + общий коммит в <project_db>.book.<fmt> (не запрошенные соседи удаляются — рядом с БД лежит ОДНА сборка); StatusArtifacts.BookFiles (book_files, map формат→абс. путь; в status --json и manifest --json). --out — только НОВЫЙ файл (существующий путь любого рода, несуществующий каталог → config_invalid, exit 10). Сбой между двумя renamewrite_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:titlebook.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>.epub0 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 125; 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:47Z20: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-244models.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 открывает стор через NewReadOnlyRunnerOpenReadOnly с 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 battery21 пакет из 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 на втором и третьем), каждая — на свой пункт, вердикт по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи: снятие сортировки фолда · потолок на единицу больше · снятие допуска в черновой волне · снятие допуска в редакторской · «свободные юниты тратят грант» · возврат к прежней терминальности (воспроизводит перерасход) · !okok && (воспроизводит перерасход) · допуск только лидера (воспроизводит усечённую главу) · фильтр согласия в тождество (воспроизводит отказ законной работе) · перестановка лога замечаний (воспроизводит потерю диагностики) · снятие отказа несогласованной форме конвейера. Все одиннадцать падают, и падает ИМЕННО адресный тест.

Таблица комплектности против §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 идёт через NewReadOnlyRunnerOpenReadOnly с 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. Пин согласия statustranslate покрывал только беспотолочный случай. Механическая правка сигнатуры протащила туда 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 (список термов) майнится из исходника ВСЕЙ книги — он полон после первой покупки и больше не двигается; отсюда мой неверный вывод «однородный источник ничего не даст». А байты банка двигает WHATdst каждого терма, и она draft-side: складывается из банкнот, которые модель уже выдала (mining.go:109), то есть растёт с каждой покупкой ДАЖЕ при полностью однородном источнике. Я меняла ТЕКСТ глав, а надо было менять то, что ПРЕДЛАГАЮТ ЧЕРНОВИКИ. Причина 2, из-за которой попытка (5) не могла сработать в принципе, — спойлер-гейт since_ch (membank/memory.go:640-647, жёсткое chapter < since_ch). Вводя терм поздними главами, я выталкивала его since_ch за пределы уже доставленных — и он не попал бы в их инъекцию НИКОГДА, какой бы dst потом ни получил. ⚠ Условия тянут в РАЗНЫЕ стороны: естественный способ заставить дельту расти (поздние главы вводят новый терм) ровно этим и убивает достижимость. Дефекту нужна ОБРАТНАЯ асимметрия: терм присутствует в РАННЕМ исходнике, а его рендеринг приходит поздно — то есть асимметрия ПОВЕДЕНИЯ МОДЕЛИ, а не текста. Пере-проверила зондом приёмки у себя: 方源 dst="" since_ch=3dst=Фан Юань 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(з)). Обязан быть в цене продажи, если книга майнит.

  1. Живого прогона с провайдером НЕТ. Всё исполнение — против фейкового провайдера батареи. Ни одного платного вызова я не делал и не был на это уполномочен. Значит: цифры «2 вызова на юнит» и «$0.00182 за юнит» — свойства фикстуры, а не рынка; поведение потолка на реальном материале не мерено.
  2. Прогон, обрезанный по объёму и упёршийся в подписной стоп банка, уходит с кодом 3, и res.Volume тогда не докладывается: остановка принадлежит банку, а не потолку. Поведение корректное, но пара «стоп банка + объёмный потолок» отдельным тестом НЕ покрыта.
  3. Платформенная половина не сделана и не заказана мне (§0 промта): --max-units в argv никто не передаёт. Пока платформа не научится считать главы → юниты по манифесту и передавать флаг, обещание «купил N глав — получил N глав» НЕ выполнено. Механизм есть, трубы нет.
  4. storeOrder доказан на моей фикстуре, а не на всех входах. Тождество опирается на: UNIQUE (book_id, src, sense, since_ch, until_ch) (store/migrate.go:203) ⇒ префикс ключа не даёт ничьей, и на отсутствие COLLATE в схеме ⇒ побайтное сравнение TEXT. Оба факта проверены грепом и чтением DDL. Чего я НЕ проверял: поведение при значениях с невалидным UTF-8 и при типовой аффинности SQLite, если в TEXT-колонку когда-нибудь ляжет число.
  5. Порог согласия (rebill_consent_usd) я НЕ трогал — сужена только СУММА. Если владелец считает, что при объёмной покупке должен ужиматься и порог, это отдельное решение.
  6. Батарея прогнана на 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. Про умолчание «доставка вперёд переделки» — прошу зафиксировать словом, а не молчанием. Покупка, ЦЕЛЬЮ которой был пере-проход, теперь потратит грант на недоставленные главы, если они есть. Ратификатор показал, что дверь пере-прохода этим не ломается (на дочитанной книге доставка-вперёд И ЕСТЬ порядок книги), но на ЧАСТИЧНО доставленной книге разница видна. Если продукту нужен режим «только переделывать» — это флаг, и это твой заказ, не мой.