1114 lines
250 KiB
Markdown
1114 lines
250 KiB
Markdown
# АРХИВ: секция «Бэкенд» журнала прогресса, эра 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.go` `clusterTouches`), а фильтр
|
||
авто-банка ключуется только по собственному src строки (`mining.go:662`), поэтому ОТКЛОНЁННАЯ
|
||
поверхность может вернуться алиасом уцелевшей строки. Это противоречит канону уже сегодня.
|
||
**Не чиню в этом паке** (правильная форма — снять алиас со строки, а не снести строку; это
|
||
предмет банковой онтологии, а не тихой порчи) — **но это заказ, который стоит выдать.**
|
||
- **Д3 — НЕ ВЗЯТ, вне моей зоны.** `LiftBankStop` в `backend/` не существует ни в каком написании;
|
||
символ живёт в `platform/internal/runs/spawn.go` и `reconcile.go`, то есть в зоне, запертой паком
|
||
P9. Движковая половина при этом здорова: порядок «карта → память» ратифицирован и исполнен
|
||
(`mining.go:191` пишет карту, `:274` метит память), отказ памяти оценён в комментарии `:275-276`
|
||
как «one extra stop, nothing lost». ⚠ Поправка к моему же раннему пингу: я сказал «гарда не
|
||
существует» — точнее «не существует В ДВИЖКЕ».
|
||
- **Сосед 131 — НАЗВАН, не чиню.** Кап инъекции считает только `src → dst`
|
||
(`membank/memory.go:517-521` `glossaryLineTokens`), а рендер дописывает к КАЖДОЙ строке
|
||
`tx.UnverifiedMarker` (`:706`) либо `genderConstraintNote(...)` (`:711`), а редакторский блок ещё и
|
||
префикс `- ` с кавычками-ёлочками (`:821`). То есть отгружаемая строка строго длиннее той, за
|
||
которую платит бюджет; на коротких именных строках перерасход велик. Тема денег и качества
|
||
инъекции — не тихий брак в тексте.
|
||
|
||
**НАХОДКИ, МЕНЯЮЩИЕ ПРОМТ (все три подписаны, ни одна не абсорбирована молча).**
|
||
1. **§3.2: маркер существовал и ВРАЛ.** Ветка `case ce.FinalText != ""` печатала «(leak cleaned,
|
||
verify)» ЛЮБОМУ флагнутому юниту с текстом, а c-lite member-drop (`export.go` ставит `DispFlagged`
|
||
при непустом тексте) попадал ровно в неё. Существующий тест не ловил: покрывал ветку только
|
||
причиной `sanitizer_stripped` (`export_cli_test.go:66`). Предмет заказа изменён с «завести маркер»
|
||
на «перестать врать и покрыть непокрытое» — разрешено оркестратором 28.08 при закрытом гейте.
|
||
2. **§3.2: подсказка промта про место правки была опасна.** Промт указывал `export.go:237` рядом с
|
||
`ApplyHeading`. Туда нельзя: `ChunkExport.FinalText` — поле артефактного контракта, а провод судит
|
||
`translated`/`withheld` предикатом «текст непуст» (`waverun.go:203-204` и `events.go:538-554`,
|
||
`runevents.go:130` `Shipped`). Маркер в пустом юните перевернул бы `withheld` → `translated` —
|
||
ровно ⛔(а) промта. `ApplyHeading` этого не делает лишь потому, что на пустом тексте он no-op.
|
||
Маркер живёт в РЕНДЕР-слое. Проверено: `FlagReason`-константы до и после ИДЕНТИЧНЫ, `exportVersion`
|
||
не двигался, `dropped_members` аддитивно и `omitempty`.
|
||
3. **§3.1: посылка «Gemini принимает ровно одно» отозвана автором** (оркестратор, 28.08) — вендор-
|
||
вердикт со ссылками и датами отправлен ему отдельно, для `00-provider-quirks.md`. Кратко:
|
||
структурная посылка ПОДТВЕРЖДЕНА (`systemInstruction` — один `object (Content)`, `Content.role` ∈
|
||
{user, model}), поведение шима вендором НЕ документировано нигде, наш замер даёт «не склеиваются»
|
||
и «инструкция первого не исполнена»; «выживает одно или ноль» остаётся ОТКРЫТЫМ и для решения
|
||
безразлично.
|
||
|
||
**ДЕФЕКТ, КОТОРЫЙ Я ВНЁС САМ И ЗАКРЫЛ (называю, потому что он показателен).** Первая редакция ветки
|
||
маркера срабатывала по `DroppedMembers > 0` БЕЗ проверки непустого текста. Юнит, у которого выпали ВСЕ
|
||
члены, ship-ит пустоту — и получил бы баннер «(INCOMPLETE)» с фразой «фрагмент отсутствует в тексте
|
||
НИЖЕ», указывающей на несуществующий текст. То есть я едва не заменил одну ложь читателю на другую.
|
||
Найдено собственным разбором ветвлений до опровергателей, закрыто гардом на обоих сайтах вызова,
|
||
покрыто состоянием «выпали все» в матрице и запинено мутацией (снятие гарда → тест падает).
|
||
|
||
**§5 ОСИ РЕВЮ.**
|
||
1. **Читатель книги.** Все четыре класса проверены на ОТГРУЖЕННОМ ТЕКСТЕ, не на структурах:
|
||
(а) c-lite — был помечен ложно, теперь помечен верно и в потоке текста; (б) юнит целиком withheld и
|
||
(в) `glossary_miss` — помечены верно и раньше, работы не требовали, теперь ещё и не попадают под
|
||
маркер по ошибке; (г) сноска с ханьцзы — накрыта существующим верным маркером.
|
||
2. **Пара, которой в репо нет.** Правка не ветвится по паре ни в одном месте: ось `system_messages` —
|
||
свойство ЭНДПОЙНТА, маркер — английский операторский словарь той же поверхности, что и соседние
|
||
баннеры, поэтому у книги с любым `target_lang` и БЕЗ langpack вывод байт-идентичен. Специально НЕ
|
||
стал класть маркер в langpack: `InjectionTextsFor` для незнакомой цели возвращает нулевое значение
|
||
молча (`lang/embedded.go:411-423`) — маркер бы исчез, и дыра снова стала бы тихой ровно для той
|
||
пары, ради которой мандат общности и написан.
|
||
3. **Детерминизм.** Голден зелёный (`go test ./internal/pipeline/ -run TestGolden -count=1`), и это
|
||
сильная улика: голден пинит ПОЛНОЕ тело запроса с обоими системными сообщениями
|
||
(`testdata/golden/capture.golden:110`), то есть глобальная свёртка сделала бы его красным, а
|
||
квирк-свёртка не трогает никого, кто квирк не объявил. `omitempty` на новом поле запинен тестом
|
||
(`TestCapabilityWithoutTheAxisMarshalsUnchanged` сверяет БАЙТЫ). Снапшот фолдит Capability только
|
||
праймари и `escalate_to` (`snapshot.go:316-340`), а в `pipeline-c1.yaml` это `deepseek-v4-pro`
|
||
(`:60`) — gemini там лишь третий в цепочке при `budget_usd: 0` и SINGLE-HOP раннере ⇒ снапшот
|
||
боевого конфига не двигается, перекупки нет.
|
||
|
||
**§8 OBSTACLE — что НЕ удалось и что НЕ проверено.**
|
||
- **Живого вызова к Gemini НЕ было** (пак платных вызовов не требует, и я их не делал). Что склейка
|
||
ПОМОГАЕТ, доказано структурно вендор-докой и логикой «при любом чтении, кроме склеивания, два
|
||
системных теряют текст» — но НЕ замерено на проводе. Решающая проба стоит денег и принадлежит
|
||
полигону: маркер на ПОСЛЕДНЕЕ системное + user-only контроль закрыли бы открытый вопрос
|
||
«выживает одно или ноль». Также: проба полигона гонялась на `gemini-3.1-flash-lite`, а в цепочке
|
||
стоит `gemini-3.1-pro-preview` — слой тот же, слаг другой, перенос не замерен.
|
||
- **Достижимость §3.1 сегодня НУЛЕВАЯ, и это не смягчение дефекта, а его честная рамка.** Раннер
|
||
исполняет SINGLE-HOP, `escalate_to` боевого конфига — `deepseek-v4-pro`, `budget_usd: 0`. Gemini не
|
||
достигается ни одним шиппинг-конфигом; дефект ждал первого конфига, который назовёт его в
|
||
`escalate_to` или в `chain[0]` лейбла. Кривая пост-чек-промахов на эскалационном хопе, которую §4.2
|
||
просил как второй замер, СНЯТА БЫТЬ НЕ МОЖЕТ по той же причине — мерить нечего.
|
||
- **Ловушка, которую я НЕ чинил и обязан назвать: `capabilities` у провайдера с `kind: anthropic`
|
||
принимается валидацией и молча игнорируется** — `clients.go` не передаёт `Cap` в Anthropic-клиент
|
||
вовсе. Это ПРЕД-СУЩЕСТВУЮЩИЙ класс (так ведут себя ВСЕ оси: `budget_field`, `temperature`,
|
||
`reasoning`, `min_max_tokens`), моя ось лишь наследует его. Чинить правильно — тащить `kind` в
|
||
`validateCapabilities`, что трогает каждую ось; в этом паке это чужой предмет. Практический риск
|
||
сегодня нулевой: провайдера `anthropic` в `models.yaml` нет.
|
||
- **Изменён СУЩЕСТВУЮЩИЙ тест — заявляю прямо, не прячу в дифе.**
|
||
`internal/pipeline/bankreport_test.go` `TestHalfLandedIsNamedPerFile` кодировал ПОРЯДОК переименований
|
||
в своей фикстуре (занимал путь rejects, чтобы упал второй rename). Порядок изменён заказом §3.3, и
|
||
фикстура пере-нацелена на путь delta. Утверждение НЕ ослаблено: по-прежнему требуется, что ровно
|
||
одна половина легла, что это ПРОВЕРЕННЫЕ байты и что отчёт называет какая. Сверх того добавлен
|
||
`TestTheRenameOrderIsWhatMakesADeclineRecover`, который пинит САМ порядок вместе с его причиной,
|
||
чтобы его нельзя было вернуть как «уборку». **Если оркестратор считает это подгонкой — я готов
|
||
откатить и вынести §3.3-половину с полусостоянием отдельным заказом.**
|
||
- **Каталог мутаций: запись `AJ-seed-alias-decline` пере-заякорена.** Моя правка сдвинула строку, на
|
||
которую она указывала, и инструмент падал жёсткой ошибкой «the source moved under the catalogue» —
|
||
это был бы хард-блокер для следующей сессии. Атакуемое свойство сохранено дословно (снять
|
||
дельта-оговорку, оставить остальное); плюс добавлены шесть СВОИХ посадок `SH1`…`SH6`, по две на
|
||
пункт, чтобы фикс был постоянным гейтом, а не разовым замером.
|
||
- **Не проверено:** поведение под реальным параллелизмом двух `bank-apply` (лок это исключает, но я
|
||
не гонял); поведение склейки на провайдере, который принимает одно системное И имеет префиксный кеш
|
||
(у gemini кеш иной) — довод «склейка кеш не портит» опирается на комментарий самого кода и на то,
|
||
что префикс сохраняет байты и позицию, живьём не мерен.
|
||
|
||
**§4 ОПРОВЕРГАТЕЛЬ — ЗАКАЗ ИСПОЛНЕН, И ОН ОКУПИЛСЯ. Шесть линз, мандат «опровергни».**
|
||
Веер шесть, а не «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` не двигался · снапшоты боевых конфигов не двигались · арм-конфиги грузятся.
|
||
|
||
**ЧТО НАЙДЕНО, НО СОЗНАТЕЛЬНО НЕ ЧИНИТСЯ — с аргументом.**
|
||
1. **Ловушка `capabilities` на `kind: anthropic`** — оказалась чинибельной дёшево, и я её ВЗЯЛ: ось
|
||
теперь отказывается на этом виде по образцу `cache_ttl` (`models.go`), с тестом. Пред-существующий
|
||
класс для ОСТАЛЬНЫХ осей (`budget_field`, `temperature`, `reasoning`, `min_max_tokens`) остаётся —
|
||
его правильная форма тащит `kind` в `validateCapabilities` и трогает каждую ось; это чужой предмет.
|
||
2. **Снапшот не покрывает модель ГЕЙТА ТЕРМИНОЛОГИИ** (`config/pipeline.go` — не фолдится сознательно,
|
||
ратифицированно), а терминолог шлёт ДВА системных. ⇒ флип оси у провайдера, которым пользуется
|
||
только этот гейт, меняет байты под НЕИЗМЕННЫМ `RequestHash`, и чекпойнты ложно попадают. Дыра НЕ
|
||
моей оси — по ней ездят все оси `capabilities` с D3.1 — но моя первая, чей смысл именно в смене
|
||
этих байтов. **Утверждение в комментарии я ослабил до правды и дыру НАЗВАЛ прямо в коде**
|
||
(`capability.go`); закрытие — фолд модели гейта, как это делает `repairSnapshot`, и это правка
|
||
снапшот-контракта, принадлежащая владельцу решения «не фолдить», а не провайдерскому квирку.
|
||
3. **Сузив отказ по поверхности, я потерял поучение в одном углу:** decline, который И записан, И
|
||
по-прежнему инертен против подписанного сид-терма, теперь отвечает `already_applied` вместо
|
||
единственной работающей инструкции «убери из glossary_seed». Сходимость сочтена более тяжёлой
|
||
обязанностью (у вызывающего, который следует опубликованному ретраю, другого хода нет), но размен
|
||
реальный: **закрывается полем отчёта, несущим стоячий факт**, и это форма шва.
|
||
|
||
**ХАРД-БЛОКЕР, НАЙДЕННЫЙ И СНЯТЫЙ (требование владельца «после тебя не осталось хард-блокеров»).**
|
||
`go test ./cmd/tmctl/` строил боевой бинарь в `os.MkdirTemp` и НИКОГДА не убирал каталог — по 20 МБ за
|
||
прогон. За этот пак накопилось **249 брошенных каталогов `tmctl-bin*`, ~5 ГБ**, и `/tmp` заполнился до
|
||
99%: батарея под `-race` начала падать с `link: mapping output file failed: no space left on device`,
|
||
что читается как сломанная сборка, а не как кончившееся место. Я на это и купился на один прогон —
|
||
сообщать «батарея красная» было бы ложью. **Причина найдена, место освобождено, харнесс починен:**
|
||
добавлен `TestMain`, снимающий каталог (`cmd/tmctl/exitcontract_test.go`); проверено исполнением —
|
||
два прогона подряд, прирост каталогов 0. Дефект пред-существующий, пак его лишь довёл до отказа.
|
||
|
||
**ЧИСЛА, С КОМАНДАМИ (§4.1).**
|
||
- `go build ./...` · `gofmt -l .` (пусто) · `go vet ./...` · `go vet -tags live ./...` — **все 0**.
|
||
- `go test ./... -count=1 -timeout=20m` → **EXIT 0**.
|
||
- `go test ./... -race -count=1 -timeout=20m` (= строка `make test`) → **EXIT 0, real 2m09s**.
|
||
- `golangci-lint run --timeout=15m ./...` (= `make lint`) → **0 issues**.
|
||
- `go test ./internal/pipeline/ -run TestGolden -count=1` → **ok** (голден пинит ПОЛНОЕ тело запроса).
|
||
- `go run ./cmd/tmmutate -root <КОПИЯ>` → **73 посадки, 0 неожиданных исходов, EXIT 0** (72 RED + один каталогизированный выживший `G-byte-gate`; все 13 моих красны); каталог вырос 60 → 73 (13 новых посадок:
|
||
`SH1`…`SH13`), у каждой из трёх заказанных тем ≥2 своих.
|
||
⚠ **Каталог гонять ТОЛЬКО на копии** — сам инструмент это требует, а я один раз запустил его на рабочем
|
||
дереве и словил таймаут; дерево спас обработчик сигнала самого инструмента (проверено: мутационного
|
||
текста не осталось, батарея зелена). Записываю как урок, а не как мелочь.
|
||
|
||
**ФАЙЛЫ (19 правленых + 5 новых; всё в `backend/`, плюс эта запись в журнале).**
|
||
`cmd/tmctl/render.go` · `cmd/tmctl/exitcontract_test.go` · `cmd/tmmutate/mutations.json` ·
|
||
`configs/models.yaml` · `internal/config/models.go` · `internal/config/models_catalog_test.go` ·
|
||
`internal/llm/capability.go` · `internal/llm/httpllm.go` · `internal/llm/provider_openai.go` ·
|
||
`internal/llm/provider_local.go` · `internal/membank/decisions.go` ·
|
||
`internal/membank/decisions_fuzz_test.go` · `internal/pipeline/bankdecisions.go` ·
|
||
`internal/pipeline/bankreport_test.go` · `internal/pipeline/bookrun.go` ·
|
||
`internal/pipeline/export.go` · `internal/pipeline/mining.go` · `internal/pipeline/runner_test.go` ·
|
||
`internal/pipeline/waverun.go`.
|
||
Новые: `cmd/tmctl/exportgap_cli_test.go` · `internal/llm/systemmessages_test.go` ·
|
||
`internal/membank/decisions_converge_test.go` · `internal/pipeline/bankdecisions_converge_test.go` ·
|
||
`internal/pipeline/systemmessages_wave_test.go`.
|
||
|
||
**⛔ ВОПРОСЫ И ПИНГИ ОРКЕСТРАТОРУ — четыре, каждый требует ЧУЖОГО решения.**
|
||
1. **Изменён СУЩЕСТВУЮЩИЙ тест, и я это заявляю, а не прячу.** `bankreport_test.go`
|
||
`TestHalfLandedIsNamedPerFile` кодировал в фикстуре ПОРЯДОК переименований, который заказ §3.3
|
||
изменил; фикстура пере-нацелена на второй rename, утверждение сохранено дословно и усилено новым
|
||
`TestTheRenameOrderIsWhatMakesADeclineRecover`, пинящим сам порядок с его причиной. **Считаешь
|
||
подгонкой — откачу, и тогда половину §3.3 с полусостоянием надо выносить отдельным заказом.**
|
||
2. **Вендор-вердикт по Gemini** — прислан отдельным сообщением, с URL и датами; три поправки к
|
||
`00-provider-quirks.md:157` (структурная посылка ≠ поведенческий вывод · `Content.parts[]`
|
||
повторяемо · «форум Google» — это два не-сотрудника из 2025). Пишешь ты, зона твоя.
|
||
3. **Дыра снапшота у модели гейта терминологии** (см. выше, п.2 «не чинится»). Закрытие — правка
|
||
снапшот-контракта; принадлежит владельцу решения «гейт не фолдится», не мне.
|
||
4. **Д1 достоин собственного заказа.** Не «расширить decline до энтити» (это против канона), а узко:
|
||
эмиссия майнера энтити-широка (`miner_emit.go` `clusterTouches`), а фильтр авто-банка ключуется
|
||
только по собственному src строки (`mining.go:662`), поэтому ОТКЛОНЁННАЯ поверхность возвращается
|
||
алиасом уцелевшей строки. Канон уже сегодня обещает «declining a surface removes EVERY window of
|
||
that surface» — то есть движок не держит обещание. Правильная форма — снять алиас со строки, а не
|
||
снести строку; это банковая онтология, и в «тихую порчу» её тянуть не следует.
|
||
|
||
**ПРАВО §9 ПРИМЕНЕНО ТРИЖДЫ, каждый раз с грунтом:** Д3 отклонён (символ живёт в `platform/`, зона
|
||
заперта; движковая половина здорова и порядок «карта → память» исполнен) · Д1 отклонён как отдельный
|
||
предмет (выше) · подсказка промта про место маркера отклонена (она перевернула бы `withheld` →
|
||
`translated`). ⚠ И один раз право §9 я применил ОШИБОЧНО: решил не трогать вторую дверь
|
||
`refuseInertDeclines`, рассудив, что её случай — «устаревший документ, а не ретрай». Опровергатель
|
||
показал траекторию из ОДНИХ ПРИНЯТЫХ вызовов. Рассуждение было неверным, исполнение это доказало,
|
||
дверь починена.
|
||
|
||
|
||
**⚠ ЗАПИСКА НИЖЕ — ПЛАН ДО ПРАВОК (§6), она СОХРАНЕНА как след дисциплины, но её выводы местами ОПРОВЕРГНУТЫ итогом выше. Не наследовать из неё: «сломан только класс (а)» (сломан ещё и стык «редактура флагнулась сама + выпал член»), «фиксация порядка записи безопасна» (она купила второй тихий вред, закрытый фильтром дельты) и «Д3 не существует» (существует, в `platform/`).**
|
||
|
||
**📝 ЗАПИСКА-ПЛАН БЭКЕНД-СЕССИИ 28.08 — ПАК «ТИХАЯ ПОРЧА» (`docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md`), §6. Правок кода на момент записи НЕТ.**
|
||
Три заказа: §3.1 инъекция банка не теряется · §3.2 дыра выдачи видна читателю файла · §3.3 повтор
|
||
принятого документа сходится. Ниже — что установлено ЧТЕНИЕМ до правок и как из этого следует план.
|
||
⚠ Всё, что помечено «проверить исполнением», в план входит как обязательство, а не как факт.
|
||
|
||
**Находка 1 (меняет §3.2). Маркер уже есть — и он ВРЁТ.** `cmd/tmctl/render.go:499` печатает для
|
||
флагнутого юнита с НЕПУСТЫМ текстом баннер «(leak cleaned, verify)», а `:38` — «[FLAG … — leak
|
||
cleaned, exported cleaned, verify]». Ветка выбирается по `ce.FinalText != ""`, и c-lite member-drop
|
||
(`export.go:226-230` ставит `DispFlagged` при непустом тексте) попадает ровно в неё. То есть юнит, у
|
||
которого ВЫПАЛ ЦЕЛЫЙ ЧЛЕН, объявляется читателю косметической чисткой санитайзера, которой не было.
|
||
Существующий тест это не ловит: `cmd/tmctl/export_cli_test.go:66` покрывает ветку только причиной
|
||
`sanitizer_stripped`. ⇒ Предмет §3.2 не «завести маркер», а «перестать врать и покрыть непокрытое»
|
||
(пере-формулировка разрешена оркестратором 28.08 при закрытом гейте «что читает человек»).
|
||
|
||
**Гейт §3.2 закрыт чтением: движок НЕ ПИШЕТ ФАЙЛОВ ВООБЩЕ.** Все команды рендерят в stdout
|
||
(`cmd/tmctl/main.go:227-228`, `:338-349`). Читаемых человеком поверхностей у движка две, обе
|
||
текстовые и обе уже несут пер-юнитный баннер: `tmctl translate` (`render.go:27-72`) и
|
||
`tmctl export --plaintext` (`render.go:478-511`). Дефолт `export` — JSON
|
||
(`export_cli_test.go:24-28`), и он же артефакт шва к платформе (`research/23` строка 76 —
|
||
«артефактный контракт (export-JSON · 101 · 100)»). Читательский файл (epub и пр.) собирает
|
||
ПЛАТФОРМА по `POST /books/{bookId}/exports` (`14-api-contract/openapi.yaml:824-870`) — чужая зона.
|
||
⇒ Адресат моего маркера — человек, читающий plaintext-выгрузку движка; платформенная половина
|
||
остаётся за платформой, и провод различение несёт (`14-api-contract/README.md:300`).
|
||
|
||
**Следствие для места правки, и оно расходится с подсказкой промта.** Промт указывал на
|
||
`export.go:237` (прецедент `ApplyHeading`). Туда маркер класть НЕЛЬЗЯ: `ChunkExport.FinalText` — это
|
||
поле `final_text` артефактного контракта, а `waverun.go:203-204` кормит провод предикатом
|
||
`oc.FinalText != ""` (`shipped`). Любой маркер, вклеенный в текст ПУСТОГО юнита, перевернул бы
|
||
`withheld` → `translated` — ровно ⛔(а) промта. `ApplyHeading` этого не делает только потому, что на
|
||
пустом тексте он no-op (`chunk/chunker.go:186-191`). Различение: титул — часть КНИГИ и обязан ехать
|
||
на провод; маркер дыры — метаданные О тексте, и в поле, по которому провод судит, ему нельзя. ⇒
|
||
маркер живёт в РЕНДЕР-слое (`cmd/tmctl/render.go`), на слой дальше проекции. Это ответ на требование
|
||
промта «либо тем же механизмом, либо объясни, почему другое».
|
||
|
||
**Классы дыр — ревизия по коду.** (а) c-lite member-drop — помечен, но ЛОЖНО (находка 1); (б) юнит
|
||
целиком withheld (`waverun.go:496-509`) и (в) gate-override `glossary_miss` (`export.go:319-323`)
|
||
дают пустой текст и попадают в ветку `render.go:502-504` «(not translated, flagged for a human)» —
|
||
помечены ВЕРНО, работы не требуют; (г) четвёртая, сноска «[1] …» мимо детектора примечаний: детектор
|
||
требует заголовочного слова (`lang/data/target-ru.txt`, `sanitizer_trailing_note`), но ханьцзы в
|
||
сноске поднимают класс `CJKLeak`, а он `CosmeticOnly` (`checks/sanitizer.go:161-163`) ⇒ юнит уходит
|
||
как `sanitizer_stripped`, и баннер «leak cleaned» для НЕГО правдив. То есть класс (г) накрыт
|
||
существующим механизмом БЕСПЛАТНО и работы в детекторе не требует — граница промта соблюдена.
|
||
**Проверить исполнением обязательно:** что сноска с ханьцзы действительно даёт `CosmeticOnly`, а не
|
||
подтягивает субстантивный класс.
|
||
|
||
**§3.1 — план и его цена.** Свёртки в `toOpenAIMessages` нет (`llm/httpllm.go:513-521`, копия
|
||
один-в-один); её комментарий `:511-513` говорит, что `CacheBoundary` на этом пути бессмысленна ⇒
|
||
свёртка кеш не портит. У Anthropic путь другой и трогать его нельзя
|
||
(`llm/provider_anthropic.go:169-179`: ведущий system-префикс → блоки, `CacheBoundary` → реальный
|
||
`cache_control`). Ось лечения — `llm.Capability` + блок `capabilities:` (провайдерный ИЛИ модельный,
|
||
`config/models.go:85`,`:146`,`:442-450`), прецедент безопасного расширения — `MinMaxTokens` с
|
||
`json:",omitempty"`. **Детерминизм:** снапшот фолдит РАЗРЕШЁННУЮ Capability только для праймари и
|
||
единственного `escalate_to` (`pipeline/snapshot.go:316-340`,`:479`,`:495`); в `pipeline-c1.yaml`
|
||
`escalate_to: deepseek-v4-pro` (`:60`), а gemini стоит лишь третьим в цепочке `default` (`:129`) при
|
||
`budget_usd: 0` (`:136`) и раннере SINGLE-HOP ⇒ объявление квирка у gemini снапшот боевого конфига
|
||
НЕ двигает. Проверить исполнением (голдены + снапшот-тесты).
|
||
⚠ **Честная граница достижимости:** при SINGLE-HOP и текущих конфигах gemini не достигается ВООБЩЕ;
|
||
дефект латентный и ждёт конфига, который назовёт gemini в `escalate_to` или в `chain[0]` лейбла.
|
||
⚠ **Развилка, которую выношу на опровергателя (§4 заказ):** дефолт оси. «Квирк объявляет тот, у кого
|
||
он есть» (нулевое значение = сегодняшнее поведение) не двигает ничего, но НОВЫЙ провайдер с той же
|
||
ловушкой снова теряет инъекцию молча — тот же класс тихого вреда. «Свёртка по умолчанию, исключение
|
||
объявляет проверенный» безопасна для незнакомого провайдера, но меняет провод боевому переводчику без
|
||
замера (запрет «эмпирика решает») и двигает снапшот. Решение — после опровергателя, с обоснованием.
|
||
⚠ **Посылка промта «Gemini принимает ровно одно системное» ОТОЗВАНА автором** (оркестратор, 28.08):
|
||
это было рассуждение; собственный замер полигона (`00-provider-quirks.md:157`) даёт 535 токенов ≈ не
|
||
выжило НИ ОДНО. Заказу это безразлично, тесту — нет: утверждается пост-фиксный инвариант «системное
|
||
РОВНО ОДНО и инъекция ВНУТРИ него», он верен при любом чтении. Вендор-вердикт со ссылкой уходит
|
||
оркестратору для `00-provider-quirks.md` (его зона).
|
||
|
||
**§3.3 — трасса контрактной сессии ПОДТВЕРЖДЕНА моим чтением, и подтверждено то, что она сама не
|
||
читала.** Отказ живёт в `membank/decisions.go:374-377`, бежит ДО фолда (`:296-300`), и
|
||
всё-или-ничего вытирает документ целиком (`:302-307`). **Порядок записи я прочитал сам:
|
||
`bankdecisions.go:458-473` коммитит ДЕЛЬТУ ПЕРВОЙ**, а собственный комментарий функции `:436-438`
|
||
говорит дословно: «delta-first is safe for an approve and NOT for a decline … convergence is what
|
||
actually carries both». ⇒ окно застревания реально, и код сам называет допущение, которое ложно.
|
||
Идемпотентная ветка уже есть (`already_applied`, `:221-224`,`:269-272`) — вводить понятие не надо.
|
||
План: сузить условие отказа так, чтобы оно не срабатывало, когда отказ УЖЕ записан в rejects, плюс
|
||
направление-гард (настояще-инертный decline обязан отказывать по-прежнему тем же текстом).
|
||
Идентичность документа НЕ вводить — сходимость по СОСТОЯНИЮ.
|
||
|
||
**Д1 и Д3 — беру право §9 и НЕ чиню, оба с грунтом.**
|
||
· **Д3 ОТКЛОНЁН как несуществующий:** гард `LiftBankStop` не найден в коде НИ В КАКОМ написании
|
||
(греп по всему `backend/`); реальный носитель — `mining.go:266-277`, и он уже реализует
|
||
ратифицированный порядок «карта → память»: `writeFileAtomic` карты на `:191`, `MarkStopPresented`
|
||
на `:274`, с комментарием `:270-273` «strictly after the map … the reverse order can mark surfaces
|
||
presented whose map never reached disk» и с явной оценкой отказа памяти как «one extra stop, nothing
|
||
lost» (`:275-276`). То есть механизм написан ВМЕСТЕ с памятью v16, а не до неё.
|
||
· **Д1 ОТКЛОНЁН как ратифицированное поведение + названный остаток:** канон определяет decline
|
||
ПОВЕРХНОСТНО («declining a surface removes EVERY window of that surface» —
|
||
`14-api-contract/openapi.yaml:2051`), и расширение до энтити противоречило бы контракту; а «память
|
||
глушит стоп» — уже НАЗВАННЫЙ и принятый двусторонний остаток в комментарии
|
||
`mining.go:308-313`. Оба — строка с причиной, не правка.
|
||
|
||
**Сосед 131 (называю, не чиню, как велит §3.4):** кап инъекции считает только «src → dst» и не
|
||
считает маркер `⟨проверить⟩` и родовые пометки (`lang/data/injection.txt` несёт их как отдельные
|
||
значения) ⇒ реальная инъекция шире бюджета. Проверю точный носитель при работе в этом коде и назову
|
||
`file:line` в итоге; чинить не буду — тема денег и качества инъекции, а не тихого брака в тексте.
|
||
|
||
**Порядок работы:** §3.1 → §3.2 → §3.3, каждый с тестом на ПОСТ-фиксный инвариант; батарея под
|
||
`-race` + линтер; посадки мутаций ≥1 на пункт с вердиктом по ДЕЛЬТЕ и ТОПИЧНОСТИ; опровергатель на
|
||
выбор места §3.1 (заказ §4). Веер агентов соразмеряю предмету: численный потолок «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).**
|
||
|
||
**⛔ Пинги:**
|
||
1. **Платформе/оркестратору:** `.bank-stop.json` СНЕСЁН (мёртвая публикация упразднённого D39.144
|
||
пер-термного экрана) — платформенные журналы (`platform-PROGRESS.md`, `STACK_DECISIONS.md`)
|
||
называют его атомарно-читаемым сайдкаром, упоминания устареют при лендинге.
|
||
2. **Потребителям банк-экспорта:** документ несёт `as_of` (граница прогона) и `run_id` (тот же id,
|
||
что стрим `events.jsonl`); аддитивно, старые читатели не ломаются; пустой `run_id` = «идентичности
|
||
нет», не «текущий».
|
||
|
||
**✅ ИТОГ БЭКЕНД-СЕССИИ 27.08 — ДОФИКС-2 «ФЛАЖОК СТАНОВИТСЯ ФЛАЖКОМ» (`docs/BACKEND_SEAM_FIX2_SESSION_PROMPT.md`). Дерево готово, НЕ закоммичено; продолжает дерево паков 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):**
|
||
1. **Движок первым, платформа второй.** Сброс флага `--verify-bank` платформой (её персистентная
|
||
колонка + особый случай в спавне, `platform/internal/runs/spawn.go`) может умереть ТОЛЬКО ПОСЛЕ
|
||
лендинга этого пака: в обратном порядке каждая книга с поднятым флажком в день лендинга перестаёт
|
||
писать авто-банк и пере-сидироваться ровно в режиме, ради которого флажок существует.
|
||
2. **`tmctl migrate` до прогонов** (голова схемы v15 → v16): `OpenReadOnly` отказывает классом 13 всем
|
||
непере-мигрированным книгам — ратифицированный порядок D39.134 п.3 это покрывает, но назван явно.
|
||
3. **Миграционная нота:** после апгрейда память пуста → ОДИН доброкачественный стоп на первом
|
||
verify-прогоне каждой книги.
|
||
|
||
**⛔ ПИНГИ ЧУЖИМ ЗОНАМ (правок не делал):**
|
||
1. **Платформе:** у неё умирает сброс флага при resume (после лендинга движка — п.1 выше) · отчёт двери
|
||
теперь v2 (`tm-bank-decisions-report-v2`: mode `refused`/`stopped`/`write_incomplete`, пофайловые
|
||
`written_*`) — её `readEngine` выбрасывает stdout при ненулевых кодах, прежний пинг об этом стал
|
||
острее: класс 15 БЕЗ отчёта неотличим от «ничего не легло» · новый exit 15 в полосе 10–19 читается
|
||
её ридером как refused по построению полосы — ратификация значения на оркестраторе при лендинге.
|
||
2. **Полигону:** `eval/bank_autonomy/parse_common.py` парсит `mined-signature.yaml` — карта теперь
|
||
несёт верхние ключи `map_version`/`map_id` (лежащие артефакты coldrun не тронуты; новые прогоны
|
||
будут в новом жанре).
|
||
3. **Фронту:** экран подписи, читающий `signature.undecided` из отчёта двери, обязан перестать трактовать
|
||
его как «стоп не погаснет» — поле информационное (D39.144), контракт поля пере-описан в коде.
|
||
4. **Майнерский хвост за потолком структурно недостижим** (§4.4, НЕ чинилось по заказу): потолок эмиссии
|
||
(200) режет ранжированный пул ДО фильтров пригодности/сида/отказов (`miner_emit.go`,
|
||
`ranked[:emitRankCap]` до `emissionEligible`). Пере-замерено своей командой (числа прежнего
|
||
сплошного читателя в дерево не попали): 2354 ранжированных · 209 годных во всём алфавите · 11 в окне
|
||
потолка · 198 невидимы постоянно; отклонение всех годных окна не подняло ни одного из хвоста
|
||
(`FINDINGS` §38). Первоисточник замера — постоянный якорь-пин `internal/miner/emissioncap_test.go`:
|
||
он утверждает ТЕКУЩЕЕ поведение и покраснеет в день сдвига потолка или порядка фильтров, приведя
|
||
двигающего к этому пингу. Двигать потолок — трогать фрозен-паритет; отдельное решение.
|
||
5. **Гипотеза «подписание промахивается мимо чекпоинтов терминолога и пере-оплачивает роль» механизмом
|
||
НЕ подтверждена** (§4.13): память предъявленного входов чекпоинта не трогает; если перерасход
|
||
существует — он существовал и до пака, отдельный предмет.
|
||
6. **Остаток оси арбитра (нашло воркфлоу-ревью, воспроизведено живьём):** второй конфиг ТОЙ ЖЕ книги
|
||
(один каталог, один book_id, другой объявленный `project_db`) пишет ту же пару файлов решений под
|
||
ДРУГИМ локом — в т.ч. мимо flock живого прогона. Межкнижная версия снята конвенцией; эта — второй
|
||
конфиг одной книги, т.е. рукотворная конфигурация оператора. Выбор лечения (второй арбитр на
|
||
идентичности файлов решений или отказ при загрузке) трогает закон п.2 — решение не паку, пинг.
|
||
Довод в коде сужен честно (`bankdecisions.go`, THE ARBITER).
|
||
|
||
**✅ ИТОГ БЭКЕНД-СЕССИИ 25.08 — ДОФИКС ТОЙ ЖЕ ДВЕРИ по приёмке (`docs/BACKEND_SEAM_FIX_SESSION_PROMPT.md`). Дерево готово, НЕ закоммичено; продолжает дерево пака 24.08, а не заменяет его.**
|
||
Все 16 пунктов §4 закрыты: блокер формата (дверь писала байты, которых её же парсер не читает, и любой
|
||
следующий вызов по книге — включая проекцию — падал) · пред-существующие поломки чинятся по одной ·
|
||
два ключа на один путь · гарды флагов на ПРИСУТСТВИЕ (`--decisions=` уходил на ПЛАТНЫЙ путь) · кап
|
||
документа решений · конверт путей на `manifest --json` · алиасы судятся по НАБОРУ · перевёрнутое окно
|
||
глав · честность отчёта о стопе · SIGTERM · десять слепых пятен гейта · **цепь целиком в репозитории**.
|
||
**Доказательная база, замеры, попунктный чек и обе итерации ревью — `backend/docs/SEAM_PACK_FINDINGS.md`
|
||
§§14–30;** здесь только то, что требует ДЕЙСТВИЯ.
|
||
|
||
**✅ ⛔-ЗАКАЗ ПРИЁМКЕ ИЗ ПАКА СНЯТ.** Цепь «стоп → `bank-apply` → резюм → стоп ГАСНЕТ» теперь живёт в
|
||
репозитории как $0-тест на фейковом провайдере (`internal/pipeline/bankchain_test.go`), на ОДНОМ проекте
|
||
от стопа до погасшего стопа. Довод пака «своими силами невозможно, надо дойти оплаченной черновой волной»
|
||
был шире правды: харнесс для этого уже был в репозитории. Мина №1 строки **199** (ливлок на `status:
|
||
auto`) закрыта исполнением, а не рассуждением.
|
||
|
||
**⚠ РАТИФИКАЦИЯ КЛАССА 14 — прежнее требование в силе, объём немного шире.** Класс `decisions_rejected`
|
||
теперь несёт и отказ по КАПУ (документ больше, чем движок применяет за один акт): это факт о ЗАПРОСЕ, а
|
||
не о деплое, поэтому 10 был бы неверен. Носители полосы поднимает оркестратор при лендинге, как и
|
||
планировалось.
|
||
|
||
**⛔ ПИНГИ ДОФИКСА (правок не делал):**
|
||
1. **`exit 2` при OOM платформа читает как успех** — общедвижковый класс; кап удерживает `bank-apply` от
|
||
того, чтобы стать первым, кто до него доедет, но самого класса не снимает.
|
||
2. **Каталог книги без права записи даёт `exit 1`** (вне полосы). Класс движка корректен и одинаков у
|
||
всех глаголов — чинить надо чтение кода 1 на платформе, которая списывает проблему прав ХОСТА в
|
||
бюджет попыток КНИГИ.
|
||
3. **Проекция отвечает «что применится», а не «почём».** Рекомендация — НЕ строить: глагол $0 по
|
||
построению, поле «$0» было бы шумом. Решать оркестратору.
|
||
4. **Карта подписи не удаляется после погасшего стопа** — пред-существующее; зафиксировано тестом как
|
||
текущее поведение. Честный ответ на «есть ли что подписать» — новое поле отчёта `signature.undecided`.
|
||
5. **Цена глагола Θ(N²) по построению** — каждый пер-решенческий помощник сканирует весь документ.
|
||
Именно поэтому кап на КОЛИЧЕСТВЕ стоит на 5 000. Поднимать его без индексации этих сканов нельзя.
|
||
6. **Апстрим-дефект `gopkg.in/yaml.v3 v3.0.1`** (эмиттер пишет блочный скаляр, который его же парсер не
|
||
читает) вылечивается по-настоящему только скаляр-типом схемы с явным стилем или переездом на
|
||
`go.yaml.in/yaml/v4` (релиз-кандидат, смена module path в ДВУХ зонах). Мой обход несёт ссылку на
|
||
исходник библиотеки и характеризационный тест, который покраснеет в день починки апстрима.
|
||
7. **Фолд поверхностей Go-резидентный:** `internal/text/norm.go` держит `trad2simp` и катакана→хирагана
|
||
в Go, а не в langpack'е. Пара, которой в репо нет, деградирует до NFKC+lowercase — не блокирована, но
|
||
это ровно тот класс, о котором ревью-вопрос общности. Шире двери, поэтому пингом.
|
||
|
||
**⚠ ЧТО В ЗАПИСИ ПАКА 24.08 (ниже) УСТАРЕЛО — читать её через эту поправку.**
|
||
1. **Пинг 2, «таймаут: 1.6с на 5 000 термов, 9.1с на 20 000» — ЧИСЛА НЕВЕРНЫ, и это ровно тот датум,
|
||
которым платформенный пак будет мерить свой таймаут.** Мой замер той же командой: 5 000 `approve` →
|
||
**5.2 с**, 20 000 → **50 с**; на минимальных `decline` (самая дешёвая форма, и потому та, которой
|
||
байтовый кап пускает больше всего) 5 000 → **11.3 с**, 12 000 → **58 с**, 28 900 → **308 с**. Брать
|
||
надо `FINDINGS` §18 и §29 п.1, не эти.
|
||
2. **Пинг 6 «однострочник при лендинге: документ решений не капирован» — СДЕЛАН.** Капов теперь два
|
||
(1 МиБ на чтение + 5 000 решений на применение), потому что байтовый не ограничивает время.
|
||
3. **Пинг 3 про флейки** пере-снят сильнее: изолированный A/B против дерева HEAD даёт побайтно
|
||
одинаковое падение с обеих сторон — `FINDINGS` §31.
|
||
|
||
**⛔ ПИНГИ, КОТОРЫЕ ЖИЛИ ТОЛЬКО В `FINDINGS` И НЕ ДОЕЗЖАЛИ ДО ЖУРНАЛА** (найдено проверкой согласованности):
|
||
карта подписи пишется обычным `os.WriteFile` мимо атомарной конвенции сайдкаров того же пакета
|
||
(пред-существующее) · цена доказывающего рендера ≈5 % времени, плюс `notCanonical` теперь тоже разбирает
|
||
документ · опубликованные пути двух файлов решений не несут версионного конверта (пинг пака §10.5) ·
|
||
схема сида без `omitempty` (пинг пака §10.6).
|
||
|
||
**⚠ Два пред-существующих `-race`-флейка в `internal/store` не тронуты** (`TestKillMinus9LosesAtMostOneCall`,
|
||
`TestTheSeamIsNotChargedToTheStoreOperationBudget`). База ДО правок, снятая на нетронутом дереве пака,
|
||
уже была красной по первому — это моя собственная атрибуция, а не наследованная.
|
||
|
||
**⛔ ПИНГ, КОТОРЫЙ ТРОГАЕТ РАТИФИЦИРОВАННЫЙ ЗАКОН (п.2) — решать не паку.** Арбитр двери — flock на
|
||
`<project_db>.lock`, а пишутся `mined_delta`/`mined_rejects`, которые `book.yaml` может направить куда
|
||
угодно. **Две книги с РАЗНЫМИ `project_db` и ОДНИМ delta друг друга не исключают:** воркфлоу-ревью
|
||
продемонстрировало гонку на 1500 одобрений с каждой стороны — одна сторона потеряла всё, `exit 0`, отчёт
|
||
«применено». Внутрикнижную версию этого §4.3 уже запрещает; межкнижная законна сегодня. Выбор — второй
|
||
арбитр или отказ при загрузке на путь вне каталога книги. В корпусе нарушителей нет. В коде стоит явное
|
||
«арбитр покрывает проект, а не пути», а не молчаливое допущение.
|
||
|
||
**🔧 В репозиторий добавлен инструмент: `backend/cmd/tmmutate`** — харнесс мутаций с каталогом из 36
|
||
посадок, у каждой названо СВОЙСТВО, за которое она отвечает, и записан ожидаемый исход. Три сессии подряд
|
||
писали такой инструмент в скретчпад и теряли его; приёмка при этом нашла восемь мест, где гейт был слеп.
|
||
⚠ **Он дважды подводил и оба раза был починен по следу:** сначала подсадил мутацию и прошёл собственную
|
||
sha256-самопроверку (эталон второй правки снимался с уже пропатченного файла), затем считал КРАСНЫМ провал
|
||
СБОРКИ — из-за чего две записи каталога, сломанные рефакторингом, целую итерацию числились пойманными
|
||
тестами, которые не могли отработать. Теперь: одно чтение и запись на файл, отпечаток всего каталога перед
|
||
каждой посадкой, зелёная базовая линия пакета ДО первой посадки и третий вердикт `NOTHING`, роняющий гейт.
|
||
`FINDINGS` §25 и §34.
|
||
|
||
**✅ ИТОГ БЭКЕНД-СЕССИИ 24.08 — ПАК «ВХОДНАЯ ДВЕРЬ ШВА», движковая половина D39.156. Дерево готово, НЕ закоммичено, передаётся на лендинг.**
|
||
Заказ §4.1–§4.6 выполнен: конвенционные дефолты `mined_delta`/`mined_rejects` (ключей не объявляет и
|
||
файлов заранее не создаёт никто) · глагол `tmctl bank-apply --config <book.yaml> --decisions <doc.json>
|
||
[--dry-run]` ($0, идемпотентен побайтово, всё-или-ничего, занятый flock — ровно 12 неблокирующе, ответ
|
||
несёт версию и явное `depth: edit_wave`) · `--keys-file` у `translate` (строка **211**) · строгость
|
||
сид-загрузчика (**212**) · `status_version` + `artifacts{}` с абсолютными путями (**213**) · дефолт
|
||
стендового корпуса маркерным резолвом (**218**, критерий оркестратора выполнен буквально).
|
||
**Вся доказательная база, замеры и попунктный чек промта — `backend/docs/SEAM_PACK_FINDINGS.md`;**
|
||
здесь только то, что требует ДЕЙСТВИЯ.
|
||
|
||
**⚠ ТРЕБУЕТ РАТИФИКАЦИИ: заведён класс полосы отказов 14 `decisions_rejected.`** Отвергнутый по существу
|
||
набор решений ≠ сломанный деплой: `platform/internal/ingest/exit.go` `DeploymentFault` относит 10 к
|
||
«условию, общему для всех книг хоста», и по 10 набор решений одного пользователя списался бы на
|
||
оператора. Старый читатель деградирует безопасно (`Refused(14)` ⇒ `OutcomeRefused`). 10 сохранён для
|
||
документа, не прочитавшегося КАК документ решений. Гейт тотальности таблицы добавлен.
|
||
|
||
**⛔ ЗАКАЗ ПРИЁМКЕ — единственный незакрытый замер (согласован со сторонним архитектором).** Цепь целиком
|
||
не гонялась ни разу: стоп красный → `bank-apply` → резюм → **стоп гаснет**. Части доказаны порознь, но
|
||
мина №1 строки **199** — livelock уровня ЦЕПИ, которого частичные тесты по построению не видят. Снять не
|
||
могу, и это проверено: банк-стоп на машине не воспроизводится (`contrast_path` → отсутствующий
|
||
`eval/exp16/data/jieba_dict_general_zh.txt`, строка **123**), а до стопа надо дойти оплаченной черновой
|
||
волной. **Команда:** на КОПИИ книги на exit 3 скормить `bank-apply` документ на всю карту подписи, затем
|
||
`translate --verify-bank`. **ДЕФЕКТ** — стоп снова с ТЕМИ ЖЕ поверхностями; **не дефект** — свежая пачка
|
||
≤200 термов реверс-секции (мина №3 строки 199). До этого прогона вердикт по циклу — PLAUSIBLE.
|
||
|
||
**⛔ ПИНГИ ЗОНАМ И СЛЕДУЮЩИМ ПАКАМ:**
|
||
1. **`rubyToCandidates` мёртв в проде** — единственный вызов из теста (`memseed_test.go:204`);
|
||
`seeding.go:36-38` строит `manualSrcs` и никуда не передаёт (Go молчит: запись в map = использование).
|
||
Строк `Source:"ruby"` в банке не возникает, а платформа держит для них ветку словаря
|
||
(`ingest/bank.go` `termOrigin` → `OriginAnnotated`).
|
||
2. **Платформенному паку:** первый контакт с непере-мигрированной книгой даёт **exit 13** — воркер решений
|
||
обязан планировать `13 → migrate → повтор` · **проекция берёт тот же лок**, поэтому правка термина при
|
||
живом прогоне даёт 12 даже на превью, на всю длину прогона · **таймаут**: 1.6с на 5 000 термов банка,
|
||
9.1с на 20 000, бюджет платформы на вызов движка — 60с (`runs/reconcile.go:280`) · `bank-apply`
|
||
создаёт `<project_db>.lock` при первом касании никогда не запускавшегося проекта.
|
||
3. **⚠ БАТАРЕЯ НЕ ДЕТЕРМИНИРОВАННО ЗЕЛЕНА, и это НЕ пак — прочесть ДО пере-рана.** Два
|
||
пред-существующих `-race`-флейка в `internal/store` срабатывают примерно раз на 2–4 полных прогона
|
||
(`go test ./... -race -count=1` ×4 → 1 чист, 2-й красный). **Красное именно в этих двух тестах
|
||
ожидаемо; любое другое красное — регрессия.** Тесты НЕ трогал (D39.121).
|
||
`TestTheSeamIsNotChargedToTheStoreOperationBudget`, `TestKillMinus9LosesAtMostOneCall`. Чередующийся
|
||
A/B на `git worktree` HEAD: оба падают на HEAD **не реже** (`kill9` 2/30 против 0/30; `seam` 2/30
|
||
против 1/30; без `-race` 0/30). Покрытие: `LockProject`/`Release` этими тестами не исполняются вовсе.
|
||
⚠ 0/30 против 2/30 НЕ значит «пак улучшает» — на таком числе это шум (Фишер p≈0.5); достаточно того,
|
||
что флейк живёт на дереве, которого пак не касался.
|
||
4. **Гейт `TestDispatchCommandsCoversTheSwitch` слеп к `case` с константой** (считает только
|
||
`*ast.BasicLit`) — команда, объявленная константой, невидима гейту, чей смысл в дефекте строки **176**.
|
||
Я вернулся к литералу (конвенция файла), дыра не эксплуатируется.
|
||
5. **`redrive` — платный путь без канала ключей аргументом.** Промт: «флаг только у `translate`», сделано
|
||
ровно так; канал у него есть (конвенционный `.env`), платформа его не зовёт. Развилка следующему паку.
|
||
6. **Однострочник при лендинге (не блокер):** документ решений читается неограниченным `os.ReadFile`,
|
||
тогда как платформа свои чтения от движка капает. Документ на 10 000 решений ≈2МБ ⇒ кап 1–16МБ.
|
||
|
||
**⚠ ПРАВИЛА, ВЫНЕСЕННЫЕ ИНЦИДЕНТАМИ — для следующих сессий, не про этот пак.** Прогон харнесса мутаций
|
||
был убит таймаутом, `finally` не отработал, и посадка просидела в дереве ~40 минут, выглядя
|
||
продакшн-кодом; сторонний ревьюер снял с него замеры и доложил несуществующий дефект. Поймал посадку
|
||
собственный пин пака. Отсюда три правила: **харнесс мутаций обязан фингерпринтить цели** (починено) ·
|
||
**снимай отпечаток дерева перед сборкой бинаря замера** (на этом наступили обе стороны) · **одиночный
|
||
прогон — не замер** (атрибуцию флейков я дважды вывел неверно из одиночных прогонов).
|
||
|
||
**Что НЕ доказано:** рантайм-вердикт по платному пути — **PLAUSIBLE**. Доказано, что ключ из
|
||
`--keys-file` доезжает до заголовка и что без флага прогон отказывает конфигом; не доказано, что
|
||
реальный провайдер примет ключ и что SaaS положит файл по этому пути — платформенная половина строки
|
||
**211**, чужая зона. Живой `translate` не гонялся: санкции на платные вызовы у пака нет. (24.08)
|
||
|
||
**⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ БЭКЕНДА. Четыре движковых дефекта, все сверены моими командами.**
|
||
|
||
1. **ОТВЕТ НА ПИНГ ПОЛИГОНА: дефект ПОДТВЕРЖДЁН, он ваш.** Эскалация на Gemini теряет инъекцию
|
||
банка памяти (строка **208**). Механизм пере-проверен: `pipeline/render.go:256-258`=`Role: "system", Content: injection, CacheBoundary: false`
|
||
штатно строит ВТОРОЕ системное сообщение, а `llm/httpllm.go` (до-паковые строки 517-523, якорь ИСТОРИЧЕСКИЙ — сигнатура сменилась лендингом D39.164: функция приняла ось `mode SystemMessagesMode` и под квирком СКЛЕИВАЕТ системный ряд, так что описанного здесь «один-в-один» больше нет)
|
||
копирует сообщения дословно — склейки нет нигде. ⚠ Лекарство того же класса в проекте УЖЕ
|
||
написано и работает для другого провайдера: `llm/provider_anthropic.go:169-179`=`inSystemPrefix := true`
|
||
сворачивает ведущий system-префикс в одно поле. Не обобщено на Gemini — это и есть работа.
|
||
Полигон ждёт ответа с 22.08 и починил у себя по вендор-доке; их находка верна.
|
||
|
||
2. **Род не производится авто-путём вовсе** (строка **210**, дополнена 23.08): замка ДВА и они
|
||
независимы, а поле `Gender` в схеме сида УЖЕ есть — ось мертва в ПИСАТЕЛЯХ, не в формате.
|
||
|
||
3. **Выпавший член юнита исчезает из отгружаемых байт БЕЗ внутритекстового маркера** —
|
||
`pipeline/waverun.go:487-490`=`the flagged member is DROPPED from the edit`. Родня строки **193**;
|
||
для читателя это молчаливая дыра в тексте.
|
||
|
||
4. **Загрузчик сида НЕстрогий** (строка **212**): голый разбор YAML без проверки имён ключей, и
|
||
`seed-lint` этого не видит. Опечатка в имени поля молча теряется — в файлах, которые владелец
|
||
правит руками чаще всего.
|
||
|
||
⚠ Плюс строки **211** (движок на SaaS не получает провайдерских ключей — блокер живого прогона) и
|
||
**213** (платформа дублирует вашу конвенцию пути): обе половинные, движковая часть ваша.
|
||
|
||
**⛔ ПИНГ ОРКЕСТРАТОРА №20 — 22.08, ЗОНЕ БЭКЕНДА: три ваших дока разошлись с деревом (аудит доков, сверено кодом; рукой не лезу — ваша зона).**
|
||
1. **`backend/README.md`, карта пакетов** объявлена сверенной по коду, а в ней НЕТ `internal/runevents` — эмиттера журнала прогона, то есть ВСЕГО шва движок↔платформа (D39.131), — и нет второго бинаря `cmd/tmvet`. Греп: `runevents` и `tmvet` в файле по нулю вхождений, при том что оба каталога в дереве.
|
||
2. **`backend/README.md`, «Известный техдолг»** держит живыми Cyrillic-хардкоды DC-чекеров, которые сам код называет ПОГАШЕННЫМ долгом (фаза 2 общности, D39.64: `isRuTarget` удалён, `TargetActive`/`TargetScriptNonLatin`), и оба его `file:line`-якоря указывают мимо.
|
||
4. **(дописано 24.08) `backend/README.md:47`=`/home/ubuntu/books/gu-zhenren/` зовёт книгу-стенд по МЁРТВОМУ пути** `/home/ubuntu/books/gu-zhenren/` — каталога нет ни там, ни в `$HOME`: книги переехали в `<репозиторий>/books` и версионируются своим git (D39.157 п.2). Там же фраза «тексты и производные ВНЕ git» стала неточной: вне ЭТОГО git. Тем же касанием — три тест-хелпера, резолвящие корпус от `$HOME/books` (строка 218); они уже в заказе движкового пака шва §4.6, так что дублировать правку не надо, но док обязан сойтись с ними.
|
||
3. **`backend/docs/components.puml`** — диаграмма, которую карта доков продаёт как правимую «одним коммитом с кодом», отстала примерно на месяц: старый exit-контракт (без 4/5 и полосы 10–19), неполный список глаголов CLI, удалённый паком-17 `failover.go` и ни следа эмиттера. ⚠ Рендерить её в svg/png НЕ надо (гардрейл) — править исходник.
|
||
|
||
*(Пак «честные числа и статусы» — ИСПОЛНЕН, ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН `f840867` (17.08; D39.149, ФЧ-5 — D39.150). Отчёт, две волны адверсариального ревью, мутационная приёмка и тело фикс-листа ФЧ-1…ФЧ-8 — в [archive/PROGRESS-2026-08-17.md](archive/PROGRESS-2026-08-17.md); живой остаток — строка бэклога 197.)*
|
||
|
||
*(Пинги закрытых паков — фикс-пак банка D39.118, блокеры контракта D39.122 — в [archive/PROGRESS-2026-08-04-09.md](archive/PROGRESS-2026-08-04-09.md), D39.125.)*
|
||
|
||
*(Закрытые бэкенд-записи 01–02.08 — общность фаза 2 · качество банка (оба этапа) · фикс-пак банка · finding-1 · пакет-чекеров · мелкая пачка · coldrun-b · ручка эффорта — в [archive/PROGRESS-2026-08-01-02.md](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](archive/PROGRESS-2026-08-14-15.md); вынесено D39.139.)*
|
||
|
||
|
||
*(Закрытая хроника пакетов №0–№4 (04–10.07) перенесена в [archive/PROGRESS-2026-07-04-10.md](archive/PROGRESS-2026-07-04-10.md) — ревизия D31.)*
|
||
|
||
*(Хроника пакетов 11–12.07 (пакет-5, D15.2-этап-А, санитайзер-фикс D33) — в [archive/PROGRESS-2026-07-10-13.md](archive/PROGRESS-2026-07-10-13.md).)*
|
||
*(Трек-A пак-1/1.5 (D39.2/D39.5) — см. записи оркестратора №6 выше.)*
|
||
|
||
*(Хроника бэкенд-паков 11–16 (19–25.07, вкл. отчёт-запись пака-15) — в [archive/PROGRESS-2026-07-13-25.md](archive/PROGRESS-2026-07-13-25.md); отчёты паков — `archive/reports/`.)*
|
||
|
||
*(Закрытые записи 25–31.07 — пак-18 · мини-прогон · фикс-пак · сквозная проверка · хендофф 26.07 · ресёрч общности фаза 1 — в [archive/PROGRESS-2026-07-25-31.md](archive/PROGRESS-2026-07-25-31.md).)*
|
||
|
||
## ⟶ Вынесено 30.08 оркестратором №20: прожитые дампы паков «писатель книги» (D39.175) и «деньги» (D39.170)
|
||
|
||
> Оба пака ПРИНЯТЫ и ЗАЛЕНДЖЕНЫ, их решения и числа живут телами нот. Здесь — исходные записки-планы и
|
||
> итоги сессий целиком, как доказательная база. ⚠ Инструкции отсюда НЕ исполнять.
|
||
### 📝 ЗАПИСКА-ПЛАН — пак «писатель книги» (промт `docs/archive/prompts/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT_2026-08-30.md`, строка 236), сессия `textmachine-main-fc`, 30.08
|
||
|
||
**Что беру (зона `backend/`, HEAD на старте `67c05be`, дерево чисто).**
|
||
1. Прод-пакет `backend/internal/bookfile` (только stdlib — `archive/zip`, `encoding/xml`; без `import "testing"`):
|
||
`container.go` — OCF-контейнер (`mimetype` первым и `zip.Store`, `container.xml`, запись entry в порядке вызовов,
|
||
нулевые таймметки) — `chunktest/epub.go` переводится на него, его OPF-заглушка и XHTML-декой остаются тестовыми;
|
||
`epub.go` — EPUB 3 из нейтральной модели `Book{Identifier, Title, Language, Modified, Description,
|
||
Chapters[]{Title, Paragraphs[]}}`: `dc:identifier`/`dc:title`/`dc:language`/`dcterms:modified`, nav
|
||
(`properties="nav"`, ⛔ НЕ в spine), один XHTML на главу, `<h1>` = заголовок главы, `<p>` = абзац;
|
||
`txt.go` — чистый текст из той же модели (заголовок строкой, абзацы через пустую строку, главы через
|
||
пустую строку; ни баннеров, ни флагов, ни `gapMarker`).
|
||
2. Сборка из `BookExport` — `backend/internal/pipeline/bookbuild.go`: `Runner.BuildBook(formats, partial)` →
|
||
`Export(false)` → дыры → отказ либо файл. Запись `writeFileAtomic` рядом с ProjectDB
|
||
(`<project_db>.book.<fmt>`, как `.manifest.json`/`.bank.json`), путь публикуется в
|
||
`StatusArtifacts.BookFiles` (`book_files`: map формат→путь, аддитивно) — приор промта принят: платформа
|
||
пути не выводит сама (17 §1). Существующий файл — перезапись: производный детерминированный артефакт, как
|
||
манифест; бэкап отказывает потому, что он точка восстановления, а файл книги пересобирается из стора.
|
||
3. Аддитивные поля `tm-export-v1` (форма не меняется, все `omitempty`): `ChunkExport.Heading` (`heading`) —
|
||
литерал `u.Members[0].Heading` в момент сборки записи (и у pending-юнита тоже, он из манифеста);
|
||
`BookExport.GhostUnits` (`ghost_units`: `[{chapter,chunk_idx}]`) — ключи ghost-строк, чтобы маркер стоял
|
||
в главе, а не только в счётчике; `BookExport.TextModified` (`text_modified`, `CCYY-MM-DDThh:mm:ssZ`) —
|
||
max(`chunk_status.updated_at`) по финальным строкам, вошедшим в файл (колонка `NOT NULL DEFAULT
|
||
datetime('now')`, `store/migrate.go:37`; в `ChunkStatus` дочитывается доп. полем). Это и есть
|
||
`dcterms:modified`: последнее изменение отгруженного текста — факт стора, не часов процесса.
|
||
4. Глагол `tmctl build --config b.yaml [--format epub,txt] [--out path] [--partial]` (по умолчанию оба формата;
|
||
`--out` — только при одном формате). Stdout — JSON-конверт `tm-build-v1` (пути, счётчики, `complete`).
|
||
Новый глагол, а не флаг `export`: `export` — чистое чтение в stdout, писать файлы из него — смешать
|
||
аудит-проекцию с артефактом; `manifest` — прецедент глагола-писателя сайдкара. `dispatchCommands` и
|
||
usage-строка расширяются — контрактное расширение того же рода, что `bank-apply` (тест
|
||
`TestParseNoArgsUsage` сам перечисляет такие расширения); чужим глаголам новые флаги ОТКАЗЫВАЮТСЯ
|
||
(дисциплина `--keys-file`/`--max-units`).
|
||
5. Отказ — новый `pipeline.RefusalBookIncomplete = "book_incomplete"` → exit **16** (полоса 10–19; таблица
|
||
тотальна по `TestTheRefusalTableIsTotal`); сообщение перечисляет дыры `глава/юнит/причина`.
|
||
6. Читательские слова — ДАННЫЕ: `<langpack_root>/<target_lang>/reader.txt` (`key<TAB>value`, плейсхолдеры
|
||
`{chapter} {unit} {dropped} {holes} {total} {ghost}`), читает ОТДЕЛЬНЫЙ загрузчик `lang.LoadReaderWords`;
|
||
в `Pack.Version()` НЕ фолдится (пин тестом «наличие reader.txt не меняет Version()» + `current_snapshot`
|
||
minirun до/после). Ключ — язык ЦЕЛИ, не пара: слова читает читатель целевого языка, и они не зависят от
|
||
исходного. Поставляю `backend/configs/langpacks/ru/reader.txt`. Каталог `<tgt>/` загрузчик пака не
|
||
читает (`Load` — только `<src>/` и `<pair>/`, `langpack.go:215-330`), так что канал вне фолда по
|
||
построению, а не по соглашению.
|
||
|
||
**Политика §3.2 — по умолчанию ОТКАЗ, `--partial` — файл с пометкой; довод:**
|
||
- Дыры: pending · withheld (`Disposition!="pending" && FinalText==""`) · incomplete (`DroppedMembers>0` при
|
||
непустом тексте) — арифметика `render.go:573-587` выносится в общий хелпер `pipeline`, которым пользуется и
|
||
`renderExport` (вывод `--plaintext` побайтово прежний) · **ghost — тоже дыра**: при укрупнении нарезки
|
||
строка-лидер несёт текст ПРЕЖНЕГО, более короткого юнита, а остаток источника лежит в ghost-строках и в
|
||
файл не попадает (acceptance: 57 строк → 37 юнитов, 20 ghost при `pending 0` — ровно «молча полная»
|
||
книга). Позиция ghost внутри главы неизвестна → маркер в конце главы с числом потерянных строк.
|
||
- Default = отказ: D29.1(б) — reader-facing выдача fail-closed; дверь платформы получит `--partial` для
|
||
контрактного «may be exported, finished or not» (14 §createExport).
|
||
- `--partial`: пометка неполноты — первый абзац первого документа spine (то, что читатель видит, открыв
|
||
книгу) + `dc:description`; маркер — отдельный абзац на месте КАЖДОЙ дыры (pending/withheld — вместо текста
|
||
юнита; incomplete — перед текстом юнита, как `gapMarker`; ghost — в конце главы). Форма — из `reader.txt`;
|
||
без пака (acceptance, `runner.go:346`) — несловесная: `⚠ {chapter}.{unit}` у дыры, `⚠ {holes}/{total}` в
|
||
пометке (символ + числа, никакого словаря в Go). В `FinalText`/`export.go` маркер не попадает.
|
||
- drift — НЕ дыра: текст файла побайтово текст прогона (`export.go`, комментарий к `exportConfigDrift`);
|
||
попадает в JSON-конверт `build` и в WARN, в файл — нет («config drift» — операторский словарь).
|
||
Следствие §4 заявляю сразу: чистого бездрейфового файла на реальной книге на этом HEAD НЕТ — чистый
|
||
путь доказывается синтетикой, реальные книги — посадки на дыры.
|
||
- Оглавление и `<h1>` при `Heading==""` — номер главы цифрами (пустая ссылка nav невалидна; цифры —
|
||
единственный лейбл, который не решает открытый К-3 «какой лейбл видит читатель у главы без заголовка»;
|
||
словесный шаблон в данных — 1 строка после ратификации, вопрос владельцу в отчёте). При `Heading!=""` —
|
||
срез известного префикса `heading+"\n\n"` из `FinalText` (`chunker.go:186-192`), `<h1>` = Heading.
|
||
- Абзац = непустая строка `FinalText`. Замерено на `minirun/export.json`: юнит 1/0 — 14 разделителей `\n\n`
|
||
и 7 одиночных `\n`, юнит 2/0 — 56 и 0: редактор чередует их без семантики, различать = выдумать
|
||
различие; строка — единица абзаца исходника-вебновеллы. Круг §4 сравнивает абзацы ингеста с абзацами,
|
||
которые положил писатель.
|
||
- Метаданные: `dc:title` = `config.Book.Title` как есть (на стенде `蛊真人` — ограничение в отчёт), `dc:language`
|
||
= `TargetLang`, `dc:identifier` = `urn:textmachine:book:<book_id>`, `dc:creator` нет (источника нет).
|
||
- 0 юнитов — отказ `sourceHasNoContent` (spine без элементов невалиден); 1 юнит — обычная книга.
|
||
|
||
**Что НЕ беру:** платформу и дверь `createExport`/`getExport`; лестницу D25.1 в коде и в `BookExport`;
|
||
строки 160/201 (`ApplyHeading` и три точки склейки не трогаю; модельные заголовки в прозе не извлекаю и не
|
||
правлю); `--resnapshot` и любые платные вызовы; `langpack_root` в копию acceptance; направление письма
|
||
(`dir`/`page-progression-direction`) — в `internal/lang` данных о направлении нет (`lang-script.txt` —
|
||
только скрипты), RTL-цели в репо нет → вопрос в отчёт, не литерал; CSS/обложку; новые зависимости.
|
||
|
||
**Чем докажу:** герметично — `bookfile` (mimetype первым/stored, nav не в spine, детерминизм двух сборок,
|
||
экранирование, 0/1 глава), `pipeline` (круг `IngestEncoded` равенством абзацев на прогоне fake-провайдера;
|
||
pending через потолок; withheld через санитайзер; **ghost — усадка исходника после прогона (посадки в
|
||
тестах не было, строю)**; drift — бамп `prompt_version`; синтетический `BookExport` на incomplete), `cmd/tmctl`
|
||
(incomplete бинарём через `setupGapProject`; парс флагов; exit 16 через totality-тест), `lang` (парс
|
||
`reader.txt`; `Version()` не меняется от его наличия). На копиях книг в песочнице с переписанными путями:
|
||
minirun — оракул июля 14/14 по `final_text/snapshot_id/disposition`, `current_snapshot` до/после
|
||
`reader.txt`, `cmp` двух ПРОЦЕССОВ, epubcheck 5.3.0 `0 fatal / 0 error`, круг ингестом; acceptance — отказ с
|
||
перечнем и `--partial`-файл с несловесными маркерами; coldrun-a — отказ 14/14 pending;
|
||
`git -C books status --short -- gu-zhenren` до/после одинаков (снят до старта: 6 строк `D …-wal/-shm`).
|
||
`make battery` + `-race`; в середине — субагент-ревью готовой части против критериев; в конце —
|
||
адверсариальный проход по направлениям §4.
|
||
|
||
### ✅ ИТОГ — пак «писатель книги» (промт `docs/archive/prompts/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT_2026-08-30.md`, строка 236), сессия `textmachine-main-fc`, 30.08
|
||
|
||
**Что построено (зона `backend/`, поверх `67c05be`; НЕ закоммичено — дерево на лендинг оркестратору; `git status`: 10 изменённых + 8 новых путей в `backend/`, плюс этот журнал).**
|
||
- `internal/bookfile/` (новый прод-пакет, только stdlib, без `testing`): `container.go` — OCF-контейнер (`mimetype` первым, `Store`, через `CreateRaw` — без data-descriptor, флаги `0x0000`; `container.xml`; entry в порядке вызовов; нулевые zip-таймметки); `epub.go` — EPUB 3 (`dc:identifier`+`unique-identifier`, `dc:title`, `dc:language`, `dcterms:modified`, nav `properties="nav"` ⛔ не в spine, XHTML на главу: `<h1>` + `<p>`); `txt.go` — чистый текст; `model.go` — модель `Book/Chapter`, `Blocks(i)` (единственное определение содержимого документа главы), `CleanText` (общая чистка XML-недопустимых символов для обоих форматов), экранирование.
|
||
- `internal/chunk/chunktest/epub.go` переведён на `bookfile.NewContainer`; тест-декои (OPF-заглушка, `<title>c</title><style>`) остались тестовыми; 9 импортёров зелёные без правки ассертов.
|
||
- `internal/lang/reader.go` — `LoadReaderWords(root, targetLang)`: `<langpack_root>/<target>/reader.txt`, читается ОТДЕЛЬНО от `Load` (в `Pack.Version()` не фолдится по построению — `Load` читает только `<src>/` и `<pair>/`); `DefaultReaderWords()` — несловесная форма (символ + числа; тест пинит отсутствие букв). Данные: `configs/langpacks/ru/reader.txt` (7 ключей: `hole.{pending,withheld,incomplete,stale,ghost}`, `notice.{holes,ghost}`).
|
||
- `internal/pipeline/holes.go` — ОДИН предикат дыры `UnitHole`/`HoleCounts` (порядок = `renderExport`); `render.go` считает шапку `--plaintext` через него (вывод побайтово прежний).
|
||
- `internal/pipeline/export.go` — аддитивно (`omitempty`): `ChunkExport.Heading` (`heading` — литерал `u.Members[0].Heading`, у pending тоже), `BookExport.GhostUnits` (`ghost_units`, ключи ghost-строк, сортированы), `BookExport.TextModified` (`text_modified` — max `chunk_status.updated_at` по ФИНАЛЬНЫМ строкам, вошедшим в экспорт); `exportConfigDrift`: строка со стадией, которой нет в текущем конфиге, — drift (находка 10 ниже). `store/chunkstatus.go` дочитывает `updated_at` в `ChunkStatus.UpdatedAt` (колонка `NOT NULL DEFAULT datetime('now')`, `migrate.go:37`; миграции — только `ADD COLUMN`: у minirun `text_modified` = `2026-07-25T19:17:53Z`, время боевого прогона, не миграции 30.08).
|
||
- `internal/pipeline/bookbuild.go` — `Runner.BuildBook(BuildOptions{Formats, Out, Partial})`: `Export(false)` → `staleUnits` → `assembleBook` (чистая функция, возвращает список дыр) → отказ `RefusalBookIncomplete` (`refusal.go`, exit **16**) либо стейдж всех форматов + общий коммит в `<project_db>.book.<fmt>` (не запрошенные соседи удаляются — рядом с БД лежит ОДНА сборка); `StatusArtifacts.BookFiles` (`book_files`, map формат→абс. путь; в `status --json` и `manifest --json`). `--out` — только НОВЫЙ файл (существующий путь любого рода, несуществующий каталог → `config_invalid`, exit 10). Сбой между двумя `rename` → `write_incomplete` (exit 15) с перечнем landed/not landed.
|
||
- `cmd/tmctl`: глагол `build --config b.yaml [--format epub,txt] [--out path] [--partial]`; stdout — JSON `tm-build-v1` (`files`, `total/pending/withheld/incomplete/stale_units`, `stale_unknown`, `ghost_rows`, `config_drift`, `complete`, `text_modified`); чужим глаголам новые флаги отказываются, `build` отказывает `--plaintext/--pairs/--json`; usage-строка расширена (`invocation_test.go` — контрактное расширение по собственному комментарию теста «Every one is a deliberate contract extension»; это ЕДИНСТВЕННАЯ правка существующего теста). `backend/README.md` — строка `cmd/tmctl` дополнена `build`.
|
||
|
||
**Политика (§3.2) как реализована — ШЕСТЬ состояний, не пять:** дыры — pending · withheld (`Disposition!="pending" && FinalText==""`, плюс юнит, чей текст после чистки не даёт ни одного абзаца) · incomplete (`DroppedMembers>0`) · **stale** (финальная строка сделана под исходник, которого в книге уже нет: `content_hash` строки ≠ `cachedRenderedContentHashes` — тот же факт, по которому резюм перекупает юнит; считается ТОЛЬКО без дрейфа, под дрейфом `stale_unknown: true`, никогда не «нет») · **ghost** (при укрупнении нарезки строка-лидер несёт текст прежнего, более короткого юнита; остаток лежит в ghost-строках и в файл не попадает: acceptance 57→37, 20 строк при `pending 0`). По умолчанию — ОТКАЗ при любой дыре с перечнем `chapter N unit M: <вид> (<причина>)` и ghost-ключами (≤50 строк); `--partial` — файл с пометкой (первые абзацы первого документа spine + `dc:description`; в txt — после заголовка книги) и маркером на месте КАЖДОЙ дыры (pending/withheld/stale — вместо текста; incomplete — перед текстом; ghost — в конце главы, счётчиком). drift — НЕ дыра (текст файла = текст прогона): в JSON-конверте и WARN, в файле нет. Оглавление/`<h1>` при `Heading==""` — номер главы цифрами. Абзац — непустая строка `FinalText` (`\r\n`/`\r`/`\n`). `dc:title` — `book.yaml` как есть (на стенде `蛊真人`), `dc:language` = `target_lang` (форма тега BCP 47 проверяется, иначе `config_invalid`), `dc:identifier` = `urn:textmachine:book:<book_id>`, автора нет.
|
||
|
||
**Сверка состава против §3 (пункт → сделано → чем доказано; все команды — на копиях в песочнице `<SB>/stand`, бинарь `go build -o <SB>/stand/tmctl ./cmd/tmctl`).**
|
||
|
||
| §3 | Сделано | Доказательство |
|
||
|---|---|---|
|
||
| 3.1 EPUB + чистый txt из `BookExport`, ОДИН файл на формат, из `tmctl` | да: `tmctl build` → `<project_db>.book.epub` / `.book.txt` | `build-minirun.json`: 2 пути, `complete: true`; `java -jar epubcheck-5.3.0/epubcheck.jar <book>.epub` → `0 fatals / 0 errors / 0 warnings / 0 infos` на всех трёх EPUB (minirun, acceptance `--partial`, coldrun-a `--partial`) |
|
||
| 3.1 без аудит-баннеров/флагов/`gapMarker` в txt | да | `TestTXTShape`, `TestBuildBookRoundTrips…` (нет `=== CHAPTER`, `TEXT MISSING`, `pending`, `flagged`); `TestBuildRefusesTheGapAndPartialMarksIt` — реальный c-lite прогон бинарём: `⚠ 1.0 −1` перед уцелевшим текстом, ни `INCOMPLETE`, ни `cjk_artifact` |
|
||
| 3.1 `--plaintext` не тронут | да | `cmp` вывода бинаря из `git archive HEAD` и нового: IDENTICAL на minirun (171932 б), acceptance (354821 б), coldrun-a (1077 б) |
|
||
| 3.1 основа — `chunktest/epub.go`, OCF в прод без `testing` | да | `TestEPUBFixtureMimetypeIsOCFConformant` и 9 импортёров зелёные без правки; `TestContainerIsOCFConformant` (флаги `0`, extra `0`, `mimetype` на фиксированных смещениях) |
|
||
| 3.1 детерминизм двумя ПРОЦЕССАМИ | да | `cmp first/<f> <copy>/<f>` — IDENTICAL для 6 файлов (две сборки в двух процессах); zip-таймметки `1980-00-00 00:00`; sha256 EPUB minirun `084ff0a9…`, совпал с независимой сборкой ревьюера |
|
||
| 3.2 пять (шесть) состояний, отказ по умолчанию / `--partial` | да | acceptance: `build` → exit 16 «pending=0 withheld=1 incomplete=1 stale=0 ghost_rows=20 of total_units=37 (stale: UNKNOWN …) · chapter 1 unit 0: withheld (cjk_artifact) · chapter 10 unit 0: incomplete — 1 member(s)… (length) · ghost rows: 1/1 1/3 …»; `--partial` → `⚠ 2/37`, `⚠ +20`, `⚠ 1.0`, `⚠ 10.0 −1`, `⚠ 10 +1`…; coldrun-a: exit 16 (14 pending) / `--partial` со словесными маркерами из `reader.txt`; minirun (только drift): записан по умолчанию, `config_drift: true`, `stale_unknown: true`, в файле `⚠` нет; тесты `TestBuildBookRefusesAPending…`, `…WithheldUnitIsAHole`, `…GhostRowsAreAHole` (посадка построена: усадка исходника после прогона), `…StaleUnitsAreAHole`, `…DriftIsReportedNotMarked`, `…RemovedStageIsDrift`, `TestAssembleBookMarksEveryHoleKind` |
|
||
| 3.2 не в снапшот | да | `current_snapshot` minirun с `ru/reader.txt` и без — `bb0b3b2aaf0d…` оба раза (копия корня пака в песочнице); `TestReaderWordsDoNotMoveThePackVersion` |
|
||
| 3.2 книга без langpack — несловесно | да | acceptance (`langpack_root` не задан): `⚠ 1.0`, `⚠ 10.0 −1`, `⚠ 2/37`; `TestReaderWordsAbsentIsTheNonVerbalForm` |
|
||
| 3.2 маркер не в `FinalText`/`export.go` | да | JSON HEAD vs new: `existing equal True`, добавлены только `text_modified`, `heading`, `ghost_units` |
|
||
| 3.3 `ApplyHeading` и три точки не тронуты; срез только известного префикса | да | `git diff HEAD -- backend/internal/chunk/chunker.go backend/internal/chunk/ingest.go backend/internal/pipeline/waverun.go` пуст; вызов `ApplyHeading` в `export.go` — контекстная строка; minirun: 10 заголовков срезаны, круг 402/402; `TestAssembleBookMarksEveryHoleKind` (упоминание заголовка в тексте не срезается) |
|
||
| 3.3 `Heading==""` → номер цифрами в nav и `<h1>` | да | acceptance nav `1`…`25`; minirun/coldrun-a — `Глава N` из `heading` |
|
||
| 3.3-бис пять требований EPUB 3; nav не в spine; `dc:language` из `TargetLang` | да | `TestEPUBCarriesTheFiveRequiredThingsAndKeepsNavOutOfTheSpine`; OPF minirun: `urn:textmachine:book:guzhenren-minirun`, `蛊真人`, `ru`, `2026-07-25T19:17:53Z`; epubcheck |
|
||
| 3.4 место/атомарность/конверт/глагол/коды | да | `status --json`/`manifest --json` копии minirun: `book_files: {epub, txt}`; `TestTheRefusalTableIsTotal`, `TestDispatchCommandsCoversTheSwitch`, `TestParseNoArgsUsage`; `TestBuildBookLeavesOneBuildBesideTheDatabase`; `TestBuildBookOutNamesANewFile` |
|
||
| 3.5 платформа не тронута; `tm-export-v1` аддитивно; пары — данные; без зависимостей | да | `git status` — только `backend/` и `docs/PROGRESS.md`; `go.mod` не менялся; греп читательских слов по новому Go-коду — только комментарии |
|
||
| §4 `make battery` (`-race`) | да, 2 настоящих прогона | `battery-1.log` (до правок ревью) и `battery-3.log` (после всех правок): `BATTERY_EXIT=0`, 18 пакетов, скипы — три прежних стендовых (`TestMinerFullBookParity`, `TestHelperEventsRun`, `TestHelperKillLoop`). ⚠ «Батарея №2» была ложным прогоном: `make` из корня репо (`No rule to make target`), мой `echo` замаскировал код — поймано по логу, перезапущено из `backend/` |
|
||
| §4 дифф `^func Test` | ни один не исчез | `git diff HEAD -- '*_test.go' \| grep '^-func Test'` пуст; `func Test` в `backend/`: 1017 → 1044 (+27) |
|
||
| §4 круг ингестом равенством | да | `tmroundtrip` (собран `go build -overlay`, файла в дереве нет): minirun 10/10 глав, 402 абзаца; acceptance `--partial` 25/25, 1568; coldrun-a `--partial` 10/10, 25 |
|
||
| §4 `git -C books status` до/после | см. obstacle | 6 строк `D …-wal/-shm` до; после — 4: у acceptance `-wal`/`-shm` ПОЯВИЛИСЬ (идентичны трекнутым; БД не изменена) |
|
||
| §4 хирургия конфигов в копиях | список | minirun/coldrun-a `book.yaml`: удалены `mined_delta:`/`mined_rejects:`; acceptance `pipeline-acceptance.yaml`: удалены `stm_depth: 2`, `overlap_tokens: 200`, оба `prompt:`; добавлен `acceptance/pairs/zh-ru.yaml` (копия minirun/pairs с `prompts_root` на `backend/prompts` этого дерева); все абсолютные пути переписаны на песочницу/этот ворктри, греп по копиям пуст; миграции v10/v7/v14 → v16, `backups/` — в песочнице |
|
||
| §4 промежуточное и финальное ревью субагентом | да, Fable 5 ×2 | ниже |
|
||
|
||
**Промежуточное ревью (Fable 5, исполнением) — 8 находок, все закрыты:** (1) MAJOR: `--format`-подмножество оставляло соседний формат прежней сборки рядом с БД при равноправном `book_files` → стейдж всех форматов + общий коммит, не запрошенные соседи удаляются, пин `TestBuildBookLeavesOneBuildBesideTheDatabase`; (2) `text_modified` считался по всем строкам всех стадий → только по финальным строкам экспорта (acceptance `20:41:47Z`→`20:40:00Z`, coldrun-a — эпоха: ничего не отгружено); (3) пустой `title` → exit 1 → `config_invalid` с именем ключа, то же для `target_lang` не в форме BCP 47; (4) `mimetype` с data-descriptor → `CreateRaw`; (5) control-символы срезались только в EPUB → `CleanText` один раз для обоих; (6) `--format epub,epub` → дедуп; (7) `backend/README.md` без `build` → дополнено; таблицы exit-кодов в `docs/` — пинг; (8) подтверждено верным: `UnitHole` = арифметика `renderExport`, `DispOK` с пустым текстом недостижим, нулевые таймметки, `reader.txt` вне `Load`.
|
||
|
||
**Финальный адверсариальный проход (Fable 5; overlay-тесты, свой бинарь, epubcheck на adversarial-книгах: краевые символы, RTL, 2000 глав) — 10 находок:**
|
||
1. MAJOR — правка исходника НА МЕСТЕ (ключи `(chapter, chunk_idx)` уцелели, текст под ними сменился) давала файл, побайтово равный прежнему, под `complete: true`. **Исправлено — шестое состояние `stale`** (см. политику; пин `TestBuildBookStaleUnitsAreAHole`: правка главы 2 → `chapter 2 unit 0: stale`, глава 1 не задета; после `translate` — снова whole). ⚠ Побочный факт: `projectRebill` сверяет content-hash ТОЛЬКО при сдвинутом снапшоте (bank-only ветка), при том же снапшоте строка идёт как «$0 resume» без сверки — проекция денег `status` слепа к правке исходника на месте, `translate` при этом перекупает (ревьюер воспроизвёл: «2 units re-bought»). Не моя строка — пинг.
|
||
2. MAJOR — `--out` перезаписывал что угодно, включая БД проекта («file is not a database») и исходник. **Исправлено:** `--out` только для НОВОГО файла; существующий путь / отсутствующий каталог → `config_invalid`; место рядом с БД по-прежнему перезаписывается. Пин `TestBuildBookOutNamesANewFile` (БД, исходник, чужой файл, несуществующий каталог; ни одного `.tmp-` после). Стенд: `--out <project_db>` → exit 10, БД цела.
|
||
3. MINOR — сбой на фазе коммита второго формата оставлял смешанную сборку и exit 1. **Исправлено:** `write_incomplete` (exit 15) с перечнем landed/not landed; комментарий файла говорит правду. Тестом не покрыто («не проверено»: сбой `rename` в только что записанном каталоге без инъекции не воспроизводится).
|
||
4. MINOR — юнит с непустым `FinalText`, дающий ноль абзацев (control-символы/`U+FFFF`), не был дырой. **Исправлено:** сборщик читает юнит по тому, что тот реально положил (ноль абзацев → withheld); счётчики отчёта — из списка дыр сборщика; `HoleCounts` остался ровно арифметикой `renderExport`.
|
||
5. MINOR — голый `\r`: txt нёс его, XML нормализует → форматы расходились. **Исправлено:** `\r\n`/`\r`/`\n` — конец строки.
|
||
6. MINOR — `--out` в несуществующий каталог → exit 1. **Исправлено:** `config_invalid`.
|
||
7. NIT — `build` молча принимал `--plaintext/--pairs/--json`. **Исправлено:** отказ.
|
||
8. NIT — `dc:title` с паддингом vs обрезанный txt. **Исправлено:** `TrimSpace` в модели и EPUB.
|
||
9. NIT — ghost-маркер по номеру главы СТАРОЙ нарезки может встать в соседнюю главу при усадке. **Не исправлено, задокументировано** (`assembleBook`): книга всё равно помечена неполной, ложна лишь координата; иного источника координаты у ghost-строки нет.
|
||
10. MINOR (унаследовано `Export`) — удаление стадии `edit` делало черновики «полной» книгой без дрейфа. **Исправлено в `exportConfigDrift`:** строка со стадией, которой нет в конфиге, — drift; под ним stale = unknown; пин `TestBuildBookRemovedStageIsDrift`. ⚠ `Status` (`checkWave`) несёт то же слепое пятно — пинг.
|
||
Подтверждено проходом как верное: все посадки A (withheld при `pending 0`, ghost-only, исчезнувшая глава, все юниты withheld, 0 юнитов, `HoleCounts` = шапка `renderExport` на всей матрице состояний); детерминизм (нет `time.Now`/`rand`/`os.Stat`/map-итерации в байты; 2 процесса под `TZ=Asia/Tokyo LANG=C` — идентично); заголовки (срез только известного префикса; pending-первый юнит с заголовком получает заголовок; пустой заголовок недостижим — `parseHeading`); nav вне spine; круг на трёх стендовых книгах воспроизведён независимо + epubcheck чист на adversarial-книгах; RTL — валиден, без `dir`, честно неподдержан; 2000 глав — 47 мс/707 КБ, ingest 38 мс; ids/hrefs по индексу; словесных литералов в Go нет; пин `Version()` подлинный; exit 16 в полосе, таблица тотальна; JSON аддитивен; `ApplyHeading` и три точки не тронуты; ни один тест не удалён; линтер 0.
|
||
|
||
**Obstacle reporting — что НЕ удалось и что осталось непроверенным.**
|
||
- **Не проверено — открывается ли в читалке.** Читалки на машине нет; «валиден» ≠ «открылся». Проверено ТОЛЬКО валидатором W3C (epubcheck 5.3.0, `0/0/0` ×3) и кругом через собственный ингест движка.
|
||
- **Не проверено — чистый бездрейфовый файл на реальной книге.** Недостижим на этом HEAD без платного перепрогона (все три копии под `config_drift: true`); чистый путь доказан синтетикой (`TestBuildBookRoundTripsThroughIngestAndPublishesItsPaths`: `complete: true`, `stale_unknown: false`, без пометок). Следствие: **stale-проверка на реальных книгах стенда не сделана** (`stale_unknown: true` у всех трёх — под дрейфом факт неразличим), доказана только харнесом.
|
||
- **Не проверено — сбой между двумя `rename`** (находка 3): ветка `write_incomplete` написана, не исполнена.
|
||
- **Не проверено — посадка на incomplete в `pipeline`-харнесе:** реальный member-drop только через бинарь (gap-харнесс `cmd/tmctl`) и acceptance (10/0, `length`); в `internal/pipeline` — синтетический `BookExport`.
|
||
- **Не проверено — направление письма.** `dir`/`page-progression-direction` не выставляются; данных о направлении в `internal/lang` нет; RTL-файл валиден, но выровнен слева — честно неподдержано (вопрос 4).
|
||
- **Не объяснено — read-only открытие ОРИГИНАЛЬНОЙ acceptance-БД.** `git -C books status --short -- gu-zhenren` до: 6 строк `D …-wal/-shm`; после: 4 — у `acceptance/guzhenren-acc-a.db` `-wal` (0 байт, 13:47:36 +03) и `-shm` (32 КБ, 13:47:49 +03) появились, идентичны трекнутым (`git diff` пуст), сама БД (mtime 24.08) не изменена. Мои команды все шли по копиям (`project_db`/`source_file` копии — в песочнице, проверено грепом; `.lock` копии — от миграции 12:56); тесты дерева acceptance-БД не открывают (греп); субагент-ревьюер завершился раньше этого времени. В `ListAgents` живы ещё три сессии (оркестратор и две, вероятно, полигонные) — кто-то из них открыл оригинал read-only. Файлы НЕ трогал (чужой репозиторий).
|
||
- **Опровергнуто (промтом):** «глава 1 minirun остаётся `""` даже с правилом» — на HEAD у minirun заголовок у всех 10 глав (`headings 10`, nav `Глава 1…Глава 10`); `""` у всех 25 глав acceptance — подтверждено.
|
||
- **Подтверждено:** числа промта §4 (minirun 14/14 ok · drift · оракул июля 14/14 по `final_text/snapshot_id/disposition`; acceptance 37 юнитов · ghost 20 · withheld 1/0 · incomplete 10/0 · drift; coldrun-a 14/14 pending · drift); миграции v10/v7/v14 → v16; epubcheck приносится и работает.
|
||
- **Ограничения формы (по промту, не исправлялись):** `dc:title` = `蛊真人` (перевод не выдумывается); автора нет; `text_modified` — «последняя ЗАПИСЬ» финальной строки ($0-резюм, переписавший строку без изменений, двигает штамп; между сборками детерминизм сохраняется); побайтовый детерминизм гарантирован для ОДНОГО бинаря (другой toolchain Go — иной deflate-поток при тех же документах); ghost-координата — старой нарезки (находка 9); `stale` под дрейфом — «неизвестно».
|
||
- **Самопойманная ошибка:** ложный прогон «батареи №2» (см. таблицу).
|
||
- **Эхо §7:** правок задания релеем владельца не поступало; единственное сообщение оркестратора (подтверждение адресата + напоминания) заказом не является; приор «артефакт рядом с ProjectDB + `StatusArtifacts`» принят с доводом.
|
||
|
||
**Вопросы владельцу (гейт между этим паком и дверью платформы, D25.1):**
|
||
1. **Лестница `run_complete / structurally_complete / quality_reviewed / publishable`:** писатель различает ровно «без дыр» (`complete: true`) и «с дырами» (отказ / `--partial` с пометкой). Что из лестницы печатать на титуле и в имени файла и какая ступень даёт копию БЕЗ пометки — решение владельца; в код не внесено намеренно.
|
||
2. **Лейбл главы без заголовка (К-3):** сейчас — номер цифрами. Словесный шаблон — одна строка в `reader.txt`, если ратифицируется; не сделано, чтобы не решать К-3 данными.
|
||
3. **Ghost и stale как дыры по умолчанию:** довод — оба дают «молча полную» книгу (acceptance: 20 строк не в файле при `pending 0`; правка исходника на месте: старый перевод под `complete: true`). Если владелец хочет «предупреждение, не отказ», — по строке в `BuildBook`.
|
||
4. **Направление письма для RTL-цели:** поле данных цели (в `reader.txt` или рядом), не Go — когда появится RTL-пара.
|
||
|
||
**Пинги оркестратору (чужие зоны, не правил):** (а) exit-код **16 `book_incomplete`** в таблицы `docs/glossary.md:64` и `docs/architecture/14-api-contract/README.md:731,758`; (б) `docs/architecture/09-target-architecture.md`: слой «писатель книги» построен; следующий шаг — дверь платформы `createExport`/`getExport` + `export_formats` из `bookfile.Formats`; дверь обязана СТРОИТЬ (`tmctl build --format <f> [--partial]`) и отдавать то, что построила, — не читать файл, лежащий рядом с БД (копия прежней сборки); (в) `projectRebill`/`status` слепы к правке исходника на месте при том же снапшоте (перекупка без предупреждения о деньгах) — строка бэклога; (г) `Status.checkWave` не видит исчезнувшую стадию (то же, что находка 10 у `Export`) — строка бэклога; (д) строки 160/201 не тронуты, `Heading` в `tm-export-v1` — тот же временный рендер (D39.122 п.2д), помечено в комментарии поля.
|
||
|
||
### 📝 ЗАПИСКА-ПЛАН — пак «деньги» (промт `docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md`, D39.165 §1б + строка 231), сессия `textmachine-e4`, 28.08
|
||
|
||
**Скоуп:** `backend/` — §3.1 потолок ОБЪЁМА рядом с денежным · §3.2 смета пере-прохода без покупки. `platform/` не трогаю (проводка `CeilingChapters` — не мой заказ, честная граница §0 промта принята).
|
||
|
||
**§3.1 — В ЧЁМ МЕРИТЬ: выходной юнит (`ManifestUnit`), кандидат (а). Обоснование.**
|
||
Ровно эта гранулярность и есть то, что движок отгружает и что публикует манифест: `ManifestUnit` строится итерацией по `r.outputUnits(chunks)` (`backend/internal/pipeline/manifest.go:330-357`), то есть по тем же `editUnit`, которыми идёт волна редактуры; `units_total` манифеста — то самое поле, из которого интейк платформы пишет `chapters.units_total` (`platform/internal/pgstore/readmodel.go:113,166-175`, читал не правил). Значит перевод «N глав → M юнитов» у платформы ТОЧНЫЙ, а не оценочный: манифест снимается `tmctl manifest` за $0 и без ключей (`runner.go:239-244` — `models.CheckKeys` только `if forWrite`) ДО первой оплаты.
|
||
⚠ **Кандидат (в), `chunk×stage`, отвергнут именно по ловушке, названной в промте.** Это биллинговая единица (`rebill.go:322`), и в ней «10 единиц» — разный объём книги при разной форме конвейера: на юнит приходится `len(Members)*nDraftStages + nEditStages` позиций (`rebill.go:207`). Продавая (а) и каппя (в), мы воспроизвели бы дефект §0 слоем ниже. Кандидат (б), чанк, — внутренний артефакт нарезки, покупателю не виден и манифестом не считается.
|
||
⚠ **Честная оговорка, которую обязан назвать:** число юнитов книги зависит от ФОРМЫ конвейера — редакторский конвейер даёт крупные `editUnit`, черновой-только даёт по юниту на чанк (`waverun.go:67-76`). Это свойство конфигурации, а не ветвление по паре; но манифест и потолок обязаны сниматься с ОДНОГО деплоя, иначе «10 юнитов» значат разное.
|
||
|
||
**§3.1 — КАК СЧИТАЮТСЯ РЕПИНЫ И РЕТРАИ (прямой ответ на вопрос промта).**
|
||
- **Ретрай и эскалационный хоп — НЕ считаются.** Они внутри юнита: ретрай — итерация цикла попыток (`stagerun.go:158-194`), эскалация — отдельный `runAttempt` (`escalation.go`), но `chunk_status` на `chunk×stage` пишется ОДНА (`stagerun.go:297-308`). Довод по существу: объёмный потолок ограничивает ДОСТАВКУ, денежный — ТРАТУ. Хвост качества (1.5%→23% по D39.165 §1) — факт денежный, и у него уже есть свой потолок; если бы ретраи жгли объём, купивший 10 глав получал бы 8, потому что две главы заикнулись, — то есть платил бы КНИГОЙ за качественный хвост движка. Это ровно та подмена, которую пак и лечит.
|
||
- **Репины и $0-резюмы — НЕ считаются.** Иначе пере-проход, пере-привязывающий 500 юнитов за $0 (`rebill.go:315-320`), сжёг бы потолок, не доставив ничего.
|
||
- ⇒ **Правило одной строкой: потолок считает выходные юниты, по которым ЭТОТ прогон сделает хотя бы один провайдерский вызов.**
|
||
|
||
**§3.1 — МЕХАНИЗМ: пре-флайт скоуп, а не счётчик в воркере.**
|
||
Допуск считается ОДИН раз ДО волн, по состоянию стора, и даёт множество юнитов-в-работе; волны просто пропускают то, что вне скоупа. Предикат «$0-обслуживаемый» берётся из УЖЕ существующего — того же, которым считает `projectRebill`/`repin`, — чтобы объёмный стоп и денежная смета не могли разойтись двумя определениями.
|
||
⚠ **Почему не счётчик со слотами в воркере (рассмотрено и отвергнуто):** волна параллельна (`runWave`, `waverun.go:236-309`), и при отказе в слоте конкурентно розданные элементы пропадали бы НАВСЕГДА — при `workers=8` и потолке 1 семь юнитов оказались бы пропущены, а освободившийся после $0-репина слот уже некому было бы отдать. Пре-флайт эту гонку растворяет.
|
||
**Грабля LiteLLM исполнена конструктивно:** ни один элемент вне скоупа не НАЧИНАЕТСЯ, поэтому пере-бой на один вызов невозможен в принципе, а не по проверке.
|
||
**Грабля OpenHands исполнена:** остановка называет свой потолок — в `BookResult`, в строке лога и в выводе CLI.
|
||
|
||
**§3.1 — ФОРМА ОТВЕТА ЧЕРЕЗ ШОВ: беру решение оркестратора и ПОДТВЕРЖДАЮ его исполнением.**
|
||
Код выхода 0, замороженный словарь (`main.go:115-140`) не трогается. **Проверил то, о чём он просил пингнуть, — не ломает:** exit 0 у платформы = статус ПРОГОНА `ready` (`platform/internal/runs/reconcile.go:703-706`, читал не правил), а не «книга дочитана»; полоса книги едет событиями юнитов, поэтому дочитанной книга не объявляется и следующую покупку не блокирует.
|
||
⚠ **Словарь потока НЕ трогаю в этом паке.** Новое значение `Finished.Outcome` было бы минором по правилу `runevents.go:29-40`, но у платформы неизвестный ВЫХОДНОЙ код читается как отказ (`ingest/exit.go:168-169`), и множить развилки в чужом пакете без контрактного минора — не мой заказ (§3.3). Различение живёт в отчёте и логе, как и решил оркестратор.
|
||
**Ортогональность цела:** `--ceiling-usd` остаётся кумулятивным книжным капом и судит в `Reserve` (`ledger.go:56-72`); объёмный потолок леджера не касается вовсе и живёт до волн. Денег не считает, холдов платформы не двигает.
|
||
|
||
**§3.2 — ФОРМА: чинится СВЁРТКА, а не заводится глагол. Это дешевле всех трёх кандидатов промта.**
|
||
Слепое окно — не в том, что `status` пишущий, а в том, что он сворачивает НЕ ТО: `projectStoredMemory` (`status.go:815-826`) материализует СОХРАНЁННЫЙ глоссарий, то есть фолд ПРОШЛОГО прогона, тогда как ближайший `translate` пере-сеет банк из ФАЙЛОВ (`seedGlossary`) и сделает это ДО гейта согласия (`bookrun.go`: `seedGlossary` → … → `checkRebillConsent`). **Собственное оправдание комментария — «exactly as it does for `translate` itself» — неверно: `translate` правку файла как раз ВИДИТ.** Значит сегодня `status` НЕДО-считывает то, что `translate` реально выставит, и это денежный дефект, а не отсутствующая функция.
|
||
**Лечение:** фолд тех же входов, что берёт `seedGlossary` (сид + рубя + мined-дельта + авто-банк), — **В ПАМЯТИ, без единой записи**. `status` при этом остаётся ЧИСТО ЧИТАЮЩИМ: он читает файлы вместо стора, а фолд не пишет никуда. Возражение оркестратора против «флага у `status`» снимается тем, что оно стояло на посылке «свёртка идёт через стор» — она не обязана.
|
||
⚠ **Записи не будет и не может быть:** `status` открывает стор через `NewReadOnlyRunner` → `OpenReadOnly` с `PRAGMA query_only=1` (`store/store.go:117-166`), и на состоянии §4.3 (прогон СОСТОЯЛСЯ ⇒ база есть) фолбэк на `Open` не срабатывает вовсе — записей НОЛЬ, даже first-touch.
|
||
**Родня 124(в) закрывается тем же:** `status` уже считает ре-билл при ЛЮБОМ дрейфе, не только банковом, — то есть после фикса он и есть «оценить пере-проход, ничего не покупая», ради которого поименован `resnapshot --dry-run`.
|
||
⚠ **Третий кандидат (`bank-apply --dry-run`) рассмотрен ПЕРВЫМ, как заказано, и отклонён с доводом:** он требует `--decisions`, а в продуктовом потоке D39.165 §3 решения к моменту продажи УЖЕ на диске — документа больше нет. Плюс он Runner-free и не держит ни статусов, ни манифеста, ни конфига (`bankdecisions.go:294-314`), то есть под смету пришлось бы втащить в него весь Runner. Ту же проекцию он сможет позвать позже — она будет одной функцией.
|
||
|
||
**§3.2 — ТОЖДЕСТВО БЕСПЛАТНОЙ СМЕТЫ И ПЛАТНОГО ПРОГОНА: доказываю, а не предполагаю.**
|
||
Канонический фолд идёт через стор и там ПЕРЕ-СОРТИРУЕТСЯ: `ORDER BY src, sense, since_ch, until_ch, status, dst` (`store/glossary.go:223`). Порядок несущий дважды — `MaterializeBank` порядок входа НЕ трогает (`membank/memory.go:283-346`), а дальше он определяет и хеш версии (`ComputeVersionScopedIn` стримит SHA-256 по строкам в порядке итерации), и ЛИНИИ отрендеренного блока через стабильную сортировку бюджета (`memory.go:608-619`), которую режет токенный бюджет.
|
||
**Почему тождество ДОКАЗУЕМО, а не правдоподобно:** таблица несёт `UNIQUE (book_id, src, sense, since_ch, until_ch)` (`store/migrate.go:203`), поэтому префикс из четырёх колонок НЕ МОЖЕТ дать ничью — порядок ТОТАЛЬНЫЙ и полностью определён значениями, а не rowid и не порядком вставки. `COLLATE` в схеме нет нигде ⇒ сравнение текста побайтное, как и `<` в Go. Алиасы читаются `ORDER BY term_id, alias`, внутри записи это сортировка по `alias` (`glossary.go:247-249`). `glossary.id` в хеш не входит намеренно (`memory.go:360-363`).
|
||
⇒ Фолд в памяти обязан: собрать тот же набор значений + отсортировать по (Src, Sense, SinceCh, UntilCh, Status, Dst) + отсортировать алиасы + то же для voices/pairs по их собственным ключам.
|
||
**Проверка — ДИФФЕРЕНЦИАЛЬНАЯ, исполнением:** тест гоняет оба пути на одном состоянии и требует совпадения построчно (кроме `ID`), совпадения `Version()`/`BaseVersion()`, совпадения отрендеренных байтов и совпадения самого числа `projectRebill`. Ассерт равенства, а не «похоже».
|
||
⚠ **Пишущий путь НЕ трогаю:** порядок ВСТАВКИ в `seedGlossary` остаётся как есть, иначе поехали бы `glossary.id` и телеметрия `injected_ids`. Добавляется чистая функция упорядочения для читающей стороны.
|
||
|
||
**Находки по дороге (в блате пака, называю сразу):**
|
||
1. ⚠ **`projectRebill` в `status` рендерит по НУЛЕВОМУ базовому банку.** `projectStoredMemory` ставит только `r.memory`, `r.baseMemory` не ставит НИКОГДА (единственные присваивания — `seeding.go:157,159`), а ветка репина берёт именно его (`rebill.go:167`) и молча пропускает позиции при `nil` (`repin.go:130`). Следствие: при движении БАЗОВЫХ строк банка `status` считает всю черновую волну пере-оплатой, то есть расходится с `translate` — вопреки собственному обещанию `status.go:206` «status can never quote a different number».
|
||
2. ⚠ **При `snapshot_drift` у `status` память вообще не материализуется:** `projectStoredMemory` зовётся под `if !rep.SnapshotDrift` (`status.go:635`), а `projectRebill` — безусловно при наличии строк (`status.go:673`), и `memoryVersion()` при `nil` отдаёт хеш ПУСТОГО банка (`snapshot.go:190-195`) ⇒ книга объявляется пере-оплаченной целиком.
|
||
3. ⚠ **Мина `omitempty` подтверждена исполнением чтения:** три РАЗНЫХ состояния — «не запускалось», «посчитано и ноль», «расчёт упал» (`status.go:668-681` логирует и оставляет нули) — на проводе неотличимы. Это ровно цена D39.166 п.2.
|
||
Первые две мой фикс §3.2 снимает по построению (фолд считается безусловно и ставит ОБА банка); третью закрываю формой поля.
|
||
|
||
**НЕ делаю:** калибровку $0.03 (строка 202) · цену от объёма исходника · строки 228/230/141/131 · словарь кодов выхода · словарь потока событий · `platform/` · коммиты. Незакоммиченную работу полигона не касаюсь.
|
||
|
||
### ✅ ИТОГ — пак «деньги» (промт `docs/archive/prompts/BACKEND_MONEY_PACK_SESSION_PROMPT_2026-08-29.md`, D39.165 §1б + строка 231), сессия `textmachine-e4`, 29.08
|
||
|
||
**Батарея зоны:** `make battery` — **21 пакет из 21, 0 FAIL**, линтер **0 issues**, `test -race` зелёный целиком.
|
||
⚠ **Проверяю ПОЛНОТОЙ СПИСКА, а не отсутствием `FAIL`, и это правило стоило мне двух ложных выводов.** Первый «зелёный» прогон был на 20 пакетов из 21 с `FAIL` в хвосте, который обёртка подала как `exit code 0`. Сверка: `comm -23 <(go list ./...) <(grep -E "^(ok|\?)" <лог>)` — пусто.
|
||
⚠ **И про `internal/store`.** Он дважды ронял батарею (`TestTheSeamIsNotChargedToTheStoreOperationBudget`, `apply migration 14: context deadline exceeded`), и я сначала списала это на память — **неверно.** Проверила как надо: развернула ЧИСТЫЙ HEAD через `git archive` и погоняла тот же тест в обоих деревьях вперемежку — **моё 4/5 падений, HEAD 4/5**, то есть от моих правок не зависит. Тест ужимает `opTimeout` до 150 мс (`migrate_test.go:408-429`) и под параллельной нагрузкой не укладывается; на разгруженной машине проходит 3/3, и итоговая батарея выше — с ним. Каталог `internal/store` я не трогала ни строкой. Оркестратору отправлено строкой.
|
||
Пропущенные тесты, как требует Makefile, называю: `TestMinerFullBookParity` (нет стендовых данных), `TestHelperEventsRun`, `TestHelperKillLoop` (точки входа подпроцессов, вне обычного прогона).
|
||
Дерево ПОСЛЕ трёх кругов: **20 отслеживаемых файлов правлено (+761/−204) и 5 заведено (3106 строк)**; новых тестов **36** (30 в `internal/pipeline`, 6 в `cmd/tmctl`). Числа взяты командами: `git diff --stat -- backend/` для правленых (новые в него не входят — они untracked), `wc -l` для заведённых, `grep -c '^func Test'` для тестов. **Не коммичу — на лендинг.** ⚠ Числа выше по тексту, где они расходятся с этими, относятся к ПЕРВОМУ кругу и оставлены как след, а не как состояние.
|
||
**Мутационных посадок 26** (11 на первом круге, 15 на втором и третьем), каждая — на свой пункт, вердикт по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи: снятие сортировки фолда · потолок на единицу больше · снятие допуска в черновой волне · снятие допуска в редакторской · «свободные юниты тратят грант» · возврат к прежней терминальности (воспроизводит перерасход) · `!ok` → `ok &&` (воспроизводит перерасход) · допуск только лидера (воспроизводит усечённую главу) · фильтр согласия в тождество (воспроизводит отказ законной работе) · перестановка лога замечаний (воспроизводит потерю диагностики) · снятие отказа несогласованной форме конвейера. Все одиннадцать падают, и падает ИМЕННО адресный тест.
|
||
|
||
#### Таблица комплектности против §3
|
||
|
||
| Пункт промта | Что сделано | Каким ИСПОЛНЕНИЕМ подтверждено |
|
||
|---|---|---|
|
||
| **§3.1** потолок ОБЪЁМА рядом с денежным | Флаг `--max-units N` у `translate`; единица — **выходной юнит** (`ManifestUnit`=`editUnit`, `units_total` манифеста); допуск считается ДО волн (`planVolume`, `backend/internal/pipeline/volume.go`), волны не начинают элемент вне скоупа | `TestTheVolumeCeilingStopsAtWhatWasBought` — 5 глав, потолок 2: отгружено ровно 2, `res.Volume` заполнен, у глав 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. **Живого прогона с провайдером НЕТ.** Всё исполнение — против фейкового провайдера батареи. Ни одного платного вызова я не делал и не был на это уполномочен. Значит: цифры «2 вызова на юнит» и «$0.00182 за юнит» — свойства фикстуры, а не рынка; поведение потолка на реальном материале не мерено.
|
||
3. **Прогон, обрезанный по объёму и упёршийся в подписной стоп банка, уходит с кодом 3**, и `res.Volume` тогда не докладывается: остановка принадлежит банку, а не потолку. Поведение корректное, но пара «стоп банка + объёмный потолок» отдельным тестом НЕ покрыта.
|
||
4. **Платформенная половина не сделана и не заказана мне** (§0 промта): `--max-units` в argv никто не передаёт. Пока платформа не научится считать `главы → юниты` по манифесту и передавать флаг, обещание «купил N глав — получил N глав» НЕ выполнено. Механизм есть, трубы нет.
|
||
5. **`storeOrder` доказан на моей фикстуре, а не на всех входах.** Тождество опирается на: `UNIQUE (book_id, src, sense, since_ch, until_ch)` (`store/migrate.go:203`) ⇒ префикс ключа не даёт ничьей, и на отсутствие `COLLATE` в схеме ⇒ побайтное сравнение TEXT. Оба факта проверены грепом и чтением DDL. Чего я НЕ проверял: поведение при значениях с невалидным UTF-8 и при типовой аффинности SQLite, если в TEXT-колонку когда-нибудь ляжет число.
|
||
6. **Порог согласия (`rebill_consent_usd`) я НЕ трогал** — сужена только СУММА. Если владелец считает, что при объёмной покупке должен ужиматься и порог, это отдельное решение.
|
||
7. Батарея прогнана на `make battery`; **`make battery-stand` (корпусные тесты) НЕ гонял** — нет стендовых данных на этой машине.
|
||
|
||
#### §9. Вопросы оркестратору — пингом, не интерпретацией в свою пользу
|
||
|
||
1. ⛔ **Продавать ли объёмом книги, которые МАЙНЯТ банк — до отдельного заказа?** Obstacle-0: вторая покупка на такой книге падает `exit 1`/`failed` без `--resnapshot`, и это открывает именно обрезание (контроль без потолка проходит чисто). Платформа `--resnapshot` уже передаёт (D39.166), значит флагманский путь цел, но ручной вызов — нет, и цена покупки на майнящей книге включает лишнюю книжную консолидацию терминолога (Obstacle-1). **Мой совет:** либо не ставить `--max-units` на майнящих книгах до заказа по джоб-гарду, либо принять оба хвоста явно. Решать тебе — я поставил громкий WARN и не стал трогать ратифицированный Р6-контур.
|
||
|
||
2. **Форма §3.2 отличается от той, что назвала строка 231.** Строка предписывает «движковый глагол ЛИБО флаг». Я не сделал ни того, ни другого: я починил СВЁРТКУ, потому что слепое окно было не в том, что `status` пишущий, а в том, что он сворачивал последний прогон вместо ближайшего. Это дешевле обоих кандидатов и ближе к уже ратифицированной двери D39.165 §3 («платформа читает смету из УЖЕ существующего `status --json`»). **Нужна ратификация формы и правка формулировки строки 231.**
|
||
3. **Поля шва.** `rebill_units`/`rebill_usd` потеряли `omitempty`, добавлено `rebill_basis`; `status_version` оставлен `tm-status-v1` (правка АДДИТИВНАЯ, потребителей у полей сейчас нет — они сняты с аллоулиста). Строка 231 обещает их возврат «вместе с глаголом» — глагола нет, значит возвращаются **вместе с этим фиксом**. Нужен контрактный минор и ре-аллоулист трёх полей. ⚠ **Уточнение от платформенной сессии `textmachine-c0`, сверено ею по коду:** пара снята с аллоулиста паком P10 **по ТАЙМИНГУ** (`platform/internal/ingest/resync.go:37-43`) — `status` проецировал СОХРАНЁННУЮ память, поэтому сразу после apply честно отдавал ноль, — а не «за отсутствием потребителя», как я написала сначала. Это ровно та причина, которую мой §3.2 и снимает. Риска для шва при этом нет: `DecodeStatus` (`resync.go:76`) — обычный `json.Unmarshal` без `DisallowUnknownFields`, поэтому новое поле её сторона просто не заметит. Возврат — слово оркестратора, он берёт минор на себя (пойдёт вместе с `session_ended`, канон 0.8.0).
|
||
4. ⛔ **ЭТОТ ПУНКТ ОТОЗВАН МНОЮ ЖЕ — подтверждать его НЕ надо.** Здесь стояла просьба подтвердить сужение СУММЫ гейта D20.2-Q2 объёмным скоупом с доводом «инертно без флага». Довод верный и НЕважный: с флагом каллер сам выбирает размер прогона, поэтому дробление обходило порог и пере-оплачивало книгу целиком, ни разу не спросив согласия (замерено: 30 глав, четыре прохода по $0.003640 при пороге $0.005460). **Действующее решение — второй круг, находка 1:** порог судится по ВСЕЙ книге; именованный кап `--accept-rebill=X` — по тому, что заплатит ЭТОТ прогон; прогон, не пере-оплачивающий ничего, гейта не встречает. Подтверждать надо ЭТО.
|
||
5. **`redrive` я флаг НЕ дал** (отказ по правилу «команда, которая на флаг не действует, его отвергает»). Соседняя правда, которую называю строкой, а не чиню: `redrive` внутри зовёт `TranslateBook` по ВСЕЙ книге (`status.go:926`), то есть у него своя денежная неожиданность — она была и до пака.
|
||
6. **Сигнал объёмного стопа в ПОТОК событий я не завёл.** По правилу `runevents.go:29-40` новый тип события — это минор, и читатель незнакомые типы игнорирует; но это твоя зона и контрактный минор, а §3.3 его в пак не клал. Если он нужен платформе — заказывай отдельно.
|
||
|
||
#### Вопросы оркестратору после второго круга — новые и изменившиеся
|
||
|
||
**A. Разблокировка майнящих книг — есть дешёвый вариант, но решение продуктовое, поэтому НЕ беру.** Причина поломки второй покупки в том, что стоп майнинга досыпает авто-банк между покупками. Если на прогоне с `--max-units` стоп майнинга НЕ запускать вовсе, авто-банк не растёт, edit-снапшот не двигается, вторая покупка идёт чисто, и хвост №1 исчезает без правки ратифицированного джоб-гарда. Цена ровно одна и она честная: **книга, продаваемая по частям, не майнит банк, пока её продают** — майнинг случается на цельном прогоне. Это размен «продавать по главам ИЛИ майнить», а не техническая деталь, поэтому его выбираешь ты. Если скажешь «да» — это несколько строк и тест.
|
||
|
||
**B. Различение доставки и переделки сейчас ЧИТАЕМО ЧЕЛОВЕКОМ, но не машиной.** `VolumeStop` несёт `Delivered`/`Reworked`/`LeftFresh`/`LeftRework`, и CLI это печатает — но платформа читает поток событий и код выхода, а туда различение не идёт. Значит покупка десяти глав, целиком ушедшая в переделку, для платформы по-прежнему выглядит как успешная доставка десяти. Полное лечение — поле в терминальном кадре потока, то есть твой контрактный минор (ты уже сказал «не сейчас, строкой»). Пишу это ещё раз потому, что после второго круга это перестало быть косметикой: именно здесь пере-оплата всё ещё может продаться как доставка, просто теперь молча на проводе, а не громко в отчёте.
|
||
|
||
**C. Порог согласия остаётся КНИЖНЫМ, и это теперь несущее решение, а не умолчание.** После отзыва сужения порог судит всю пере-оплату книги. Следствие, которое надо принять явно: **любая** покупка через `--max-units` на сдвинутом банке встречает гейт и требует `--accept-rebill`. Платформа его передаёт (при `BankMoved`), так что путь рабочий, но «тихих» пере-проходов больше не будет ни одного — и это ровно то, чего требует Р6. Если владелец захочет, чтобы мелкие покупки проходили без вопроса, это НЕ порог надо двигать, а заводить учёт уже-данного согласия — отдельный механизм, отдельный заказ.
|
||
|
||
**D. Проводка `--max-units` гейчена тобой — я это поддерживаю и после второго круга поддерживаю сильнее.** Пока различение доставки и переделки не доезжает до платформы машинным каналом (пункт B), платформа не может отличить «купил 10 глав и получил 10» от «купил 10 глав и получил 10 переделок». Механизм готов, труба — нет, и открывать её раньше пункта B я бы не стала.
|
||
|
||
#### Дополнение к вопросам после третьего круга
|
||
|
||
**E. Три развилки закрыты РАТИФИКАЦИЕЙ, а не моим словом — фиксирую, чтобы ты не пере-решал их заново.**
|
||
— «капнутое согласие» — ЗДОРОВО (согласие, а не обход; раскрытие теперь запинено);
|
||
— «денominация» — был ДЕФЕКТ, вылечен аддитивным `rebill_output_units`;
|
||
— «доставка вперёд переделки» — ЗДОРОВО, порядок безопасен (байты вне-очередного юнита дословно совпали с прямым прогоном), выигрыш замерен A/B (6/6 против 4/6).
|
||
Если пере-решаешь — пере-решай явно: под каждым лежит исполнение, а не рассуждение.
|
||
|
||
**F. Аллоулист: полей теперь ЧЕТЫРЕ, и четвёртое обязательно.** К `rebill_units`/`rebill_usd`/`rebill_basis` добавилось **`rebill_output_units`**. ⚠ **Возвращать `rebill_units` БЕЗ него нельзя**: `rebill_units` считает `chunk×stage`, все прочие «units» документа — выходные юниты, отношение внутри одной книги непостоянно, и платформа, отмерившая пере-проход по `rebill_units` и передавшая это число в `--max-units`, купит кратно больше книги. Ратификатор измерил: `rebill_units: 15` двумя строками под `total_units: 6`.
|
||
|
||
**G. Про умолчание «доставка вперёд переделки» — прошу зафиксировать словом, а не молчанием.** Покупка, ЦЕЛЬЮ которой был пере-проход, теперь потратит грант на недоставленные главы, если они есть. Ратификатор показал, что дверь пере-прохода этим не ломается (на дочитанной книге доставка-вперёд И ЕСТЬ порядок книги), но на ЧАСТИЧНО доставленной книге разница видна. Если продукту нужен режим «только переделывать» — это флаг, и это твой заказ, не мой.
|