diff --git a/CLAUDE.md b/CLAUDE.md index cf832c9a..88adfa68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Go-бэкенд издательского художественного пер - **Тесты и гейты не подгонять под зелень:** править или удалять тест/голден/гейт, чтобы он прошёл, — НЕДОПУСТИМО; несогласие с тестом — вопрос оркестратору пингом, не правка (D39.121). - **Мандат самопроверки в промтах сессий (обязателен, решение владельца):** промты полигон-сессий — и вообще любых пишущих код / запросы к моделям — ДОЛЖНЫ явно требовать ревью ИСПОЛНЕНИЕМ: своего кода + сформированных запросов к моделям + полученных результатов. Сессии регулярно ошибаются и багуют, и это искажает результат; **самоотчёт «проверено» без исполнения регулярно оказывается ложным и стоит денег**. Бэкенд-промты — явный бэкенд-ревью после кода (обычно отрабатывает по опыту, но требовать явно). Пост-хок адверсариальная верификация оркестратора при лендинге — второй рубеж, НЕ замена самопроверки. ⚠ **Исполнения мало: промт заказывает адверсариальный проход сессии по СВОЕЙ готовой работе; глубину и веер сессия выбирает под предмет, промт даёт направление — что в этом паке уязвимо.** - ⚠ Тяжёлая модель — один агент максимум. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает. + ⚠ **Fable 5** — обычно хватает одного-двух агентов, но это РЕКОМЕНДАЦИЯ, не потолок: веер под предмет выбирает сессия. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает. - **Git-координация мультисессий:** - **Коммитит только оркестратор.** Сессии зон — бэкенд, полигон, фронт, платформа — не коммитят: своё дерево готовят и передают на лендинг (исключение: пре-рег фризы полигона). Чужую зону не трогает НИКТО. - **Форма коммита — ТОЛЬКО с явным списком путей: `git commit -- <путь> <путь>`.** Причина механическая: голый `git commit` уносит **ВЕСЬ индекс**, включая чужие застейдженные файлы, и чужая работа уезжает под твоим сообщением. Pathspec-форма индекс не трогает. diff --git a/docs/BACKEND_MONEY_PACK_SESSION_PROMPT.md b/docs/BACKEND_MONEY_PACK_SESSION_PROMPT.md index 0742ead7..c6b9c6d2 100644 --- a/docs/BACKEND_MONEY_PACK_SESSION_PROMPT.md +++ b/docs/BACKEND_MONEY_PACK_SESSION_PROMPT.md @@ -195,7 +195,7 @@ content-hash: бесплатная смета назовёт одно число 5. **Адверсариальный проход по своей готовой работе — заказан; глубину и веер выбираешь сама.** ⚠ Куда смотреть в ЭТОМ паке: деньги (перерасход либо отказ законной работе) · тождество бесплатной сметы и платного прогона · шов (форма ответа, argv). Дефекты, которые внёс сам пак, называй первыми — приёмке они не видны. - ⚠ Тяжёлая модель — один агент максимум. Модель задавай ЯВНО и знай, сколько их работает. + ⚠ **Fable 5** — обычно хватает одного-двух агентов, но это РЕКОМЕНДАЦИЯ, не потолок: веер под предмет выбирает сессия. Модель задавай ЯВНО и знай, сколько их работает. ## §5. Оси ревью — 1–3, вправе заменить с аргументом diff --git a/docs/PLATFORM_P11_SESSION_PROMPT.md b/docs/PLATFORM_P11_SESSION_PROMPT.md index d2752525..38bc092b 100644 --- a/docs/PLATFORM_P11_SESSION_PROMPT.md +++ b/docs/PLATFORM_P11_SESSION_PROMPT.md @@ -150,7 +150,7 @@ D39.166 и фикс-раунды). Если строка описывает со 5. **Адверсариальный проход по своей готовой работе — заказан; глубину и веер выбираешь сама.** ⚠ Куда смотреть в ЭТОМ паке: что видит тот, у кого отозвали доступ · может ли починка вернуть холд дважды или чужой · поток под нагрузкой. Дефекты, которые внёс сам пак, называй первыми — приёмке они не видны. - ⚠ Тяжёлая модель — один агент максимум. Модель задавай ЯВНО и знай, сколько их работает. + ⚠ **Fable 5** — обычно хватает одного-двух агентов, но это РЕКОМЕНДАЦИЯ, не потолок: веер под предмет выбирает сессия. Модель задавай ЯВНО и знай, сколько их работает. ## §5. Оси ревью — 1–3, вправе заменить с аргументом diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 278caa10..d4810f86 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -233,6 +233,143 @@ > пере-прохода) · **229** (снапшот не фолдит модель внутренних гейтов, латентна) · **230** (размен > «сходимость против поучения») · **141**-остаток · **131**. Плюс потолок объёма из `D39.165` §1б. +### 📝 ЗАПИСКА-ПЛАН — пак «деньги» (промт `docs/BACKEND_MONEY_PACK_SESSION_PROMPT.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:733-744`) материализует СОХРАНЁННЫЙ глоссарий, то есть фолд ПРОШЛОГО прогона, тогда как ближайший `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/BACKEND_MONEY_PACK_SESSION_PROMPT.md`, D39.165 §1б + строка 231), сессия `textmachine-e4`, 29.08 + +**Батарея зоны:** `make battery` — **exit 0**, линтер **0 issues**, `test -race` зелёный целиком (прогнана начисто ПОСЛЕ всех правок адверсариального прохода). +Пропущенные тесты, как требует Makefile, называю: `TestMinerFullBookParity` (нет стендовых данных), `TestHelperEventsRun`, `TestHelperKillLoop` (точки входа подпроцессов, вне обычного прогона). +Дерево: 16 файлов правлено, 5 заведено, **+404/−183**; новых тестов **22** (19 в `internal/pipeline`, 2 в `cmd/tmctl/invocation_test.go`, 1 в `cmd/tmctl/render_test.go`). **Не коммичу — на лендинг.** +**Мутационных посадок 11**, каждая — на свой пункт, вердикт по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи: снятие сортировки фолда · потолок на единицу больше · снятие допуска в черновой волне · снятие допуска в редакторской · «свободные юниты тратят грант» · возврат к прежней терминальности (воспроизводит перерасход) · `!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,Granted,Deferred,Free}`), строка лога, строка вывода CLI | `TestAVolumeStopIsNotAMoneyStop` — денежный стоп остаётся типизированным `*CeilingHalt` (exit 4), объёмный не ошибка вовсе и несёт `res.Volume` | +| §3.1 — счёт РЕПИНОВ | Не считаются: считаются только юниты, по которым прогон сделает ≥1 вызов | `TestAResumeDoesNotSpendItsVolumeOnFreeUnits` — вторая покупка 2 юнитов на книге, где 2 уже сделаны: `Free=2`, `Granted=2`, `Deferred=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 — потолок не отказывает законной работе | Гейт согласия (D20.2-Q2) считает СУММУ по допущенным юнитам; порог остаётся книжным | `TestABoundedRunIsNotAskedToConsentToTheWholeBook` — порог книги между ценой одного юнита и всей книги: без потолка гейт отказывает, с `--max-units 1` прогон идёт и делает ровно 1 вызов | +| §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.3 — чего в паке нет | Константу $0.03, цену от объёма исходника, строки 228/230/141/131, `platform/` — не трогал | — | + +#### Адверсариальный проход по СВОЕЙ готовой работе — что он нашёл + +Веер: шесть линз (деньги · тождество фолда · шов · регрессии пака · параллелизм/детерминизм · качество моих же тестов), тяжёлая модель — РОВНО ОДИН агент (денежная линза), остальным попроще; затем по одному независимому опровергателю на каждую находку. Сырых находок 10, после опровержения осталось 7 настоящих. **Все семь внёс сам этот пак — приёмке они не видны, поэтому называю первыми.** + +**1. ⛔ ПЕРЕРАСХОД: юнит с флагнутым членом и без edit-строки считался БЕСПЛАТНЫМ.** `unitsServedFree` спрашивал терминальность у `resolveChunkState`, а тот отдаёт `ChunkFlagged`, как только флагнута ЛЮБАЯ строка, — ДО проверки, что все ожидаемые позиции на месте. Юнит, у которого черновик одного члена флагнут, а edit-строки ещё нет (состояние после подписного стопа банка, Ctrl-C или любого обрыва в редакторской волне), признавался терминальным и свободным, проходил БЕЗ списания гранта и потом оплачивался редактором. Прогон платил за юнитов больше, чем куплено, и, поскольку `Deferred` оставался нулём, **даже не сообщал об объёмном стопе.** +**Лечение:** предикат заменён на `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` ⇒ фильтр тождественный ⇒ число байт-в-байт прежнее) — это и делает правку ратифицированного гейта безопасной. **Доказано:** `TestABoundedRunIsNotAskedToConsentToTheWholeBook`; мутация «фильтр в тождество» воспроизводит отказ дословно. + +**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.** `checkRebillConsent` теперь проецирует пере-оплату по ДОПУЩЕННЫМ юнитам, а не по всей книге; ПОРОГ (5% проектной цены книги) остался книжным и не тронут. Правка **инертна без `--max-units`**: `scope == nil` ⇒ фильтр тождественный ⇒ число байт-в-байт прежнее для каждого существующего прогона. Подтверждено оркестратором при приёмке 29.08. + +**Опровергнуто и НЕ чинилось (3 из 10):** три находки опровергатель снял как уже вылеченные тем же проходом (я чинил по мере поступления, и опровергатели читали уже исправленный код) — они и есть пункты 3, 4 и 7(а/б) выше. + +#### §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)** объёмным скоупом — иначе прогон, купивший один юнит, получал отказ за пере-оплату всей книги. Правка ИНЕРТНА без `--max-units` (скоуп `nil` ⇒ фильтр тождественный ⇒ число байт-в-байт прежнее), и это то, что делает её безопасной. Прошу подтвердить. +5. **`redrive` я флаг НЕ дал** (отказ по правилу «команда, которая на флаг не действует, его отвергает»). Соседняя правда, которую называю строкой, а не чиню: `redrive` внутри зовёт `TranslateBook` по ВСЕЙ книге (`status.go:926`), то есть у него своя денежная неожиданность — она была и до пака. +6. **Сигнал объёмного стопа в ПОТОК событий я не завёл.** По правилу `runevents.go:29-40` новый тип события — это минор, и читатель незнакомые типы игнорирует; но это твоя зона и контрактный минор, а §3.3 его в пак не клал. Если он нужен платформе — заказывай отдельно. + ## Полигон **📌 ПИНГ ПОЛИГОНУ от оркестратора №19 (28.08, D39.164) — вендор-вердикт по Gemini для `docs/experiments/00-provider-quirks.md`.**