From ab0ab998a077b210c8e71178fb6780a7a4d4a284 Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 6 Sep 2026 02:22:07 +0300 Subject: [PATCH] Close five register rows the landing fixed, two of them major --- platform/docs/DEFECT_REGISTER.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 777ca041..384fa4a0 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -23,9 +23,9 @@ | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | |---|---|---|---|---|---|---| -| PD-410 | bug | **major** | `internal/pgstore/readmodel.go`, греп `draftWork` (⚠ адрес пере-нацелен 06.09: строка уехала на ~130 позиций правками пака формы заказа); ⚠ **контраст ряда «движку передаётся ТОЛЬКО `--ceiling-usd`» БОЛЬШЕ НЕ ВЕРЕН** — `--max-units` едет в argv с 05.09 (`internal/runs/spawn.go`, греп `maxUnitsFor`), и draft-волна по ВСЕМ чанкам книги больше не фанится: её гейтит `volumeScope.allows` (`backend/internal/pipeline/volume.go`) | **Полоса и платёжная модель считают, что прогон работает над диапазоном «C глав от edit-фронта» в обеих волнах, а движок гонит волны последовательно от СВОИХ фронтов до денег — и на continuation над недочерновленной книгой (`draft_before ≥ chapters_before + C`) формула даёт `draftWork=0`: движок тратит весь потолок на черновики, полоса стоит 0/C весь прогон со stage `editing` на прогоне, который только черновит.** Зеркало дефекта, который `draftWork` чинил (тот закрывал «ready на 50%», этот открывает «0% при сделанной работе»). Это НЕ локальная ошибка формулы: любая полоса из (баз, C, D, E) где-то соврёт, пока платёжная модель («довести до конца C глав от `e0`») и движковый план работ («деньги в порядке волн от фронтов») не согласованы — лечение требует либо передавать движку план (диапазон/волны), либо выводить total полосы из движкового плана; `ceiling_chapters` сегодня до движка не доезжает вовсе. Архитектурное, ОТДЕЛЬНЫМ паком по слову оркестратора 28.08 («остановись и скажи»); подтверждено воркфлоу арифметикой на существующем зелёном пине (`TestARunOverADraftedBacklogOwesOnlyTheLastPass` держит `draftWork=0` в родственной точке) ⚠ **ПЕРЕ-ДИСПОЗИЦИОНИРОВАНА паком P12 (30.08): направление ЕСТЬ** — D39.165 §1, четыре части (цена от объёма исходника · стоп объёма в движке · хвост качества в тариф). ⛔ **(г) константу по сегодняшним числам НЕ калибровать** — гейт строка бэклога 202. Что остаётся открытым: не решение, а МЕХАНИКА, и она — отдельный пак (слово оркестратора 28.08), паком P12 не тронута. ⚠ **ПРИЧИНА СНЯТА ПАКОМ «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ; и снята она НЕ так, как обещала строка 280 — расхождение названо.** Строка велела УДАЛИТЬ `draftWork`; зона этого не сделала и объясняет почему: дефект `draftWork` в том, что draft-волна фанилась по ВСЕЙ книге, а формула считала купленный диапазон. С проводкой `translate --max-units` волна больше по всей книге не фанится (`backend/internal/pipeline/volume.go`, `volumeScope.allows` гейтит и draft-волну), то есть **посылка формулы стала истинной, а сама формула — нужной**: непочерновленный остаток заказа это реальная работа прогона. Удалить её значило бы сломать полосу по-новому. ⇒ снята ПРИЧИНА, носитель оставлен. Полоса при этом осталась в ГЛАВАХ (канон §Progress говорит «in chapters»), перевод её в юниты — отдельный контрактный вопрос, в этот минор не входит. | open | воркфлоу-ревью P9 28.08 (линза bar:double-count), стоп-решение сессии P9 28.08: тянет архитектуру, не чинится в паке | +| PD-410 | bug | **major** | `internal/pgstore/readmodel.go`, греп `draftWork` (⚠ адрес пере-нацелен 06.09: строка уехала на ~130 позиций правками пака формы заказа); ⚠ **контраст ряда «движку передаётся ТОЛЬКО `--ceiling-usd`» БОЛЬШЕ НЕ ВЕРЕН** — `--max-units` едет в argv с 05.09 (`internal/runs/spawn.go`, греп `maxUnitsFor`), и draft-волна по ВСЕМ чанкам книги больше не фанится: её гейтит `volumeScope.allows` (`backend/internal/pipeline/volume.go`) | **Полоса и платёжная модель считают, что прогон работает над диапазоном «C глав от edit-фронта» в обеих волнах, а движок гонит волны последовательно от СВОИХ фронтов до денег — и на continuation над недочерновленной книгой (`draft_before ≥ chapters_before + C`) формула даёт `draftWork=0`: движок тратит весь потолок на черновики, полоса стоит 0/C весь прогон со stage `editing` на прогоне, который только черновит.** Зеркало дефекта, который `draftWork` чинил (тот закрывал «ready на 50%», этот открывает «0% при сделанной работе»). Это НЕ локальная ошибка формулы: любая полоса из (баз, C, D, E) где-то соврёт, пока платёжная модель («довести до конца C глав от `e0`») и движковый план работ («деньги в порядке волн от фронтов») не согласованы — лечение требует либо передавать движку план (диапазон/волны), либо выводить total полосы из движкового плана; `ceiling_chapters` сегодня до движка не доезжает вовсе. Архитектурное, ОТДЕЛЬНЫМ паком по слову оркестратора 28.08 («остановись и скажи»); подтверждено воркфлоу арифметикой на существующем зелёном пине (`TestARunOverADraftedBacklogOwesOnlyTheLastPass` держит `draftWork=0` в родственной точке) ⚠ **ПЕРЕ-ДИСПОЗИЦИОНИРОВАНА паком P12 (30.08): направление ЕСТЬ** — D39.165 §1, четыре части (цена от объёма исходника · стоп объёма в движке · хвост качества в тариф). ⛔ **(г) константу по сегодняшним числам НЕ калибровать** — гейт строка бэклога 202. Что остаётся открытым: не решение, а МЕХАНИКА, и она — отдельный пак (слово оркестратора 28.08), паком P12 не тронута. ⚠ **ПРИЧИНА СНЯТА ПАКОМ «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ; и снята она НЕ так, как обещала строка 280 — расхождение названо.** Строка велела УДАЛИТЬ `draftWork`; зона этого не сделала и объясняет почему: дефект `draftWork` в том, что draft-волна фанилась по ВСЕЙ книге, а формула считала купленный диапазон. С проводкой `translate --max-units` волна больше по всей книге не фанится (`backend/internal/pipeline/volume.go`, `volumeScope.allows` гейтит и draft-волну), то есть **посылка формулы стала истинной, а сама формула — нужной**: непочерновленный остаток заказа это реальная работа прогона. Удалить её значило бы сломать полосу по-новому. ⇒ снята ПРИЧИНА, носитель оставлен. ⛔ **Испр. 06.09: здесь стояло «полоса при этом осталась в ГЛАВАХ, перевод её в юниты в этот минор не входит» — НЕВЕРНО, и это был третий носитель одной моей ошибки (отчёт зоны, состав минора, этот ряд).** `runDone`/`runTotal` первой ветвью считают в ЮНИТАХ, когда `runs.ordered_units` не null (`internal/pgstore/readmodel.go`, греп `ordered_units is not null`), и это пиньнуто `runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought`. `draftWork` при этом остаётся носителем ГЛАВНОЙ ветви — той, что считает заказ в главах, — и довод выше её и касается. ⇒ настоящее следствие: канон 0.11.0 описывает `Progress` как «in chapters» для ВСЕХ трёх форм прогона, а сборка для символьного заказа шлёт юниты; недостающий случай внесён в состав минора и отдан оркестратору 06.09. | fixed(628cc56) | воркфлоу-ревью P9 28.08 (линза bar:double-count), стоп-решение сессии P9 28.08: тянет архитектуру, не чинится в паке | | PD-424 | bug | **major** | `internal/runs/reconcile.go` `reopen` (вердикт `deferred`) и `reconcileOne`, `internal/pgstore/runs.go` `StalledRuns`, `internal/pgstore/observe.go` | **ЖИВОЙ прогон с намертво заблокированной расплатой невидим на ВСЕХ поверхностях `PD-385` и не поддаётся `run abandon` — счётчик неудач не растёт НИКОГДА.** Цепь, воспроизведённая охотником приёмки на десяти проходах: юнит исчез без маркера → `restart` → `settle` возвращает `settlementBlocked, nil` (не ошибку) → `reopen` отказывается открыть новую попытку поверх незакрытого холда и возвращает вердикт `deferred` → `case deferred: return nil` → `reconcileOne` считает проход УСПЕШНЫМ и вдобавок зовёт `ClearRunDeferral`. Итог: `reconcile_failures` 0, `reconcile_after` NULL, `StalledRuns(5)` пуст, гейдж 0, холд заморожен, движок дёргается каждым проходом. Вывести из состояния может только пользователь, нажав Stop, — и никто ему об этом не говорит. ⚠ Это ТА ЖЕ болезнь, что `PD-384`/`PD-385`, но в ЖИВОЙ фазе, куда пак P11 не дошёл: он лечил вторую фазу (расплату) и её поверхности. ⚠ **СУЖЕНА пак P12 (30–31.08), НЕ ЗАКРЫТА — половина «невидим» вылечена, половина «ручки нет» стоит.** Приор зоны принят и исполнен: блокированная расплата — НЕУДАЧА владеющей фазы, и она считается. `restart` больше не выбрасывает вердикт `settle`: на `settlementBlocked` он возвращает `errSettlementBlocked` ДО `reopen` (прежний путь платил за запрос, чтобы узнать то, что `settle` только что сказал, писал WARN, винящий рестарт в блокировке расплаты, и отвечал `nil`, который вызывающий считал успешным проходом). `reconcileOne` ключует на этом сентинеле причину — ОДНА фраза на оба фазовых пути — и ЗАДЕРЖКУ: расписание РАСПЛАТЫ, а не живой фазы, потому что открытая резервация и есть resume-гейт пользователя, и получасовой живой бэкофф держал бы его resume за блокировку, с которой он ничего сделать не может. Вердикт `deferred` у `reopen` оставлен как был: после правки он достижим только на `settlementRaced` с моментально открытой резервацией — самоисправляется следующим проходом, и фаза расплаты прямо аргументирует, что гонка не должна попадать на счётчик оператора. Гард выключения (`ctx.Err()`) стоит: счёт, придуманный ОСТАНОВКОЙ демона, — тот самый дефект, который соседняя фаза уже нашла. Итог, проверенный пином `runs.TestALiveRunWhoseSettlementIsBlockedIsCountedAndReachesTheOperator`: `reconcile_failures` доходит до порога, строка появляется в `runs --stalled` (половина `live`, не `settling`), гейдж `tm_platform_runs_stalled` = 1, отсрочка не превышает `settlementBackoffCap`. Посадка M13 КРАСНАЯ адресно. ⚠ **ЧТО ОСТАЁТСЯ ОТКРЫТЫМ и почему не взято этим паком:** терминальная РУЧКА. `run abandon` ветвится по `runs.finished_at` и на живом прогоне уходит в живую ветку, а `AbandonRun` отказывает над попыткой, ещё называющей юнит. Ручка «процесса нет, закрой деньги» — НОВАЯ разрушительная операторская поверхность над деньгами, и её дизайн (кто вправе звать, чем доказывается отсутствие процесса, что будет, если процесс вернётся) — решение своего размера, а не хвост этой правки. Сегодня пользователь выводит прогон из состояния кнопкой Stop, и теперь об этом хотя бы говорят все три поверхности `PD-385`. ⚠ **РУЧКА ПОСТРОЕНА паком «закрыть цикл» (04.09) — строка СУЖЕНА до своего остатка, не закрыта.** Форма: та же команда `tmplatformctl run abandon`, потому что рантбук уже посылает оператора именно к ней; новой команды нет. **Кто вправе звать** — оператор из CLI и только оттуда: терминальный вердикт над деньгами на HTTP-поверхность не выходит, как и `run unquarantine`. **Чем доказывается отсутствие процесса** — систему СПРАШИВАЮТ, а не верят ей на слово: команда сама зовёт `runner.Alive` по имени юнита стоящей попытки, и ответов **ЧЕТЫРЕ**, а не два — «нет» пропускает, «есть» отказывает, **недостижимая шина отказывает тоже** (отсутствие ответа не есть отсутствие процесса), и — четвёртый, найденный адверсариальным проходом уже по готовой работе — **«это НЕ ТОТ systemd» отказывает тоже**: `systemctl --user` отвечает про менеджер СПРАШИВАЮЩЕГО, а прогоны живут у пользователя демона, и там юнит, о котором чужой менеджер не слышал, неотличим от кончившегося (замерено: `ActiveState=inactive`, выход 0). Различитель — владелец `TM_PLATFORM_STATE_DIR`, ⚠ **поэтому переменная деплоя обязана быть экспортирована в оболочке оператора, иначе команда ОТКАЖЕТ** (рантбук об этом теперь говорит). Пины: `cmd/tmplatformctl/abandon_proof_test.go` — четыре ответа различителя, три отказа и путь «gone» через саму команду, плюс отказ над ЖИВЫМ транзиентным юнитом под systemd-гейтом. Второе условие проверяет хранилище ВНУТРИ транзакции: `reconcile_failures >= abandonAfter`, тот же пол, что у settling-ветви, — видеть и иметь право уничтожить это разные разрешения. **Деньги:** попытка с базовой линией и отчитанной цифрой рассчитывается по РАЗНИЦЕ (та же арифметика, что печатает `StalledRuns`), холд закрывается как `settled` с ценой в леджере; попытку без базовой линии ценить нечем — холд возвращается ЦЕЛЫМ, довод settling-ветви, доехавший до живой половины. **Что будет, если процесс вернётся** — названо прямо, потому что это цена ручки: живой движок продолжит тратить против СВОЕГО книжного потолка, а холд аккаунта уже закрыт, значит эту трату не оплатит никто и провайдеру платит ДЕПЛОЙ. Она ограничена (потолком этого же прогона) и не эксплуатируема аккаунтом: попасть в состояние можно только через прогон, который собственный реконсилятор деплоя не смог закончить. Тем же касанием закрыта долговечная половина `PD-418`. Пины: `internal/runs/abandon_orphan_test.go` — отказ без доказательства · отказ ниже пола · вердикт `orphan` со списанием по отчёту · целый холд там, где цены нет · осиротевшая попытка ЖИВОГО прогона. ⚠ **ОСТАТОК, ради которого строка остаётся открытой:** отсутствие процесса доказывается ОДИН раз, в момент команды, и между этим ответом и коммитом транзакции есть окно, в которое юнит может подняться заново. Окно узкое, его цена названа выше, но оно есть, и закрыть его может только фактически транзакционная проба — арбитр в хранилище, а не ещё одна проверка в CLI. | open | приёмка оркестратора №19 по паку P11 (охотник вне карты, воспроизведено 10 проходами) | -| PD-440 | bug | **major** | ⚠ **два из трёх адресов УМЕРЛИ вместе с лечением, и это пере-написано, а не пере-нацелено:** `DefaultPerChapter` и `BookRunContext.ChaptersLeft` удалены паком формы заказа. Живой носитель лечения — `internal/pricing/pricing.go`, греп `func (m Model) Hold`; аргумент потолка по-прежнему `internal/runs/spawn.go`, греп `func (m meter) bookCap`; движковая половина — `backend/internal/pipeline/stagerun.go` (резерв под шаг при `waves.workers` параллельных вызовах) | **КНИГУ ЧЕРЕЗ API НЕЛЬЗЯ ДОЧИТАТЬ НИКАКИМИ ДЕНЬГАМИ — покупка перестаёт покупать.** Прирост книжного потолка за прогон равен `chaptersLeft × DefaultPerChapter`, то есть максимум `chaptersLeft × $0.03`. Движок перед редакторским шагом РЕЗЕРВИРУЕТ оценку на каждый параллельный вызов (`waves.workers`, на стенде 4 × ≈$0.069 = ≈$0.28) и останавливается на ПЕРВОЙ отказанной резервации, не дожидаясь уже летящих. Как только прирост становится меньше стоимости одного шага волны, каждый следующий прогон встаёт МГНОВЕННО и не продвигает книгу ни на юнит — а прирост только УБЫВАЕТ, потому что `chaptersLeft` уменьшается с каждой дочитанной главой. ⚠ **Воспроизведено живьём дважды подряд, платным прогоном 04.09** (книга `bk_SS5VES2JELESJSTR`, 10 глав, после первой готовой главы `chaptersLeft = 9`, прирост $0.27): прогоны `run_CR2RN76NAKYKAJE5` и `run_VSXPMJE53KJQQAOS` — оба `paused/credit_exhausted` за 10–15 секунд, `committed` не сдвинулся ни на микро-доллар (278319 до и после), холд вернулся целиком. Движковая строка обоих: `book USD ceiling reached ($0.548319) (committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828) … reserve ceiling reached`. Это ПРОДУКТОВЫЙ СТОП, а не «шкала короче»: занижение ставки ×4.47 (`D39.179` п.1) до сих пор читалось как неудобство, и вот порог, за которым оно становится недостижимостью результата. ⚠ **Денег сам стоп НЕ жжёт** — отменённые вызовы этих двух прогонов не успели уйти (латентность 20 и 31 мс, `model_actual` пуст, 0 токенов); неучтённый расход — отдельная строка `PD-441`. ⚠ **Чинится СОГЛАСОВАННО, одной стороной нельзя:** поднять ставку — платформенная половина и она меняет всю шкалу покупки; не бросать летящие вызовы и/или считать резерв по фактической конкуренции — движковая. Проверка без Go: `sqlite3 -header -column "file:?mode=ro" "select trace_id, count(*), round(sum(cost_usd),6) from request_log group by trace_id;"` ⚠ **РАДИУС ПЕРЕ-СНЯТ 04.09 ЗАМЕРОМ ЗА $0 (заказ оркестратора №22): дефект НЕ про хвост длинной книги, а про ПОРОГ, ниже которого книга не переводится ВООБЩЕ.** Порог считается из измеренного, а не из оценки: движок в собственном отказе назвал резерв ОДНОГО редакторского вызова до шестого знака — `committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828, ch5/chunk0/edit` при потолке `$0.548319`. Отсюда волна из четырёх стоит **$0.277633 — наблюдённая величина: три реально стоявших резерва ($0.207805) плюс отказанный ($0.069828)**. ⚠ Не `4 × $0.069828 = $0.279312`: первая редакция ряда взяла именно это произведение, а оно СКОНСТРУИРОВАНО — вызовы волны не равны между собой, промпты чанков разной длины, и `3 × $0.069828 = $0.209484` расходится с наблюдёнными `$0.207805` на `$0.001679` (средний реальный резерв $0.069268 против отказанного $0.069828). Произведение оставлено рядом как ВЕРХНЯЯ оценка, и держать оба стоит: порог считается по обоим одинаково — `$0.277633 / $0.03 = 9.25` и `$0.279312 / $0.03 = 9.31`, — то есть **вывод не зависит от выбора числа**. ⚠ Счёт «три» выведен, а не напечатан: он следует из `waves.workers: 4` минус отказанный, и подтверждается отношением `$0.207805 / $0.069828 = 2.976`. Порог — `chaptersLeft × $0.03 ≥ стоимости волны`, то есть **книга проходит только с 10 непереведённых глав и больше**. Поправка числа — оркестратора №22, 04.09. Следствия: **последние ДЕВЯТЬ глав любой книги недостижимы**, и **книга из ≤9 глав не переводится никогда** — ни первой покупкой, ни повторными, потому что потолок КУМУЛЯТИВНЫЙ (`bookCap = committed + increment`, `internal/runs/spawn.go:212-214`) и запас каждого прогона равен ровно инкременту, сколько бы книга ни потратила раньше. ⚠ **Предъявлено живьём и бесплатно:** пятиглавая книга `bk_P5UCXKHDO4HFWSH3` заведена через настоящий интейк ($0 — `manifest` без ключей), и `GET /v0/books/{id}/run-options` вернул `max_chapters: 5`, то есть **максимум, который пользователь вообще может купить этой книге, — $0.15 против $0.279312 за волну**; для книги пака после первой дочитанной главы тот же вызов вернул `max_chapters: 9` ($0.27). Разрезка короткой книги ПОБАЙТНО та же (юниты по главам 2·1·1·2·1 против тех же 2·1·1·2·1 у первых пяти глав длинной), то есть короткая книга содержит ровно тот кусок `ch5/chunk0`, чей резерв измерен. ⚠ **ЧЕГО В ЗАМЕРЕ НЕТ, называю прямо:** сама короткая книга НЕ запускалась — её прогон способен потратить до $0.15 на черновую волну, а санкция владельца на платные прогоны закрылась вместе с паком; арифметика замкнута, живой прогон короткой книги остаётся неснятым. ⚠ **ЛЕЧЕНИЕ В ДЕРЕВЕ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ.** Константа `DefaultPerChapter` удалена целиком: цена берётся из проекции движка (`manifest --json`: `expected_usd` по главам, `step_max_usd`), а холд считается `k × ожидаемое(заказ) + step_max` — **аддитивно, не `max(...)`**, потому что последнему вызову заказа нужен запас под ЦЕЛУЮ резервацию сверх уже списанного. Пины: `pricing.TestTheLastTwoChaptersOfABookAreBuyable` (последние две главы и последняя глава покупаются), `pricing.TestTheHoldIsTheCushionedBillPlusOneWholeReservation`. ⚠ И вторая половина той же стены, найденная этим же паком: книжный `expected_usd` ВКЛЮЧАЕТ `book_once_usd` — плоские $2.00 на боевом `pipeline-c1` независимо от длины книги, — поэтому в основу холда он НЕ кладётся (иначе пятиглавая книга за $0.25 снова непокупаема); пин `pricing.TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase`. | open | платный сквозной прогон через API, пак «закрыть цикл» 04.09 (воспроизведено дважды) | +| PD-440 | bug | **major** | ⚠ **два из трёх адресов УМЕРЛИ вместе с лечением, и это пере-написано, а не пере-нацелено:** `DefaultPerChapter` и `BookRunContext.ChaptersLeft` удалены паком формы заказа. Живой носитель лечения — `internal/pricing/pricing.go`, греп `func (m Model) Hold`; аргумент потолка по-прежнему `internal/runs/spawn.go`, греп `func (m meter) bookCap`; движковая половина — `backend/internal/pipeline/stagerun.go` (резерв под шаг при `waves.workers` параллельных вызовах) | **КНИГУ ЧЕРЕЗ API НЕЛЬЗЯ ДОЧИТАТЬ НИКАКИМИ ДЕНЬГАМИ — покупка перестаёт покупать.** Прирост книжного потолка за прогон равен `chaptersLeft × DefaultPerChapter`, то есть максимум `chaptersLeft × $0.03`. Движок перед редакторским шагом РЕЗЕРВИРУЕТ оценку на каждый параллельный вызов (`waves.workers`, на стенде 4 × ≈$0.069 = ≈$0.28) и останавливается на ПЕРВОЙ отказанной резервации, не дожидаясь уже летящих. Как только прирост становится меньше стоимости одного шага волны, каждый следующий прогон встаёт МГНОВЕННО и не продвигает книгу ни на юнит — а прирост только УБЫВАЕТ, потому что `chaptersLeft` уменьшается с каждой дочитанной главой. ⚠ **Воспроизведено живьём дважды подряд, платным прогоном 04.09** (книга `bk_SS5VES2JELESJSTR`, 10 глав, после первой готовой главы `chaptersLeft = 9`, прирост $0.27): прогоны `run_CR2RN76NAKYKAJE5` и `run_VSXPMJE53KJQQAOS` — оба `paused/credit_exhausted` за 10–15 секунд, `committed` не сдвинулся ни на микро-доллар (278319 до и после), холд вернулся целиком. Движковая строка обоих: `book USD ceiling reached ($0.548319) (committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828) … reserve ceiling reached`. Это ПРОДУКТОВЫЙ СТОП, а не «шкала короче»: занижение ставки ×4.47 (`D39.179` п.1) до сих пор читалось как неудобство, и вот порог, за которым оно становится недостижимостью результата. ⚠ **Денег сам стоп НЕ жжёт** — отменённые вызовы этих двух прогонов не успели уйти (латентность 20 и 31 мс, `model_actual` пуст, 0 токенов); неучтённый расход — отдельная строка `PD-441`. ⚠ **Чинится СОГЛАСОВАННО, одной стороной нельзя:** поднять ставку — платформенная половина и она меняет всю шкалу покупки; не бросать летящие вызовы и/или считать резерв по фактической конкуренции — движковая. Проверка без Go: `sqlite3 -header -column "file:?mode=ro" "select trace_id, count(*), round(sum(cost_usd),6) from request_log group by trace_id;"` ⚠ **РАДИУС ПЕРЕ-СНЯТ 04.09 ЗАМЕРОМ ЗА $0 (заказ оркестратора №22): дефект НЕ про хвост длинной книги, а про ПОРОГ, ниже которого книга не переводится ВООБЩЕ.** Порог считается из измеренного, а не из оценки: движок в собственном отказе назвал резерв ОДНОГО редакторского вызова до шестого знака — `committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828, ch5/chunk0/edit` при потолке `$0.548319`. Отсюда волна из четырёх стоит **$0.277633 — наблюдённая величина: три реально стоявших резерва ($0.207805) плюс отказанный ($0.069828)**. ⚠ Не `4 × $0.069828 = $0.279312`: первая редакция ряда взяла именно это произведение, а оно СКОНСТРУИРОВАНО — вызовы волны не равны между собой, промпты чанков разной длины, и `3 × $0.069828 = $0.209484` расходится с наблюдёнными `$0.207805` на `$0.001679` (средний реальный резерв $0.069268 против отказанного $0.069828). Произведение оставлено рядом как ВЕРХНЯЯ оценка, и держать оба стоит: порог считается по обоим одинаково — `$0.277633 / $0.03 = 9.25` и `$0.279312 / $0.03 = 9.31`, — то есть **вывод не зависит от выбора числа**. ⚠ Счёт «три» выведен, а не напечатан: он следует из `waves.workers: 4` минус отказанный, и подтверждается отношением `$0.207805 / $0.069828 = 2.976`. Порог — `chaptersLeft × $0.03 ≥ стоимости волны`, то есть **книга проходит только с 10 непереведённых глав и больше**. Поправка числа — оркестратора №22, 04.09. Следствия: **последние ДЕВЯТЬ глав любой книги недостижимы**, и **книга из ≤9 глав не переводится никогда** — ни первой покупкой, ни повторными, потому что потолок КУМУЛЯТИВНЫЙ (`bookCap = committed + increment`, `internal/runs/spawn.go:212-214`) и запас каждого прогона равен ровно инкременту, сколько бы книга ни потратила раньше. ⚠ **Предъявлено живьём и бесплатно:** пятиглавая книга `bk_P5UCXKHDO4HFWSH3` заведена через настоящий интейк ($0 — `manifest` без ключей), и `GET /v0/books/{id}/run-options` вернул `max_chapters: 5`, то есть **максимум, который пользователь вообще может купить этой книге, — $0.15 против $0.279312 за волну**; для книги пака после первой дочитанной главы тот же вызов вернул `max_chapters: 9` ($0.27). Разрезка короткой книги ПОБАЙТНО та же (юниты по главам 2·1·1·2·1 против тех же 2·1·1·2·1 у первых пяти глав длинной), то есть короткая книга содержит ровно тот кусок `ch5/chunk0`, чей резерв измерен. ⚠ **ЧЕГО В ЗАМЕРЕ НЕТ, называю прямо:** сама короткая книга НЕ запускалась — её прогон способен потратить до $0.15 на черновую волну, а санкция владельца на платные прогоны закрылась вместе с паком; арифметика замкнута, живой прогон короткой книги остаётся неснятым. ⚠ **ЛЕЧЕНИЕ В ДЕРЕВЕ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ.** Константа `DefaultPerChapter` удалена целиком: цена берётся из проекции движка (`manifest --json`: `expected_usd` по главам, `step_max_usd`), а холд считается `k × ожидаемое(заказ) + step_max` — **аддитивно, не `max(...)`**, потому что последнему вызову заказа нужен запас под ЦЕЛУЮ резервацию сверх уже списанного. Пины: `pricing.TestTheLastTwoChaptersOfABookAreBuyable` (последние две главы и последняя глава покупаются), `pricing.TestTheHoldIsTheCushionedBillPlusOneWholeReservation`. ⚠ И вторая половина той же стены, найденная этим же паком: книжный `expected_usd` ВКЛЮЧАЕТ `book_once_usd` — плоские $2.00 на боевом `pipeline-c1` независимо от длины книги, — поэтому в основу холда он НЕ кладётся (иначе пятиглавая книга за $0.25 снова непокупаема); пин `pricing.TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase`. | fixed(628cc56) | платный сквозной прогон через API, пак «закрыть цикл» 04.09 (воспроизведено дважды) | | PD-441 | bug | **major, деньги** | движковая половина — `backend/internal/pipeline/stagerun.go`, ветвь с комментарием `No 2xx ever arrived: nothing was billed` (`releaseReservation` + строка `request_log` с нулевой ценой); платформенная — `internal/runs/reconcile.go` `settleOne`, который берёт цифру движка как истину | **ЛЕДЖЕР ДЕНЕГ — НИЖНЯЯ ГРАНИЦА, и потолок сам её создаёт: halt отменяет вызовы, которые УЖЕ УШЛИ к провайдеру, и их цена не записывается никуда.** Замерено 04.09 на `run_FS3O4UO5EDTKVF42`: волна пустила четыре редакторских вызова, один дошёл (6846+13729 токенов, **$0.063404**, 156444 мс) и своим коммитом сорвал книжный потолок, а три оставшихся были отменены В ТОТ ЖЕ МИГ, отработав **120756, 156469 и 156470 мс** — с `cost_usd 0`, нулевыми токенами и пустым `model_actual`. Вызов, проживший 2–2.6 минуты, до провайдера дошёл почти наверняка. **Порядок величин:** соседние ЗАВЕРШЁННЫЕ вызовы того же прогона стоили $0.017409 и $0.063404 ⇒ незаписанное — порядка **$0.05–0.19 на ОДНО срабатывание потолка** против **$0.278319** за всю книгу. То есть механизм, который защищает от перерасхода, сам создаёт неучтённый расход до двух третей стоимости книги за одно срабатывание. ⚠ **Дыра не в принципе, а в ПОКРЫТИИ, и это делает строку заказом, а не наблюдением.** Консервативная оценка в движке уже построена и ратифицирована (`stagerun.go`, «paid 2xx with zero usage; settling the reservation estimate to keep the ceiling honest», pack-13 point-9): проект уже принял правило «нельзя ослеплять потолок нулём там, где вызов был платным». Но условие требует `resp` — ОТВЕТА. Вызов, отменённый в полёте, ответа не получает, уходит другой веткой, и та честно пишет «nothing was billed» — что верно для транспортного отказа, не выпустившего запрос, и НЕВЕРНО для отмены на 156-й секунде. **Направление лечения (именно направление):** сеттлить оценку и при отмене вызова, который успел уйти, различая по ФАКТУ ухода; латентность — кандидат-различитель (20 мс против 156 000 мс разводит два случая без всякого рассуждения), но порог обязан быть обоснован замером, а не назначен. ⚠ Списание провайдером НЕ ДОКАЗАНО: биллинга провайдера у зоны нет, утверждается ровно наблюдаемое — вызовы шли минутами и их цены в леджере нет. ⚠ Правка `stagerun.go` — ЧУЖАЯ зона, отсюда только строка. Смежная строка единого бэклога — **78**. Воспроизведение: `sqlite3 -header -column "file:?mode=ro" "select id, ts, stage, role, latency_ms, cost_usd, err from request_log where err='context canceled' and latency_ms > 1000;"` | open | платный сквозной прогон через API, пак «закрыть цикл» 04.09 (усилено разбором оркестратора №22 по коду движка) | | PD-438 | bug | **major** | `internal/ingest/tail.go` `apply` (греп `ErrForeignStreamAhead`), `internal/ingest/tail.go` `tailFrom` (греп `mine := want`), `internal/runs/reconcile.go` `drainJournal` | **Владение потоком не переживало проход свипа, и чужие события ложились на оплаченную попытку.** `mine` — возвращаемое значение, а не колонка: следующий проход выводит его заново из `pos.LastSeq > 0`. Пока тейлер ПРОХОДИЛ мимо чужого handshake'а, двигая байтовый хинт, следующий проход читал чужую область с курсором, который говорит «это наше»: чужой `ceiling` ставил `paused` живому оплаченному прогону, `unit_done` писал главы, которых никто не покупал, а чужой `seq`, попавший на наш, давал `ErrPayloadConflict` и карантинил здоровую проекцию. ⚠ **Воспроизведено в двух вариантах, до и после правки P13** (копия дерева, `Tail` дважды — форма, в которой его зовёт `drainJournal`): на `HEAD 494c4ef` законный чужой hello травит проекцию (`pass 2 applied a FOREIGN event (seq [2])`), а чужой мажор карантинит; после пере-упорядочивания пункта 3 P13 ОБА травят — то есть заказанная правка расширила старый класс с законных чужих строк на любые. Конфликт payload воспроизводится одинаково на обеих версиях. ⚠ Лечение в дереве пака P13 (03.09): чтение ОСТАНАВЛИВАЕТСЯ на чужом handshake'е, если платформа именовала поток попытки и наши строки уже применены (`pos.LastSeq > 0`), и курсор на нём остаётся — владение выводится заново каждый проход, ценой одного пере-чтения строки. Прежняя семантика «пройти мимо» сохранена там, где она безопасна: пока `LastSeq == 0`, наш handshake ещё может лежать ниже по файлу (пин `TestAForeignStreamBeforeOursIsWalkedPastRatherThanStoppedOn`), и у безымянной легаси-попытки (`want == ""`) — пин `TestAnotherAttemptsStreamInTheSameJournalIsSkipped` зелёный. Пины: `TestAForeignStreamDoesNotOwnOurAttemptOnTheNextPass` (три формы чужого hello × три следующих строки). ⚠ **ДОФИКС 03.09: обоснование «вред ограничен по времени» ОПРОВЕРГНУТО приёмкой исполнением, и оно было моё.** Довод «чужой handshake ⇒ нашего процесса в книге уже нет» неверен: платформа отдаёт КАЖДОМУ спавну одной и той же попытки один и тот же id потока (`internal/runs/spawn.go`, греп `engineStreamID(l.RunID, l.AttemptNo)`), а движок, найдя, что этот id уже писал события книги, минтит СВЕЖИЙ и продолжает (`backend/internal/pipeline/events.go`, греп `fresh := obs.NewTraceID()`). Значит «чужой» hello может писать ЖИВОЙ процесс нашей же попытки, и прогон способен простоять припаркованным весь свой срок. Радиус сужен опровергателем приёмки: путь — конъюнкция двух признанных сбоев (претензия на юнит, отчитавшаяся ошибкой, плюс исчезновение юнита без exit-marker), а не рядовой рестарт. **Наблюдаемость дана в том же дофиксе:** тейлер сообщает о парковке отдельным сигналом (`internal/ingest/tail.go`, греп `ErrForeignStreamAhead`) вместо тихого `nil`; свип пишет WARN с прогоном, попыткой и смещением; и парковка теперь СТОИТ РЯДОМ с карантином в условии канала починки (`maybeResync`, греп `!l.Quarantined && !parked`), то есть у припаркованной попытки снова есть свежесть через `tmctl status`. Пины: `TestTheParkIsReportedSoTheCallerCanTellItFromBeingCaughtUp` (без DSN), `TestAParkedAttemptStillGetsTheRepairChannel` (DSN). ⚠ Чего по-прежнему НЕТ: строки в `runs`, гейджа и колонки — парковка живёт длину одного прохода и в БД не пишется; дать ей строку значит завести колонку, то есть миграцию, а она этим нарядом не заказана ⚠ **ОСТАТОК НАЗВАН 04.09 приёмкой:** лечение в дереве и заленджено (`6ae3e76`), но ПРИПАРКОВАННАЯ попытка не видна ни в `runs`, ни гейджем, ни колонкой — у этого класса нет ни одного из трёх сигналов, которые были у карантина, а `maybeResync` до дофикса такую попытку пропускал. Дофикс дал ей имя (`ErrForeignStreamAhead`), WARN с троттлингом и вход в ресинк; поверхностная видимость для ОПЕРАТОРА остаётся открытой и идёт райдером платформенного пака. ⚠ **ДИСПОЗИЦИЯ ПАКА «ЗАКРЫТЬ ЦИКЛ» (04.09): ОТЛОЖЕНО СОЗНАТЕЛЬНО, и вот довод, а не отсутствие времени.** Все три недостающих сигнала — строка в `runs`, гейдж, колонка — читают ХРАНИЛИЩЕ, а парковка в хранилище не пишется: она живёт длину одного прохода и выводится заново каждым свипом. Дать ей любой из трёх значит завести колонку на `run_attempts`, то есть миграцию и новое состояние, которое надо СНИМАТЬ, — а снимать его некому, кроме того же свипа, который его поставил. Это не райдер, а отдельная работа со своим дизайном («парковка как СОСТОЯНИЕ, а не как исход прохода»), и сделать её заодно с дверью выдачи значило бы решить её мимоходом. Что у класса ЕСТЬ сегодня: имя (`ErrForeignStreamAhead`), WARN с троттлингом и вход в канал починки — оператор, который смотрит в ЖУРНАЛ, парковку видит; невидима она тому, кто смотрит только в таблицу. Строка остаётся открытой ровно на этом. | open | самопроверка пака P13 (веер, линза шва; воспроизведено сессией на копии) | @@ -41,11 +41,11 @@ | PD-448 | bug | minor | `internal/runs/runs.go:170`=`ErrNotResumable` (отказ, который получает проигравший) и `internal/runs/control_test.go:846` `TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer` (пин гарантии) | **ДВОЙНОЙ КЛИК ПО «ПРОДОЛЖИТЬ»: проигравший гонку получает ОТКАЗ вместо прогона, когда два вызова не успевают пройти проверку состояния в одном окне.** Гарантия зоны сформулирована в шапке самого теста: «Both calls pass the state check together and race for attempt N+1 on the unique index; the loser must answer with the run the winner re-opened, not with a not-found». Замер 05.09: под нагрузкой машины (`load average 42–48` на 8 ядрах, тринадцать чужих `tmmutate`) вызовы СЕРИАЛИЗУЮТСЯ — победитель успевает перевести прогон в `translating` до того, как проигравший дойдёт до своей проверки, — и проигравший получает `runs: the run cannot be continued: it is translating` (`control_test.go:863`). ⚠ **Пользовательский смысл: человек, дважды нажавший «продолжить», видит ошибку, хотя прогон в этот момент СТАРТОВАЛ.** ⚠ **Деньги не затронуты:** отказ приходит ДО взятия холда, так что второй холд не берётся — та половина гарантии («ровно один холд») держится и на отказном пути. ⚠ **Это НЕ `PD-420`:** тот ряд про другой тест (`internal/pgstore/runs_test.go` `TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError`); проверено грепом, имени этого теста в реестре не было ни в одном ряду. **Воспроизведение:** полный пакет `go test ./internal/runs/` при загруженной машине — упал 2 раза из 2; ИЗОЛИРОВАННО (`-run TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer -count=5`) — 5/5 `ok` за 10 с. То есть окно существует и открывается голоданием по процессору, а не правкой кода. **Направление лечения (не решение):** проигравшему различать «прогон уже идёт, потому что его только что открыл параллельный резюм» от «прогон идёт сам по себе» — первое обязано вернуть прогон, второе законно отказывает. | open | замер зоны 05.09 при ревизии документации: батарея краснела дважды, разобрано до конкретного теста | | PD-443 | bug | minor | `internal/exports/exports.go` `Build`/`Sweep`, `internal/pgstore/queries/exports.sql`, миграция `00032_exports.sql` (`on delete cascade`) | **Артефакт экспорта может пережить СТРОКУ, которая одна умеет его найти, и тогда его не удалит никто.** Путь артефакта детерминирован (`//.`), но каталог со строками не сверяет ничто: GC удаляет только те пути, которые НЕСУТ строки. Три живых пути, найдены адверсариальным проходом по готовой работе: **(а)** демон убит между `rename` движка и `FinishExport` — путь в строку так и не попал, свип пере-водит её в `failed`, файл остаётся навсегда (не экзотика: `KillMode=mixed` в юните и дренаж очереди на 10 с делают это штатным исходом рестарта под нагрузкой); **(б)** книга удалена — `on delete cascade` сносит строки, файлы и каталог книги под `Dir` не трогает никто (сегодня у `DeleteBook` вызывающих нет, но внешний ключ уже стоит и ловушка взведена); **(в)** артефакт, чей unlink не прошёл, — ⚠ **ЭТОТ ПУТЬ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ**: `path` теперь переживает смену состояния и снимается только после реального удаления (`UnlinkedExports`/`ForgetExportPath`), то есть unlink стал ПОВТОРЯЕМЫМ; пин `TestAFileTheSweepCouldNotRemoveKeepsItsRowPointingAtIt`. Четвёртый путь — временные файлы движка (`.<имя>.tmp-*`) после SIGKILL — тоже закрыт (`exports.Service.discard`), потому что `tmctl build` контекста не читает и его всегда добивает SIGKILL. ⚠ **Лечение (а) и (б) — одно и то же и это НЕ райдер:** сверка каталога со строками, то есть реконсилятор файловой системы со своим дизайном (что считать сиротой, как отличить чужой файл от своего, что делать с каталогом книги, которой нет). Делать его заодно с дверью значило бы решить мимоходом. **Радиус сегодня ограничен:** TTL двери — сутки по умолчанию, файлы лежат под одним каталогом деплоя, и оператор видит рост диска раньше, чем что-либо ломается. ⚠ **ДВА СИБЛИНГА, НАЗВАННЫЕ ПРИЁМКОЙ 04.09 — один закрыт, один остаётся здесь.** (I) STAGING-файл движка (`.<имя>.tmp-*`) переживает смерть демона ПОСРЕДИ сборки: `discard` живёт в том же процессе, поэтому при `kill -9` убирать его некому, а следующая сборка того же экспорта не случится — строка уже не `pending`. Это тот же класс, что (а), и лечится тем же реконсилятором каталога. (II) ⚠ **ВТОРОЙ ВОРКЕР НА ОДНОЙ СБОРКЕ — ЗАКРЫТО ТЕМ ЖЕ ДОФИКСОМ, а не только названо:** оба писали бы по ОДНОМУ пути (он выводится из id экспорта), и уборка проигравшего снесла бы файл, только что опубликованный выигравшим. Клейм сделан ИСКЛЮЧАЮЩИМ — `where … and state = 'pending' and started_at is null`, — поэтому второй воркер получает `ErrExportSettled` и до сборки не доходит. Сегодня недостижимо (River ведёт одно задание рода за раз), но именно это превращает вторую реплику из потерянного артефакта в дубль сборки. Пин — в `TestAQueuedBuildAndAClaimedOneAreJudgedOnDifferentClocks`. Воспроизведение (а): `kill -9` демона между строкой лога `export built` и следующей записью в БД. | open | адверсариальный проход пака «закрыть цикл» по своей же работе, 04.09 | | PD-444 | bug | minor | `internal/httpapi/bank.go:142`=`Invalid(w, r)` (ветка строгого разбора) против `internal/httpapi/bank.go:146`=`Invalid(w, r, items...)` (ветка валидации); механизм — `internal/httpapi/problem.go:173`=`Errors []Item` | **Дверь правок банка отвечает `400 invalid_request` БЕЗ единого указателя на то, ЧТО не так.** Замерено живым прогоном 04.09: две попытки с чужими именами членов (`decisions` вместо `corrections`) вернули голое тело, оба ответа `Content-Length: 119` — ни `errors[]`, ни имени члена, ни позиции. ⚠ **Сам отказ ВЕРЕН и оспаривать его нечего:** схема канона объявляет `additionalProperties: false` на обоих уровнях, и незнакомый член — это клиент, уверенный, что он что-то задал; молчаливая версия этого — правка, применённая наполовину. Дефект в другом: канон нигде не требует МОЛЧАТЬ о том, какой член виноват, а механизм у зоны уже построен и на соседней ветке применяется — `validateCorrections` возвращает `[]Item` и отдаёт его в `Invalid`. Ветка строгого разбора теряет даже то, что у неё в руках: `encoding/json` называет поле в тексте ошибки (`json: unknown field "decisions"`), а обработчик его не только не отдаёт, но и не логирует — соседняя ветка «тело не доехало» логирует. Цена: клиент двери — редактор пользователя, и `400` без адреса отлаживается перебором. ⚠ Лечение НЕ трогает канон: `errors[]` в нём уже объявлен, довести до ответа нужно ветку разбора. | open | живой платный прогон пака «закрыть цикл», наблюдение H12, 04.09 | -| PD-446 | doc | minor | канон `docs/architecture/14-api-contract/` §`PausedReason`; носители в зоне — `internal/pgstore/books.go:1183`=`PausedCreditExhausted = ingest.PausedCreditExhausted` и `internal/httpapi/v0.go:994`=`ContractHaltReason` | **`paused_reason: credit_exhausted` при 34% НЕТРОНУТОГО баланса — слово называет кредит там, где исчерпан потолок ПРОГОНА.** Замерено 04.09: в один и тот же момент прогон стоял с `paused_reason: credit_exhausted`, а `GET /v0/usage` отвечал `state: ok, halt_reason: null` при остатке `0.102494` из `0.30`. ⚠ **Оба ответа ВЕРНЫ, и счётная половина этого дефекта уже вылечена — пере-открывать её нельзя:** halt читается с АККАУНТА, а не с паузы последнего прогона (`ReadUsage`, разбор в `books.go` над телом), и именно поэтому `halt_reason` здесь честно пуст. Остаётся ОДНО: у `PausedReason` и `AccountHaltReason` разные словари с одним и тем же единственным значением, и это слово — `credit_exhausted`. Пользователь, читающий «кредит исчерпан» рядом с «остаток 34%», получает противоречие, которого в фактах нет. ⚠ Зона правку канона своей рукой не делает: кандидат в состав минора — значение `PausedReason` обязано называть ПРОГОН (`run_ceiling_reached`), словарь аккаунта не трогается. Пока минор не принят, ряд держит вопрос открытым. ⚠ **ЗАКРЫТО ДЕРЕВОМ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ.** У паузы прогона появилось СВОЁ слово: `run_limit_reached` (`ingest.PausedRunLimitReached`), и `CeilingPause(ScopeBook)` отдаёт теперь его, а не `credit_exhausted`. Аккаунтное слово осталось за аккаунтом (`ReadUsage`, `balance <= 0`). ⚠ Разделены и ДВА вердикта реконсилятора, которые делили одно слово: `ceilingSpent` → `run_limit_reached`, `creditUnavailable` → `credit_exhausted` (`internal/runs/reconcile.go`, греп `if v == creditUnavailable`) — лечения противоположны (купить снова против пополнить), и пользователь, которому сказали не то, идёт делать не то. Миграция 00033 пере-называет и старые строки. Пины: `pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets`, `runs.TestAnInterruptedRunWithNothingLeftIsPausedAndStaysPausedThroughAResume`, `runs.TestAnInterruptedRunThatTheBalanceCannotCarryIsPaused`. | open | живой платный прогон пака «закрыть цикл», наблюдение H14, 04.09 | +| PD-446 | doc | minor | канон `docs/architecture/14-api-contract/` §`PausedReason`; носители в зоне — `internal/pgstore/books.go:1183`=`PausedCreditExhausted = ingest.PausedCreditExhausted` и `internal/httpapi/v0.go:994`=`ContractHaltReason` | **`paused_reason: credit_exhausted` при 34% НЕТРОНУТОГО баланса — слово называет кредит там, где исчерпан потолок ПРОГОНА.** Замерено 04.09: в один и тот же момент прогон стоял с `paused_reason: credit_exhausted`, а `GET /v0/usage` отвечал `state: ok, halt_reason: null` при остатке `0.102494` из `0.30`. ⚠ **Оба ответа ВЕРНЫ, и счётная половина этого дефекта уже вылечена — пере-открывать её нельзя:** halt читается с АККАУНТА, а не с паузы последнего прогона (`ReadUsage`, разбор в `books.go` над телом), и именно поэтому `halt_reason` здесь честно пуст. Остаётся ОДНО: у `PausedReason` и `AccountHaltReason` разные словари с одним и тем же единственным значением, и это слово — `credit_exhausted`. Пользователь, читающий «кредит исчерпан» рядом с «остаток 34%», получает противоречие, которого в фактах нет. ⚠ Зона правку канона своей рукой не делает: кандидат в состав минора — значение `PausedReason` обязано называть ПРОГОН (`run_ceiling_reached`), словарь аккаунта не трогается. Пока минор не принят, ряд держит вопрос открытым. ⚠ **ЗАКРЫТО ДЕРЕВОМ ПАКА «ФОРМА ЗАКАЗА» (05.09), статус флипает ЛЕНДИНГ.** У паузы прогона появилось СВОЁ слово: `run_limit_reached` (`ingest.PausedRunLimitReached`), и `CeilingPause(ScopeBook)` отдаёт теперь его, а не `credit_exhausted`. Аккаунтное слово осталось за аккаунтом (`ReadUsage`, `balance <= 0`). ⚠ Разделены и ДВА вердикта реконсилятора, которые делили одно слово: `ceilingSpent` → `run_limit_reached`, `creditUnavailable` → `credit_exhausted` (`internal/runs/reconcile.go`, греп `if v == creditUnavailable`) — лечения противоположны (купить снова против пополнить), и пользователь, которому сказали не то, идёт делать не то. Миграция 00033 пере-называет и старые строки. Пины: `pgstore.TestTheCeilingScopeDecidesWhichPauseTheRunGets`, `runs.TestAnInterruptedRunWithNothingLeftIsPausedAndStaysPausedThroughAResume`, `runs.TestAnInterruptedRunThatTheBalanceCannotCarryIsPaused`. | fixed(628cc56) | живой платный прогон пака «закрыть цикл», наблюдение H14, 04.09 | | PD-435 | bug | minor | `internal/pgstore/readmodel.go` `runDone`/`runTotal`/`runStage` (читают монотонный `editWave`), `internal/pgstore/runs.go` `StartRun` (снятие базлайнов) | **Полоса прогона на деплое, где редактора УБРАЛИ, не доходит до единицы: знаменатель тарифицирует edit-волну, которой не будет.** Вторая половина `PD-403`; первая (счёт книги) закрыта паком P12 эпохой формы конвейера, эта — нет, и попытка закрыть её тем же носителем была ОТКАЧЕНА тем же паком: полоса, переведённая на ПРИСВАИВАЕМУЮ эпоху, перестаёт быть монотонной — замерено, один прогон читал 4/4, затем 2/2 на своих же двух попытках при неизменной `structure_version`, что канон запрещает прямо (строка 200, «одна монотонная дробь на всю работу прогона»). ⚠ **Почему это не однострочник:** правильный носитель — форма, под которой работает ЭТОТ прогон, записанная на самом прогоне; а в `StartRun` она ещё НЕ ИЗВЕСТНА — движок объявляет её первым progress-событием прогона, и попытка снять её раньше это ровно `PD-401`, закрытый пином. Значит запись должна происходить на ПЕРВОМ объявлении прогона, и тогда нужен разбор, что делать со второй попыткой того же прогона, объявившей другую форму (сегодняшний ответ — ничего, потому что флаг монотонен). Лечение: колонка формы на `runs`, заполняемая первым объявлением, плюс решение о смене формы между попытками одного прогона. Пин обратной стороны уже стоит и покраснеет, если кто-то снова переведёт полосу на эпоху: `pgstore.TestTheRunsBarIsMonotoneAcrossAShapeBoundaryItSpans`. ⚠ **Почему `minor`, когда родительская `PD-403` была `major` — вопрос приёмки, отвечаю доводом, а не весом.** У `PD-403` мажорной её делали ДЕНЬГИ: шкала покупки продавала уже переведённые главы повторно. Эта половина ЗАКРЫТА — `ChaptersLeft` считается через эпоху, дважды не продаётся ничего, и `PD-410` (тоже про деньги, ручка «купить N глав» ограничивает не главы, а доллары) остаётся `major` именно поэтому. Здесь не двигается ни один микро-доллар: прогон делает всю купленную работу, закрывается `ready`, леджер сходится, следующая покупка предлагает правильный остаток. Врёт ДРОБЬ и подпись (`editing` на деплое без редактора) — контрактно видимо и потому не `info`, но это отчёт о работе, а не её оплата. Плюс достижимость: нужна смена ФОРМЫ деплоя под книгой, которая уже прошла редактирующий пайплайн, — не обычный путь, в отличие от `PD-410`, который кусает на КАЖДОЙ покупке. Если приёмка сочтёт довод слабым — поднять до `major` дешевле, чем спорить: работа от веса не меняется | open | адверсариальный проход пака P12 (30–31.08), замерено на живом Postgres через продовые пути записи | | PD-433 | hardening | minor | `internal/pgstore/identity.go:106`=`if !errors.Is(err, errIdentityRace)` (ретрай), `:118`=`var errIdentityRace`, ветвь `on conflict … do nothing` в `upsertIdentityOnce` | **Ветвь, обслуживающая ЛЕГИТИМНУЮ гонку двух первых логинов одной новой личности, не исполняется НИ ОДНИМ тестом.** Форма та же, что у `PD-380` и `PD-86`: единственная точка принуждения объявленного свойства, мутация переживает полную батарею, транзитивной страховки нет. Механика: под READ COMMITTED `for update` в `LockIdentity` не блокирует строку, которой ещё нет, поэтому оба гонщика проходят; проигравший видит `created == 0`, получает `errIdentityRace` и ретраит — и именно ретрай спасает его от 500 на пути логина. ⛔ **Снос ветви — только с доказательством недостижимости:** она обслуживает легитимную гонку, и её снос = 500 на легитимном пути (класс `PD-369`, который этот же пак и закрывал). Диспозиция: запинить конкурентным тестом ЛИБО доказать недостижимость. Форма теста, если пинить: в пакете `pgstore`, ~40 раундов, в каждом СВЕЖАЯ личность и 4 горутины на `UpsertIdentity`; утверждать, что все четыре вернули `nil`, что userID у всех ОДИН, что `identities` держит одну строку, что грант посева записан ОДИН раз и что число строк в `users` равно числу раундов — последнее ловит настоящую утечку, осиротевшего пользователя от проигравшего, который успел `CreateUser` до проигрыша. Проверять посадкой (снять арм `errIdentityRace` — тест обязан покраснеть), потому что вероятностный тест без посадки не доказывает, что он вообще кусает | open | пак P12 (30.08), греп непокрытых ветвей по своим путям | | PD-434 | bug | minor | `internal/pgstore/sink.go` `ApplyStatus`, `internal/runs/reconcile.go` `maybeResync` | **Канал ПОЧИНКИ прогресса не чинит прогресс: ресинк не материализует ничего, что читает экран.** `maybeResync` существует ровно для прогона, чей поток в карантине или чей курсор не двигался — «это теперь единственный источник», говорит его собственный комментарий. Но `ApplyStatus` писал пер-волновые цифры отчёта ТОЛЬКО в `runs.draft_done/draft_total/edit_done/edit_total`, у которых не было ни одного читателя (`PD-411`), и не трогает ни `unit_resolutions`, ни `chapters` — а вся полоса выведена из `chapters`. Значит полоса такого прогона стоит всю его жизнь, как бы исправно ни отвечал `tmctl status`. ⚠ **Строка заведена паком P12 при сносе `PD-411`, и это главное в ней:** мёртвые колонки ПРЯТАЛИ гап — код выглядел так, будто канал починки чинит, и комментарий `maybeResync` прямо утверждал «ApplyStatus now materializes the same four counters as the stream». Обе лгущие фразы исправлены, сам гап НЕ лечится сносом и вынесен сюда, чтобы снос не выдал себя за лечение. Лечение: либо ресинк складывает отчёт в `chapters` (и тогда нужен разбор, как он не спорит с потоком, который те же строки пишет из `unit_resolutions`), либо зона признаёт, что карантинный прогон полосы не показывает, и говорит об этом на поверхности. Пин формы «ресинк НЕ двигает счётчики глав» уже стоит — `pgstore.TestTheResyncRecordsFreshnessAndShapeAndNoProgress` — и он покраснеет, если канал научится материализовать: это указатель на строку, а не её лечение | open | пак P12 (30.08), вскрыто сносом `PD-411` | -| PD-375 | bug | minor | `internal/runs/runs.go`, греп `quote.Hold > acct.Balance`, `internal/httpapi/v0_test.go:359`, `internal/runs/control_test.go` | **Верхнюю половину проверки `ceiling_chapters` не исполняет НИ ОДИН тест: снятие второго операнда `in.CeilingChapters > bounds.Max` проходит ВСЮ батарею.** Собственный доккомментарий называет проверку несущей — «число, решающее СКОЛЬКО ДЕНЕГ резервируется, не может быть выбрано вызывающим односторонне», — и канон требует того же от сервера (`max_chapters` уже прижат к остатку, «a client MUST NOT clamp it again»). Единственный тест, называющий `ErrCeilingOutOfBounds`, это таблица соответствия ошибки коду 409 в `httpapi/v0_test.go`, которая `Start` не зовёт, а самая тесная фикстура просит 10 глав у книги, где их 100. Код сегодня ВЕРЕН; дефект в том, что править эту строку можно безнаказанно. Замерено на живом стенде: чистая сборка отвечает `409 ceiling_unavailable/bounds_moved`, сборка с мутацией отдаёт 202 и открывает резервацию `24000000` микро на книге из ТРЁХ глав, а движку уходит `--ceiling-usd 24.200000`. Вес: рефутер сузил major → minor, потому что код верен и ни один сегодняшний клиент до вреда не доходит. Воспроизведение: `docs/p8-review/axis1-money/a1-mut5-ceiling-bound.sh`, готовый пин `docs/p8-review/axis1-money/r1_ceiling_bound_test.go.txt` ⚠ **НОСИТЕЛЬ УМЕР ВМЕСТЕ С ФОРМОЙ ЗАКАЗА (05.09), строка пере-написана ПО СУЩЕСТВУ, а не пере-нацелена.** Проверки `in.CeilingChapters < bounds.Min || > bounds.Max` больше НЕТ: шкала в главах и `pricing.Bounds` удалены вместе с per-chapter константой (строка бэклога 280), заказ судится ценой — `Quote` клампит объём остатком книги, а `quote.Hold > acct.Balance` отбивает то, что баланс не несёт. **Класс дефекта — «несущую половину не исполняет ни один тест» — закрыт НОВЫМ пином, а не исчезновением кода:** `TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold` держит обе половины (неподъёмный заказ отбит ДО холда и деньги не двинулись; заказ больше книги = вся книга, а не бОльшая резервация). Проверено посадкой: снятие отказа краснит этот тест ИМЕНЕМ. | open | ревью-пак P8-REVIEW, ось 1 (посадка мутации + живая проба, подтверждено рефутером) | +| PD-375 | bug | minor | `internal/runs/runs.go`, греп `quote.Hold > acct.Balance`, `internal/httpapi/v0_test.go:359`, `internal/runs/control_test.go` | **Верхнюю половину проверки `ceiling_chapters` не исполняет НИ ОДИН тест: снятие второго операнда `in.CeilingChapters > bounds.Max` проходит ВСЮ батарею.** Собственный доккомментарий называет проверку несущей — «число, решающее СКОЛЬКО ДЕНЕГ резервируется, не может быть выбрано вызывающим односторонне», — и канон требует того же от сервера (`max_chapters` уже прижат к остатку, «a client MUST NOT clamp it again»). Единственный тест, называющий `ErrCeilingOutOfBounds`, это таблица соответствия ошибки коду 409 в `httpapi/v0_test.go`, которая `Start` не зовёт, а самая тесная фикстура просит 10 глав у книги, где их 100. Код сегодня ВЕРЕН; дефект в том, что править эту строку можно безнаказанно. Замерено на живом стенде: чистая сборка отвечает `409 ceiling_unavailable/bounds_moved`, сборка с мутацией отдаёт 202 и открывает резервацию `24000000` микро на книге из ТРЁХ глав, а движку уходит `--ceiling-usd 24.200000`. Вес: рефутер сузил major → minor, потому что код верен и ни один сегодняшний клиент до вреда не доходит. Воспроизведение: `docs/p8-review/axis1-money/a1-mut5-ceiling-bound.sh`, готовый пин `docs/p8-review/axis1-money/r1_ceiling_bound_test.go.txt` ⚠ **НОСИТЕЛЬ УМЕР ВМЕСТЕ С ФОРМОЙ ЗАКАЗА (05.09), строка пере-написана ПО СУЩЕСТВУ, а не пере-нацелена.** Проверки `in.CeilingChapters < bounds.Min || > bounds.Max` больше НЕТ: шкала в главах и `pricing.Bounds` удалены вместе с per-chapter константой (строка бэклога 280), заказ судится ценой — `Quote` клампит объём остатком книги, а `quote.Hold > acct.Balance` отбивает то, что баланс не несёт. **Класс дефекта — «несущую половину не исполняет ни один тест» — закрыт НОВЫМ пином, а не исчезновением кода:** `TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold` держит обе половины (неподъёмный заказ отбит ДО холда и деньги не двинулись; заказ больше книги = вся книга, а не бОльшая резервация). Проверено посадкой: снятие отказа краснит этот тест ИМЕНЕМ. | fixed(628cc56) | ревью-пак P8-REVIEW, ось 1 (посадка мутации + живая проба, подтверждено рефутером) | | PD-377 | doc | minor | `internal/pgstore/credits.go:162`=`The ceiling to hand the engine is the amount held`, `internal/runs/spawn.go` `bookCap`, `cmd/tmplatformctl/main.go` `balance` | **Доккомментарий `Hold` на входе в денежный путь неверен ОБЕИМИ половинами с миграции 00011.** Он обещает «the ceiling to hand the engine is the amount held; read it back with OpenReservations». Движку передаётся не сумма холда, а `run_attempts.ceiling_arg_micro_usd` = committed книги плюс прирост (`spawn.go` `bookCap`), и сама 00011 говорит это прямым текстом («It is NOT ceiling_micro_usd»); начиная со ВТОРОГО прогона книги числа расходятся тем сильнее, чем дороже книга — на стенде до 17 раз (`60000` холда против `1060000` в аргументе). Вторая половина не работает даже механически: `Reservation.Ceiling` — поле, которое только сканируется и не читается никем, единственный потребитель `OpenReservations` печатает `Amount`/`BookID`/`OpenedAt`/`EngineRunID`. ⚠ Рефутер снял два из трёх исходных якорей: доккомментарии в применённых миграциях `00007`/`00009` зона не правит после лендинга (у всех 25 файлов миграций ровно по одному коммиту), а исправление уже лежит в следующем файле того же каталога. Остаётся Go-доккомментарий. Воспроизведение: `docs/p8-review/axis1-money/a1-ceiling-column-doc.sql` | open | ревью-пак P8-REVIEW, ось 1 (живой стенд, сужено рефутером до одного якоря) | | PD-386 | bug | minor | `cmd/tmplatformd/runner.go:474`=`out := []sweepPass{{"runs", sweepBudget, s.runs.Sweep}}` и четыре следующих прохода того же `one()`, `cmd/tmplatformd/runner_test.go` | **Такт свипа последователен, и бюджеты пяти проходов СКЛАДЫВАЮТСЯ: сумма объявленных — 37 минут при `TM_PLATFORM_SWEEP_EVERY` 15 секунд.** Пак P8-FIX дал каждому проходу свой бюджет и закрыл «один проход съедает дедлайн другого», но проходы по-прежнему идут подряд в одной горутине: runs 2м, readmodel 10м, intake 21м, idempotency 2м, observe 2м. Ничто эту сумму не ограничивает и ничто её не пинит — у функции `sweep` нет ни одного теста (в пакете два теста, оба про другое), и посадка «фаза runs уходит из головы такта в хвост» пережила ПОЛНУЮ батарею. Замерено пробой на реальной функции: за 4 секунды при такте 100 мс фаза прогонов отработала 40 раз сама по себе и 9 раз позади прохода материализации. Следствия по оси: калибровки самого лечения заданы в проходах и минутах и молча растягиваются (`StalledAfter` 5 «неудач подряд» и бэкофф, про который комментарий обещает «about a quarter of an hour», превращаются в часы); обещание `Stop` «the reconciler re-issues the stop on its next pass» задерживается на ту же величину; телеметрия стоит ПОСЛЕДНЕЙ. ⚠ Рефутер сузил: 37 минут — сумма ОБЪЯВЛЕННЫХ бюджетов, а не достижимая длительность (idempotency это один индексированный DELETE, а проход интейка сам себя режет). Воспроизведение: `docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt` | open | ревью-пак P8-REVIEW, ось 3 (проба на реальной функции + посадка мутации, сужено рефутером) | | PD-387 | doc | minor | `deploy/README.md:592`=`сколько всего может занять один проход свипа`, `cmd/tmplatformd/runner.go` `one`, `internal/config/config.go` `SweepBudget` | **Рантбук называет `TM_PLATFORM_SWEEP_BUDGET` ручкой «одного прохода свипа» и не говорит, КАКОГО из пяти, — а два бюджета из пяти оператору недоступны вовсе.** Один такт прогоняет ПЯТЬ проходов подряд, и только три берут `sweepBudget`; проход материализации берёт `refreshSweepBudget` 10 минут, проход интейка — `intakeSweepBudget` = `jobs.JobTimeout` + `readmodel.MaterializeBudget` + минута = 21 минута, и обе константы оператору недоступны вовсе. Собственный доккомментарий кода при этом ТОЧЕН («SweepBudget is what ONE pass of the RUN sweep may take») — расходится именно операторская проза, и расходится в разделе «Застрявшая работа: что оператор делает, когда свип не справляется», то есть там, где по ней и будут действовать. Родня `PD-368`: та про то, что поднимать надо пару, эта про то, что ручка не покрывает такт ⚠ Арифметика 37 минут пере-проверена и держится: 3×2 + 10 + 21. Воспроизведение — `docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt` (последовательность тактов) плюс `sed -n '264,272p' deploy/README.md` | open | ревью-пак P8-REVIEW, ось 3 (находка рефутера) | @@ -137,7 +137,7 @@ | PD-408 | doc | info | `internal/runs/bank.go` (бюджет двери = `s.runBudget()`), `internal/runner/bankapply.go` (`errOut` без лимита; `Stderr: firstLine`) | **Две операционные оговорки двери правок, названные воркфлоу-ревью; обе — цена конфигурации, не дефект пути.** (1) Бюджет двери — та же ручка `TM_PLATFORM_RUN_BUDGET`, что у прохода свипа; движок выбирал потолок 5000 решений против ЖЁСТКИХ 60 с («пять раз внутри бюджета»), и оператор, понизивший ручку (к чему соседние комментарии подталкивают), делает легальный документ-максимум навсегда неприменимым — вечный `503` вместо «разбей документ»; связка ручки и капа нигде не названа. (2) stderr глагола читается в НЕограниченный `bytes.Buffer`, хотя потребляется только первая строка, — не-тот бинарь по сконфигурированному пути (полудеплой, обёртка) может раздуть демона до OOM за 60-секундный бюджет; stdout той же команды капнут 64 МиБ | open | воркфлоу-ревью P9 28.08 (линзы door:lock-lifecycle · door:crash-windows), диспозиция оркестратора 28.08: строкой | | PD-428 | doc | info, деньги | `internal/pricing` (`TM_PLATFORM_USD_PER_CHAPTER`, `Pricing.Ceiling`) | **Цена продажи не знает о накладных, которые масштабируются КНИГОЙ, а не грантом.** Замер движкового охотника (лендинг `6ec9f8a`): терминолог переигрывается на КАЖДОЙ покупке ЦЕЛИКОМ по книге — три покупки по одному юниту дали три полнокнижных консолидации по $0.005460 каждая, при том что сам юнит дешевле. То есть книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. ⚠ **Сегодня это НЕ дефект платформы и заведено только как калибровка:** продажа идёт ГЛАВАМИ (`Ceiling(chapters)`), а не юнитами, так что нарезки, при которой накладные обгоняют полезную работу, в продукте нет. Строка существует, чтобы факт не потерялся к моменту, когда мелкая нарезка появится: любая будущая единица продажи мельче главы обязана нести в цене эту книжную составляющую, иначе COGS растёт быстрее выручки на самых дешёвых покупках. Носитель — константа цены, а не код движка ⚠⚠ **НОСИТЕЛЬ УМЕР И ПОСЫЛКА ПЕРЕВЕРНУЛАСЬ — паком «форма заказа» 05.09, строка пере-написана ПО СУЩЕСТВУ.** Названные тут `TM_PLATFORM_USD_PER_CHAPTER` и `Pricing.Ceiling(глав)` удалены вместе со ставкой (строка бэклога 280). ⚠ И оговорка ряда «сегодня это НЕ дефект платформы: продажа идёт ГЛАВАМИ, так что нарезки, при которой накладные обгоняют полезную работу, в продукте нет» — **больше не верна**: этот же пак ввёл заказ В ЗНАКАХ, который разрешается в ПРЕФИКС ЮНИТОВ, то есть единицу МЕЛЬЧЕ главы, и ровно её ряд и ждал. **Что с этим сделано и чего НЕ сделано, раздельно.** Книжная составляющая ТЕПЕРЬ ВИДНА и названа: движок публикует её отдельным числом `book_once_usd` (плоские $2.00 на боевом `pipeline-c1`), платформа читает его и НЕ кладёт в основу холда — иначе короткая книга непокупаема, — а кладёт СВЕРХ, когда баланс несёт, и говорит покупателю `term_consistency_funded: false`, когда не несёт (`pricing.Model.Hold`, пин `TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase`). **НЕ сделано главное, ради чего ряд заведён:** цена мелкой покупки по-прежнему не несёт книжной составляющей ПРОПОРЦИОНАЛЬНО — заказ в один юнит и заказ во всю книгу видят один и тот же бонд, поэтому COGS на самых дешёвых покупках растёт быстрее выручки ровно так, как ряд и предупреждал. Ряд остаётся `open` и с этого дня ПРЕДМЕТЕН, а не гипотетичен. Носитель — `pricing.Model.Hold` и `ingest.BookPrice.BookOnceUSD`. | open | движковый пак «деньги» (охотник), передано оркестратором №19 сессии P11 | | PD-421 | hardening | info | `internal/pgstore/sessions.go` `StillLive` и `SweepSessions`, `docs/STACK_DECISIONS.md` §13 | **Открытый поток теряет свою сессию по ПОДМЕТАНИЮ строки, а не по клаузе бездействия, — и это остаток закрытия `PD-379`, названный прямо.** Проверка живости потока намеренно НЕ содержит клаузы `idle_expires_at`: окно бездействия скользит на ЗАПРОСЕ, а поток — один запрос на всю жизнь, поэтому гашение по idle рвало бы связь активному читателю. Но `SweepSessions` раз в час УДАЛЯЕТ строки и по бездействию тоже, а «строки нет» ОБЯЗАНО значить «мертва» — иначе отозванная сессия держала бы поток до свипа, то есть дыра ровно в час. Следствие: сессия, протухшая по бездействию и подметённая, теряет поток с опозданием до часа. Это не idle гасит поток, а отсутствие строки; к тому моменту любой другой запрос того же вызывающего — 401. Лечение, если сочтётся недопустимым, — скольжение окна бездействия ИЗ потока, но это правка ПОЛИТИКИ §13: открытая вкладка держала бы сессию до абсолютного потолка, а это слово владельца | open | пак P11 (назван при закрытии `PD-379`) | -| PD-422 | bug | info | `internal/runs/runs.go:408`=`resnapshot := book.BankMoved || book.HasPriorRun`, `internal/pgstore/books.go:1035`=`HasPriorRun bool` | **`--resnapshot` платформа передаёт УСЛОВНО, а условие ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит.** Флаг выводится под `if book.BankMoved`, а единственный писатель `bank_moved_at` — дверь правок банка. На книге, которая МАЙНИТ банк, вторая покупка без правок банка идёт без флага, и движковый джоб-гард останавливает прогон (`exit 1` ⇒ `failed` на стороне платформы): авто-банк растёт от покупки к покупке, edit-снапшот съезжает, а гард банк-онли-движение от смены конфига не отличает. ⚠ Сегодня БЕСПРЕДМЕТНО: проводка `tmctl translate --max-units` на платформе гейчена оркестратором до лечения, а без неё вторая покупка этой формы не возникает. Строка заведена, чтобы условность не всплыла сюрпризом при снятии гейта. Найдено бэкенд-сессией `textmachine-e4` (пак «деньги»), проверено чтением платформенной стороны сессией P11 ⚠ **БЕСПРЕДМЕТНОСТЬ КОНЧИЛАСЬ И ДЕФЕКТ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ (05.09), статус флипает ЛЕНДИНГ.** Гейт на проводку `--max-units` снят строкой 280, флаг едет в argv — значит условность `--resnapshot` перестала быть теоретической ровно в тот момент. Условие расширено: `resnapshot := book.BankMoved || book.HasPriorRun` (`internal/runs/runs.go`, греп `book.HasPriorRun`). Довод, почему флаг на КАЖДОМ продолжении безопасен: гард срабатывает ПО ДЖОБУ, то есть только на главах, которых прогон касается, а объёмный потолок допускает НОВУЮ книгу прежде пере-делки (`backend/internal/pipeline/volume.go`, проход `unitFresh` затем `unitRework`) — продолжение тратит грант на недоставленные главы. Согласие при этом фондированное: собственный холд прогона, никогда бланкетная форма. Пин: `runs.TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection` (первая покупка флага НЕ несёт, вторая несёт, правки банка не было). | open | бэкенд-пак «деньги» + пак P11 (сверка шва) | +| PD-422 | bug | info | `internal/runs/runs.go:408`=`resnapshot := book.BankMoved || book.HasPriorRun`, `internal/pgstore/books.go:1035`=`HasPriorRun bool` | **`--resnapshot` платформа передаёт УСЛОВНО, а условие ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит.** Флаг выводится под `if book.BankMoved`, а единственный писатель `bank_moved_at` — дверь правок банка. На книге, которая МАЙНИТ банк, вторая покупка без правок банка идёт без флага, и движковый джоб-гард останавливает прогон (`exit 1` ⇒ `failed` на стороне платформы): авто-банк растёт от покупки к покупке, edit-снапшот съезжает, а гард банк-онли-движение от смены конфига не отличает. ⚠ Сегодня БЕСПРЕДМЕТНО: проводка `tmctl translate --max-units` на платформе гейчена оркестратором до лечения, а без неё вторая покупка этой формы не возникает. Строка заведена, чтобы условность не всплыла сюрпризом при снятии гейта. Найдено бэкенд-сессией `textmachine-e4` (пак «деньги»), проверено чтением платформенной стороны сессией P11 ⚠ **БЕСПРЕДМЕТНОСТЬ КОНЧИЛАСЬ И ДЕФЕКТ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ (05.09), статус флипает ЛЕНДИНГ.** Гейт на проводку `--max-units` снят строкой 280, флаг едет в argv — значит условность `--resnapshot` перестала быть теоретической ровно в тот момент. Условие расширено: `resnapshot := book.BankMoved || book.HasPriorRun` (`internal/runs/runs.go`, греп `book.HasPriorRun`). Довод, почему флаг на КАЖДОМ продолжении безопасен: гард срабатывает ПО ДЖОБУ, то есть только на главах, которых прогон касается, а объёмный потолок допускает НОВУЮ книгу прежде пере-делки (`backend/internal/pipeline/volume.go`, проход `unitFresh` затем `unitRework`) — продолжение тратит грант на недоставленные главы. Согласие при этом фондированное: собственный холд прогона, никогда бланкетная форма. Пин: `runs.TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection` (первая покупка флага НЕ несёт, вторая несёт, правки банка не было). | fixed(628cc56) | бэкенд-пак «деньги» + пак P11 (сверка шва) | | PD-439 | standards | info | `docs/DEFECT_REGISTER.md` (строки `PD-59`, `PD-115`, `PD-122`, `PD-273`, `PD-380`, `PD-407`, `PD-44`), шапка регистра (словарь статусов), `internal/gates/register_test.go` (греп `registerStatus`) | **Семь строк несут в колонке статуса не статус, и каждая из них невидима для всякого счёта, который эту колонку читает.** Словарь шапки — `open` · `fixed()` · `accepted-risk(<кем, когда>)`; в дереве встречаются `open (наблюдаемость закрыта P5; ops и конфигурация — нет)`, `open (грейс-половина закрыта D39.162)`, `open (добавление закрыто P5; удаление — нет)`, `closed (решение владельца 17.08)`, `fixed` без коммита (дважды) и `**закрыт ратификацией**, работа уходит строкой 103`. Зонный `awk` сверяет ячейку с `open` ТОЧНО, поэтому три аннотированных открытых ряда не попадают ни в одно число, которое зона печатала (включая «open=96»), а `fixed` без коммита нарушает правило «закрытие — только с коммитом фикса». ⚠ Найдено новым гейтом класса: он читает статус по словарю с аннотацией, считает такие ряды открытыми, называет нечитаемые поимённо и краснеет, если нечитаемый ряд стоит под заголовком «Открытые» и несёт маркеры тревоги. Строки НЕ правлю: смена статуса — акт лендинга, а здесь под вопросом и форма, и содержание вердикта | open | самопроверка пака P13 (гейт класса, первый прогон) | ## Принятый риск