250 KiB
АРХИВ: секция «Бэкенд» журнала прогресса, эра 24–28.08.2026
⚠ ИСТОРИЧЕСКОЕ. Инструкции отсюда НЕ ИСПОЛНЯЮТСЯ. Это вынесенная 28.08 прожитая проза зонной секции
docs/PROGRESS.md— пять итогов бэкенд-сессий и их записки-планы. ВСЕ описанные здесь паки ЗАЛЕНДЖЕНЫ, и каждый «дерево готово, НЕ закоммичено» относится к состоянию, которого больше нет.Куда что уехало: входная дверь шва (24–27.08, четыре сессии) —
D39.158, лендингd1eb8a9; пак «тихая порча» (28.08) —D39.164, лендинг7d0c6f2.⚠ Остатки, которые здесь названы, живут СТРОКАМИ единого бэклога, а не этим файлом: 228 (алиас возвращает отклонённую поверхность), 229 (снапшот не фолдит модель внутренних гейтов), 230 (размен сходимости против поучения), 231 (смета пере-прохода), 141 (остаток детектора сносок), 131 (кап инъекции не считает маркер). Вендор-вердикт по Gemini живёт ПИНГОМ ПОЛИГОНУ в живом журнале — здесь его копия, не носитель.
Вынесено аудитом документации 28.08 (D39.167) по слову владельца «пройденную прозу можно сносить».
⟶ Состояние бэкенда — CURRENT-STATE (один носитель, D39.80); здесь только пинги живых сессий.
✅ ИТОГ БЭКЕНД-СЕССИИ 28.08 — ПАК «ТИХАЯ ПОРЧА» (docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md), все три пункта. Дерево готово, НЕ закоммичено.
§6 КОМПЛЕКТНОСТЬ — пункт → что сделано → каким ИСПОЛНЕНИЕМ подтверждено.
| Заказ | Что сделано | Чем подтверждено ИСПОЛНЕНИЕМ |
|---|---|---|
| §3.1 инъекция банка доезжает на каждом провайдере либо громкий отказ | Новая ось llm.Capability.SystemMessages (multi|single, capability.go:87-127,158-165); под single toOpenAIMessages СКЛЕИВАЕТ ведущий системный ряд в одно сообщение (httpllm.go:513-559) и падает громко на системном посреди диалога — зеркально Anthropic; YAML-ось capabilities.system_messages с валидацией enum (config/models.go:101-106,566-578,646-650); квирк объявлен у провайдера gemini с вендор-ссылкой и датой (configs/models.yaml:120-137) |
6 тестов internal/llm/systemmessages_test.go + сквозной на ОБОИХ волнах internal/pipeline/systemmessages_wave_test.go. Мутация «ось игнорируется» → падает TestSingleSystemEndpointCarriesTheBankOnBothWaves обеими под-ветками и …CarriesTheInjection с текстом got 2 (roles [system system user]). Мутация «ось теряется по дороге из YAML» → тот же тест. Каталог: SH1, SH2 |
| §3.2 дыра выдачи видна читателю файла | ⚠ Заказ пере-формулирован по находке (разрешено оркестратором 28.08): маркер уже существовал и ВРАЛ. ChunkOutcome/ChunkExport несут DroppedMembers (bookrun.go:69-75, export.go:45-51, waverun.go:488, export.go:227-230); cmd/tmctl/render.go печатает маркер В ПОТОКЕ ТЕКСТА (gapMarker, :26-63) в обоих рендерах и честные счётчики withheld/incomplete |
Сквозной прогон БОЕВОГО бинаря cmd/tmctl/exportgap_cli_test.go: tmctl translate → tmctl export --plaintext, текст ДО и ПОСЛЕ на ОДНОМ состоянии (ниже). Матрица семи состояний. Мутации SH3 (маркер пуст) и SH4 (счёт выпавших не ведётся) |
| §3.3 повтор принятого документа сходится | refuseSeedConflicts больше не срабатывает, когда отказ УЖЕ на записи (membank/decisions.go:365-393, новый rejectsHoldSurface :690-700); порядок записи в writeDecisionFiles изменён на REJECTS-FIRST с обоснованием (bankdecisions.go:427-441,458-489) |
5 тестов membank/decisions_converge_test.go + 3 дверных pipeline/bankdecisions_converge_test.go (реальные файлы, реальные классы отказа). Мутации SH5 (сужение снято) и SH6 (порядок возвращён) |
РЕШЕНИЯ ПО ПРОПУСКАМ И РОДНЕ — строкой каждое, как просил оркестратор.
- Четвёртая дыра (строка 141) — ВЗЯТА БЕСПЛАТНО, работы не потребовала. Проверено исполнением
(проба по
internal/checks, файл удалён после замера): сноска[1] 蛊真人 — название романа.мимо детектора примечаний идёт, но поднимает классCJKLeak, а онCosmeticOnly(checks/sanitizer.go:161-163) ⇒StripCosmeticвырезает ханьцзы, юнит уходитsanitizer_stripped, и баннер «leak cleaned, verify» для НЕГО правдив. Механизм этого же заказа накрывает класс без правки детектора — граница промта соблюдена буквально. ⚠ Смежное, НЕ взятое и названное строкой: сноска БЕЗ ханьцзы ([1] Название романа.) не поднимает ни одного класса (замер:total=0), едет как есть — но текста при этом НЕ теряет, это лишняя добавка, а не дыра. Маркер на ней дезинформировал бы (⛔ анти-скоуп промта). Ловля таких сносок — работа В ДЕТЕКТОРЕ, промт её прямо запретил. - Д1 — НЕ ВЗЯТ, отдельным предметом. Право §9. Канон определяет decline ПОВЕРХНОСТНО
(«declining a surface removes EVERY window of that surface»,
14-api-contract/openapi.yaml:2051), и расширять его до энтити значило бы менять ратифицированный контракт из бэкенд-сессии. Вторая половина Д1 («память глушит стоп») — уже НАЗВАННЫЙ и принятый двусторонний остаток в комментарииmining.go:308-313, а не необнаруженный дефект. ⚠ Но одна траектория Д1 реальна и требует ЧУЖОГО решения: эмиссия майнера энтити-широка (miner_emit.goclusterTouches), а фильтр авто-банка ключуется только по собственному 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-521glossaryLineTokens), а рендер дописывает к КАЖДОЙ строкеtx.UnverifiedMarker(:706) либоgenderConstraintNote(...)(:711), а редакторский блок ещё и префикс-с кавычками-ёлочками (:821). То есть отгружаемая строка строго длиннее той, за которую платит бюджет; на коротких именных строках перерасход велик. Тема денег и качества инъекции — не тихий брак в тексте.
НАХОДКИ, МЕНЯЮЩИЕ ПРОМТ (все три подписаны, ни одна не абсорбирована молча).
- §3.2: маркер существовал и ВРАЛ. Ветка
case ce.FinalText != ""печатала «(leak cleaned, verify)» ЛЮБОМУ флагнутому юниту с текстом, а c-lite member-drop (export.goставитDispFlaggedпри непустом тексте) попадал ровно в неё. Существующий тест не ловил: покрывал ветку только причинойsanitizer_stripped(export_cli_test.go:66). Предмет заказа изменён с «завести маркер» на «перестать врать и покрыть непокрытое» — разрешено оркестратором 28.08 при закрытом гейте. - §3.2: подсказка промта про место правки была опасна. Промт указывал
export.go:237рядом сApplyHeading. Туда нельзя:ChunkExport.FinalText— поле артефактного контракта, а провод судитtranslated/withheldпредикатом «текст непуст» (waverun.go:203-204иevents.go:538-554,runevents.go:130Shipped). Маркер в пустом юните перевернул быwithheld→translated— ровно ⛔(а) промта.ApplyHeadingэтого не делает лишь потому, что на пустом тексте он no-op. Маркер живёт в РЕНДЕР-слое. Проверено:FlagReason-константы до и после ИДЕНТИЧНЫ,exportVersionне двигался,dropped_membersаддитивно иomitempty. - §3.1: посылка «Gemini принимает ровно одно» отозвана автором (оркестратор, 28.08) — вендор-
вердикт со ссылками и датами отправлен ему отдельно, для
00-provider-quirks.md. Кратко: структурная посылка ПОДТВЕРЖДЕНА (systemInstruction— одинobject (Content),Content.role∈ {user, model}), поведение шима вендором НЕ документировано нигде, наш замер даёт «не склеиваются» и «инструкция первого не исполнена»; «выживает одно или ноль» остаётся ОТКРЫТЫМ и для решения безразлично.
ДЕФЕКТ, КОТОРЫЙ Я ВНЁС САМ И ЗАКРЫЛ (называю, потому что он показателен). Первая редакция ветки
маркера срабатывала по DroppedMembers > 0 БЕЗ проверки непустого текста. Юнит, у которого выпали ВСЕ
члены, ship-ит пустоту — и получил бы баннер «(INCOMPLETE)» с фразой «фрагмент отсутствует в тексте
НИЖЕ», указывающей на несуществующий текст. То есть я едва не заменил одну ложь читателю на другую.
Найдено собственным разбором ветвлений до опровергателей, закрыто гардом на обоих сайтах вызова,
покрыто состоянием «выпали все» в матрице и запинено мутацией (снятие гарда → тест падает).
§5 ОСИ РЕВЮ.
- Читатель книги. Все четыре класса проверены на ОТГРУЖЕННОМ ТЕКСТЕ, не на структурах:
(а) c-lite — был помечен ложно, теперь помечен верно и в потоке текста; (б) юнит целиком withheld и
(в)
glossary_miss— помечены верно и раньше, работы не требовали, теперь ещё и не попадают под маркер по ошибке; (г) сноска с ханьцзы — накрыта существующим верным маркером. - Пара, которой в репо нет. Правка не ветвится по паре ни в одном месте: ось
system_messages— свойство ЭНДПОЙНТА, маркер — английский операторский словарь той же поверхности, что и соседние баннеры, поэтому у книги с любымtarget_langи БЕЗ langpack вывод байт-идентичен. Специально НЕ стал класть маркер в langpack:InjectionTextsForдля незнакомой цели возвращает нулевое значение молча (lang/embedded.go:411-423) — маркер бы исчез, и дыра снова стала бы тихой ровно для той пары, ради которой мандат общности и написан. - Детерминизм. Голден зелёный (
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.goTestHalfLandedIsNamedPerFileкодировал ПОРЯДОК переименований в своей фикстуре (занимал путь rejects, чтобы упал второй rename). Порядок изменён заказом §3.3, и фикстура пере-нацелена на путь delta. Утверждение НЕ ослаблено: по-прежнему требуется, что ровно одна половина легла, что это ПРОВЕРЕННЫЕ байты и что отчёт называет какая. Сверх того добавленTestTheRenameOrderIsWhatMakesADeclineRecover, который пинит САМ порядок вместе с его причиной, чтобы его нельзя было вернуть как «уборку». Если оркестратор считает это подгонкой — я готов откатить и вынести §3.3-половину с полусостоянием отдельным заказом. - Каталог мутаций: запись
AJ-seed-alias-declineпере-заякорена. Моя правка сдвинула строку, на которую она указывала, и инструмент падал жёсткой ошибкой «the source moved under the catalogue» — это был бы хард-блокер для следующей сессии. Атакуемое свойство сохранено дословно (снять дельта-оговорку, оставить остальное); плюс добавлены шесть СВОИХ посадокSH1…SH6, по две на пункт, чтобы фикс был постоянным гейтом, а не разовым замером. - Не проверено: поведение под реальным параллелизмом двух
bank-apply(лок это исключает, но я не гонял); поведение склейки на провайдере, который принимает одно системное И имеет префиксный кеш (у gemini кеш иной) — довод «склейка кеш не портит» опирается на комментарий самого кода и на то, что префикс сохраняет байты и позицию, живьём не мерен.
§4 ОПРОВЕРГАТЕЛЬ — ЗАКАЗ ИСПОЛНЕН, И ОН ОКУПИЛСЯ. Шесть линз, мандат «опровергни».
Веер шесть, а не «2–4» из §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 не двигался · снапшоты боевых конфигов не двигались · арм-конфиги грузятся.
ЧТО НАЙДЕНО, НО СОЗНАТЕЛЬНО НЕ ЧИНИТСЯ — с аргументом.
- Ловушка
capabilitiesнаkind: anthropic— оказалась чинибельной дёшево, и я её ВЗЯЛ: ось теперь отказывается на этом виде по образцуcache_ttl(models.go), с тестом. Пред-существующий класс для ОСТАЛЬНЫХ осей (budget_field,temperature,reasoning,min_max_tokens) остаётся — его правильная форма тащитkindвvalidateCapabilitiesи трогает каждую ось; это чужой предмет. - Снапшот не покрывает модель ГЕЙТА ТЕРМИНОЛОГИИ (
config/pipeline.go— не фолдится сознательно, ратифицированно), а терминолог шлёт ДВА системных. ⇒ флип оси у провайдера, которым пользуется только этот гейт, меняет байты под НЕИЗМЕННЫМRequestHash, и чекпойнты ложно попадают. Дыра НЕ моей оси — по ней ездят все осиcapabilitiesс D3.1 — но моя первая, чей смысл именно в смене этих байтов. Утверждение в комментарии я ослабил до правды и дыру НАЗВАЛ прямо в коде (capability.go); закрытие — фолд модели гейта, как это делаетrepairSnapshot, и это правка снапшот-контракта, принадлежащая владельцу решения «не фолдить», а не провайдерскому квирку. - Сузив отказ по поверхности, я потерял поучение в одном углу: decline, который И записан, И
по-прежнему инертен против подписанного сид-терма, теперь отвечает
already_appliedвместо единственной работающей инструкции «убери из glossary_seed». Сходимость сочтена более тяжёлой обязанностью (у вызывающего, который следует опубликованному ретраю, другого хода нет), но размен реальный: закрывается полем отчёта, несущим стоячий факт, и это форма шва.
ХАРД-БЛОКЕР, НАЙДЕННЫЙ И СНЯТЫЙ (требование владельца «после тебя не осталось хард-блокеров»).
go test ./cmd/tmctl/ строил боевой бинарь в os.MkdirTemp и НИКОГДА не убирал каталог — по 20 МБ за
прогон. За этот пак накопилось 249 брошенных каталогов tmctl-bin*, ~5 ГБ, и /tmp заполнился до
99%: батарея под -race начала падать с link: mapping output file failed: no space left on device,
что читается как сломанная сборка, а не как кончившееся место. Я на это и купился на один прогон —
сообщать «батарея красная» было бы ложью. Причина найдена, место освобождено, харнесс починен:
добавлен TestMain, снимающий каталог (cmd/tmctl/exitcontract_test.go); проверено исполнением —
два прогона подряд, прирост каталогов 0. Дефект пред-существующий, пак его лишь довёл до отказа.
ЧИСЛА, С КОМАНДАМИ (§4.1).
go build ./...·gofmt -l .(пусто) ·go vet ./...·go vet -tags live ./...— все 0.go test ./... -count=1 -timeout=20m→ EXIT 0.go test ./... -race -count=1 -timeout=20m(= строкаmake test) → EXIT 0, real 2m09s.golangci-lint run --timeout=15m ./...(=make lint) → 0 issues.go test ./internal/pipeline/ -run TestGolden -count=1→ ok (голден пинит ПОЛНОЕ тело запроса).go run ./cmd/tmmutate -root <КОПИЯ>→ 73 посадки, 0 неожиданных исходов, EXIT 0 (72 RED + один каталогизированный выжившийG-byte-gate; все 13 моих красны); каталог вырос 60 → 73 (13 новых посадок:SH1…SH13), у каждой из трёх заказанных тем ≥2 своих. ⚠ Каталог гонять ТОЛЬКО на копии — сам инструмент это требует, а я один раз запустил его на рабочем дереве и словил таймаут; дерево спас обработчик сигнала самого инструмента (проверено: мутационного текста не осталось, батарея зелена). Записываю как урок, а не как мелочь.
ФАЙЛЫ (19 правленых + 5 новых; всё в backend/, плюс эта запись в журнале).
cmd/tmctl/render.go · cmd/tmctl/exitcontract_test.go · cmd/tmmutate/mutations.json ·
configs/models.yaml · internal/config/models.go · internal/config/models_catalog_test.go ·
internal/llm/capability.go · internal/llm/httpllm.go · internal/llm/provider_openai.go ·
internal/llm/provider_local.go · internal/membank/decisions.go ·
internal/membank/decisions_fuzz_test.go · internal/pipeline/bankdecisions.go ·
internal/pipeline/bankreport_test.go · internal/pipeline/bookrun.go ·
internal/pipeline/export.go · internal/pipeline/mining.go · internal/pipeline/runner_test.go ·
internal/pipeline/waverun.go.
Новые: cmd/tmctl/exportgap_cli_test.go · internal/llm/systemmessages_test.go ·
internal/membank/decisions_converge_test.go · internal/pipeline/bankdecisions_converge_test.go ·
internal/pipeline/systemmessages_wave_test.go.
⛔ ВОПРОСЫ И ПИНГИ ОРКЕСТРАТОРУ — четыре, каждый требует ЧУЖОГО решения.
- Изменён СУЩЕСТВУЮЩИЙ тест, и я это заявляю, а не прячу.
bankreport_test.goTestHalfLandedIsNamedPerFileкодировал в фикстуре ПОРЯДОК переименований, который заказ §3.3 изменил; фикстура пере-нацелена на второй rename, утверждение сохранено дословно и усилено новымTestTheRenameOrderIsWhatMakesADeclineRecover, пинящим сам порядок с его причиной. Считаешь подгонкой — откачу, и тогда половину §3.3 с полусостоянием надо выносить отдельным заказом. - Вендор-вердикт по Gemini — прислан отдельным сообщением, с URL и датами; три поправки к
00-provider-quirks.md:157(структурная посылка ≠ поведенческий вывод ·Content.parts[]повторяемо · «форум Google» — это два не-сотрудника из 2025). Пишешь ты, зона твоя. - Дыра снапшота у модели гейта терминологии (см. выше, п.2 «не чинится»). Закрытие — правка снапшот-контракта; принадлежит владельцу решения «гейт не фолдится», не мне.
- Д1 достоин собственного заказа. Не «расширить decline до энтити» (это против канона), а узко:
эмиссия майнера энтити-широка (
miner_emit.goclusterTouches), а фильтр авто-банка ключуется только по собственному src строки (mining.go:662), поэтому ОТКЛОНЁННАЯ поверхность возвращается алиасом уцелевшей строки. Канон уже сегодня обещает «declining a surface removes EVERY window of that surface» — то есть движок не держит обещание. Правильная форма — снять алиас со строки, а не снести строку; это банковая онтология, и в «тихую порчу» её тянуть не следует.
ПРАВО §9 ПРИМЕНЕНО ТРИЖДЫ, каждый раз с грунтом: Д3 отклонён (символ живёт в platform/, зона
заперта; движковая половина здорова и порядок «карта → память» исполнен) · Д1 отклонён как отдельный
предмет (выше) · подсказка промта про место маркера отклонена (она перевернула бы withheld →
translated). ⚠ И один раз право §9 я применил ОШИБОЧНО: решил не трогать вторую дверь
refuseInertDeclines, рассудив, что её случай — «устаревший документ, а не ретрай». Опровергатель
показал траекторию из ОДНИХ ПРИНЯТЫХ вызовов. Рассуждение было неверным, исполнение это доказало,
дверь починена.
⚠ ЗАПИСКА НИЖЕ — ПЛАН ДО ПРАВОК (§6), она СОХРАНЕНА как след дисциплины, но её выводы местами ОПРОВЕРГНУТЫ итогом выше. Не наследовать из неё: «сломан только класс (а)» (сломан ещё и стык «редактура флагнулась сама + выпал член»), «фиксация порядка записи безопасна» (она купила второй тихий вред, закрытый фильтром дельты) и «Д3 не существует» (существует, в platform/).
📝 ЗАПИСКА-ПЛАН БЭКЕНД-СЕССИИ 28.08 — ПАК «ТИХАЯ ПОРЧА» (docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md), §6. Правок кода на момент записи НЕТ.
Три заказа: §3.1 инъекция банка не теряется · §3.2 дыра выдачи видна читателю файла · §3.3 повтор
принятого документа сходится. Ниже — что установлено ЧТЕНИЕМ до правок и как из этого следует план.
⚠ Всё, что помечено «проверить исполнением», в план входит как обязательство, а не как факт.
Находка 1 (меняет §3.2). Маркер уже есть — и он ВРЁТ. cmd/tmctl/render.go:499 печатает для
флагнутого юнита с НЕПУСТЫМ текстом баннер «(leak cleaned, verify)», а :38 — «[FLAG … — leak
cleaned, exported cleaned, verify]». Ветка выбирается по ce.FinalText != "", и c-lite member-drop
(export.go:226-230 ставит DispFlagged при непустом тексте) попадает ровно в неё. То есть юнит, у
которого ВЫПАЛ ЦЕЛЫЙ ЧЛЕН, объявляется читателю косметической чисткой санитайзера, которой не было.
Существующий тест это не ловит: cmd/tmctl/export_cli_test.go:66 покрывает ветку только причиной
sanitizer_stripped. ⇒ Предмет §3.2 не «завести маркер», а «перестать врать и покрыть непокрытое»
(пере-формулировка разрешена оркестратором 28.08 при закрытом гейте «что читает человек»).
Гейт §3.2 закрыт чтением: движок НЕ ПИШЕТ ФАЙЛОВ ВООБЩЕ. Все команды рендерят в stdout
(cmd/tmctl/main.go:227-228, :338-349). Читаемых человеком поверхностей у движка две, обе
текстовые и обе уже несут пер-юнитный баннер: tmctl translate (render.go:27-72) и
tmctl export --plaintext (render.go:478-511). Дефолт export — JSON
(export_cli_test.go:24-28), и он же артефакт шва к платформе (research/23 строка 76 —
«артефактный контракт (export-JSON · 101 · 100)»). Читательский файл (epub и пр.) собирает
ПЛАТФОРМА по POST /books/{bookId}/exports (14-api-contract/openapi.yaml:824-870) — чужая зона.
⇒ Адресат моего маркера — человек, читающий plaintext-выгрузку движка; платформенная половина
остаётся за платформой, и провод различение несёт (14-api-contract/README.md:300).
Следствие для места правки, и оно расходится с подсказкой промта. Промт указывал на
export.go:237 (прецедент ApplyHeading). Туда маркер класть НЕЛЬЗЯ: ChunkExport.FinalText — это
поле final_text артефактного контракта, а waverun.go:203-204 кормит провод предикатом
oc.FinalText != "" (shipped). Любой маркер, вклеенный в текст ПУСТОГО юнита, перевернул бы
withheld → translated — ровно ⛔(а) промта. ApplyHeading этого не делает только потому, что на
пустом тексте он no-op (chunk/chunker.go:186-191). Различение: титул — часть КНИГИ и обязан ехать
на провод; маркер дыры — метаданные О тексте, и в поле, по которому провод судит, ему нельзя. ⇒
маркер живёт в РЕНДЕР-слое (cmd/tmctl/render.go), на слой дальше проекции. Это ответ на требование
промта «либо тем же механизмом, либо объясни, почему другое».
Классы дыр — ревизия по коду. (а) c-lite member-drop — помечен, но ЛОЖНО (находка 1); (б) юнит
целиком withheld (waverun.go:496-509) и (в) gate-override glossary_miss (export.go:319-323)
дают пустой текст и попадают в ветку render.go:502-504 «(not translated, flagged for a human)» —
помечены ВЕРНО, работы не требуют; (г) четвёртая, сноска «[1] …» мимо детектора примечаний: детектор
требует заголовочного слова (lang/data/target-ru.txt, sanitizer_trailing_note), но ханьцзы в
сноске поднимают класс CJKLeak, а он CosmeticOnly (checks/sanitizer.go:161-163) ⇒ юнит уходит
как sanitizer_stripped, и баннер «leak cleaned» для НЕГО правдив. То есть класс (г) накрыт
существующим механизмом БЕСПЛАТНО и работы в детекторе не требует — граница промта соблюдена.
Проверить исполнением обязательно: что сноска с ханьцзы действительно даёт CosmeticOnly, а не
подтягивает субстантивный класс.
§3.1 — план и его цена. Свёртки в toOpenAIMessages нет (llm/httpllm.go:513-521, копия
один-в-один); её комментарий :511-513 говорит, что CacheBoundary на этом пути бессмысленна ⇒
свёртка кеш не портит. У Anthropic путь другой и трогать его нельзя
(llm/provider_anthropic.go:169-179: ведущий system-префикс → блоки, CacheBoundary → реальный
cache_control). Ось лечения — llm.Capability + блок capabilities: (провайдерный ИЛИ модельный,
config/models.go:85,:146,:442-450), прецедент безопасного расширения — MinMaxTokens с
json:",omitempty". Детерминизм: снапшот фолдит РАЗРЕШЁННУЮ Capability только для праймари и
единственного escalate_to (pipeline/snapshot.go:316-340,:479,:495); в pipeline-c1.yaml
escalate_to: deepseek-v4-pro (:60), а gemini стоит лишь третьим в цепочке default (:129) при
budget_usd: 0 (:136) и раннере SINGLE-HOP ⇒ объявление квирка у gemini снапшот боевого конфига
НЕ двигает. Проверить исполнением (голдены + снапшот-тесты).
⚠ Честная граница достижимости: при SINGLE-HOP и текущих конфигах gemini не достигается ВООБЩЕ;
дефект латентный и ждёт конфига, который назовёт gemini в escalate_to или в chain[0] лейбла.
⚠ Развилка, которую выношу на опровергателя (§4 заказ): дефолт оси. «Квирк объявляет тот, у кого
он есть» (нулевое значение = сегодняшнее поведение) не двигает ничего, но НОВЫЙ провайдер с той же
ловушкой снова теряет инъекцию молча — тот же класс тихого вреда. «Свёртка по умолчанию, исключение
объявляет проверенный» безопасна для незнакомого провайдера, но меняет провод боевому переводчику без
замера (запрет «эмпирика решает») и двигает снапшот. Решение — после опровергателя, с обоснованием.
⚠ Посылка промта «Gemini принимает ровно одно системное» ОТОЗВАНА автором (оркестратор, 28.08):
это было рассуждение; собственный замер полигона (00-provider-quirks.md:157) даёт 535 токенов ≈ не
выжило НИ ОДНО. Заказу это безразлично, тесту — нет: утверждается пост-фиксный инвариант «системное
РОВНО ОДНО и инъекция ВНУТРИ него», он верен при любом чтении. Вендор-вердикт со ссылкой уходит
оркестратору для 00-provider-quirks.md (его зона).
§3.3 — трасса контрактной сессии ПОДТВЕРЖДЕНА моим чтением, и подтверждено то, что она сама не
читала. Отказ живёт в membank/decisions.go:374-377, бежит ДО фолда (:296-300), и
всё-или-ничего вытирает документ целиком (:302-307). Порядок записи я прочитал сам:
bankdecisions.go:458-473 коммитит ДЕЛЬТУ ПЕРВОЙ, а собственный комментарий функции :436-438
говорит дословно: «delta-first is safe for an approve and NOT for a decline … convergence is what
actually carries both». ⇒ окно застревания реально, и код сам называет допущение, которое ложно.
Идемпотентная ветка уже есть (already_applied, :221-224,:269-272) — вводить понятие не надо.
План: сузить условие отказа так, чтобы оно не срабатывало, когда отказ УЖЕ записан в rejects, плюс
направление-гард (настояще-инертный decline обязан отказывать по-прежнему тем же текстом).
Идентичность документа НЕ вводить — сходимость по СОСТОЯНИЮ.
Д1 и Д3 — беру право §9 и НЕ чиню, оба с грунтом.
· Д3 ОТКЛОНЁН как несуществующий: гард LiftBankStop не найден в коде НИ В КАКОМ написании
(греп по всему backend/); реальный носитель — mining.go:266-277, и он уже реализует
ратифицированный порядок «карта → память»: writeFileAtomic карты на :191, MarkStopPresented
на :274, с комментарием :270-273 «strictly after the map … the reverse order can mark surfaces
presented whose map never reached disk» и с явной оценкой отказа памяти как «one extra stop, nothing
lost» (:275-276). То есть механизм написан ВМЕСТЕ с памятью v16, а не до неё.
· Д1 ОТКЛОНЁН как ратифицированное поведение + названный остаток: канон определяет decline
ПОВЕРХНОСТНО («declining a surface removes EVERY window of that surface» —
14-api-contract/openapi.yaml:2051), и расширение до энтити противоречило бы контракту; а «память
глушит стоп» — уже НАЗВАННЫЙ и принятый двусторонний остаток в комментарии
mining.go:308-313. Оба — строка с причиной, не правка.
Сосед 131 (называю, не чиню, как велит §3.4): кап инъекции считает только «src → dst» и не
считает маркер ⟨проверить⟩ и родовые пометки (lang/data/injection.txt несёт их как отдельные
значения) ⇒ реальная инъекция шире бюджета. Проверю точный носитель при работе в этом коде и назову
file:line в итоге; чинить не буду — тема денег и качества инъекции, а не тихого брака в тексте.
Порядок работы: §3.1 → §3.2 → §3.3, каждый с тестом на ПОСТ-фиксный инвариант; батарея под
-race + линтер; посадки мутаций ≥1 на пункт с вердиктом по ДЕЛЬТЕ и ТОПИЧНОСТИ; опровергатель на
выбор места §3.1 (заказ §4). Веер агентов соразмеряю предмету: численный потолок «2–4» из §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).
⛔ Пинги:
- Платформе/оркестратору:
.bank-stop.jsonСНЕСЁН (мёртвая публикация упразднённого D39.144 пер-термного экрана) — платформенные журналы (platform-PROGRESS.md,STACK_DECISIONS.md) называют его атомарно-читаемым сайдкаром, упоминания устареют при лендинге. - Потребителям банк-экспорта: документ несёт
as_of(граница прогона) иrun_id(тот же id, что стримevents.jsonl); аддитивно, старые читатели не ломаются; пустойrun_id= «идентичности нет», не «текущий».
✅ ИТОГ БЭКЕНД-СЕССИИ 27.08 — ДОФИКС-2 «ФЛАЖОК СТАНОВИТСЯ ФЛАЖКОМ» (docs/BACKEND_SEAM_FIX2_SESSION_PROMPT.md). Дерево готово, НЕ закоммичено; продолжает дерево паков 24–25.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):
- Движок первым, платформа второй. Сброс флага
--verify-bankплатформой (её персистентная колонка + особый случай в спавне,platform/internal/runs/spawn.go) может умереть ТОЛЬКО ПОСЛЕ лендинга этого пака: в обратном порядке каждая книга с поднятым флажком в день лендинга перестаёт писать авто-банк и пере-сидироваться ровно в режиме, ради которого флажок существует. tmctl migrateдо прогонов (голова схемы v15 → v16):OpenReadOnlyотказывает классом 13 всем непере-мигрированным книгам — ратифицированный порядок D39.134 п.3 это покрывает, но назван явно.- Миграционная нота: после апгрейда память пуста → ОДИН доброкачественный стоп на первом verify-прогоне каждой книги.
⛔ ПИНГИ ЧУЖИМ ЗОНАМ (правок не делал):
- Платформе: у неё умирает сброс флага при resume (после лендинга движка — п.1 выше) · отчёт двери
теперь v2 (
tm-bank-decisions-report-v2: moderefused/stopped/write_incomplete, пофайловыеwritten_*) — еёreadEngineвыбрасывает stdout при ненулевых кодах, прежний пинг об этом стал острее: класс 15 БЕЗ отчёта неотличим от «ничего не легло» · новый exit 15 в полосе 10–19 читается её ридером как refused по построению полосы — ратификация значения на оркестраторе при лендинге. - Полигону:
eval/bank_autonomy/parse_common.pyпарситmined-signature.yaml— карта теперь несёт верхние ключиmap_version/map_id(лежащие артефакты coldrun не тронуты; новые прогоны будут в новом жанре). - Фронту: экран подписи, читающий
signature.undecidedиз отчёта двери, обязан перестать трактовать его как «стоп не погаснет» — поле информационное (D39.144), контракт поля пере-описан в коде. - Майнерский хвост за потолком структурно недостижим (§4.4, НЕ чинилось по заказу): потолок эмиссии
(200) режет ранжированный пул ДО фильтров пригодности/сида/отказов (
miner_emit.go,ranked[:emitRankCap]доemissionEligible). Пере-замерено своей командой (числа прежнего сплошного читателя в дерево не попали): 2354 ранжированных · 209 годных во всём алфавите · 11 в окне потолка · 198 невидимы постоянно; отклонение всех годных окна не подняло ни одного из хвоста (FINDINGS§38). Первоисточник замера — постоянный якорь-пинinternal/miner/emissioncap_test.go: он утверждает ТЕКУЩЕЕ поведение и покраснеет в день сдвига потолка или порядка фильтров, приведя двигающего к этому пингу. Двигать потолок — трогать фрозен-паритет; отдельное решение. - Гипотеза «подписание промахивается мимо чекпоинтов терминолога и пере-оплачивает роль» механизмом НЕ подтверждена (§4.13): память предъявленного входов чекпоинта не трогает; если перерасход существует — он существовал и до пака, отдельный предмет.
- Остаток оси арбитра (нашло воркфлоу-ревью, воспроизведено живьём): второй конфиг ТОЙ ЖЕ книги
(один каталог, один 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
§§14–30; здесь только то, что требует ДЕЙСТВИЯ.
✅ ⛔-ЗАКАЗ ПРИЁМКЕ ИЗ ПАКА СНЯТ. Цепь «стоп → bank-apply → резюм → стоп ГАСНЕТ» теперь живёт в
репозитории как $0-тест на фейковом провайдере (internal/pipeline/bankchain_test.go), на ОДНОМ проекте
от стопа до погасшего стопа. Довод пака «своими силами невозможно, надо дойти оплаченной черновой волной»
был шире правды: харнесс для этого уже был в репозитории. Мина №1 строки 199 (ливлок на status: auto) закрыта исполнением, а не рассуждением.
⚠ РАТИФИКАЦИЯ КЛАССА 14 — прежнее требование в силе, объём немного шире. Класс decisions_rejected
теперь несёт и отказ по КАПУ (документ больше, чем движок применяет за один акт): это факт о ЗАПРОСЕ, а
не о деплое, поэтому 10 был бы неверен. Носители полосы поднимает оркестратор при лендинге, как и
планировалось.
⛔ ПИНГИ ДОФИКСА (правок не делал):
exit 2при OOM платформа читает как успех — общедвижковый класс; кап удерживаетbank-applyот того, чтобы стать первым, кто до него доедет, но самого класса не снимает.- Каталог книги без права записи даёт
exit 1(вне полосы). Класс движка корректен и одинаков у всех глаголов — чинить надо чтение кода 1 на платформе, которая списывает проблему прав ХОСТА в бюджет попыток КНИГИ. - Проекция отвечает «что применится», а не «почём». Рекомендация — НЕ строить: глагол $0 по построению, поле «$0» было бы шумом. Решать оркестратору.
- Карта подписи не удаляется после погасшего стопа — пред-существующее; зафиксировано тестом как
текущее поведение. Честный ответ на «есть ли что подписать» — новое поле отчёта
signature.undecided. - Цена глагола Θ(N²) по построению — каждый пер-решенческий помощник сканирует весь документ. Именно поэтому кап на КОЛИЧЕСТВЕ стоит на 5 000. Поднимать его без индексации этих сканов нельзя.
- Апстрим-дефект
gopkg.in/yaml.v3 v3.0.1(эмиттер пишет блочный скаляр, который его же парсер не читает) вылечивается по-настоящему только скаляр-типом схемы с явным стилем или переездом наgo.yaml.in/yaml/v4(релиз-кандидат, смена module path в ДВУХ зонах). Мой обход несёт ссылку на исходник библиотеки и характеризационный тест, который покраснеет в день починки апстрима. - Фолд поверхностей Go-резидентный:
internal/text/norm.goдержитtrad2simpи катакана→хирагана в Go, а не в langpack'е. Пара, которой в репо нет, деградирует до NFKC+lowercase — не блокирована, но это ровно тот класс, о котором ревью-вопрос общности. Шире двери, поэтому пингом.
⚠ ЧТО В ЗАПИСИ ПАКА 24.08 (ниже) УСТАРЕЛО — читать её через эту поправку.
- Пинг 2, «таймаут: 1.6с на 5 000 термов, 9.1с на 20 000» — ЧИСЛА НЕВЕРНЫ, и это ровно тот датум,
которым платформенный пак будет мерить свой таймаут. Мой замер той же командой: 5 000
approve→ 5.2 с, 20 000 → 50 с; на минимальныхdecline(самая дешёвая форма, и потому та, которой байтовый кап пускает больше всего) 5 000 → 11.3 с, 12 000 → 58 с, 28 900 → 308 с. Брать надоFINDINGS§18 и §29 п.1, не эти. - Пинг 6 «однострочник при лендинге: документ решений не капирован» — СДЕЛАН. Капов теперь два (1 МиБ на чтение + 5 000 решений на применение), потому что байтовый не ограничивает время.
- Пинг 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.
⛔ ПИНГИ ЗОНАМ И СЛЕДУЮЩИМ ПАКАМ:
rubyToCandidatesмёртв в проде — единственный вызов из теста (memseed_test.go:204);seeding.go:36-38строитmanualSrcsи никуда не передаёт (Go молчит: запись в map = использование). СтрокSource:"ruby"в банке не возникает, а платформа держит для них ветку словаря (ingest/bank.gotermOrigin→OriginAnnotated).- Платформенному паку: первый контакт с непере-мигрированной книгой даёт exit 13 — воркер решений
обязан планировать
13 → migrate → повтор· проекция берёт тот же лок, поэтому правка термина при живом прогоне даёт 12 даже на превью, на всю длину прогона · таймаут: 1.6с на 5 000 термов банка, 9.1с на 20 000, бюджет платформы на вызов движка — 60с (runs/reconcile.go:280) ·bank-applyсоздаёт<project_db>.lockпри первом касании никогда не запускавшегося проекта. - ⚠ БАТАРЕЯ НЕ ДЕТЕРМИНИРОВАННО ЗЕЛЕНА, и это НЕ пак — прочесть ДО пере-рана. Два
пред-существующих
-race-флейка вinternal/storeсрабатывают примерно раз на 2–4 полных прогона (go test ./... -race -count=1×4 → 1 чист, 2-й красный). Красное именно в этих двух тестах ожидаемо; любое другое красное — регрессия. Тесты НЕ трогал (D39.121).TestTheSeamIsNotChargedToTheStoreOperationBudget,TestKillMinus9LosesAtMostOneCall. Чередующийся A/B наgit worktreeHEAD: оба падают на HEAD не реже (kill92/30 против 0/30;seam2/30 против 1/30; без-race0/30). Покрытие:LockProject/Releaseэтими тестами не исполняются вовсе. ⚠ 0/30 против 2/30 НЕ значит «пак улучшает» — на таком числе это шум (Фишер p≈0.5); достаточно того, что флейк живёт на дереве, которого пак не касался. - Гейт
TestDispatchCommandsCoversTheSwitchслеп кcaseс константой (считает только*ast.BasicLit) — команда, объявленная константой, невидима гейту, чей смысл в дефекте строки 176. Я вернулся к литералу (конвенция файла), дыра не эксплуатируется. redrive— платный путь без канала ключей аргументом. Промт: «флаг только уtranslate», сделано ровно так; канал у него есть (конвенционный.env), платформа его не зовёт. Развилка следующему паку.- Однострочник при лендинге (не блокер): документ решений читается неограниченным
os.ReadFile, тогда как платформа свои чтения от движка капает. Документ на 10 000 решений ≈2МБ ⇒ кап 1–16МБ.
⚠ ПРАВИЛА, ВЫНЕСЕННЫЕ ИНЦИДЕНТАМИ — для следующих сессий, не про этот пак. Прогон харнесса мутаций
был убит таймаутом, finally не отработал, и посадка просидела в дереве ~40 минут, выглядя
продакшн-кодом; сторонний ревьюер снял с него замеры и доложил несуществующий дефект. Поймал посадку
собственный пин пака. Отсюда три правила: харнесс мутаций обязан фингерпринтить цели (починено) ·
снимай отпечаток дерева перед сборкой бинаря замера (на этом наступили обе стороны) · одиночный
прогон — не замер (атрибуцию флейков я дважды вывел неверно из одиночных прогонов).
Что НЕ доказано: рантайм-вердикт по платному пути — PLAUSIBLE. Доказано, что ключ из
--keys-file доезжает до заголовка и что без флага прогон отказывает конфигом; не доказано, что
реальный провайдер примет ключ и что SaaS положит файл по этому пути — платформенная половина строки
211, чужая зона. Живой translate не гонялся: санкции на платные вызовы у пака нет. (24.08)
⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ БЭКЕНДА. Четыре движковых дефекта, все сверены моими командами.
-
ОТВЕТ НА ПИНГ ПОЛИГОНА: дефект ПОДТВЕРЖДЁН, он ваш. Эскалация на 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 и починил у себя по вендор-доке; их находка верна. -
Род не производится авто-путём вовсе (строка 210, дополнена 23.08): замка ДВА и они независимы, а поле
Genderв схеме сида УЖЕ есть — ось мертва в ПИСАТЕЛЯХ, не в формате. -
Выпавший член юнита исчезает из отгружаемых байт БЕЗ внутритекстового маркера —
pipeline/waverun.go:487-490=the flagged member is DROPPED from the edit. Родня строки 193; для читателя это молчаливая дыра в тексте. -
Загрузчик сида НЕстрогий (строка 212): голый разбор YAML без проверки имён ключей, и
seed-lintэтого не видит. Опечатка в имени поля молча теряется — в файлах, которые владелец правит руками чаще всего.
⚠ Плюс строки 211 (движок на SaaS не получает провайдерских ключей — блокер живого прогона) и 213 (платформа дублирует вашу конвенцию пути): обе половинные, движковая часть ваша.
⛔ ПИНГ ОРКЕСТРАТОРА №20 — 22.08, ЗОНЕ БЭКЕНДА: три ваших дока разошлись с деревом (аудит доков, сверено кодом; рукой не лезу — ваша зона).
backend/README.md, карта пакетов объявлена сверенной по коду, а в ней НЕТinternal/runevents— эмиттера журнала прогона, то есть ВСЕГО шва движок↔платформа (D39.131), — и нет второго бинаряcmd/tmvet. Греп:runeventsиtmvetв файле по нулю вхождений, при том что оба каталога в дереве.backend/README.md, «Известный техдолг» держит живыми Cyrillic-хардкоды DC-чекеров, которые сам код называет ПОГАШЕННЫМ долгом (фаза 2 общности, D39.64:isRuTargetудалён,TargetActive/TargetScriptNonLatin), и оба егоfile:line-якоря указывают мимо.- (дописано 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, так что дублировать правку не надо, но док обязан сойтись с ними. backend/docs/components.puml— диаграмма, которую карта доков продаёт как правимую «одним коммитом с кодом», отстала примерно на месяц: старый exit-контракт (без 4/5 и полосы 10–19), неполный список глаголов CLI, удалённый паком-17failover.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.)
(Закрытые бэкенд-записи 01–02.08 — общность фаза 2 · качество банка (оба этапа) · фикс-пак банка · finding-1 · пакет-чекеров · мелкая пачка · coldrun-b · ручка эффорта — в archive/PROGRESS-2026-08-01-02.md.)
(Закрытые бэкенд-записи 14–15.08 — эмиттер шва (D39.131) · tmctl migrate (D39.134) · пере-пин цен DeepSeek (D39.137) — в archive/PROGRESS-2026-08-14-15.md; вынесено D39.139.)
(Закрытая хроника пакетов №0–№4 (04–10.07) перенесена в archive/PROGRESS-2026-07-04-10.md — ревизия D31.)
(Хроника пакетов 11–12.07 (пакет-5, D15.2-этап-А, санитайзер-фикс D33) — в archive/PROGRESS-2026-07-10-13.md.) (Трек-A пак-1/1.5 (D39.2/D39.5) — см. записи оркестратора №6 выше.)
(Хроника бэкенд-паков 11–16 (19–25.07, вкл. отчёт-запись пака-15) — в archive/PROGRESS-2026-07-13-25.md; отчёты паков — archive/reports/.)
(Закрытые записи 25–31.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, дерево чисто).
- Прод-пакет
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). - Сборка из
BookExport—backend/internal/pipeline/bookbuild.go:Runner.BuildBook(formats, partial)→Export(false)→ дыры → отказ либо файл. ЗаписьwriteFileAtomicрядом с ProjectDB (<project_db>.book.<fmt>, как.manifest.json/.bank.json), путь публикуется вStatusArtifacts.BookFiles(book_files: map формат→путь, аддитивно) — приор промта принят: платформа пути не выводит сама (17 §1). Существующий файл — перезапись: производный детерминированный артефакт, как манифест; бэкап отказывает потому, что он точка восстановления, а файл книги пересобирается из стора. - Аддитивные поля
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: последнее изменение отгруженного текста — факт стора, не часов процесса. - Глагол
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). - Отказ — новый
pipeline.RefusalBookIncomplete = "book_incomplete"→ exit 16 (полоса 10–19; таблица тотальна поTestTheRefusalTableIsTotal); сообщение перечисляет дырыглава/юнит/причина. - Читательские слова — ДАННЫЕ:
<langpack_root>/<target_lang>/reader.txt(key<TAB>value, плейсхолдеры{chapter} {unit} {dropped} {holes} {total} {ghost}), читает ОТДЕЛЬНЫЙ загрузчикlang.LoadReaderWords; вPack.Version()НЕ фолдится (пин тестом «наличие reader.txt не меняет Version()» +current_snapshotminirun до/после). Ключ — язык ЦЕЛИ, не пара: слова читает читатель целевого языка, и они не зависят от исходного. Поставляю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, navproperties="nav"⛔ не в spine, XHTML на главу:<h1>+<p>);txt.go— чистый текст;model.go— модельBook/Chapter,Blocks(i)(единственное определение содержимого документа главы),CleanText(общая чистка XML-недопустимых символов для обоих форматов), экранирование.internal/chunk/chunktest/epub.goпереведён наbookfile.NewContainer; тест-декои (OPF-заглушка,<title>c</title><style>) остались тестовыми; 9 импортёров зелёные без правки ассертов.internal/lang/reader.go—LoadReaderWords(root, targetLang):<langpack_root>/<target>/reader.txt, читается ОТДЕЛЬНО отLoad(вPack.Version()не фолдится по построению —Loadчитает только<src>/и<pair>/);DefaultReaderWords()— несловесная форма (символ + числа; тест пинит отсутствие букв). Данные:configs/langpacks/ru/reader.txt(7 ключей:hole.{pending,withheld,incomplete,stale,ghost},notice.{holes,ghost}).internal/pipeline/holes.go— ОДИН предикат дырыUnitHole/HoleCounts(порядок =renderExport);render.goсчитает шапку--plaintextчерез него (вывод побайтово прежний).internal/pipeline/export.go— аддитивно (omitempty):ChunkExport.Heading(heading— литералu.Members[0].Heading, у pending тоже),BookExport.GhostUnits(ghost_units, ключи ghost-строк, сортированы),BookExport.TextModified(text_modified— maxchunk_status.updated_atпо ФИНАЛЬНЫМ строкам, вошедшим в экспорт);exportConfigDrift: строка со стадией, которой нет в текущем конфиге, — drift (находка 10 ниже).store/chunkstatus.goдочитываетupdated_atвChunkStatus.UpdatedAt(колонкаNOT NULL DEFAULT datetime('now'),migrate.go:37; миграции — толькоADD COLUMN: у miniruntext_modified=2026-07-25T19:17:53Z, время боевого прогона, не миграции 30.08).internal/pipeline/bookbuild.go—Runner.BuildBook(BuildOptions{Formats, Out, Partial}):Export(false)→staleUnits→assembleBook(чистая функция, возвращает список дыр) → отказRefusalBookIncomplete(refusal.go, exit 16) либо стейдж всех форматов + общий коммит в<project_db>.book.<fmt>(не запрошенные соседи удаляются — рядом с БД лежит ОДНА сборка);StatusArtifacts.BookFiles(book_files, map формат→абс. путь; вstatus --jsonиmanifest --json).--out— только НОВЫЙ файл (существующий путь любого рода, несуществующий каталог →config_invalid, exit 10). Сбой между двумяrename→write_incomplete(exit 15) с перечнем landed/not landed.cmd/tmctl: глаголbuild --config b.yaml [--format epub,txt] [--out path] [--partial]; stdout — JSONtm-build-v1(files,total/pending/withheld/incomplete/stale_units,stale_unknown,ghost_rows,config_drift,complete,text_modified); чужим глаголам новые флаги отказываются,buildотказывает--plaintext/--pairs/--json; usage-строка расширена (invocation_test.go— контрактное расширение по собственному комментарию теста «Every one is a deliberate contract extension»; это ЕДИНСТВЕННАЯ правка существующего теста).backend/README.md— строкаcmd/tmctlдополненаbuild.
Политика (§3.2) как реализована — ШЕСТЬ состояний, не пять: дыры — pending · withheld (Disposition!="pending" && FinalText=="", плюс юнит, чей текст после чистки не даёт ни одного абзаца) · incomplete (DroppedMembers>0) · stale (финальная строка сделана под исходник, которого в книге уже нет: content_hash строки ≠ cachedRenderedContentHashes — тот же факт, по которому резюм перекупает юнит; считается ТОЛЬКО без дрейфа, под дрейфом stale_unknown: true, никогда не «нет») · ghost (при укрупнении нарезки строка-лидер несёт текст прежнего, более короткого юнита; остаток лежит в ghost-строках и в файл не попадает: acceptance 57→37, 20 строк при pending 0). По умолчанию — ОТКАЗ при любой дыре с перечнем chapter N unit M: <вид> (<причина>) и ghost-ключами (≤50 строк); --partial — файл с пометкой (первые абзацы первого документа spine + dc:description; в txt — после заголовка книги) и маркером на месте КАЖДОЙ дыры (pending/withheld/stale — вместо текста; incomplete — перед текстом; ghost — в конце главы, счётчиком). drift — НЕ дыра (текст файла = текст прогона): в JSON-конверте и WARN, в файле нет. Оглавление/<h1> при Heading=="" — номер главы цифрами. Абзац — непустая строка FinalText (\r\n/\r/\n). dc:title — book.yaml как есть (на стенде 蛊真人), dc:language = target_lang (форма тега BCP 47 проверяется, иначе config_invalid), dc:identifier = urn:textmachine:book:<book_id>, автора нет.
Сверка состава против §3 (пункт → сделано → чем доказано; все команды — на копиях в песочнице <SB>/stand, бинарь go build -o <SB>/stand/tmctl ./cmd/tmctl).
| §3 | Сделано | Доказательство |
|---|---|---|
3.1 EPUB + чистый txt из BookExport, ОДИН файл на формат, из tmctl |
да: tmctl build → <project_db>.book.epub / .book.txt |
build-minirun.json: 2 пути, complete: true; java -jar epubcheck-5.3.0/epubcheck.jar <book>.epub → 0 fatals / 0 errors / 0 warnings / 0 infos на всех трёх EPUB (minirun, acceptance --partial, coldrun-a --partial) |
3.1 без аудит-баннеров/флагов/gapMarker в txt |
да | TestTXTShape, TestBuildBookRoundTrips… (нет === CHAPTER, TEXT MISSING, pending, flagged); TestBuildRefusesTheGapAndPartialMarksIt — реальный c-lite прогон бинарём: ⚠ 1.0 −1 перед уцелевшим текстом, ни INCOMPLETE, ни cjk_artifact |
3.1 --plaintext не тронут |
да | cmp вывода бинаря из git archive HEAD и нового: IDENTICAL на minirun (171932 б), acceptance (354821 б), coldrun-a (1077 б) |
3.1 основа — chunktest/epub.go, OCF в прод без testing |
да | TestEPUBFixtureMimetypeIsOCFConformant и 9 импортёров зелёные без правки; TestContainerIsOCFConformant (флаги 0, extra 0, mimetype на фиксированных смещениях) |
| 3.1 детерминизм двумя ПРОЦЕССАМИ | да | cmp first/<f> <copy>/<f> — IDENTICAL для 6 файлов (две сборки в двух процессах); zip-таймметки 1980-00-00 00:00; sha256 EPUB minirun 084ff0a9…, совпал с независимой сборкой ревьюера |
3.2 пять (шесть) состояний, отказ по умолчанию / --partial |
да | acceptance: build → exit 16 «pending=0 withheld=1 incomplete=1 stale=0 ghost_rows=20 of total_units=37 (stale: UNKNOWN …) · chapter 1 unit 0: withheld (cjk_artifact) · chapter 10 unit 0: incomplete — 1 member(s)… (length) · ghost rows: 1/1 1/3 …»; --partial → ⚠ 2/37, ⚠ +20, ⚠ 1.0, ⚠ 10.0 −1, ⚠ 10 +1…; coldrun-a: exit 16 (14 pending) / --partial со словесными маркерами из reader.txt; minirun (только drift): записан по умолчанию, config_drift: true, stale_unknown: true, в файле ⚠ нет; тесты TestBuildBookRefusesAPending…, …WithheldUnitIsAHole, …GhostRowsAreAHole (посадка построена: усадка исходника после прогона), …StaleUnitsAreAHole, …DriftIsReportedNotMarked, …RemovedStageIsDrift, TestAssembleBookMarksEveryHoleKind |
| 3.2 не в снапшот | да | current_snapshot minirun с ru/reader.txt и без — bb0b3b2aaf0d… оба раза (копия корня пака в песочнице); TestReaderWordsDoNotMoveThePackVersion |
| 3.2 книга без langpack — несловесно | да | acceptance (langpack_root не задан): ⚠ 1.0, ⚠ 10.0 −1, ⚠ 2/37; TestReaderWordsAbsentIsTheNonVerbalForm |
3.2 маркер не в FinalText/export.go |
да | JSON HEAD vs new: existing equal True, добавлены только text_modified, heading, ghost_units |
3.3 ApplyHeading и три точки не тронуты; срез только известного префикса |
да | git diff HEAD -- backend/internal/chunk/chunker.go backend/internal/chunk/ingest.go backend/internal/pipeline/waverun.go пуст; вызов ApplyHeading в export.go — контекстная строка; minirun: 10 заголовков срезаны, круг 402/402; TestAssembleBookMarksEveryHoleKind (упоминание заголовка в тексте не срезается) |
3.3 Heading=="" → номер цифрами в nav и <h1> |
да | acceptance nav 1…25; minirun/coldrun-a — Глава N из heading |
3.3-бис пять требований EPUB 3; nav не в spine; dc:language из TargetLang |
да | TestEPUBCarriesTheFiveRequiredThingsAndKeepsNavOutOfTheSpine; OPF minirun: urn:textmachine:book:guzhenren-minirun, 蛊真人, ru, 2026-07-25T19:17:53Z; epubcheck |
| 3.4 место/атомарность/конверт/глагол/коды | да | status --json/manifest --json копии minirun: book_files: {epub, txt}; TestTheRefusalTableIsTotal, TestDispatchCommandsCoversTheSwitch, TestParseNoArgsUsage; TestBuildBookLeavesOneBuildBesideTheDatabase; TestBuildBookOutNamesANewFile |
3.5 платформа не тронута; tm-export-v1 аддитивно; пары — данные; без зависимостей |
да | git status — только backend/ и docs/PROGRESS.md; go.mod не менялся; греп читательских слов по новому Go-коду — только комментарии |
§4 make battery (-race) |
да, 2 настоящих прогона | battery-1.log (до правок ревью) и battery-3.log (после всех правок): BATTERY_EXIT=0, 18 пакетов, скипы — три прежних стендовых (TestMinerFullBookParity, TestHelperEventsRun, TestHelperKillLoop). ⚠ «Батарея №2» была ложным прогоном: make из корня репо (No rule to make target), мой echo замаскировал код — поймано по логу, перезапущено из backend/ |
§4 дифф ^func Test |
ни один не исчез | git diff HEAD -- '*_test.go' | grep '^-func Test' пуст; func Test в backend/: 1017 → 1044 (+27) |
| §4 круг ингестом равенством | да | tmroundtrip (собран go build -overlay, файла в дереве нет): minirun 10/10 глав, 402 абзаца; acceptance --partial 25/25, 1568; coldrun-a --partial 10/10, 25 |
§4 git -C books status до/после |
см. obstacle | 6 строк D …-wal/-shm до; после — 4: у acceptance -wal/-shm ПОЯВИЛИСЬ (идентичны трекнутым; БД не изменена) |
| §4 хирургия конфигов в копиях | список | minirun/coldrun-a book.yaml: удалены mined_delta:/mined_rejects:; acceptance pipeline-acceptance.yaml: удалены stm_depth: 2, overlap_tokens: 200, оба prompt:; добавлен acceptance/pairs/zh-ru.yaml (копия minirun/pairs с prompts_root на backend/prompts этого дерева); все абсолютные пути переписаны на песочницу/этот ворктри, греп по копиям пуст; миграции v10/v7/v14 → v16, backups/ — в песочнице |
| §4 промежуточное и финальное ревью субагентом | да, Fable 5 ×2 | ниже |
Промежуточное ревью (Fable 5, исполнением) — 8 находок, все закрыты: (1) MAJOR: --format-подмножество оставляло соседний формат прежней сборки рядом с БД при равноправном book_files → стейдж всех форматов + общий коммит, не запрошенные соседи удаляются, пин TestBuildBookLeavesOneBuildBesideTheDatabase; (2) text_modified считался по всем строкам всех стадий → только по финальным строкам экспорта (acceptance 20:41:47Z→20:40:00Z, coldrun-a — эпоха: ничего не отгружено); (3) пустой title → exit 1 → config_invalid с именем ключа, то же для target_lang не в форме BCP 47; (4) mimetype с data-descriptor → CreateRaw; (5) control-символы срезались только в EPUB → CleanText один раз для обоих; (6) --format epub,epub → дедуп; (7) backend/README.md без build → дополнено; таблицы exit-кодов в docs/ — пинг; (8) подтверждено верным: UnitHole = арифметика renderExport, DispOK с пустым текстом недостижим, нулевые таймметки, reader.txt вне Load.
Финальный адверсариальный проход (Fable 5; overlay-тесты, свой бинарь, epubcheck на adversarial-книгах: краевые символы, RTL, 2000 глав) — 10 находок:
- 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»). Не моя строка — пинг. - MAJOR —
--outперезаписывал что угодно, включая БД проекта («file is not a database») и исходник. Исправлено:--outтолько для НОВОГО файла; существующий путь / отсутствующий каталог →config_invalid; место рядом с БД по-прежнему перезаписывается. ПинTestBuildBookOutNamesANewFile(БД, исходник, чужой файл, несуществующий каталог; ни одного.tmp-после). Стенд:--out <project_db>→ exit 10, БД цела. - MINOR — сбой на фазе коммита второго формата оставлял смешанную сборку и exit 1. Исправлено:
write_incomplete(exit 15) с перечнем landed/not landed; комментарий файла говорит правду. Тестом не покрыто («не проверено»: сбойrenameв только что записанном каталоге без инъекции не воспроизводится). - MINOR — юнит с непустым
FinalText, дающий ноль абзацев (control-символы/U+FFFF), не был дырой. Исправлено: сборщик читает юнит по тому, что тот реально положил (ноль абзацев → withheld); счётчики отчёта — из списка дыр сборщика;HoleCountsостался ровно арифметикойrenderExport. - MINOR — голый
\r: txt нёс его, XML нормализует → форматы расходились. Исправлено:\r\n/\r/\n— конец строки. - MINOR —
--outв несуществующий каталог → exit 1. Исправлено:config_invalid. - NIT —
buildмолча принимал--plaintext/--pairs/--json. Исправлено: отказ. - NIT —
dc:titleс паддингом vs обрезанный txt. Исправлено:TrimSpaceв модели и EPUB. - NIT — ghost-маркер по номеру главы СТАРОЙ нарезки может встать в соседнюю главу при усадке. Не исправлено, задокументировано (
assembleBook): книга всё равно помечена неполной, ложна лишь координата; иного источника координаты у ghost-строки нет. - 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):
- Лестница
run_complete / structurally_complete / quality_reviewed / publishable: писатель различает ровно «без дыр» (complete: true) и «с дырами» (отказ /--partialс пометкой). Что из лестницы печатать на титуле и в имени файла и какая ступень даёт копию БЕЗ пометки — решение владельца; в код не внесено намеренно. - Лейбл главы без заголовка (К-3): сейчас — номер цифрами. Словесный шаблон — одна строка в
reader.txt, если ратифицируется; не сделано, чтобы не решать К-3 данными. - Ghost и stale как дыры по умолчанию: довод — оба дают «молча полную» книгу (acceptance: 20 строк не в файле при
pending 0; правка исходника на месте: старый перевод подcomplete: true). Если владелец хочет «предупреждение, не отказ», — по строке вBuildBook. - Направление письма для RTL-цели: поле данных цели (в
reader.txtили рядом), не Go — когда появится RTL-пара.
Пинги оркестратору (чужие зоны, не правил): (а) exit-код 16 book_incomplete в таблицы docs/glossary.md:64 и docs/architecture/14-api-contract/README.md:731,758; (б) docs/architecture/09-target-architecture.md: слой «писатель книги» построен; следующий шаг — дверь платформы createExport/getExport + export_formats из bookfile.Formats; дверь обязана СТРОИТЬ (tmctl build --format <f> [--partial]) и отдавать то, что построила, — не читать файл, лежащий рядом с БД (копия прежней сборки); (в) projectRebill/status слепы к правке исходника на месте при том же снапшоте (перекупка без предупреждения о деньгах) — строка бэклога; (г) Status.checkWave не видит исчезнувшую стадию (то же, что находка 10 у Export) — строка бэклога; (д) строки 160/201 не тронуты, Heading в tm-export-v1 — тот же временный рендер (D39.122 п.2д), помечено в комментарии поля.
📝 ЗАПИСКА-ПЛАН — пак «деньги» (промт docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md, D39.165 §1б + строка 231), сессия textmachine-e4, 28.08
Скоуп: backend/ — §3.1 потолок ОБЪЁМА рядом с денежным · §3.2 смета пере-прохода без покупки. platform/ не трогаю (проводка CeilingChapters — не мой заказ, честная граница §0 промта принята).
§3.1 — В ЧЁМ МЕРИТЬ: выходной юнит (ManifestUnit), кандидат (а). Обоснование.
Ровно эта гранулярность и есть то, что движок отгружает и что публикует манифест: ManifestUnit строится итерацией по r.outputUnits(chunks) (backend/internal/pipeline/manifest.go:330-357), то есть по тем же editUnit, которыми идёт волна редактуры; units_total манифеста — то самое поле, из которого интейк платформы пишет chapters.units_total (platform/internal/pgstore/readmodel.go:113,166-175, читал не правил). Значит перевод «N глав → M юнитов» у платформы ТОЧНЫЙ, а не оценочный: манифест снимается tmctl manifest за $0 и без ключей (runner.go:239-244 — models.CheckKeys только if forWrite) ДО первой оплаты.
⚠ Кандидат (в), chunk×stage, отвергнут именно по ловушке, названной в промте. Это биллинговая единица (rebill.go:322), и в ней «10 единиц» — разный объём книги при разной форме конвейера: на юнит приходится len(Members)*nDraftStages + nEditStages позиций (rebill.go:207). Продавая (а) и каппя (в), мы воспроизвели бы дефект §0 слоем ниже. Кандидат (б), чанк, — внутренний артефакт нарезки, покупателю не виден и манифестом не считается.
⚠ Честная оговорка, которую обязан назвать: число юнитов книги зависит от ФОРМЫ конвейера — редакторский конвейер даёт крупные editUnit, черновой-только даёт по юниту на чанк (waverun.go:67-76). Это свойство конфигурации, а не ветвление по паре; но манифест и потолок обязаны сниматься с ОДНОГО деплоя, иначе «10 юнитов» значат разное.
§3.1 — КАК СЧИТАЮТСЯ РЕПИНЫ И РЕТРАИ (прямой ответ на вопрос промта).
- Ретрай и эскалационный хоп — НЕ считаются. Они внутри юнита: ретрай — итерация цикла попыток (
stagerun.go:158-194), эскалация — отдельныйrunAttempt(escalation.go), ноchunk_statusнаchunk×stageпишется ОДНА (stagerun.go:297-308). Довод по существу: объёмный потолок ограничивает ДОСТАВКУ, денежный — ТРАТУ. Хвост качества (1.5%→23% по D39.165 §1) — факт денежный, и у него уже есть свой потолок; если бы ретраи жгли объём, купивший 10 глав получал бы 8, потому что две главы заикнулись, — то есть платил бы КНИГОЙ за качественный хвост движка. Это ровно та подмена, которую пак и лечит. - Репины и $0-резюмы — НЕ считаются. Иначе пере-проход, пере-привязывающий 500 юнитов за $0 (
rebill.go:315-320), сжёг бы потолок, не доставив ничего. - ⇒ Правило одной строкой: потолок считает выходные юниты, по которым ЭТОТ прогон сделает хотя бы один провайдерский вызов.
§3.1 — МЕХАНИЗМ: пре-флайт скоуп, а не счётчик в воркере.
Допуск считается ОДИН раз ДО волн, по состоянию стора, и даёт множество юнитов-в-работе; волны просто пропускают то, что вне скоупа. Предикат «$0-обслуживаемый» берётся из УЖЕ существующего — того же, которым считает projectRebill/repin, — чтобы объёмный стоп и денежная смета не могли разойтись двумя определениями.
⚠ Почему не счётчик со слотами в воркере (рассмотрено и отвергнуто): волна параллельна (runWave, waverun.go:236-309), и при отказе в слоте конкурентно розданные элементы пропадали бы НАВСЕГДА — при workers=8 и потолке 1 семь юнитов оказались бы пропущены, а освободившийся после $0-репина слот уже некому было бы отдать. Пре-флайт эту гонку растворяет.
Грабля LiteLLM исполнена конструктивно: ни один элемент вне скоупа не НАЧИНАЕТСЯ, поэтому пере-бой на один вызов невозможен в принципе, а не по проверке.
Грабля OpenHands исполнена: остановка называет свой потолок — в BookResult, в строке лога и в выводе CLI.
§3.1 — ФОРМА ОТВЕТА ЧЕРЕЗ ШОВ: беру решение оркестратора и ПОДТВЕРЖДАЮ его исполнением.
Код выхода 0, замороженный словарь (main.go:115-140) не трогается. Проверил то, о чём он просил пингнуть, — не ломает: exit 0 у платформы = статус ПРОГОНА ready (platform/internal/runs/reconcile.go:703-706, читал не правил), а не «книга дочитана»; полоса книги едет событиями юнитов, поэтому дочитанной книга не объявляется и следующую покупку не блокирует.
⚠ Словарь потока НЕ трогаю в этом паке. Новое значение Finished.Outcome было бы минором по правилу runevents.go:29-40, но у платформы неизвестный ВЫХОДНОЙ код читается как отказ (ingest/exit.go:168-169), и множить развилки в чужом пакете без контрактного минора — не мой заказ (§3.3). Различение живёт в отчёте и логе, как и решил оркестратор.
Ортогональность цела: --ceiling-usd остаётся кумулятивным книжным капом и судит в Reserve (ledger.go:56-72); объёмный потолок леджера не касается вовсе и живёт до волн. Денег не считает, холдов платформы не двигает.
§3.2 — ФОРМА: чинится СВЁРТКА, а не заводится глагол. Это дешевле всех трёх кандидатов промта.
Слепое окно — не в том, что status пишущий, а в том, что он сворачивает НЕ ТО: projectStoredMemory (status.go:815-826) материализует СОХРАНЁННЫЙ глоссарий, то есть фолд ПРОШЛОГО прогона, тогда как ближайший translate пере-сеет банк из ФАЙЛОВ (seedGlossary) и сделает это ДО гейта согласия (bookrun.go: seedGlossary → … → checkRebillConsent). Собственное оправдание комментария — «exactly as it does for translate itself» — неверно: translate правку файла как раз ВИДИТ. Значит сегодня status НЕДО-считывает то, что translate реально выставит, и это денежный дефект, а не отсутствующая функция.
Лечение: фолд тех же входов, что берёт seedGlossary (сид + рубя + мined-дельта + авто-банк), — В ПАМЯТИ, без единой записи. status при этом остаётся ЧИСТО ЧИТАЮЩИМ: он читает файлы вместо стора, а фолд не пишет никуда. Возражение оркестратора против «флага у status» снимается тем, что оно стояло на посылке «свёртка идёт через стор» — она не обязана.
⚠ Записи не будет и не может быть: status открывает стор через NewReadOnlyRunner → OpenReadOnly с PRAGMA query_only=1 (store/store.go:117-166), и на состоянии §4.3 (прогон СОСТОЯЛСЯ ⇒ база есть) фолбэк на Open не срабатывает вовсе — записей НОЛЬ, даже first-touch.
Родня 124(в) закрывается тем же: status уже считает ре-билл при ЛЮБОМ дрейфе, не только банковом, — то есть после фикса он и есть «оценить пере-проход, ничего не покупая», ради которого поименован resnapshot --dry-run.
⚠ Третий кандидат (bank-apply --dry-run) рассмотрен ПЕРВЫМ, как заказано, и отклонён с доводом: он требует --decisions, а в продуктовом потоке D39.165 §3 решения к моменту продажи УЖЕ на диске — документа больше нет. Плюс он Runner-free и не держит ни статусов, ни манифеста, ни конфига (bankdecisions.go:294-314), то есть под смету пришлось бы втащить в него весь Runner. Ту же проекцию он сможет позвать позже — она будет одной функцией.
§3.2 — ТОЖДЕСТВО БЕСПЛАТНОЙ СМЕТЫ И ПЛАТНОГО ПРОГОНА: доказываю, а не предполагаю.
Канонический фолд идёт через стор и там ПЕРЕ-СОРТИРУЕТСЯ: ORDER BY src, sense, since_ch, until_ch, status, dst (store/glossary.go:223). Порядок несущий дважды — MaterializeBank порядок входа НЕ трогает (membank/memory.go:283-346), а дальше он определяет и хеш версии (ComputeVersionScopedIn стримит SHA-256 по строкам в порядке итерации), и ЛИНИИ отрендеренного блока через стабильную сортировку бюджета (memory.go:608-619), которую режет токенный бюджет.
Почему тождество ДОКАЗУЕМО, а не правдоподобно: таблица несёт UNIQUE (book_id, src, sense, since_ch, until_ch) (store/migrate.go:203), поэтому префикс из четырёх колонок НЕ МОЖЕТ дать ничью — порядок ТОТАЛЬНЫЙ и полностью определён значениями, а не rowid и не порядком вставки. COLLATE в схеме нет нигде ⇒ сравнение текста побайтное, как и < в Go. Алиасы читаются ORDER BY term_id, alias, внутри записи это сортировка по alias (glossary.go:247-249). glossary.id в хеш не входит намеренно (memory.go:360-363).
⇒ Фолд в памяти обязан: собрать тот же набор значений + отсортировать по (Src, Sense, SinceCh, UntilCh, Status, Dst) + отсортировать алиасы + то же для voices/pairs по их собственным ключам.
Проверка — ДИФФЕРЕНЦИАЛЬНАЯ, исполнением: тест гоняет оба пути на одном состоянии и требует совпадения построчно (кроме ID), совпадения Version()/BaseVersion(), совпадения отрендеренных байтов и совпадения самого числа projectRebill. Ассерт равенства, а не «похоже».
⚠ Пишущий путь НЕ трогаю: порядок ВСТАВКИ в seedGlossary остаётся как есть, иначе поехали бы glossary.id и телеметрия injected_ids. Добавляется чистая функция упорядочения для читающей стороны.
Находки по дороге (в блате пака, называю сразу):
- ⚠
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». - ⚠ При
snapshot_driftуstatusпамять вообще не материализуется:projectStoredMemoryзовётся подif !rep.SnapshotDrift(status.go:635), аprojectRebill— безусловно при наличии строк (status.go:673), иmemoryVersion()приnilотдаёт хеш ПУСТОГО банка (snapshot.go:190-195) ⇒ книга объявляется пере-оплаченной целиком. - ⚠ Мина
omitemptyподтверждена исполнением чтения: три РАЗНЫХ состояния — «не запускалось», «посчитано и ноль», «расчёт упал» (status.go:668-681логирует и оставляет нули) — на проводе неотличимы. Это ровно цена D39.166 п.2. Первые две мой фикс §3.2 снимает по построению (фолд считается безусловно и ставит ОБА банка); третью закрываю формой поля.
НЕ делаю: калибровку $0.03 (строка 202) · цену от объёма исходника · строки 228/230/141/131 · словарь кодов выхода · словарь потока событий · platform/ · коммиты. Незакоммиченную работу полигона не касаюсь.
✅ ИТОГ — пак «деньги» (промт docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md, D39.165 §1б + строка 231), сессия textmachine-e4, 29.08
Батарея зоны: make battery — 21 пакет из 21, 0 FAIL, линтер 0 issues, test -race зелёный целиком.
⚠ Проверяю ПОЛНОТОЙ СПИСКА, а не отсутствием FAIL, и это правило стоило мне двух ложных выводов. Первый «зелёный» прогон был на 20 пакетов из 21 с FAIL в хвосте, который обёртка подала как exit code 0. Сверка: comm -23 <(go list ./...) <(grep -E "^(ok|\?)" <лог>) — пусто.
⚠ И про internal/store. Он дважды ронял батарею (TestTheSeamIsNotChargedToTheStoreOperationBudget, apply migration 14: context deadline exceeded), и я сначала списала это на память — неверно. Проверила как надо: развернула ЧИСТЫЙ HEAD через git archive и погоняла тот же тест в обоих деревьях вперемежку — моё 4/5 падений, HEAD 4/5, то есть от моих правок не зависит. Тест ужимает opTimeout до 150 мс (migrate_test.go:408-429) и под параллельной нагрузкой не укладывается; на разгруженной машине проходит 3/3, и итоговая батарея выше — с ним. Каталог internal/store я не трогала ни строкой. Оркестратору отправлено строкой.
Пропущенные тесты, как требует Makefile, называю: TestMinerFullBookParity (нет стендовых данных), TestHelperEventsRun, TestHelperKillLoop (точки входа подпроцессов, вне обычного прогона).
Дерево ПОСЛЕ трёх кругов: 20 отслеживаемых файлов правлено (+761/−204) и 5 заведено (3106 строк); новых тестов 36 (30 в internal/pipeline, 6 в cmd/tmctl). Числа взяты командами: git diff --stat -- backend/ для правленых (новые в него не входят — они untracked), wc -l для заведённых, grep -c '^func Test' для тестов. Не коммичу — на лендинг. ⚠ Числа выше по тексту, где они расходятся с этими, относятся к ПЕРВОМУ кругу и оставлены как след, а не как состояние.
Мутационных посадок 26 (11 на первом круге, 15 на втором и третьем), каждая — на свой пункт, вердикт по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи: снятие сортировки фолда · потолок на единицу больше · снятие допуска в черновой волне · снятие допуска в редакторской · «свободные юниты тратят грант» · возврат к прежней терминальности (воспроизводит перерасход) · !ok → ok && (воспроизводит перерасход) · допуск только лидера (воспроизводит усечённую главу) · фильтр согласия в тождество (воспроизводит отказ законной работе) · перестановка лога замечаний (воспроизводит потерю диагностики) · снятие отказа несогласованной форме конвейера. Все одиннадцать падают, и падает ИМЕННО адресный тест.
Таблица комплектности против §3
| Пункт промта | Что сделано | Каким ИСПОЛНЕНИЕМ подтверждено |
|---|---|---|
| §3.1 потолок ОБЪЁМА рядом с денежным | Флаг --max-units N у translate; единица — выходной юнит (ManifestUnit=editUnit, units_total манифеста); допуск считается ДО волн (planVolume, backend/internal/pipeline/volume.go), волны не начинают элемент вне скоупа |
TestTheVolumeCeilingStopsAtWhatWasBought — 5 глав, потолок 2: отгружено ровно 2, res.Volume заполнен, у глав 3–5 нет ни одной строки chunk_status, вызовов провайдера ровно 4 (2 юнита × 2 стадии) |
| §3.1 — «проверять ПЕРЕД началом единицы» (грабля LiteLLM) | Свойство конструктивное: элемент вне скоупа не начинается вовсе | Тот же тест, клауза (3): отсутствие строк у N+1 и точный счёт вызовов; мутация «потолок на единицу больше» (Granted <= MaxUnits) — падают 3 теста |
| §3.1 — «остановка НАЗЫВАЕТ свой потолок» (грабля OpenHands) | BookResult.Volume (VolumeStop{MaxUnits, Delivered, Reworked, Free, LeftFresh, LeftRework} — ⚠ поля ПЕРЕСТРОЕНЫ на втором круге, см. находку 2), строка лога, строка вывода CLI |
TestAVolumeStopIsNotAMoneyStop — денежный стоп остаётся типизированным *CeilingHalt (exit 4), объёмный не ошибка вовсе и несёт res.Volume |
| §3.1 — счёт РЕПИНОВ | Не считаются: считаются только юниты, по которым прогон сделает ≥1 вызов | TestAResumeDoesNotSpendItsVolumeOnFreeUnits — вторая покупка 2 юнитов на книге, где 2 уже сделаны: Free=2, Delivered=2, LeftFresh=1, свежих вызовов ровно 4. Мутация «свободные тратят грант» — тест падает |
| §3.1 — счёт РЕТРАЕВ | Не считаются: ретрай/эскалация внутри юнита | TestRetriesDoNotBurnTheVolumeCeiling — провайдер отдаёт finish_reason: length на первом черновике: юнитов всё равно 2, вызовов >4 |
| §3.1 — форма ответа через шов | Решение оркестратора исполнено: код выхода 0, словарь main.go:115-140 не тронут, словарь runevents не тронут |
res.ExitCode()==0 в тесте; сверено чтением platform/internal/runs/reconcile.go:703-706 — exit 0 = статус ПРОГОНА ready, книга дочитанной не объявляется |
| §3.1 — ортогональность | --max-units не касается леджера, резерваций и кумулятивной семантики --ceiling-usd |
TestAVolumeStopIsNotAMoneyStop; MaxUnits не входит ни в BriefHash, ни в снапшот-пейлоад |
| §3.2 смета БЕЗ покупки | Читающие поверхности сворачивают банк в памяти из тех же входов, что seedGlossary (projectFoldedMemory), вместо СОХРАНЁННОГО глоссария |
TestABankEditIsVisibleToTheEstimateBeforeAnythingIsBought — прогон состоялся → bank-apply → старая проекция 0, новая ≠0 на ОДНОМ состоянии |
| §3.2 — $0 абсолютный | Ни одного провайдерского вызова | Тот же тест: rec.count() не изменился; леджер (committed/reserved) не двинулся |
| §3.2 — read-only сверх first-touch | Ни одной записи | Тот же тест: sha256 файла .db до и после совпадает; status идёт через NewReadOnlyRunner→OpenReadOnly с PRAGMA query_only=1 |
| §3.2 — ключей не требует | Путь не строит ни одного LLM-клиента (models.CheckKeys только if forWrite) |
Пин cmd/tmctl/keysfile_test.go не тронут и зелёный |
| §3.2 — тождество сметы и прогона | storeOrder воспроизводит ORDER BY стора; фолд один на оба пути |
TestTheInMemoryFoldIsIdenticalToTheStoreRoundTrip — оба пути на одном состоянии: строки, voices, pairs, Version(), BaseVersion() и карта отрендеренных content-hash совпадают. Мутация «убрать сортировку» — падает с топичным «row 0 differs» |
§3.2 — мина omitempty |
rebill_units/rebill_usd больше не omitempty; добавлено rebill_basis (pending/stored/none/failed) |
TestTheWireTellsFreeFromUnknown — «нечего пере-оплачивать» и «посчитано, и это ноль» на проводе различимы |
| §3.2 — книга остаётся инспектируемой | Отказ фолда не валит чтение: фолбэк на сохранённый глоссарий + честный basis + WARN |
TestTheEstimateSurvivesABookWhoseFoldRefuses — сид-коллизия: status отвечает, basis=stored |
| §3.1 — потолок не отказывает законной работе И не даёт обойти порог дроблением | ⚠ Строка ПЕРЕПИСАНА после отзыва первой редакции — см. второй круг, находка 1. Порог судится по ВСЕЙ книге (дробить бесполезно); именованный кап --accept-rebill=X судится по тому, что заплатит ЭТОТ прогон (законная работа не отклоняется); прогон, который не пере-оплачивает ничего, гейта не встречает вовсе |
TestABoundedRunCannotOutrunTheConsentThreshold (обе стороны на одном состоянии) + TestARunThatRePaysNothingIsNotAskedForConsent (третья ветка). Мутации: порог по прогону · кап по книге · снятие ранней ветки — все три падают адресно |
| §3.1 — потолок не даёт перерасхода на неполном юните | Терминальность = ПОКРЫТИЕ всех ожидаемых позиций (unitFullyRecorded), а не вердикт resolveChunkState |
TestAFlaggedMemberWithNoEditRowIsNotFree — флагнутый член + ноль edit-строк; без фикса юнит признаётся свободным и оплачивается сверх потолка |
| §3.1 — неопределённость всегда в сторону «платно» | !ok || h != ContentHash ⇒ невоспроизводимый хеш = платно |
TestAnUnreproducibleHashIsNeverFree (6 состояний, включая skipped и снятую стадию) |
| §3.1 — допускается ВЕСЬ юнит, а не его лидер | Допуск помечает каждого члена | TestTheCeilingAdmitsEveryMemberOfAUnit — мелкая нарезка ⇒ 3 чанка в главе; мутация «только лидер» даёт усечённую главу |
| §3.1 — форма конвейера, где «юнит» двусмыслен | planVolume ОТКАЗЫВАЕТ (переводчик последней стадией при живых редакторских) |
TestTheCeilingRefusesAPipelineWhoseUnitsAreTwoThings; без флага — поведение не меняется |
| §3.1 — черновой-только конвейер | Та же единица, другая форма | TestTheVolumeCeilingHoldsOnADraftOnlyPipeline — 1 вызов на юнит, 2 гранта = 2 вызова |
| §3.1 — стоп назван в выводе CLI | Строка с тремя действенными числами + «это ЗАВЕРШЕНИЕ, не пауза» | TestRenderTranslateNamesTheVolumeCeiling; необрезанный прогон о потолке молчит |
| §3.2 — «неизвестно» никогда не несёт числа | Решение вынесено в чистую rebillOutcome/projectable |
TestAFailedBasisNeverCarriesAFigure — таблица на 5 состояний |
| §3.2 — падающий фолд не уносит свою диагностику | Замечания в лог ДО обработки ошибки | TestAFailedFoldStillReportsWhatItSkipped |
| §3.1 — отчёт различает ДОСТАВКУ и ПЕРЕ-ДЕЛКУ (добавлено 2-м кругом) | VolumeStop раздвоен на Delivered/Reworked и LeftFresh/LeftRework; грант идёт сперва в новое; приглашение купить — только про никогда-не-доставленные |
TestAFullyDeliveredBookOffersNothingLeftToBuy (0 доставлено / 1 переделан / 0 к покупке) · TestAPurchaseBuysNewBookBeforeReMakingOldBook (грант уходит в новое, а не в переделку) · TestRenderTranslateNeverSellsWhatIsAlreadyOwned. Мутация «схлопнуть два прохода в один» падает |
| §3.1 — движок говорит ЮНИТАМИ, не главами (добавлено 2-м кругом) | Все строки отчёта и CLI переведены на «units»; главы — слово платформы, конвертирует манифест | TestRenderTranslateNamesTheVolumeCeiling + TestAFullyDeliveredBookOffersNothingLeftToBuy пинят формулировку |
| §3.2 — смета дана и в той единице, которой сайзится покупка (3-й круг) | Аддитивное rebill_output_units рядом с rebill_units (chunk×stage); имя rebill_units не тронуто |
TestTheEstimateIsAlsoGivenInTheUnitAPurchaseIsSizedIn; фикстура специально недегенеративна; мутация «выдать Rows за юниты» падает |
| §3.2 — один документ = один банк (2-й круг) | unsigned_bank_terms считается по ТОМУ ЖЕ банку, что и денежные поля |
Материализация помнит свои строки (r.bankRows); проверено батареей |
| §5 ось 3 — детерминизм | MaxUnits не входит ни в снапшот, ни в BriefHash; байты не двигаются |
TestTheVolumeCeilingMovesNoRequestByte — снапшоты обеих волн не сдвинулись, каждое тело обрезанного прогона есть среди тел безпотолочного; мутация «подмешать MaxUnits в снапшот» падает |
| §3.3 — чего в паке нет | Константу $0.03, цену от объёма исходника, строки 228/230/141/131, platform/ — не трогал |
— |
Адверсариальный проход по СВОЕЙ готовой работе — что он нашёл
Веер: шесть линз (деньги · тождество фолда · шов · регрессии пака · параллелизм/детерминизм · качество моих же тестов), тяжёлая модель — РОВНО ОДИН агент (денежная линза), остальным попроще; затем по одному независимому опровергателю на каждую находку. Сырых находок 10, после опровержения осталось 7 настоящих. Все семь внёс сам этот пак — приёмке они не видны, поэтому называю первыми.
1. ⛔ ПЕРЕРАСХОД: юнит с флагнутым членом и без edit-строки считался БЕСПЛАТНЫМ. unitsServedFree спрашивал терминальность у resolveChunkState, а тот отдаёт ChunkFlagged, как только флагнута ЛЮБАЯ строка, — ДО проверки, что все ожидаемые позиции на месте. Юнит, у которого черновик одного члена флагнут, а edit-строки ещё нет (состояние после подписного стопа банка, Ctrl-C или любого обрыва в редакторской волне), признавался терминальным и свободным, проходил БЕЗ списания гранта и потом оплачивался редактором. Прогон платил за юнитов больше, чем куплено, и, поскольку остаток оставался нулём, даже не сообщал об объёмном стопе.
Лечение: предикат заменён на unitFullyRecorded — покрытие ВСЕХ ожидаемых позиций строками; полнота и есть терминальность, и именно она предсказывает «вызова не будет». Доказано: TestAFlaggedMemberWithNoEditRowIsNotFree (обрыв редактора на прогоне 1 → флагнутый член + ноль edit-строк; прогон 2 с потолком 1). Возврат к прежнему предикату мутацией — тест падает с «the other unit was judged free and paid for anyway».
2. ⛔ ОТКАЗ ЗАКОННОЙ РАБОТЕ: гейт согласия судил ВСЮ книгу при обрезанном прогоне. Нашёл сам, до линз. checkRebillConsent стоял ПЕРЕД planVolume и проецировал пере-оплату по всем строкам, тогда как прогон с --max-units 2 пере-оплатит максимум два юнита. Р6 требует согласия на КОНКРЕТНУЮ трату — а тут отказ (или запрос согласия) шёл на деньги, которые никто не потратит. Это ровно то, о чём предупреждает сам rebill.go: «число, которое одобрили и не списали, — это то, из-за чего такие числа перестают читать».
Лечение: скоуп считается ДО гейта; СУММА сужена до допущенных юнитов, ПОРОГ оставлен книжным (5% книги — свойство книги, не покупки). Инертно без флага (scope==nil ⇒ фильтр тождественный ⇒ число байт-в-байт прежнее) — это и делает правку ратифицированного гейта безопасной. Доказано на первом круге мутацией «фильтр в тождество». ⚠ Это решение ОТОЗВАНО на втором круге (находка 1 ниже) — читать вместе с ним, а не отдельно.
3. Отказ redrive учил ЛОЖНОМУ факту об движке. Текст говорил «redrive не гоняет книжную волну, которую это могло бы ограничить» — неправда: redrive зовёт TranslateBook (status.go:984). Мой же комментарий четырьмя строками выше говорил обратное и верное. Лечение: у redrive теперь свой текст, называющий НАСТОЯЩУЮ причину (деструктивный сброс ограничен своим селектором, и потолок в юнитах ограничил бы вторую половину команды, читаясь как ограничение целого). Тест дотянут: он проверяет не только имя флага, но и что причина не врёт про адресата.
4. Рефактор seedGlossary терял диагностику падающего прогона. Замечания (пропуск ruby-алиаса по омонимии, дроп строк авто-банка) собирались в gatherBankInputs, но логировались ПОСЛЕ проверки ошибки — то есть на прогоне, который умер на более поздней проверке, они пропадали. До рефактора они писались по месту и доезжали. Лечение: замечания уходят в лог ДО обработки ошибки; gatherBankInputs для этого возвращает частичные входы вместе с ошибкой. Доказано: TestAFailedFoldStillReportsWhatItSkipped; перестановка двух блоков мутацией — тест падает.
5. rebill_basis: failed мог ехать рядом с большим ПОСЧИТАННЫМ числом. Если банк не материализовался вовсе, projectRebill всё равно запускался — против ПУСТОГО банка, поэтому все строки выглядели протухшими и на провод уезжала пере-оплата всей книги, а CLI печатал рядом «цифры ниже нули, потому что их не удалось посчитать». Базис, противоречащий собственным числам, хуже отсутствия базиса. Лечение: решение вынесено в чистую rebillOutcome (+ projectable) — проекция не запускается без банка, «неизвестно» никогда не несёт числа. Табличный тест на все пять состояний; инвариант недостижим из фикстуры (нужен одновременный отказ и фолда, и стора), поэтому он и вынесен в функцию — иначе его нечем было бы держать.
6. Потолок отказывается от формы конвейера, в которой «юнит» — два разных объекта. Если ПОСЛЕДНЯЯ стадия — переводчик, а редакторские стадии есть (конфиг такой порядок не запрещает), outputUnits отдаёт синглтон на ЧАНК, а редакторская волна всё ещё группирует их в юниты: потолок, выданный в синглтонах, допустил бы чанк одного члена, и редактор пошёл бы по юниту с недоделанным черновиком за полную цену. Лечение: planVolume на такой форме ОТКАЗЫВАЕТ громко (без флага — ничего не меняется). Тест + мутация.
7. Мои собственные тесты доказывали меньше, чем обещали — три дыры, все закрыты.
(а) Мульти-членный юнит не проверялся ни одной фикстурой (все главы были однопредложенные ⇒ у каждого юнита ровно один член), поэтому цикл по u.Members в допуске был непокрыт: мутация «помечать только лидера» проходила ВСЮ батарею. Закрыто TestTheCeilingAdmitsEveryMemberOfAUnit (мелкая нарезка черновика под крупным редакторским потолком ⇒ 3 чанка в главе); мутация теперь падает с «buyer gets a truncated chapter they paid for in full».
(б) Ветка «хеш не воспроизведён ВООБЩЕ» была непокрыта — а это направляющий инвариант всего потолка («любая неопределённость — в сторону „платно“»). Мутация !ok || … → ok && … проходила батарею. Закрыто TestAnUnreproducibleHashIsNeverFree.
(в) Тест тождества фолда сравнивал ТОЛЬКО черновую волну. Путь A не гонял TranslateBook, поэтому строк chunk_status не было, и renderedContentHashes не давал ни одной edit-позиции — а edit-волна ровно та, куда попадают mined-строки фикстуры (базовый банк их исключает). То есть половина, ради которой фикстура и заводилась, не проверялась. Закрыто: перед сравнением идёт настоящий прогон, и покрытие обеих волн теперь АССЕРТИТСЯ, а не предполагается (len(hashes)!=0 от этого не защищал: две черновые карты тоже равны).
⚠ ОТДЕЛЬНОЙ СТРОКОЙ, потому что это правка РАТИФИЦИРОВАННОГО контура и она обязана быть видна в акте, а не выводиться из диффа: я тронула гейт согласия D20.2-Q2.
⛔ ЧИТАТЬ ВМЕСТЕ С НАХОДКОЙ 1 ВТОРОГО КРУГА — здесь описана ПЕРВАЯ, ОТОЗВАННАЯ редакция. Она сужала СУММУ до допущенных юнитов при книжном ПОРОГЕ и защищалась доводом «инертно без флага». Довод верный и неважный: с флагом размер прогона выбирает каллер, и дробление обходило порог. Действующая редакция: порог — по всей книге (дробить бесполезно), именованный кап — по тому, что заплатит этот прогон (законная работа не отклоняется), прогон без пере-оплаты гейта не встречает. Инертность без флага сохранена и в ней: при scope == nil обе проекции — один объект, и число байт-в-байт прежнее.
Опровергнуто и НЕ чинилось (3 из 10): три находки опровергатель снял как уже вылеченные тем же проходом (я чинил по мере поступления, и опровергатели читали уже исправленный код) — они и есть пункты 3, 4 и 7(а/б) выше.
Второй круг приёмки — что нашли и что я с этим сделала
Оркестратор прогнал по паку ещё два аппарата: слепой верификатор (читал ТОЛЬКО заказ и дифф, отчёта не видел) и искателя вне карты отчёта. Плюс я прогнала свой второй код-ревью — шесть линз, тяжёлая модель один агент, по опровергателю на находку. Итог круга: двенадцать настоящих находок, все внесены паком, все вылечены — плюс одна чужая, проверенная и переданная строкой.
1. ⛔ ОТЗЫВ МОЕГО ЖЕ РЕШЕНИЯ: сужение суммы гейта D20.2-Q2 открывало обход порога дроблением. Первый круг я закрыла тем, что сумму пере-оплаты считаю по допущенным юнитам, а порог оставляю книжным, и защитила это доводом «правка инертна без флага». Довод верный и НЕважный: с флагом каллер САМ выбирает, насколько мал каждый прогон, поэтому --resnapshot --max-units N в цикле пере-оплачивает книгу целиком, ни разу не перейдя порог и ни разу не спросив согласия. Измерено искателем на реальных числах: 30 глав за $0.109200, порог $0.005460, безпотолочный прогон ОТКАЗЫВАЕТ, а --max-units 1 проходит четыре раза подряд по $0.003640. Порог обхода арифметический: покупка ≤5% книги — гейт молчит; на 500-главной вебновелле «10 глав» это 2%.
Лечение — разделены два РАЗНЫХ вопроса, а не один компромисс. «Надо ли вообще спрашивать человека» — политика КНИГИ, судится по всей её пере-оплате, которая от размера покупки не сжимается. «--accept-rebill=X» — инструкция каллера о ТРАТЕ, судится по тому, что этот прогон реально заплатит (платформа фондирует этот кап из холда прогона, то есть он размерен покупкой, а не книгой). Отказ печатает ОБА числа: собрать их в одно — и есть способ соврать в любую сторону.
Доказано: TestABoundedRunCannotOutrunTheConsentThreshold — обе стороны на одном состоянии: потолок 1 без согласия ОТКАЗЫВАЕТ (обход закрыт), а именованный кап $0.002, покрывающий собственный счёт прогона ~$0.00182, ЧЕСТЁН (законная работа не отклонена).
2. ⛔ VolumeStop не различал ДОСТАВКУ и ПЕРЕ-ДЕЛКУ — и это была нехватка модели, а не сбой счётчика. Один счётчик на оба вида оплаченного юнита давал ложь, на которую оператор действует: книга переведена на 100%, банк подвинут, покупка №1 печатает «29 остались», №2 — «28», ни одной новой главы не создано, exit 0 и ready подтверждают успех. А CLI подавал остаток как «что осталось КУПИТЬ».
Лечение — переделан тип, а не подлатан счёт. Юнит классифицируется тремя состояниями (unitFresh / unitFree / unitRework), и обе половины отчёта раздвоены: Delivered/Reworked и LeftFresh/LeftRework. Приглашение купить произносится ТОЛЬКО про никогда-не-доставленные юниты; покупка, целиком ушедшая в переделку, обязана это сказать. Раздвоение бесплатно: оно падает из того же теста полноты позиций, которым считается «бесплатно».
Доказано: TestAFullyDeliveredBookOffersNothingLeftToBuy (0 доставлено, 1 переделан, 0 никогда-не-доставленных, остаток назван пере-проходом) + TestRenderTranslateNeverSellsWhatIsAlreadyOwned (строка CLI не предлагает купить то, чем уже владеют).
3. ⛔ status/export начали читать авто-банк, который писался НЕ атомарно. Дыра, которую пак открыл в чужом файле: до него авто-банк читал только тот прогон, который его и писал, поэтому os.WriteFile с усечением был безопасен. Как только читающие поверхности стали сворачивать банк сами, файл попал в класс, ради которого существует artifact.go, — и окно усечения даёт либо ошибку разбора (покупателю устаревшие цифры), либо валидный ПРЕФИКС списка термов: фолд банка, которого не существовало, и rebill_units, который ни один прогон не выставит.
Лечение: writeFileAtomic, как у трёх соседей; авто-банк вписан в перечень сайдкаров в шапке artifact.go. Доказано: TestTheAutoBankIsWrittenAtomically — инструмент inode (переименование меняет его ВСЕГДА, запись на месте — никогда; сильнее сверки байтов).
4. Пин согласия status↔translate покрывал только беспотолочный случай. Механическая правка сигнатуры протащила туда nil, и расхождение под потолком пин бы не поймал. Дописан второй ассерт: под объёмным потолком отказ гейта обязан по-прежнему цитировать КНИЖНОЕ число — то самое, которое печатает status, — потому что докстринг status.go обещает это дословно.
5. Отказ денежного потолка съедал объёмный отчёт. Когда прогон встаёт по деньгам, результата нет, и оператор не узнаёт, что ему было выдано и сколько успело доехать. Добавлена строка WARN на этом пути: стоп ниже — НЕ объёмный, вот что было выдано и что осталось недоставленным.
6. Инструмент «ничего не записано» был слеп к WAL — и, дотянутый, сразу поймал факт. Дайджест мерил только основной файл, а SQLite в WAL кладёт коммит в -wal. Дотянула — и увидела изменение. Разобрала, а не подогнала: данные не двигались (основной файл байт-в-байт), а появились ПУСТОЙ -wal и -shm: так устроен любой WAL-читатель. Поэтому ассерт теперь точный: основной файл неизменен И журнал не несёт ни одного кадра; -shm не проверяется намеренно, требовать его отсутствия значило бы требовать от SQLite невозможного. Гарантию держит query_only, дайджест — подтверждение.
Косметика того же круга: висячие ссылки на bankfold.go/bankfold_identity_test.go исправлены на реальные bankmaterialize* · backend/README.md (позиция №2 онбординга зоны) теперь знает про ВТОРОЙ потолок, его ортогональность денежному, «стоп = завершение» и предупреждение про майнящие книги · в volume.go явно записано, что нулевое значение unitClass обязано быть unitFresh (пустая карта = книга, которую не гоняли).
7. ⛔ ПОТОЛОК ТРАТИЛ ПОКУПКУ НА ПЕРЕ-ДЕЛКУ ВПЕРЁД ДОСТАВКИ. Допуск шёл одним проходом в порядке книги, а после правки банка уже доставленные юниты — это как раз РАННИЕ. Воспроизведено ревью: покупка 2 юнитов на пятиглавой книге с двумя доставленными пере-делала главы 1–2 и не начинала главу 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 не зависит от того, шёл ли чанк i−1; редакторская волна берёт только СВОИ члены и делает свежий Select с nil-sticky. Он прогнал книгу, где глава 4 переведена и отредактирована в прогоне, где глава 1 не запускалась вовсе, — тела запросов дословно совпали с телами прямого сплошного прогона. Проверил и остальные кандидаты на кросс-юнитную связь: стоп майнинга строит кандидатов по ВСЕМ чанкам (от скоупа не зависит), свёртка банкнот — множественная (порядок не важен), счётчики событий засеиваются книжно и инкрементятся под once-key. (б) A/B на одной книге, четыре покупки по 2 юнита: доставка-вперёд отдаёт 6/6, порядок книги — 4/6, причём при порядке книги две покупки подряд печатали «Still in the book: 4 unit(s) NEVER delivered» не сдвинувшись. (в) Дверь пере-прохода не сломана: на полностью доставленной книге доставка-вперёд ЕСТЬ порядок книги (свежих юнитов нет), то есть чистый пере-проход работает как прежде.
⚠ Его требование выполнено — и оно было существенным: это не живой дефект, а ТРОПА ГНИЕНИЯ. Схлопывание двух проходов обратно в один оставляло батарею ЗЕЛЁНОЙ, то есть следующая сессия, «прибирающая» цикл или добавляющая четвёртый класс юнита, тихо вернула бы дефект. Закрыто TestAPurchaseBuysNewBookBeforeReMakingOldBook; мутация «схлопнуть в один проход по порядку книги» падает — и падает ровно на вторичном следствии, которое ратификатор и предсказал: при порядке книги та же покупка не просто перераспределяется, а ПОЛУЧАЕТ ОТКАЗ гейта согласия.
Дополнительно из второго ревью (пережили опровержение и вылечены):
— тройное повторение самого дорогого расчёта. Обрезанный прогон пере-рендеривал всю книгу до трёх раз: в planVolume и в каждой из двух проекций гейта, при том что между ними ничего не менялось. Заведён мемо, срок жизни которого — жизнь БАНКА: materializeBanks его сбрасывает, потому что переживший свой банк мемо отдал бы хеши банка, которого у прогона больше нет, — ровно тот класс тихой неверности, ради устранения которого пак и существует. Мемо — скорость, а его инвалидация — корректность, поэтому запинена именно она (TestTheWireHashMemoDiesWithItsBank).
— предупреждение про майнящие книги не было покрыто ничем — а незакрытое тестом предупреждение молча перестаёт эмититься. Закрыто, вместе с обратной стороной: на книге БЕЗ потолка оно не должно возникать.
Четвёртый круг — денежный охотник приёмки: 6 находок, 3 вылечены, 3 переданы
1. ⛔ БЛОКИРУЮЩАЯ, и она пробивала саму цель пака — ВЫЛЕЧЕНА. planVolume классифицирует ДО черновой волны, но между волнами стоит стоп майнинга, который пере-сеивает банк прямо посреди прогона (mining.go, ветка авто-продолжения), после чего waverun пере-считывает edit-снапшот. А free-юниты допускаются ВНЕ гранта — потому что бесплатная работа ничего не стоит. Значит каждый из них судился по снапшоту, который прогон сам же и заменил, и любой, чьи инжектируемые байты новый банк изменил, получал свежий ПЛАТНЫЙ вызов редактора, не разрешённый никаким грантом, — а строка отчёта называла его «rode along at $0». Охотник замерил: 4 вызова при гранте 1, $0.007280 объявлены бесплатными.
Лечение — не гард, а ПЕРЕ-ПЛАН: вопрос «бесплатен ли юнит» задаётся заново, против снапшота, который стал реальным, в последний момент перед редакторской волной — то есть «проверять перед единицей» по-прежнему держится. Ставший платным берёт слот, если он есть; если нет — его редактура НЕ идёт, и он честно отчитывается как доставленный-но-не-пере-сделанный.
⚠ И сделано СТРУКТУРНО, а не вызовом: волна ОТКАЗЫВАЕТСЯ работать со скоупом, спланированным против другого снапшота. Без этого удаление вызова пере-плана оставляло всю батарею зелёной, а потолок молча переставал держать — я это проверила мутацией. Теперь удаление вызова роняет прогон с адресным сообщением.
Доказано: TestFreeUnitsAreReJudgedWhenTheBankMovesMidRun (снапшот не двигался → ничего не меняется; двигался → ни одного «free», ровно один слот занят, остальные удержаны и отказаны allowsUnit) + TestTheEditWaveRefusesAStalePlan. Две мутации.
⚠ Своей фикстурой конечное следствие я не воспроизвела — ПЯТЬ попыток, — и разбор этого провала оказался ценнее самих попыток. Две причины, обе названы приёмкой и обе проверены мной на своём дереве.
Причина 1: я искала не на той стороне дельты. У mined-дельты две стороны. WHICH (список термов) майнится из исходника ВСЕЙ книги — он полон после первой покупки и больше не двигается; отсюда мой неверный вывод «однородный источник ничего не даст». А байты банка двигает WHAT — dst каждого терма, и она draft-side: складывается из банкнот, которые модель уже выдала (mining.go:109), то есть растёт с каждой покупкой ДАЖЕ при полностью однородном источнике. Я меняла ТЕКСТ глав, а надо было менять то, что ПРЕДЛАГАЮТ ЧЕРНОВИКИ.
Причина 2, из-за которой попытка (5) не могла сработать в принципе, — спойлер-гейт since_ch (membank/memory.go:640-647, жёсткое chapter < since_ch). Вводя терм поздними главами, я выталкивала его since_ch за пределы уже доставленных — и он не попал бы в их инъекцию НИКОГДА, какой бы dst потом ни получил. ⚠ Условия тянут в РАЗНЫЕ стороны: естественный способ заставить дельту расти (поздние главы вводят новый терм) ровно этим и убивает достижимость. Дефекту нужна ОБРАТНАЯ асимметрия: терм присутствует в РАННЕМ исходнике, а его рендеринг приходит поздно — то есть асимметрия ПОВЕДЕНИЯ МОДЕЛИ, а не текста.
Пере-проверила зондом приёмки у себя: 方源 dst="" since_ch=3 → dst=Фан Юань since_ch=3, банк двинулся, а content_hash главы 1 MOVED=false, вызовов ровно два. То есть мой прогон был верен, а гипотеза о причине — нет.
⚠ Диагностика на будущее, две строки: дампить авто-банк между покупками и смотреть у терма ДВА поля — что dst перешёл из пустого в непустой и что since_ch ≤ номера уже доставленной главы. Второго я не проверяла.
Сценарный тест теперь есть — TestAMidRunBankMoveNeverBillsBeyondTheGrant (рецепт приёмки: четыре главы, побайтово одинаковые, различаются только банкнотами). Я пере-проверила его двумя своими посадками, а не приняла на слово: снятие вызова пере-плана падает структурным отказом; снятие вызова И гарда воспроизводит дефект целиком — 4 вызова вместо 2, $0.007280 вместо $0.003640, обе уже купленные главы пере-отредактированы, и отчёт зовёт их «2 rode along at $0».
⚠ Оба пина стоят рядом и не заменяют друг друга (различение оркестратора, точнее моего): инвариантный утверждает «пере-план вызывается», сценарный — «покупатель не платит второй раз», а платит покупатель именно за второе.
⚠ Жёсткое равенство == 2×грант оставлено намеренно: гейт терминолога в фикстуре выключен явно, счёт детерминирован, и равенство ловит не только перерасход, но и молчаливую ПОТЕРЮ оплаченной работы.
⚠ Условие достижимости УЗКОЕ, и это записано с обеих сторон: нужна конъюнкция трёх условий (терм на WHICH-списке · его dst приходит поздно · since_ch покрывает уже доставленную главу). В фикстуре приёмки два терма из трёх её не выполняют и остаются инертными. Оценку «покупка №50 переоплачивает 490 юнитов» автор находки признал ПОТОЛКОМ ТЯЖЕСТИ, когда сработало, а не ожидаемым случаем; частоту на реальной книге никто не мерил. На диспозицию не влияет — лечение структурное и покрывает класс независимо от узости входа.
2. ⛔ Флагнутый юнит объявлялся доставленным — ВЫЛЕЧЕНА. Допуск решается ДО работы (этим и ограничиваются деньги), но решение ЗАПЛАТИТЬ за юнит — не факт существования главы: юнит может вернуться флагнутым и не отгрузить ничего. Отчёт печатал «2 NEW unit(s) delivered» поверх одного читаемого юнита. Лечение: счётчики выравниваются ПОСЛЕ волн, по фактическим исходам (reconcile), и заведён отдельный счётчик «оплачено, но флагнуто» с прямой формулировкой: деньги потрачены, текста нет, покупкой не чинится — это redrive. Тест + мутация.
6. Подсказка обещала то, чего грант не сделает — ВЫЛЕЧЕНА. rebill_output_units подавался как «та единица, в которой считает --max-units» — правда, и в одиночку вводит в заблуждение: грант идёт СПЕРВА в недоставленное, поэтому на книге, где оно есть, --max-units N доставит новое и не пере-сделает ничего. Оговорка была в коде, но не там, где читает оператор. Теперь в подсказке. Тест.
3 и 4 — ПОДТВЕРЖДАЮ, НЕ вылечила, и говорю почему. Обе об одном: ось «свежий/пере-делка» выведена из ПОЛНОТЫ СТРОК, а не из факта отгрузки. Добавление стадии делает доставленную книгу «никогда не доставлявшейся» (#3); юнит, прерванный между волнами, второй раз считается свежим и тратит слот повторно (#4). Правильный носитель у движка уже есть — реестр анонсов (events_outbox.once_key, unitOnceKey = книга+волна+юнит), то есть собственное определение «этот юнит был отгружен читателю», монотонное на всю жизнь книги. Но читающего метода у стора нет (есть только EnqueueOnce, который ПИШЕТ), а internal/store я не трогала ни строкой за весь пак и не считаю правильным открывать его в хвосте четвёртого круга. Это отдельный заказ на одну функцию чтения + перевод оси на неё.
⛔ ПОПРАВКА к моей же формулировке, внесена оркестратором при приёмке и принята. Я написала «деньги целы: ошибается слово, а не потолок». Слишком мягко — слово тут ПРОДАЁТ. Отчёт, называющий дочитанную книгу «3 unit(s) NEVER delivered», через строку CLI приглашает их купить, и покупатель платит за уже полученное. Деньги целы В ДВИЖКЕ — потолок держит, счёт вызовов верен, — но не в том, что увидит платформа и предъявит покупателю. Диспозиция не меняется (предмет отдельный, internal/store в хвосте пака не открываю), но цена ошибки — покупательская, а не редакторская, и в строке заказа стоит именно так.
5 — цифру принимаю, договорённость в силе, но она теперь важнее. Терминолог переигрывается каждой покупкой: три покупки по 1 юниту дали три полнокнижных консолидации по usd=0.005460 каждая, при том что сам юнит дешевле. Накладные масштабируются КНИГОЙ, а не грантом — книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. Мы согласились, что место этой траты — в цене; при мелкой нарезке продажи она перестаёт быть накладной и становится основной статьёй, и цена обязана это знать.
Сверка по буллетам промта — что закрыто и чем
§3.2, «родня 124(в)» — заказано сказать ПРЯМО, говорю: да, моя форма закрывает и resnapshot --dry-run. status считает ре-билл при ЛЮБОМ дрейфе, не только банковом (status.go: проекция идёт при наличии строк, а не под ConfigDrift), поэтому после починки свёртки он И ЕСТЬ «оценить пере-проход, ничего не покупая» — ровно та способность, ради которой строка 124(в) поименована будущей дверью. Отдельного глагола resnapshot --dry-run заводить не нужно. Это удешевляет проект на один глагол и один документ.
§3.2, «ключей не требует» — закрыто структурно, а не тестом, и вот почему это правильно: models.CheckKeys вызывается ТОЛЬКО под if forWrite (runner.go:260-266), а status/export идут через NewReadOnlyRunner. Своего глагола пак не завёл, поэтому пин keysfile_test.go, перечисляющий читающие глаголы, покрывает мой путь без изменений — и он зелёный (6 тестов). Новый тест здесь ничего бы не доказал сверх пина.
§5 ось 2 («пара, которой в репо нет») — проверено: в volume.go и bankmaterialize.go ветвлений по паре/книге нет. Единственное упоминание языка — lang.TargetChecksFor(r.Book.TargetLang), и это ДАННЫЕ: лукап по встроенным файлам target-<lang>.txt (lang/embedded.go:106-118), а не свитч. Строка перенесена из seedGlossary дословно, новой пар-логики пак не вносит. Пара, которой в репо нет, работает добавлением файла данных.
§5 ось 3 (детерминизм) — была заявлена без доказательства, теперь доказана: TestTheVolumeCeilingMovesNoRequestByte — снапшоты обеих волн не двигаются от установки потолка, и КАЖДОЕ тело запроса обрезанного прогона встречается среди тел безпотолочного (в том числе когда потолок отдаёт юниты ВНЕ порядка книги). Мутация «подмешать MaxUnits в снапшот» падает с точным сообщением. Это несущее: сдвинь потолок снапшот — и передача флага пере-покупала бы всю уже оплаченную книгу.
§3.3, соседи — называю строкой, не чиню:
— redrive зовёт TranslateBook по ВСЕЙ книге (status.go:926): своя денежная неожиданность, была до пака. Флаг --max-units я ему сознательно не дал, отказ несёт настоящую причину.
— мёртвая переменная manualSrcs в прежнем seedGlossary (писалась, не читалась никем) — исчезла при выносе гathera; ничего с ней не потерялось, проверено грепом по старой версии.
— джоб-гард не отличает банк-онли-движение от смены конфига (stagerun.go:47-59): он и есть корень хвоста про майнящие книги. Ратифицированный Р6-контур, чужой предмет — не трогала.
Диспозиция ВСЕХ находок ревью — 25 сырых, ни одна не потеряна
Сведено из журналов двух ревью-воркфлоу (10 + 15 сырых находок) плюс трёх опус-ратификаций. Каждая строка — либо ВЫЛЕЧЕНА (с тестом и мутацией), либо ОТКЛОНЕНА с доводом, либо НАЗВАНА и НЕ вылечена с указанием, почему и чей это предмет. Пустых клеток нет намеренно: находка без диспозиции — это находка, которую следующая сессия найдёт заново.
| # | Находка | Диспозиция |
|---|---|---|
| 1 | Отказ --max-units учил ЛОЖНОМУ факту про redrive |
✅ свой текст с настоящей причиной + тест, проверяющий, что причина не врёт про адресата |
| 2 | Рефактор seedGlossary терял замечания падающего прогона |
✅ лог ДО обработки ошибки; тест + мутация |
| 3 | Мульти-членный юнит не покрыт ни одной фикстурой | ✅ мелкая нарезка (3 чанка в главе); мутация «допускать лидера» падает |
| 4 | Ветка «хеш не воспроизведён ВООБЩЕ» не покрыта | ✅ TestAnUnreproducibleHashIsNeverFree, 6 состояний |
| 5 | Тест тождества сравнивал только черновую волну | ✅ прогон ПЕРЕД сравнением + ассерт покрытия ОБЕИХ волн |
| 6 | ⛔ Перерасход: флагнутый член без edit-строки считался бесплатным | ✅ unitFullyRecorded вместо вердикта resolveChunkState; репро-тест + мутация |
| 7 | ⛔ Вторая покупка на МАЙНЯЩЕЙ книге падает exit 1 |
⚠ НЕ вылечено, названо. Корень — джоб-гард (stagerun.go:47-59), ратифицированный Р6-контур, чужой предмет. Мною добавлен громкий WARN + тест на него; проводка гейчена оркестратором |
| 8 | Трата стопа майнинга/терминолога вне потолка и переигрывается | ⚠ НЕ вылечено, названо. Резать её объёмом семантически неверно (не единица доставки); место — в ЦЕНЕ, платформенная сторона |
| 9 | Форма конвейера, где «юнит» — два объекта | ✅ planVolume отказывает громко; тест + мутация |
| 10 | rebill_basis: failed рядом с посчитанной цифрой |
✅ решение вынесено в чистую rebillOutcome; табличный тест на 5 состояний + мутация |
| 11 | ⛔ Потолок тратил покупку на переделку вперёд доставки | ✅ доставка-вперёд; ратифицировано опусом (порядок безопасен, A/B 6/6 против 4/6); тест + мутация на ТРОПУ ГНИЕНИЯ |
| 12 | Объёмный стоп не доезжает машинным каналом | ⚠ НЕ вылечено, и это РАТИФИЦИРОВАННАЯ отсрочка: промт §3.1 сам поместил различение в отчёт/лог, а не в поток; словарь потока — зона оркестратора, он сказал «не сейчас, строкой». Названо вопросом B |
| 13 | Ветка proj.Rows == 0 не покрыта |
✅ TestARunThatRePaysNothingIsNotAskedForConsent; мутация падает |
| 14 | export не покрыт после правки банка |
✅ решено ОТКАТОМ export + пином осознанного расхождения |
| 15 | Нет теста на проводку --max-units через CLI |
✅ TestTranslateWiresMaxUnits; мутация «убрать проводку» падает |
| 16 | Предупреждение про майнящие книги не покрыто | ✅ тест на обе стороны (есть потолок / нет потолка) + мутация |
| 17,20 | Смешанное происхождение цифр в отказе гейта | ✅ каждое число из своей проекции; скоупная оговорка несёт скоупный ре-пин |
| 18 | Тройное повторение самого дорогого расчёта | ✅ мемо со сроком жизни БАНКА; запинена ИНВАЛИДАЦИЯ (корректность), не скорость |
| 19 | Капнутое согласие — дыра? | ✅ РАТИФИЦИРОВАНО ЗДОРОВЫМ опусом (согласие, не обход); по его же рекомендации запинено РАСКРЫТИЕ, которое этим согласием и делает |
| 21 | ⛔ Строка стопа сообщала ЮНИТЫ как ГЛАВЫ | ✅ движок говорит только «units»; тесты пере-пинены на верную формулировку |
| 22 | Отчёт называл несуществующий тест и снятые поля | ✅ таблица и §9 исправлены; отозванный пункт помечен ОТОЗВАННЫМ, а не переписан молча |
| 23 | Денominация: rebill_units (chunk×stage) против --max-units (юниты) |
✅ РАТИФИЦИРОВАНО ДЕФЕКТОМ опусом (замерено 15 против 6); аддитивное rebill_output_units, rebill_units НЕ переименован; тест + мутация |
| 24 | Семантика export.ConfigDrift изменена «за компанию» |
✅ ОТКАЧЕНО: чужое потребляемое поле не пере-определяется побочным эффектом; расхождение запинено, чтобы не «починили» обратно |
| 25 | Один документ status описывал ДВА банка |
✅ материализация помнит свои строки; счёт неподписанных — по тому же банку |
Итого: 22 вылечены с тестом и мутацией, 3 названы и не вылечены (7, 8, 12) — у каждой указан предмет и чей он.
⚠ Мой рефактор сломал ШЕСТЬ упоминаний в чужом доке — я насчитала два и ошиблась в адресе
⛔ ИСПРАВЛЕНО ПО ПРИЁМКЕ 29.08: первая редакция этого раздела была неверна дважды, и обе ошибки — метода, а не арифметики. Оставляю разбор, а не переписываю молча.
Ошибка 1 — счёт. Я насчитала ДВА сломанных якоря; сломано было ШЕСТЬ упоминаний (те же два плюс четыре про AttachRubyAliasesToManual). Причина: я взяла число из counts.py --lint, который по собственному объявлению content-сверяет только якоря С ТОКЕНОМ («без токена — несверяемы: 728»). Эту его слепоту я в этой же сессии сама и процитировала — и всё равно приняла его счёт за полный. Правильный инструмент был на расстоянии одного грепа: grep -n "seeding\.go" по доку.
Ошибка 2 — адрес. Я разрешала якорь по НОМЕРУ СТРОКИ, а надо по СОДЕРЖИМОМУ. В старом файле на :157 действительно лежала материализация базового банка, и я записала переезд туда. Но док в строке 76 говорил не о ней, а о InjectivityCollisions — warning-only диагностике, которая никуда из seeding.go не уезжала, а просто съехала со 157 на 64. На bankmaterialize.go:326, куда я её отправила, лежит r.baseMemory = membank.MaterializeBank(baseIn, …) — к этому якорю отношения не имеющее. Номер совпал, предмет — нет.
Верная таблица (сверено содержимым, каждая строка проверена грепом в дереве):
| Что говорил док | Было | Стало |
|---|---|---|
InjectVoice is FALSE… — условный фолд voice/address |
seeding.go:130-133 |
bankmaterialize.go:291-294 (текст перенесён ДОСЛОВНО) |
InjectivityCollisions — warning-only диагностика dst-коллизий |
seeding.go:157 |
seeding.go:64 — из файла НЕ уезжала |
AttachRubyAliasesToManual — ×4 упоминания |
seeding.go:43-50, :47 |
bankmaterialize.go:88-94, сам вызов на :92 |
Правку в доке делал оркестратор — docs/architecture/ не моя зона. Мой вклад здесь: сломала я, а нашла и починила приёмка, потому что я доверилась инструменту вместо грепа и номеру вместо содержимого. Урок годен шире этого случая: якорь разрешается тем, ЧТО он утверждает, а не тем, на какой строке лежал.
§8. Obstacle — что НЕ удалось и что НЕ проверено
⛔ 0. ГЛАВНОЕ, и это НЕ вылечено: объёмный потолок на книге, которая МАЙНИТ банк, ломает вторую покупку. Найдено денежной линзой, воспроизведено дважды независимо (линзой и опровергателем, каждым на своей копии репозитория, с контролем).
Механика: обрезанная покупка черновит часть книги, а стоп майнинга получает НЕобрезанный список чанков (waverun.go:154 передаёт весь chunks) и досыпает dst по мере появления новых банкнот. Значит авто-банк РАСТЁТ от покупки к покупке; следующая покупка сворачивает его в обогащённый банк, edit-снапшот съезжает, а edit-джобы ПРЕДЫДУЩЕЙ покупки к нему пришпилены. Джоб-гард (stagerun.go:47-59) банк-онли-движение не отличает от смены конфига и падает нетипизированной ошибкой ⇒ exit 1, платформа пишет failed — вместо задуманного «завершение, код 0». Контроль (без --max-units, книга целиком за один прогон, потом резюм) проходит чисто — то есть путь открывает именно обрезание.
⚠ Поправка опровергателя, важная и снижающая цену: --resnapshot пере-оплату НЕ вызывает в общем случае — после подъёма снапшота джоба работает обычный ре-пин по content_hash, и уже отредактированные главы возвращаются за $0; платит только та глава, в тексте которой новый терм реально встречается.
⚠ ПОПРАВКА к моей же оценке, найдена оркестратором при приёмке и проверена мной по коду — я ЗАНИЗИЛА область. Я написала «платформа --resnapshot уже передаёт, флагманский путь цел, бьёт по ручному вызову». Неверно: платформа передаёт его НЕ всегда, а под условием book.BankMoved (platform/internal/runs/runs.go:288, читал не правил), и этот флаг ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит (platform/internal/pgstore/books.go:985-993: «a correction landed»). Значит бьёт именно по ФЛАГМАНСКОМУ пути: майнящая книга, вторая покупка, правок не было ⇒ флага нет ⇒ гард ⇒ failed. Решение оркестратора: механизм лендится, но ПРОВОДКА --max-units на платформе гейчена до лечения. Смягчение, которое я сделал: planVolume при живом майнинге пишет громкий WARN, называя и эффект, и --resnapshot. Чего я НЕ делал: не трогал джоб-гард — это ратифицированный Р6-контур и та самая мина D39.165 §3 / эрраты 28.08-и, её лечение назначено платформенной половине. Заказывай отдельно, если хочешь, чтобы банк-онли-движение джоб-гард пропускал сам.
⛔ 1. Стоп майнинга и терминолог тратят ВНЕ объёмного потолка — и переплачиваются каждой покупкой. Тоже подтверждено. Мой пункт был мягче («потолок режет юниты, книго-уровневые вызовы — только деньги»); линза дожала: кандидатное пространство терминолога зависит от набора уже отчерновленных чанков, поэтому между покупками оно МЕНЯЕТСЯ, request_hash другой, и чекпойнты не переигрываются за $0. Книга, проданная десятью покупками, платит десять книжных консолидаций там, где цельный прогон платит одну. Ограничивают это только gates.terminology.budget_usd и --ceiling-usd. Не чинил: резать терминолога объёмом семантически неверно (он не единица доставки), а переносить его за пределы обрезанного прогона — это пере-проектирование майнинга (родня строки 203(з)). Обязан быть в цене продажи, если книга майнит.
- Живого прогона с провайдером НЕТ. Всё исполнение — против фейкового провайдера батареи. Ни одного платного вызова я не делал и не был на это уполномочен. Значит: цифры «2 вызова на юнит» и «$0.00182 за юнит» — свойства фикстуры, а не рынка; поведение потолка на реальном материале не мерено.
- Прогон, обрезанный по объёму и упёршийся в подписной стоп банка, уходит с кодом 3, и
res.Volumeтогда не докладывается: остановка принадлежит банку, а не потолку. Поведение корректное, но пара «стоп банка + объёмный потолок» отдельным тестом НЕ покрыта. - Платформенная половина не сделана и не заказана мне (§0 промта):
--max-unitsв argv никто не передаёт. Пока платформа не научится считатьглавы → юнитыпо манифесту и передавать флаг, обещание «купил N глав — получил N глав» НЕ выполнено. Механизм есть, трубы нет. storeOrderдоказан на моей фикстуре, а не на всех входах. Тождество опирается на:UNIQUE (book_id, src, sense, since_ch, until_ch)(store/migrate.go:203) ⇒ префикс ключа не даёт ничьей, и на отсутствиеCOLLATEв схеме ⇒ побайтное сравнение TEXT. Оба факта проверены грепом и чтением DDL. Чего я НЕ проверял: поведение при значениях с невалидным UTF-8 и при типовой аффинности SQLite, если в TEXT-колонку когда-нибудь ляжет число.- Порог согласия (
rebill_consent_usd) я НЕ трогал — сужена только СУММА. Если владелец считает, что при объёмной покупке должен ужиматься и порог, это отдельное решение. - Батарея прогнана на
make battery;make battery-stand(корпусные тесты) НЕ гонял — нет стендовых данных на этой машине.
§9. Вопросы оркестратору — пингом, не интерпретацией в свою пользу
-
⛔ Продавать ли объёмом книги, которые МАЙНЯТ банк — до отдельного заказа? Obstacle-0: вторая покупка на такой книге падает
exit 1/failedбез--resnapshot, и это открывает именно обрезание (контроль без потолка проходит чисто). Платформа--resnapshotуже передаёт (D39.166), значит флагманский путь цел, но ручной вызов — нет, и цена покупки на майнящей книге включает лишнюю книжную консолидацию терминолога (Obstacle-1). Мой совет: либо не ставить--max-unitsна майнящих книгах до заказа по джоб-гарду, либо принять оба хвоста явно. Решать тебе — я поставил громкий WARN и не стал трогать ратифицированный Р6-контур. -
Форма §3.2 отличается от той, что назвала строка 231. Строка предписывает «движковый глагол ЛИБО флаг». Я не сделал ни того, ни другого: я починил СВЁРТКУ, потому что слепое окно было не в том, что
statusпишущий, а в том, что он сворачивал последний прогон вместо ближайшего. Это дешевле обоих кандидатов и ближе к уже ратифицированной двери D39.165 §3 («платформа читает смету из УЖЕ существующегоstatus --json»). Нужна ратификация формы и правка формулировки строки 231. -
Поля шва.
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). -
⛔ ЭТОТ ПУНКТ ОТОЗВАН МНОЮ ЖЕ — подтверждать его НЕ надо. Здесь стояла просьба подтвердить сужение СУММЫ гейта D20.2-Q2 объёмным скоупом с доводом «инертно без флага». Довод верный и НЕважный: с флагом каллер сам выбирает размер прогона, поэтому дробление обходило порог и пере-оплачивало книгу целиком, ни разу не спросив согласия (замерено: 30 глав, четыре прохода по $0.003640 при пороге $0.005460). Действующее решение — второй круг, находка 1: порог судится по ВСЕЙ книге; именованный кап
--accept-rebill=X— по тому, что заплатит ЭТОТ прогон; прогон, не пере-оплачивающий ничего, гейта не встречает. Подтверждать надо ЭТО. -
redriveя флаг НЕ дал (отказ по правилу «команда, которая на флаг не действует, его отвергает»). Соседняя правда, которую называю строкой, а не чиню:redriveвнутри зовётTranslateBookпо ВСЕЙ книге (status.go:926), то есть у него своя денежная неожиданность — она была и до пака. -
Сигнал объёмного стопа в ПОТОК событий я не завёл. По правилу
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. Про умолчание «доставка вперёд переделки» — прошу зафиксировать словом, а не молчанием. Покупка, ЦЕЛЬЮ которой был пере-проход, теперь потратит грант на недоставленные главы, если они есть. Ратификатор показал, что дверь пере-прохода этим не ломается (на дочитанной книге доставка-вперёд И ЕСТЬ порядок книги), но на ЧАСТИЧНО доставленной книге разница видна. Если продукту нужен режим «только переделывать» — это флаг, и это твой заказ, не мой.