From 3a487608cbdfc5a8c35a00fe8d74b985fdc29c79 Mon Sep 17 00:00:00 2001 From: heaven Date: Thu, 10 Sep 2026 19:03:11 +0300 Subject: [PATCH] Point the orchestrator's file:line anchors at the lines they name after the engine landing moved them, and leave the engine and polygon zones the re-measured addresses for theirs. --- docs/BACKLOG.md | 4 ++-- docs/PROGRESS.md | 27 ++++++++++++++++++++++++++- docs/architecture/05-decisions-log.md | 4 ++-- docs/architecture/15-money-path.md | 4 ++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 4b4fcac5..c1f8b681 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -239,7 +239,7 @@ | 302 | **РЕЗКА xhtml ПО ЯКОРЯМ `nav`: текст цел, ГРАНУЛЯРНОСТЬ ТЕРЯЕТСЯ.** Массовая форма EPUB — несколько глав в ОДНОМ документе, разделённых якорями, и `nav` объявляет на них отдельные цели. Структурный пак 1 такие цели СХЛОПЫВАЕТ: одна глава вместо трёх, текст не теряется (формулировка бэкенд-сессии 05.09: «текст цел, гранулярность потеряна»). ⚠ **Ложь при этом уже не едет** — тем же паком ратифицировано: схлопнулась хоть одна цель ⇒ книга НЕ `declared`, а `delimited`, плюс счёт `targets collapsed: N` в WARN. То есть провенанс честен, а разрез груб. ⇒ заказ: резать документ по якорям целей. ⛔ **Почему НЕ сделано паком 1 и почему это дорого:** это единственное место, трогающее `extractXHTML`, у которого **четыре пина байт-паритета** (`ingest_test.go:323, 359, 423, 467` — void-теги, несбалансированный ruby, raw-text-элементы, голый амперсанд), и ошибка там ТИХАЯ. Заходить в него в конце пака, ради гранулярности, при живом гейте разреза — плохой размен. ⚠ Строкой закрывается ГРАНУЛЯРНОСТЬ, а не провенанс: продать такую книгу по главам платформа и так не предложит | бэкенд | когда-нибудь (после пака 2) | книга с тремя якорями в одном документе даёт три главы; четыре пина байт-паритета целы | **D39.207** | | 303 | **НЕ-CJK ПУТЬ ПОСТРОЕН НАПОЛОВИНУ: чанкер безъюнитную грамматику УЖЕ УМЕЕТ, ингест по ней НЕ РЕЖЕТ.** `detectChapterUnit` итерирует `UnitOrdered` и без юнита возвращает 0 ⇒ грамматика вида «Kapitel 12» / «Chapter 12» (маркер + номер, БЕЗ юнита) книгу не режет; при этом `matchHeaderLine` чанкера юнита не требует и на это есть живой тест (`TestStripHeadingGenericLatinMarker`). То есть заголовок такой книги движок умеет СНЯТЬ, но не умеет по нему РАЗРЕЗАТЬ. ⇒ ревью-вопрос проекта «заработает ли пара, которой в репо НЕТ, без правки Go» закрыт для языков С ЮНИТОМ (доказано сквозным тестом `TestANewLanguageCutsFromItsDataFileAlone`: без файла корейская книга — одна глава, с `ko/structure.txt` — три, разница только в данных) и **ОТКРЫТ для языков без юнита**. Это названный ВХОД в пак 2, а не открытие по ходу. ⚠⚠ **ГРАНИЦА ЗАМЕРА, названная самой зоной и записанная здесь в её настоящей силе:** утверждение проверено **ПО КОДУ и ПО ТЕСТУ НА ФИКСТУРЕ**; **живая не-CJK книга через ингест НЕ прогонялась**. «На живой книге» под этой строкой НЕ стоит, и ссылаться на неё как на замер живого поведения нельзя | бэкенд | скоро (вход пака 2) | безъюнитная грамматика режет книгу; замер на живой не-CJK книге | **D39.207** | | 304 | ⚠ **РИСК NULLABLE ДЕНЕЖНЫХ КОЛОНОК — НЕ ТЕОРЕТИЧЕСКИЙ, А ОТЛОЖЕННЫЙ, и это вскрыла ЛОЖНАЯ ПРИЧИНА в отчёте зоны.** Платформенная зона объяснила чистый `sqlc diff` тем, что новые колонки лежат «в таблицах, для которых sqlc не держит запросов» — **догадка, и неверная**: `queries/credits.sql` и `queries/exports.sql` читают `books`, `chapters` и `runs`. Верная причина: ни один из этих запросов не выбирает `*` и не называет новых колонок, поэтому сгенерированный код не двигается (греп по `*.sql.go` — ноль вхождений). ⇒ **`books` ЛЕЖИТ в мире sqlc**, значит риск NULLABLE денежных колонок наступит на ПЕРВОМ же запросе, который их выберет, — не «никогда», а «пока не спросили». ⚠ Нашла это зона САМА, четвёртым заходом по прямому вопросу владельца «нет ли чего, что ты умолчала»; ни один гейт поймать это не мог — сторожат поведение и форму, а не то, что отчёт о себе ГОВОРИТ (`D39.202` п.9) | платформа | скоро | первый запрос, выбирающий денежные колонки, не роняет генерацию | **D39.208** | -| 306 | ⛔ **СЕМЬСОТ ЯКОРЕЙ БЕЗ ТОКЕНА НЕПРОВЕРЯЕМЫ, И 59% ИЗ НИХ УЖЕ НЕ УКАЗЫВАЮТ ТУДА, КУДА УКАЗЫВАЛИ.** Замер Fable 5 06.09 (выборка 80, seed фиксирован, метод — blame-триаж: коммит последней правки СТРОКИ ДОКА → цель на том коммите → сравнение): цела **31** · уехала **25** · переписана либо исчезла **22** · не судится 2. ⇒ **59% (±11 п.п.)**. ⚠ Линт сверяет по токену 202 и честно печатает «без токена — несверяемы: 780», но ЗАГОЛОВОК «0 проблемных якорей» читается как «всё цело» — прибор отвечает на свой вопрос, а читают его как ответ на другой (`D39.202`). ⚠ **Дрейф сосредоточен там, где он ОЖИДАЕМ:** 34 из 47 сдвинувшихся — в ЗАМОРОЖЕННЫХ отчётах, чьи тела улика по `D23.3` и обязаны описывать дерево НА ДАТУ; их чинить НЕЛЬЗЯ, им нужен штамп HEAD в шапке. **Опасны 13 в ЖИВЫХ носителях**, и часть из них — в ШАПКЕ журнала, то есть в первом, что читает сессия. ⇒ заказ, тремя шагами и без велосипедов: **(1)** заголовок линта печатает ПОКРЫТИЕ («сверено N · несверяемы M · из них с изменившейся целью — K»), чтобы гейт перестал врать формой; **(2)** ОДНОРАЗОВАЯ механическая миграция тем же скриптом: «цела» → дописать токен автоматически, «уехала» с единственным попаданием → пере-навести, «исчезла» → список на руки; замороженным отчётам вместо пере-наводки штамп коммита и исключение из линта; **(3)** правило «токен при касании» остаётся удержанием линии; **(4)** ⛔ **грамматика УЛИКИ — `path:line@`**, которую линт сверяет через `git show :path`, а НЕ против HEAD (~20 строк в `counts.py`). Без неё автор, цитирующий мёртвый указатель как ПРЕДМЕТ записи, вынужден снимать форму якоря руками — я так и сделал 06.09 и объявил это в тексте, но это подгонка ЗАПИСИ под модель прибора, а не починка модели: **законно как последнее исключение, незаконно как привычка**. Та же форма закрывает и ЗАМОРОЖЕННЫЕ отчёты, которым сегодня штамп HEAD стоит прозой (`research/31`/`32`). ⚠ Сегодня без (2) правка любого дока превращается в игру «бей крота»: каждое касание вскрывает соседний голый якорь. ⚠⚠ **ПЕРЕ-ЗАМЕР 06.09 ГРАММАТИКОЙ САМОГО ЛИНТЕРА — МАСШТАБ ОКАЗАЛСЯ В РАЗЫ МЕНЬШЕ, И ЭТО МЕНЯЕТ ЗАКАЗ.** По 117 живым докам: **214 якорей с токеном** (это и есть те, про кого «0 проблемных») и **1776 без токена**; за конец файла целят 66, однозначно разрешаются 9, и семь из девяти — ВНЕШНИЕ цитаты (стандартная библиотека Go, чужой фронтовый код). ⇒ **настоящий внутрирепозиторный протухший якорь ОДИН** (испр. 06.09 автором замера): `docs/architecture/05-decisions-log.md:32`=`Эррата 28.08-и` — **в ШАПКЕ журнала, которую сессия читает первой**; цель нашлась `git log -S` по тексту якоря и пере-навёдена с токеном. ⚠ **Второй кандидат (`PROGRESS.md:71` → `README.md:95`) оказался АРТЕФАКТОМ РАЗРЕШЕНИЯ ИМЕНИ, а не дефектом** — и он же служит уликой к правилу (2) строки 299. ⭐ **Дешёвый метод для ведра «исчезла», исполненный, а не описанный:** `git log -S'<текст якоря>' -- <док> | tail -1` даёт коммит, ВВЕДШИЙ якорь; `git show <коммит>:<цель>` — что стояло в ту минуту ДОСЛОВНО; `git grep` — сегодняшний адрес. Угадывания нет ни на одном шаге. ⛔ **ОПИСАНИЕ ЧУЖОГО МЕТОДА В ЭТОЙ СТРОКЕ БЫЛО ЛОЖНЫМ — СНЯТО 06.09.** Абзац писал, что «59 % на выборке 80» получены наивным резолвингом (голое `mining.go:40` из подкаталога ищут от корня, не находят, объявляют мёртвым). **Наивный резолвинг — метод АГЕНТСКИХ чисел** («42 %», «19 из 32 мертвы»), и опровергнуты им именно они; тем же способом автор пере-замера получил «31 мёртвый из 36» и не понёс их, потому что стал сверять расхождение. Замер Fable 5 устроен иначе и так же описан в ГОЛОВЕ этой строки: берутся только ЗОННО-ПРЕФИКСНЫЕ пути, `git blame` даёт коммит последней правки СТРОКИ ДОКА, `git show <тот коммит>:<цель>` — цель на ту минуту, и сравнивается СОДЕРЖИМОЕ с HEAD. ⇒ строка противоречила сама себе: голова описывала blame-триаж, хвост объявлял его наивным резолвингом. **Класс строки 299, только внутри бэклога.** ⚠ **ДВА ЧИСЛА ОТВЕЧАЮТ НА РАЗНЫЕ ВОПРОСЫ, и потому ни одно не опровергает другое.** Пере-замер грамматикой линтера спрашивает «указывает ли якорь в СУЩЕСТВУЮЩУЮ строку HEAD и совпадает ли токен» — ответ: 214 с токеном, 1776 без, за конец файла 66, однозначно разрешаются 9, настоящий внутрирепозиторный протухший ОДИН. Замер blame спрашивает «изменилось ли СОДЕРЖИМОЕ цели с той минуты, когда строка дока была написана» — ответ: ~59 % на выборке 80. Первый вопрос про HEAD, второй про ДРЕЙФ; строка обязана нести оба, а прежняя редакция вычитала одно из другого. ⚠⚠ **И вопрос HEAD слеп ПО ПОСТРОЕНИЮ к тому самому якорю, ради которого строка заведена:** якорь шапки журнала был написан голым — `seeding.go:143-153`, без слэша, — а `docs/scripts/counts.py:595`=`"~" in rel or "/" not in rel` относит такую форму к «неразбираемым» и пропускает МОЛЧА, ДО всякой проверки файла. ⚠ **Испр. 06.09 по ревью Fable 5: файл НЕ переименовывали** — `seeding.go` жив и сегодня (128 строк), а `bankmaterialize.go` ДОБАВЛЕН `6ec9f8a` 29.08, и в него ПЕРЕЕХАЛ код. Прежняя редакция этой строки писала «переименовали», и следствие «ветвь «файла нет» не выстрелила» было лишним: голое имя до той ветви не доходит по построению. Прибор не видел ровно того случая, с которого начался заказ, — и «0 проблемных» был честен про свой вопрос и слеп про этот. ⚠ И одно слово в этой строке названо неточно: пере-замер сделан ГРАММАТИКОЙ линта (его правила разрешения путей и целей), но не самим линтом и не сверкой токенов — у голых якорей токенов нет — этот якорь линт не видит по построению, и два абзаца строки описывают РАЗНЫЕ приборы одним словом. ⚠ И у метода есть ложные срабатывания: две находки этого же прохода я проверил руками — одна («`STACK.md:11` исчезло») оказалась ЛОЖНОЙ, цель жива и точна | оркестратор | скоро | линт печатает покрытие; голых якорей в ЖИВЫХ доках нет | **D39.202** | +| 306 | ⛔ **СЕМЬСОТ ЯКОРЕЙ БЕЗ ТОКЕНА НЕПРОВЕРЯЕМЫ, И 59% ИЗ НИХ УЖЕ НЕ УКАЗЫВАЮТ ТУДА, КУДА УКАЗЫВАЛИ.** Замер Fable 5 06.09 (выборка 80, seed фиксирован, метод — blame-триаж: коммит последней правки СТРОКИ ДОКА → цель на том коммите → сравнение): цела **31** · уехала **25** · переписана либо исчезла **22** · не судится 2. ⇒ **59% (±11 п.п.)**. ⚠ Линт сверяет по токену 202 и честно печатает «без токена — несверяемы: 780», но ЗАГОЛОВОК «0 проблемных якорей» читается как «всё цело» — прибор отвечает на свой вопрос, а читают его как ответ на другой (`D39.202`). ⚠ **Дрейф сосредоточен там, где он ОЖИДАЕМ:** 34 из 47 сдвинувшихся — в ЗАМОРОЖЕННЫХ отчётах, чьи тела улика по `D23.3` и обязаны описывать дерево НА ДАТУ; их чинить НЕЛЬЗЯ, им нужен штамп HEAD в шапке. **Опасны 13 в ЖИВЫХ носителях**, и часть из них — в ШАПКЕ журнала, то есть в первом, что читает сессия. ⇒ заказ, тремя шагами и без велосипедов: **(1)** заголовок линта печатает ПОКРЫТИЕ («сверено N · несверяемы M · из них с изменившейся целью — K»), чтобы гейт перестал врать формой; **(2)** ОДНОРАЗОВАЯ механическая миграция тем же скриптом: «цела» → дописать токен автоматически, «уехала» с единственным попаданием → пере-навести, «исчезла» → список на руки; замороженным отчётам вместо пере-наводки штамп коммита и исключение из линта; **(3)** правило «токен при касании» остаётся удержанием линии; **(4)** ⛔ **грамматика УЛИКИ — `path:line@`**, которую линт сверяет через `git show :path`, а НЕ против HEAD (~20 строк в `counts.py`). Без неё автор, цитирующий мёртвый указатель как ПРЕДМЕТ записи, вынужден снимать форму якоря руками — я так и сделал 06.09 и объявил это в тексте, но это подгонка ЗАПИСИ под модель прибора, а не починка модели: **законно как последнее исключение, незаконно как привычка**. Та же форма закрывает и ЗАМОРОЖЕННЫЕ отчёты, которым сегодня штамп HEAD стоит прозой (`research/31`/`32`). ⚠ Сегодня без (2) правка любого дока превращается в игру «бей крота»: каждое касание вскрывает соседний голый якорь. ⚠⚠ **ПЕРЕ-ЗАМЕР 06.09 ГРАММАТИКОЙ САМОГО ЛИНТЕРА — МАСШТАБ ОКАЗАЛСЯ В РАЗЫ МЕНЬШЕ, И ЭТО МЕНЯЕТ ЗАКАЗ.** По 117 живым докам: **214 якорей с токеном** (это и есть те, про кого «0 проблемных») и **1776 без токена**; за конец файла целят 66, однозначно разрешаются 9, и семь из девяти — ВНЕШНИЕ цитаты (стандартная библиотека Go, чужой фронтовый код). ⇒ **настоящий внутрирепозиторный протухший якорь ОДИН** (испр. 06.09 автором замера): `docs/architecture/05-decisions-log.md:33`=`Эррата 28.08-и` — **в ШАПКЕ журнала, которую сессия читает первой**; цель нашлась `git log -S` по тексту якоря и пере-навёдена с токеном. ⚠ **Второй кандидат (`PROGRESS.md:71` → `README.md:95`) оказался АРТЕФАКТОМ РАЗРЕШЕНИЯ ИМЕНИ, а не дефектом** — и он же служит уликой к правилу (2) строки 299. ⭐ **Дешёвый метод для ведра «исчезла», исполненный, а не описанный:** `git log -S'<текст якоря>' -- <док> | tail -1` даёт коммит, ВВЕДШИЙ якорь; `git show <коммит>:<цель>` — что стояло в ту минуту ДОСЛОВНО; `git grep` — сегодняшний адрес. Угадывания нет ни на одном шаге. ⛔ **ОПИСАНИЕ ЧУЖОГО МЕТОДА В ЭТОЙ СТРОКЕ БЫЛО ЛОЖНЫМ — СНЯТО 06.09.** Абзац писал, что «59 % на выборке 80» получены наивным резолвингом (голое `mining.go:40` из подкаталога ищут от корня, не находят, объявляют мёртвым). **Наивный резолвинг — метод АГЕНТСКИХ чисел** («42 %», «19 из 32 мертвы»), и опровергнуты им именно они; тем же способом автор пере-замера получил «31 мёртвый из 36» и не понёс их, потому что стал сверять расхождение. Замер Fable 5 устроен иначе и так же описан в ГОЛОВЕ этой строки: берутся только ЗОННО-ПРЕФИКСНЫЕ пути, `git blame` даёт коммит последней правки СТРОКИ ДОКА, `git show <тот коммит>:<цель>` — цель на ту минуту, и сравнивается СОДЕРЖИМОЕ с HEAD. ⇒ строка противоречила сама себе: голова описывала blame-триаж, хвост объявлял его наивным резолвингом. **Класс строки 299, только внутри бэклога.** ⚠ **ДВА ЧИСЛА ОТВЕЧАЮТ НА РАЗНЫЕ ВОПРОСЫ, и потому ни одно не опровергает другое.** Пере-замер грамматикой линтера спрашивает «указывает ли якорь в СУЩЕСТВУЮЩУЮ строку HEAD и совпадает ли токен» — ответ: 214 с токеном, 1776 без, за конец файла 66, однозначно разрешаются 9, настоящий внутрирепозиторный протухший ОДИН. Замер blame спрашивает «изменилось ли СОДЕРЖИМОЕ цели с той минуты, когда строка дока была написана» — ответ: ~59 % на выборке 80. Первый вопрос про HEAD, второй про ДРЕЙФ; строка обязана нести оба, а прежняя редакция вычитала одно из другого. ⚠⚠ **И вопрос HEAD слеп ПО ПОСТРОЕНИЮ к тому самому якорю, ради которого строка заведена:** якорь шапки журнала был написан голым — `seeding.go:143-153`, без слэша, — а `docs/scripts/counts.py:595`=`"~" in rel or "/" not in rel` относит такую форму к «неразбираемым» и пропускает МОЛЧА, ДО всякой проверки файла. ⚠ **Испр. 06.09 по ревью Fable 5: файл НЕ переименовывали** — `seeding.go` жив и сегодня (128 строк), а `bankmaterialize.go` ДОБАВЛЕН `6ec9f8a` 29.08, и в него ПЕРЕЕХАЛ код. Прежняя редакция этой строки писала «переименовали», и следствие «ветвь «файла нет» не выстрелила» было лишним: голое имя до той ветви не доходит по построению. Прибор не видел ровно того случая, с которого начался заказ, — и «0 проблемных» был честен про свой вопрос и слеп про этот. ⚠ И одно слово в этой строке названо неточно: пере-замер сделан ГРАММАТИКОЙ линта (его правила разрешения путей и целей), но не самим линтом и не сверкой токенов — у голых якорей токенов нет — этот якорь линт не видит по построению, и два абзаца строки описывают РАЗНЫЕ приборы одним словом. ⚠ И у метода есть ложные срабатывания: две находки этого же прохода я проверил руками — одна («`STACK.md:11` исчезло») оказалась ЛОЖНОЙ, цель жива и точна | оркестратор | скоро | линт печатает покрытие; голых якорей в ЖИВЫХ доках нет | **D39.202** | | 307 | ⛔ **СПАН ПОД ПОДПИСЬЮ «СКОЛЬКО ЗАКАЗАНО» — ЛОЖЬ ЯРЛЫКОМ, И ЧЕСТНАЯ ФОРМА ДРУГАЯ.** Для заказа, выраженного ЗНАКАМИ, `Run.ordered_chapters` несёт не заказанное, а **СПАН** — сколько глав заказ ЗАТРАГИВАЕТ, считая главу затронутой независимо от того, куплена ли она целиком (`pricing.QuoteUnits` ставит `Chapters: max(chapters, 1)`, вызывающий передаёт `chaptersSpanning`, и сам код называет это «coarser of the two figures»). ⚠ **Замер зоны:** заказ на ОДИН юнит из четырёх в первой главе даёт `1` — **сто процентов завышения на самом дешёвом заказе**, то есть на том, которым сервис пробуют впервые. ⇒ число под именем «ordered chapters», превышающее заказанное, — ложь ярлыком, сколь угодно последовательная. ⭐ **Разбор, снявший спор:** нужда, которую зона защищала («видно, докуда дошли деньги»), настоящая, а носитель неверный — «докуда» есть **ПОЗИЦИЯ** («достигает главы N»), а не **КОЛИЧЕСТВО** («2 главы»), и позиция у платформы УЖЕ ЕСТЬ (`Quote.ThroughChapter`, `BookOrder.ThroughUnitID`). ⚠ Спан ≠ позиция для заказа-продолжения: спан считает затронутые от текущей точки, позиция — ординал. ⇒ **честная форма:** `ordered_chapters: null` для символьного заказа (зеркало `delivered_chapters: null` — «заказано не в главах») + аддитивно `ordered_characters` (эхо запроса) + позиция ординалом, если экрану нужно «докуда». Это смена формы (поле становится нуллабельным) ⇒ минор. ⚠ **Цена смены почти пуста и это ЗАМЕРЕНО:** `ordered_chapters` вошёл в канон только `75ae6f0`, до него в файле НОЛЬ вхождений — клиент 0.10.0 поля не видел никогда, а `ceiling_chapters` тот же минор уже отвергает с 400. ⚠ Поведение менять НЕ требуется: политика «заказ выражен в знаках и меряется в них насквозь» верна и остаётся. До лендинга формы канон описывает ДЕФЕКТ с ⛔-пометкой, а не дизайн | бэкенд+платформа | скоро | `ordered_chapters` нуллабелен, размер заказа эхом, позиция отдельно | **D39.208** | | 308 | **ВИД ПРОГОНА ЗАКОДИРОВАН НУЛ�ём, И ЭТО ЕДИНСТВЕННАЯ ЕГО МЕТКА ВО ВСЕЙ ПЛАТФОРМЕ.** Явного признака ре-прохода у `LiveRun` НЕТ: `Resnapshot` не годится — он ставится и ОБЫЧНОМУ продолжению после правки банка, а валидация `OrderedChapters == 0 && !Resnapshot` читается «ноль допустим только под пере-снапшот», то есть ре-проход ⇒ resnapshot, а не наоборот. ⇒ **вид прогона различается ЗНАЧЕНИЕМ денежного поля**, и держится это на двух вызовах `max(n, 1)` (`pricing.QuoteUnits`, `runs.chaptersSpanning`). ⚠ **Опасность растёт после лендинга формы заказа** (строка 307): снаружи символьный заказ станет `null`, и единственное место, где `max(n,1)` что-то значит, уходит ВНУТРЬ — следующий читатель колонки (отчёт, CLI) напечатает спан под старым словом и не узнает об этом. ⇒ заказ: **явный признак вида** (`RePass`) вместо кодирования вида нулём. ⛔ **Рантайм-пояса `== 0 && OrderedUnits == nil` ставить НЕЛЬЗЯ** — символьный заказ не может записать ноль ни при каких данных, а гейт от несуществующего состояния завтра прочтут как свидетельство, что состояние бывает. Честная защита до рефакторинга — **ПИН инварианта** «символьный заказ на один юнит пишет в колонку 1, никогда 0» (замер у зоны уже есть — сто процентов завышения на самом дешёвом заказе; норма `D39.209` требует превратить замер в пин) плюс одно предложение в комментарий колонки. ⚠ Это рефакторинг в ДЕНЕЖНОМ коде без дефекта сегодня — отдельным паком, не минором формы заказа | платформа | когда-нибудь (после строки 307) | вид прогона читается признаком, а не значением денежного поля | **D39.208** | | 309 | ⛔ **ГЕЙТ ВЕРСИИ КОНТРАКТА СВЕРЯЕТ ЧИСЛА, А НЕ ФОРМЫ — И ПОТОМУ НЕ ВИДИТ ГЛАВНОГО РАСХОЖДЕНИЯ.** `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` сверяет константу сборки с `info.version` канона. Пока эти два числа равны, гейт ЗЕЛЁН — **даже если провод и документ описывают РАЗНЫЕ формы**. Замерено 06.09 трижды за сутки: канон 0.11.0 объявлял полосу «in chapters», а сборка слала юниты; обещал `ordered_chapters: 0` для символьного заказа, а приезжал спан 2; перечислял три значения словаря, а движок слал четыре. **Ни одно из трёх гейт не покраснил**, потому что число совпадало. ⇒ заказ: сверять ПОЛЯ схемы с проводом, а не только версию. ⭐ **Дешёвая форма названа Fable 5 и уже наполовину оплачена нормой:** тест платформы пишет РЕАЛЬНЫЕ ответы в `examples/*.json` (по действующему правилу «фикстуры снимать реальным бинарём»), канон несёт их в `examples:`, линт сравнивает — тогда фраза «both report 0» умерла бы о файл примера, а не о верификатора через сутки. Прозу это не ловит, ЗНАЧЕНИЯ — ловит. ⚠ Вторая половина той же нормы — **авторство**: утверждение канона о ЗНАЧЕНИИ пишет ЗОНА из своих пинов и называет пин в сообщении коммита; оркестратор ратифицирует текст, а не сочиняет его. Три сегодняшние лжи в каноне написаны тем, у кого не было прогона | оркестратор+платформа | скоро | поля схемы сверяются с проводом порождёнными примерами | **D39.208** | @@ -285,7 +285,7 @@ | 351 | **ЛИНТ СЛОВАРЯ КЛАССИФИКАТОРА ПИНИТ ОДНО НАПРАВЛЕНИЕ: слово, УБРАННОЕ из Go, не ловит ничто.** Гейт `TestEveryClassifierPromptSpeaksTheEnginesVocabulary` (лендинг `D39.225`) утверждает «каждое слово из `terminology.Types`/`Genders` названо в промте пары» — и потому сжимается вместе со своим предметом: выкинутый класс `title` оставляет батарею зелёной (замер 07.09 на копии: 19 ok / 0 FAIL). Вырожденный предел закрыт стражем `len(classifierVocabulary()) == 0`, общий случай — нет. Настоящий дом — пин САМОГО словаря, которого в репозитории нет (research/33 Д1: 0 тестов на `terminology.Types`). ⚠ Граница названа зоной самостоятельно, в заказ пака не входила | бэкенд | когда-нибудь | пин словаря (голден или рефлексивный тест) | `D39.225`, research/33 Д1 | | 352 | **ПЯТЬ РУКОПИСНЫХ СПИСКОВ АРМ-КОНФИГОВ ЖИВУТ РЯДОМ С ДВУМЯ ГЕЙТАМИ, КОТОРЫЕ ТЕПЕРЬ ГЛОБЯТ.** `pipeline/promptlabel_test.go:45` · `config/echoregen_shipping_test.go:35` · `config/content_labels_test.go:529` и `:575` · `config/prompt_pack_test.go:239` перечисляют армы поимённо; новый арм автоматически попадает в `TestAnArmIsTheProductionConfigWithADifferentEditor` и `TestSwapArmConfigs` и проходит мимо всех пяти. Найдено приёмкой `D39.225`, вне заказа пака; предсуществующее | бэкенд | когда-нибудь (со следующим касанием армов) | свести пять списков к глобу | `D39.225` | | 353 | **КОНТРАКТ СЕКЦИИ ПРЕДЛОЖЕНИЙ В САЙДКАРЕ БАНКА ЗАПИНЕН ПО ПОЛЯМ И ПОРЯДКУ, НО НЕ ИСЧЕРПЫВАЮЩЕ — строка заведена ПРАВИЛОМ ОСТАНОВКИ, а не находкой.** Три круга приёмки `D39.225` находили в одной новой секции всё более узкие дыры: сперва семь полей без утверждений, потом `Freq` и своп списков, потом ранжирование и класс пустого `Dst`. Каждая была настоящей и предъявлена посадкой; каждый следующий круг стоил больше и покупал меньше. Сегодня запинены: все решающие поля против стоп-строки · порядок против стоп-СТРОК на двух уровнях · рефлективный страж «ни одно поле проекции не нулевое» (ловит поле, добавленное завтра и не заполненное). ⚠ **Дописка 08.09 (приёмка `D39.226`):** у поля `SettledByBank` стоп-строки нет свидетеля в списке «полей, которых сайдкар намеренно НЕ несёт» (`bankexport_test.go`) — то есть его сознательное отсутствие в проекции не утверждено ничем; закрывать вместе с первым читателем секции. ⚠ Границу называет сам гейт: поле, которое отрастил `BankStopRow` и которое СЛЕДОВАЛО бы проецировать, механически не ловится — это суждение того, кто поле добавляет. ⇒ вернуться к исчерпывающему контракту, когда у секции появится ЧИТАТЕЛЬ (платформенная половина строки **224**): пока читателя нет, расхождение ничем не судится, а в день его появления разница станет семантикой задним числом — второй ярус класса A `18-bank-ontology.md` | бэкенд + платформа | когда-нибудь (гейт: появление читателя секции, строка 224) | исчерпывающий пин контракта секции вместе с её первым потребителем | `D39.225` п.10 | -| 354 | ⛔ **ОПЛАЧЕННЫЙ `2xx` С ОБОРВАННЫМ ТЕЛОМ — КЛАСС, СТОИВШИЙ 32 % ЖИВОГО ПРОГОНА, И СТРАНИЦА КВИРКОВ О НЁМ МОЛЧИТ.** Живой прогон 08.09 (10 глав, `committed=$1.259217`): два вызова редактора вернулись `2xx` с нечитаемым JSON и были ОПЛАЧЕНЫ — `$0.407606`, то есть **32 % бюджета прогона**. Движок класс ЗНАЕТ и обрабатывает штатно: `backend/internal/pipeline/disposition.go:79`=`FlagDecodeError FlagReason` `FlagDecodeError` — «2xx with an unreadable body — billed, conservatively settled, flagged». ⛔ Но `docs/experiments/00-provider-quirks.md` — страница, которую канон велит читать ПЕРЕД правкой адаптеров и вызовов, — о нём не несёт НИ СТРОКИ: греп по `decode`/`unexpected end of JSON`/«оборван» даёт **0 при 250 строках файла** (контроль: слово `provider` там встречается 3 раза, то есть спрошено существующее). ⇒ следующая сессия, читающая квирки перед платным прогоном, не узнает о поведении, которое съедает треть бюджета. ⛔ **испр. 08.09: МЕХАНИЗМ НАЗВАН ЗДЕСЬ НЕВЕРНО — это не свойство провайдера, а дефект движка, и носитель у него теперь строка 360.** Полигон замером exp-23 п.5 показал, что за подписью «нечитаемое тело» стоит ТАЙМАУТ ЧТЕНИЯ по `attempt_s`, который движок сам же выбрасывает в `_`. ⇒ эта строка остаётся ТОЛЬКО про пробел страницы квирков: она обязана предупреждать, ЧТО оператор увидит и почему, со ссылкой на 360, а не приписывать поведение вендору. ⚠ Зона — ПОЛИГОН (`docs/experiments/` по зонной таблице), живой сессии полигона нет; строка заведена оркестратором как носитель, писать в файл полигону | полигон | скоро (перед следующим платным прогоном любой зоны) | строка в `00-provider-quirks.md` с частотой, стоимостью и тем, что движок делает с классом | живой прогон 08.09, пак «правда оператору и деньги» | +| 354 | ⛔ **ОПЛАЧЕННЫЙ `2xx` С ОБОРВАННЫМ ТЕЛОМ — КЛАСС, СТОИВШИЙ 32 % ЖИВОГО ПРОГОНА, И СТРАНИЦА КВИРКОВ О НЁМ МОЛЧИТ.** Живой прогон 08.09 (10 глав, `committed=$1.259217`): два вызова редактора вернулись `2xx` с нечитаемым JSON и были ОПЛАЧЕНЫ — `$0.407606`, то есть **32 % бюджета прогона**. Движок класс ЗНАЕТ и обрабатывает штатно: `backend/internal/pipeline/disposition.go:80`=`FlagDecodeError FlagReason` `FlagDecodeError` — «2xx with an unreadable body — billed, conservatively settled, flagged». ⛔ Но `docs/experiments/00-provider-quirks.md` — страница, которую канон велит читать ПЕРЕД правкой адаптеров и вызовов, — о нём не несёт НИ СТРОКИ: греп по `decode`/`unexpected end of JSON`/«оборван» даёт **0 при 250 строках файла** (контроль: слово `provider` там встречается 3 раза, то есть спрошено существующее). ⇒ следующая сессия, читающая квирки перед платным прогоном, не узнает о поведении, которое съедает треть бюджета. ⛔ **испр. 08.09: МЕХАНИЗМ НАЗВАН ЗДЕСЬ НЕВЕРНО — это не свойство провайдера, а дефект движка, и носитель у него теперь строка 360.** Полигон замером exp-23 п.5 показал, что за подписью «нечитаемое тело» стоит ТАЙМАУТ ЧТЕНИЯ по `attempt_s`, который движок сам же выбрасывает в `_`. ⇒ эта строка остаётся ТОЛЬКО про пробел страницы квирков: она обязана предупреждать, ЧТО оператор увидит и почему, со ссылкой на 360, а не приписывать поведение вендору. ⚠ Зона — ПОЛИГОН (`docs/experiments/` по зонной таблице), живой сессии полигона нет; строка заведена оркестратором как носитель, писать в файл полигону | полигон | скоро (перед следующим платным прогоном любой зоны) | строка в `00-provider-quirks.md` с частотой, стоимостью и тем, что движок делает с классом | живой прогон 08.09, пак «правда оператору и деньги» | | 356 | **КНИГА У СВОЕГО ПОЖИЗНЕННОГО ПОТОЛКА РОЛИ НАВСЕГДА ОСТАЁТСЯ БЕЗ КОНСОЛИДИРОВАННОГО БАНКА, и ни одно сообщение не говорит именно этого.** `store/ledger.go` `RoleSpentUSD` суммирует чекпойнты роли за ВСЮ жизнь книги, предполётный срез сеет `probe := spent` и обрывается на `probe+want > budgetUSD`; при `fits == 0` цикл попыток не запускается вовсе. ⚠ Оператор ПРИ ЭТОМ извещён — `terminologist.go:936` печатает `batches_planned=N batches_running=0 batches_dropped=N spent_usd budget_usd` ДО первого вызова (сверено оркестратором 08.09), поэтому «тихо» здесь неверно, и находка приёмки в этой части ОТКЛОНЕНА доводом зоны. Верен остаток: сообщение говорит про ЭТОТ прогон, а не про то, что книга исчерпала пожизненный бюджет роли и консолидации не получит НИКОГДА, сколько её ни перезапускай. Свойство `budget_usd`, а не фильтра пака «правда оператору и деньги»: тот на решение бюджета не влияет. ⇒ решить, что показывать оператору и владельцу: отдельное сообщение «книга у пожизненного потолка роли», подъём потолка как явный акт, или сознательное молчание с записью | бэкенд | когда-нибудь (гейт: первая книга, упёршаяся в пожизненный потолок роли) | сообщение об исчерпании пожизненного бюджета либо ратифицированное молчание | приёмка `D39.226`, линза «вне карты» | | 359 | **ТРИ ЯКОРЯ В ЧУЖИХ ЗОНАХ ПРОТУХЛИ ОТ ДВУХ ДВИЖКОВЫХ ЛЕНДИНГОВ — адреса уже найдены, править владельцам зон.** Линтер `counts.py --lint` даёт 6 проблемных якорей при 121 живом доке; три из них — фронтовые и пингнуты в `frontend/docs/frontend-PROGRESS.md` (зона ЗАМОРОЖЕНА). Остальные три, с новыми адресами, снятыми оркестратором 08.09: **(а) полигон** — `docs/experiments/23-editor-tier.md:7739` и `:7740` ссылаются на «×2 дешевле glm» в `configs/pipeline-c1.yaml:95` и `pipeline-arm-glm.yaml:6`; ⚠ цель ИСЧЕЗЛА, а не сдвинулась (греп даёт 0): долларовые числа сняты из шапки арма приёмкой `D39.226`, потому что стояли на опровергнутом ценовом базисе — значит якорь надо не пере-навести, а пере-написать по факту. **(б) бэкенд** — `backend/docs/DISCLOSURE_LAW_DESIGN.md:40` ссылается на `internal/pipeline/quality.go:233`, токен уехал на `:235`. ⚠ Оркестратор в чужие зоны не пишет; строка — носитель, чтобы адреса не искали заново | полигон + бэкенд | когда-нибудь (попутно с любым касанием этих файлов) | пере-наведённый якорь у бэкенда, пере-написанный по факту — у полигона | `counts.py --lint` 08.09 | | 360 | ⛔⛔ **ТАЙМАУТ ЧТЕНИЯ МАСКИРУЕТСЯ ПОД НЕЧИТАЕМОЕ ТЕЛО, РЕТРАИТСЯ И БИЛЛИТСЯ ПО ОЦЕНКЕ — 23–34 % ЦЕНЫ РЕДАКТОРСКОГО ПРОГОНА, И НОСИТЕЛЯ У КЛАССА НЕ БЫЛО.** Механизм доказан кодом: `backend/internal/llm/httpllm.go` в чтении ответа делает `data, _ := io.ReadAll(io.LimitReader(...))` — **ошибка чтения выбрасывается**, а обрыв распознаётся по РАЗМЕРУ (читают на байт дальше лимита), поэтому тело, оборванное дедлайном `attempt_s` посреди генерации, от целого НЕ ОТЛИЧИМО. Обрезок идёт в `json.Unmarshal` → `unexpected end of JSON input` → `BilledDecodeError` («2xx body decode failed (call IS billed)»), и возвращается он с флагом retryable **`true`** ⇒ вызов повторяется. ⚠ **испр. 08.09, две мои ошибки, обе пере-проверены по коду:** (1) ретрай ОДИН, а не `max_attempts` — `httpllm.go` «The first occurrence retries once … a second billed-decode is terminal»; (2) сеттл ОДИН на стадию-вызов, а не на попытку — `stagerun.go` зовёт `SettleWithCheckpoint(resv, estimate, …)` единожды, `att.cumCost, att.runCost = estimate, estimate`. ⇒ **направление ошибки ОБРАТНОЕ тому, что я записала:** два HTTP-вызова к провайдеру дают ОДНУ запись в леджер, то есть леджер НЕДО-считает на один полный счёт, если вендор биллит оба. Пользователь не переплачивает — недо-счёт бьёт по деплою, ровно как и говорит ратифицированная строка **331**. ⛔⛔ **И РАЗЛИЧИТЕЛЬ ВЫБРАН НЕ ПО ТОМУ ПРИЗНАКУ — вот точная форма дефекта.** Код о двойной оплате ЗНАЕТ и от неё защищается, дословно: «A truncated (>16 MiB) body is deterministic — retrying is pointless (**each retry is a new billed 2xx**), so terminal; a broken connection / garbage is retryable». Но защита ключуется РАЗМЕРОМ, а тело, оборванное НАШИМ дедлайном, маленькое ⇒ проваливается в ветвь «broken connection» и ретраится, хотя довод «каждый ретрай — новый оплаченный 2xx» относится к нему в полной мере. ⇒ лечение не в новом механизме, а в ПРИЗНАКЕ: отличать «мы сами оборвали чтение» от «провайдер прислал мусор». ⭐ **И это ровно то, что `research/21` §Q6 запретил заранее:** в списке «не берём» стоит «reclass empty-2xx как retryable (**double-bill**)». ⛔ Замер полигона (`docs/experiments/23-editor-tier.md`, п.5 «ЧЕТЫРЕ ТАЙМАУТА»): латентности `480390 / 480505 / 480577 / 480528` мс = **ровно 2 × `attempt_s: 240`**, класс назван `decode_error`, цена — **$0.475893 = 23.8 % руки строками, $0.689215 = 34.5 % юнитами**; полигон квалифицировал это как дефект ДВИЖКА и пингнул оркестратору («не наша зона»). ⚠ **Носителя не было: греп `attempt_s` и «таймаут чтения» по живому бэклогу — 0 хитов при 255 рядах** (контроль: `decode` — 3 хита), то есть спрошено существующее. ⛔ **И это ТА ЖЕ физика, что строка 331** — «вызов оборвали МЫ, провайдер, возможно, уже сгенерировал и выставит счёт», — но движок закрывает её ПРОТИВОПОЛОЖНО: оценкой здесь и НУЛЁМ в ветви `No 2xx ever arrived: nothing was billed` (`stagerun.go`). ⚠ **испр. 08.09: прежняя редакция называла спусковым крючком нулевой ветви ПОТОЛОК — это протухло.** Замер `PD-441` снят 04.09, а `81a89e9` (05.09, `D39.206`) сменил поведение: потолок теперь останавливает ДОПУСК, а летящие вызовы доигрывают и биллятся нормально (`waverun.go`: «A ceiling no longer cancels, so siblings run on» и «stopped admissions … every skipped item is simply not started»). ⇒ конкретная дорогая картина из `PD-441` (три вызова по 120–156 с, отменённые в один миг и ушедшие в леджер нулём) СЕГОДНЯ НЕ ВОЗНИКАЕТ. Нулевая ветвь ЖИВА и достижима иначе — стоп прогона, убитый процесс, дедлайн до первого байта, — и расхождение бухгалтерии остаётся, но дешевле и реже, чем я записала. ⚠ **Граница честности:** что два вызова прогона 08.09 ($0.407606 = 32 % бюджета, «unexpected end of JSON input») были именно таймаутами — ВЫВЕДЕНО по совпадению подписи с замером полигона, а НЕ измерено: сырого леджера того прогона в дереве нет (`.db` новее 07.09 — 0 при 163). ⇒ инженерная половина (не выбрасывать ошибку чтения, отличать таймаут от нечитаемого тела, не ретраить вызов, оборванный СВОИМ дедлайном) — заказ без развилок; ДЕНЕЖНАЯ половина требует слова владельца: платит ли пользователь за вызов, который оборвали мы, и если да — по оценке или по нулю. Пока ответа нет, платформа биллит `committed_usd`, куда оценка уже попала ⭐ **КОНСТРУКЦИЯ ЛЕЧЕНИЯ СОШЛАСЬ (консилиум с чистым Fable 5, 08.09; обе стороны объявили сходимость).** **Граница денег — НЕ статус-строка, а факт ДОСТАВКИ запроса**, и он измеряется `httptrace.ClientTrace` (`WroteRequest` / `GotFirstResponseByte`) без байта на проводе — различение проверено исполнением на клиенте той же сборки. Отсюда: **(1)** ошибка `ReadAll` не выбрасывается; после 2xx пять исходов, до заголовков три; **(2)** self-cut (наш дедлайн при доставленном запросе) — свой класс `attempt_timeout`, **чекпойнт + флаг, НЕ ретраить**, и это же чинит ТРЕТЬЮ утечку: сегодня нулевая ветвь `releaseReservation` чекпойнта не пишет ⇒ **резюм ПЕРЕКУПАЕТ вызов** (проверено, в строке 331 этого нет); **(3)** запрос НЕ доставлен (дедлайн до `WroteRequest`) — единственный случай, где «ничего не куплено» верно по построению: как сегодня; **(4)** разрыв соединения после доставки — один ретрай, на исчерпании инфра-пауза, не флаг; **(5)** дедлайн = `clamp(queue_slack_s + max_tokens/tok_s_floor, attempt_s, attempt_max_s)`, пол по умолчанию 35.5 tok/s (единственный опубликованный вендором), `attempt_s` становится ПОЛОМ ⇒ ни один конфиг не теряет времени; в снапшот не входит (`AttemptS` — 0 из 120 файлов); **(6)** сумма сеттла — за ОДНОЙ функцией `settleUSDForCutCall`, не YAML-ручкой: денежную политику нельзя переключать молча конфигом. ⚠ **Тест-тождество `8496 → 239 с` обязан пинить ФОРМУЛУ, а не число** — иначе через полгода его «починят» под красивую константу. ⛔ Расходится ровно ОДИН бит и он владельца: что уходит в сеттл — оценка или ноль; всё остальное одинаково во всех формах, поэтому пак пишется ДО его слова | бэкенд | блокер-очереди для ПЛАТНЫХ прогонов (класс съедает четверть-треть бюджета; $0-работа им не блокируется) | ошибка чтения не выбрасывается · таймаут отличён от нечитаемого тела и НЕ ретраится · денежная диспозиция ратифицирована владельцем · строка 331 закрыта тем же решением | замер полигона exp-23 п.5 + разбор Fable 5 08.09, пере-проверено оркестратором по коду | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b9ed5f99..e14226f2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -179,6 +179,18 @@ ## Бэкенд +#### ⚠ ПИНГ ОРКЕСТРАТОРА №23 (10.09) — ТРИ ЯКОРЯ `backend/docs/` УКАЗЫВАЮТ НЕ ТУДА; ЧИНИТЬ ВАМ, Я В ВАШУ ЗОНУ НЕ ЛЕЗУ + +Линтер `python3 docs/scripts/counts.py --lint` краснеет на `backend/docs/DISCLOSURE_LAW_DESIGN.md`. +Адреса пере-снял своим прибором (греп по тому же токену), чтобы вам не мерить заново: + +- строка **40** → `backend/internal/pipeline/quality.go:233` → **235**, токен `the UNSIGNED BANK count is unknown, not zero`; +- строка **41** → `backend/internal/pipeline/status.go:768` → **808**, токен `the unsigned-term count is unknown, not zero`; +- строка **42** → `backend/internal/pipeline/status.go:851` → **891**, токен `reported as unknown, not as none`. + +Два из трёх сдвинул мой лендинг `3f05fab` (пак оборванных вызовов правил `status.go`); первый уехал раньше и +не от меня. Токены живы во всех трёх целях — уехали только номера строк. + #### Пак «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ» (08.09, промт `docs/archive/prompts/BACKEND_CUT_CALLS_SESSION_PROMPT.md`, вход HEAD `b3b6bac`). ✅ **ЗАЛЕНДЕН 10.09 — `3f05fab`, 28 путей; принят актом `D39.232`** (строка «НЕ КОММИЧУ — ждёт лендинга» снята оркестратором по факту) **BASELINE НА ВХОДЕ, снят СВОИМ прогоном (§5.6), сходится со строкой оркестратора.** @@ -3259,6 +3271,19 @@ CJK-ONLY.** Сопоставление заголовков в интейке и ## Полигон +#### ⚠ ПИНГ ОРКЕСТРАТОРА №23 (10.09) — ПЯТЬ ЯКОРЕЙ В ВАШИХ ДОКАХ УКАЗЫВАЮТ НЕ ТУДА + +`docs/experiments/` — ваша зона, рукой не трогаю. Адреса пере-снял своим прибором по тому же токену: + +- `00-provider-quirks.md:163` → `backend/internal/llm/httpllm.go:517-522` → **739–751**, токен `SystemMessagesSingle`; +- `00-provider-quirks.md:164` → `backend/configs/models.yaml:137` → **173** (`system_messages: single`) + и `backend/internal/llm/httpllm.go:528` → **750** (`func toOpenAIMessages`); +- `23-editor-tier.md:5274` → тот же `models.yaml` → **173**; `:5276` → тот же `httpllm.go` → **750**. + +Все пять сдвинул мой лендинг `3f05fab` (пак оборванных вызовов правил `httpllm.go` и `models.yaml`). +Ещё два якоря `23-editor-tier.md:7739-7740` (`pipeline-c1.yaml:95`, `pipeline-arm-glm.yaml:6`) уехали +РАНЬШЕ и не от меня — цели я не трогал, токенов по прежним адресам там нет. + **📌 02.09 · ПИНГ ОРКЕСТРАТОРУ (два входа в бэкенд) + ИТОГ СЛЕПОГО ЧТЕНИЯ. Решения владельца от 02.09 внутри.** Носители: пре-реги и результаты — `eval/dovodka/blind-read/` (`PREREG-BLIND-READ.md` + `RESULT-BLIND-READ.md` с семью эрратами, `PREREG-2-BLIND-READ.md` + `RESULT-2-BLIND-READ.md`), прибор дрейфа — `eval/dovodka/dreif.py`. @@ -3377,7 +3402,7 @@ CJK-ONLY.** Сопоставление заголовков в интейке и вычисляется нашим же `roster.cost` (`buy.py:93-95`), а не приходит от вендора; на 213 клетках у четырёх вендоров из шести арифметика сходится до последнего знака, а «офф-пик = ½» в ЗАПИСАННЫХ ценах НЕ наблюдается. -**⚠ Пинг оркестратора №18 — 20.08 (протухшие цены в квирках, чинить вашей рукой).** `docs/experiments/00-provider-quirks.md` до сих пор утверждает, что цены после катовера НЕ изменились («flash $0.14/$0.28»). Это опровергнуто пере-пином 13–15.08 (D39.137): таблица запинена ПИКОМ — flash 0.44/1.32 при кэш-хите **0.014**, pro 1.32/3.96 при кэш-хите **0.044** за 1M (сверено с `backend/configs/models.yaml:182,198`=`cached_per_m` и телом D39.137 §2а), и счёт шиппинга = 100% DeepSeek ⇒ ×4.2–4.4 в пике. Квирки — ваша зона, рукой не трогаю; поправьте при ближайшем касании, потому что по ним считают деньги проб. ⚠ **ВТОРОЙ пункт этого пинга СНЯТ 05.09 как ЛОЖНОЕ ОТРИЦАНИЕ оркестратора.** Он утверждал, что факт про пиковые окна цен «в квирках ОТСУТСТВУЕТ (греп = 0)». Факт ТАМ ЕСТЬ (`docs/experiments/00-provider-quirks.md`, греп `ПИК DEEPSEEK`) — и несёт поправку, которой не было в моей копии: **пик только ПО БУДНЯМ, выходные целиком идут по офф-пику**. То есть оркестратор, живший по своей копии, отодвигал платное на вдвое дороже по выходным. Копия снята, единственный носитель — квирки. Отрицание было получено грепом, который не мог сработать; урок общий: **команда, на которой стоит отрицание, приводится в тексте, чтобы её мог воспроизвести другой.** ⚠ Третий пункт того же аудита (`eval/README.md` держит мёртвый СТОП на платные прогоны) сведён ниже, в пинг №20 — один носитель на факт. +**⚠ Пинг оркестратора №18 — 20.08 (протухшие цены в квирках, чинить вашей рукой).** `docs/experiments/00-provider-quirks.md` до сих пор утверждает, что цены после катовера НЕ изменились («flash $0.14/$0.28»). Это опровергнуто пере-пином 13–15.08 (D39.137): таблица запинена ПИКОМ — flash 0.44/1.32 при кэш-хите **0.014**, pro 1.32/3.96 при кэш-хите **0.044** за 1M (сверено с `backend/configs/models.yaml:221,237`=`cached_per_m` и телом D39.137 §2а), и счёт шиппинга = 100% DeepSeek ⇒ ×4.2–4.4 в пике. Квирки — ваша зона, рукой не трогаю; поправьте при ближайшем касании, потому что по ним считают деньги проб. ⚠ **ВТОРОЙ пункт этого пинга СНЯТ 05.09 как ЛОЖНОЕ ОТРИЦАНИЕ оркестратора.** Он утверждал, что факт про пиковые окна цен «в квирках ОТСУТСТВУЕТ (греп = 0)». Факт ТАМ ЕСТЬ (`docs/experiments/00-provider-quirks.md`, греп `ПИК DEEPSEEK`) — и несёт поправку, которой не было в моей копии: **пик только ПО БУДНЯМ, выходные целиком идут по офф-пику**. То есть оркестратор, живший по своей копии, отодвигал платное на вдвое дороже по выходным. Копия снята, единственный носитель — квирки. Отрицание было получено грепом, который не мог сработать; урок общий: **команда, на которой стоит отрицание, приводится в тексте, чтобы её мог воспроизвести другой.** ⚠ Третий пункт того же аудита (`eval/README.md` держит мёртвый СТОП на платные прогоны) сведён ниже, в пинг №20 — один носитель на факт. ⚠ **ВТОРОЙ пункт этого пинга ПЕРЕ-СНЯТ 05.09: спора нет и не было.** Он утверждал, что квирки и `docs/STACK.md` говорят «ручки эффорта у `deepseek-v4-pro` НЕТ» — оба говорят ОБРАТНОЕ (квирки §3г: «параметр `low` на `pro` ТЕПЕРЬ ДОХОДИТ ДО МОДЕЛИ», $0.038276, −79 prompt_tokens; `STACK.md:11` — то же). ⚠ Это был ЧЕТВЁРТЫЙ носитель фантомного противоречия, снесённого D39.200 п.4(а), и он пережил первую зачистку. Живой остаток один и он не спор: **вендор-сверка САМОГО МАППИНГА (`low` → что именно) и ВЕЛИЧИНА эффекта** — замера поведением после 13.08 нет ни у кого. Это боевой РЕДАКТОР и апекс эскалации. **Закрывается вендор-сверкой, а не выбором стороны** (гардрейл владельца) — и вы это только что делали по Gemini, то есть заход дешёвый. Носитель в трекере — строка **237**(б). diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 9d855837..52173137 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -30,8 +30,8 @@ > ⚠ **Эррата 27.08-е (D39.159 п.8, финал): `PD-398` ЗАКРЫТА — у построенного гейта появился пин.** `selftest_tail_vocab()` гоняется на каждом `--check`, четыре утверждения, проверен ПОСАДКОЙ трёх мутаций самого гейта (все три пойманы, базовая линия молчит). ⚠ Первая редакция пина молчала на одной из трёх: утверждение про вес проверяло `cells()`, а мутация меняет то, чем пользуется `register()`. Пин, проверенный одним прогоном вместо посадки, — ровно тот класс, который эта строка описывает; поймано только потому, что посадку сделал. Регистр 96 → 95 открытых. > ⚠ **Эррата 27.08-ж (D39.160 п.2) — ошибка ОРКЕСТРАТОРА, найденная исполнителем пака.** Нота утверждала, что после сноса отменённой двери «канон и деплой СОВПАДУТ точно», а промт минора (§3.2-бис) — что счётчики `pending_decisions`/`complete` «навсегда нули». **Верно по ПУТЯМ, неверно по ПОЛЯМ:** проекция `GET /bank` продолжает их слать, и это не нули — `bankCountsTx` (`platform/internal/pgstore/readmodel.go:330`) считает `proposed`-строки, о чём говорит её собственный комментарий. Клиент 0.5.0 лишние поля игнорирует, но аллоулист-норма нарушена до монтажа (2в). Носитель — `PD-399`. ⚠ Контрактная сессия принесла это ПИНГОМ по §11 промта, вместо того чтобы тихо подогнать работу под неверную посылку; это и есть поведение, которого норма требует. > ⚠ **Эррата 27.08-з (D39.156, состав пункта 2в): «воркер решений → глагол перед возобновлением» СНЯТ — посылка изменилась.** Пункт писался, когда двери в контракте не было и подразумевалось НАКОПЛЕНИЕ: платформа копит решения у себя и скармливает их движку перед `resume`. С дверью канона 0.5.0 накопления не существует — правка ПРИМЕНЯЕТСЯ в момент подачи, и гарантия «до возобновления» у синхронной формы СИЛЬНЕЕ воркерной: применено прежде, чем клиент получил `200`. Проверено исполнением с обеих сторон: движок на стопе ВЫХОДИТ (`cmd/tmctl/main.go:95`, код 3), флок не-блокирующий и отпускается ядром на выходе процесса (`store/store.go:187`), между стопом и возобновлением живого процесса на проекте нет — запинено `pipeline/bankchain_test.go:62-64`; кап 5000 решений выведен ИМЕННО из синхронности («the call stops fitting the caller's timeout»). Остаётся не воркер, а пер-книжная сериализация в обработчике. `Resume` «с решениями как они есть» не тронут. -> ⚠ **Эррата 28.08-и (D39.165 §3, размер мины) — ошибка ОРКЕСТРАТОРА, найденная опровергателем промта P10.** Нота утверждает: «первый же ПРОДОЛЖАЮЩИЙ прогон после первой же правки банка УПАДЁТ». **Переоценено.** Гард снапшота стреляет по СУЩЕСТВУЮЩЕМУ джобу (`backend/internal/pipeline/stagerun.go:47`=`r.Store.EnsureJob` — `EnsureJob` создаёт джоб стадии в момент, когда стадия впервые исполняется), а правка в ГЛАВНОМ окне — стоп подписи `awaiting_bank` — двигает edit-снапшот, когда edit-джобов ЕЩЁ НЕТ: возобновление создаёт их свежими, и гард молчит. Драфт-волна mined-строк не видит вовсе (`backend/internal/pipeline/bankmaterialize.go:321`=`if row.Source == "mined"` (⚠ адрес испр. 06.09; ФОРМУЛИРОВКА испр. по эррате 06.09-ж: переименования НЕ было — `seeding.go` жив, а код ВЫДЕЛЕН в `bankmaterialize.go` (`6ec9f8a` 29.08). Линт не мог угадать адрес по другой и более простой причине: прежний якорь был написан ГОЛЫМ именем, а такое имя отсеивается на `docs/scripts/counts.py:595`=`"~" in rel or "/" not in rel` ДО всякой проверки файла — нашёл `git log -S` по тексту якоря)). **Дефект СТОИТ, но его триггер уже: правка, сделанная ПОСЛЕ появления edit-джобов** — пауза потолком посреди редактуры и ДОЧИТАННАЯ книга. ⚠ Срочность при этом НЕ падает: флагманский случай продукта («поправил имя героя в дочитанной книге») — ровно тот, где edit-джобы существуют, то есть мина бьёт именно по нему. **Цена ошибки была бы прямой:** репро на потоке `awaiting_bank` показало бы ЗЕЛЕНЬ без фикса, и пак мог быть отозван как мнимый. Промт P10 §4.2 исправлен: репро обязано фиксировать состояние «edit-джоб существует ДО правки». -> ⚠ **Эррата 28.08-к (D39.165 §3 + решение оркестратора о глава-полосе) — ДВЕ ошибки, обе найдены широким самопроходом платформенной сессии, обе доказаны исполнением.** **(1) Посылка «смета УЖЕ публикуется в `status --json`» верна только ПОСЛЕ свёртки.** `bank-apply` пишет только ФАЙЛЫ решений, а `status` считает ре-билл от СОХРАНЁННОГО глоссария (`backend/internal/pipeline/status.go:962`=`seed-FILE edit` ⚠ (адрес испр. 06.09: цитата УЕХАЛА, не исчезла; в теле `D39.165` она осталась по прежнему адресу — тело ноты не переписывается, D23.3)``, `projectStoredMemory` — его собственный комментарий: «A seed-FILE edit not yet re-run is NOT reflected here… that drift surfaces on the next translate's re-seed»). Свёртка происходит внутри СЛЕДУЮЩЕГО `translate`, поэтому сразу после правки движок отвечает `units=0`/`drift=false`. Следствие: продажа «затронуто N юнитов» и холд от сметы В ТЕКУЩЕМ ШВЕ НЕДОСТИЖИМЫ — для них нужен движковый глагол «свернуть банк и оценить ВНЕ translate», которого нет. **(2) Решение оркестратора «полоса пере-прохода — в ГЛАВАХ» ОТМЕНЯЕТСЯ: его посылка опровергнута.** Я рассудил, что $0-репин двигает полосу, потому что идёт через тот же `resumeFromChunkStatus`, — и не проверил анонс. Движок анонсирует юнит ОДИН РАЗ на жизнь книги (announce-once, `backend/internal/pipeline/events.go:49,143-162`=`announce-once keys`), пере-проход не ре-анонсирует ни репины, ни пере-переводы ⇒ `done` остался бы НУЛЁМ навсегда. Это ровно тот класс, от которого предостерегает памятка «не выводить из соседнего механизма, не проверив свой». ⚠ **Что при этом НЕ отменяется:** запрет класть ЮНИТЫ в поле, объявленное в главах, стоит — но объявленная в каноне «одна единица работы» запретом не является, потому что она НЕ молчаливая. +> ⚠ **Эррата 28.08-и (D39.165 §3, размер мины) — ошибка ОРКЕСТРАТОРА, найденная опровергателем промта P10.** Нота утверждает: «первый же ПРОДОЛЖАЮЩИЙ прогон после первой же правки банка УПАДЁТ». **Переоценено.** Гард снапшота стреляет по СУЩЕСТВУЮЩЕМУ джобу (`backend/internal/pipeline/stagerun.go:48`=`r.Store.EnsureJob` ⚠ (адрес испр. 10.09: строку сдвинул лендинг `3f05fab`) — `EnsureJob` создаёт джоб стадии в момент, когда стадия впервые исполняется), а правка в ГЛАВНОМ окне — стоп подписи `awaiting_bank` — двигает edit-снапшот, когда edit-джобов ЕЩЁ НЕТ: возобновление создаёт их свежими, и гард молчит. Драфт-волна mined-строк не видит вовсе (`backend/internal/pipeline/bankmaterialize.go:321`=`if row.Source == "mined"` (⚠ адрес испр. 06.09; ФОРМУЛИРОВКА испр. по эррате 06.09-ж: переименования НЕ было — `seeding.go` жив, а код ВЫДЕЛЕН в `bankmaterialize.go` (`6ec9f8a` 29.08). Линт не мог угадать адрес по другой и более простой причине: прежний якорь был написан ГОЛЫМ именем, а такое имя отсеивается на `docs/scripts/counts.py:595`=`"~" in rel or "/" not in rel` ДО всякой проверки файла — нашёл `git log -S` по тексту якоря)). **Дефект СТОИТ, но его триггер уже: правка, сделанная ПОСЛЕ появления edit-джобов** — пауза потолком посреди редактуры и ДОЧИТАННАЯ книга. ⚠ Срочность при этом НЕ падает: флагманский случай продукта («поправил имя героя в дочитанной книге») — ровно тот, где edit-джобы существуют, то есть мина бьёт именно по нему. **Цена ошибки была бы прямой:** репро на потоке `awaiting_bank` показало бы ЗЕЛЕНЬ без фикса, и пак мог быть отозван как мнимый. Промт P10 §4.2 исправлен: репро обязано фиксировать состояние «edit-джоб существует ДО правки». +> ⚠ **Эррата 28.08-к (D39.165 §3 + решение оркестратора о глава-полосе) — ДВЕ ошибки, обе найдены широким самопроходом платформенной сессии, обе доказаны исполнением.** **(1) Посылка «смета УЖЕ публикуется в `status --json`» верна только ПОСЛЕ свёртки.** `bank-apply` пишет только ФАЙЛЫ решений, а `status` считает ре-билл от СОХРАНЁННОГО глоссария (`backend/internal/pipeline/status.go:1011`=`seed-FILE edit` ⚠ (адрес испр. 06.09 и 10.09 — второй раз строку сдвинул лендинг `3f05fab`: цитата УЕХАЛА, не исчезла; в теле `D39.165` она осталась по прежнему адресу — тело ноты не переписывается, D23.3)``, `projectStoredMemory` — его собственный комментарий: «A seed-FILE edit not yet re-run is NOT reflected here… that drift surfaces on the next translate's re-seed»). Свёртка происходит внутри СЛЕДУЮЩЕГО `translate`, поэтому сразу после правки движок отвечает `units=0`/`drift=false`. Следствие: продажа «затронуто N юнитов» и холд от сметы В ТЕКУЩЕМ ШВЕ НЕДОСТИЖИМЫ — для них нужен движковый глагол «свернуть банк и оценить ВНЕ translate», которого нет. **(2) Решение оркестратора «полоса пере-прохода — в ГЛАВАХ» ОТМЕНЯЕТСЯ: его посылка опровергнута.** Я рассудил, что $0-репин двигает полосу, потому что идёт через тот же `resumeFromChunkStatus`, — и не проверил анонс. Движок анонсирует юнит ОДИН РАЗ на жизнь книги (announce-once, `backend/internal/pipeline/events.go:49,143-162`=`announce-once keys`), пере-проход не ре-анонсирует ни репины, ни пере-переводы ⇒ `done` остался бы НУЛЁМ навсегда. Это ровно тот класс, от которого предостерегает памятка «не выводить из соседнего механизма, не проверив свой». ⚠ **Что при этом НЕ отменяется:** запрет класть ЮНИТЫ в поле, объявленное в главах, стоит — но объявленная в каноне «одна единица работы» запретом не является, потому что она НЕ молчаливая. > ⚠ **Эррата 29.08-а (D39.172, две строки, объявленные заведёнными) — ошибка ОРКЕСТРАТОРА №19.** Тело ноты дважды утверждает «Заведено строкой» / «строка заведена» — про `Touch`, выбрасывающий `RowsAffected`, и про пересборку `tmctl` в рецепте стенда. **На момент ратификации ни одной из этих строк не существовало:** последняя строка регистра платформы была `PD-430`, и проверка грепом по `Touch|RowsAffected|пересбор` давала только совпадения слов в чужих строках. Утверждение о будущем записано как о свершившемся — ровно тот класс, который эта же смена ловила у сессий трижды. **СНЯТА 29.08: строки заведены зоной — `PD-431` (`Touch`) и `PD-432` (пересборка `tmctl`), `PD-423` получил вторую точку. Проверено грепом по регистру.** Тело ноты не переписывается (D23.3). ⚠ Сюда же третий пункт того же абзаца: `PD-423` предписано ПЕРЕ-ПРОВЕРИТЬ (у сессии `sqlc` тест зелёный в трёх прогонах), и пометки в строке регистра тоже нет. > ⚠ **Эррата 30.08-б (D39.165 §2 и §3, две находки платформенной сессии P12).** (а) **Якорь протух:** §2 цитирует фразу канона о `chapters_done` по `openapi.yaml:1542-1545` — текст уехал на **`1554-1559`**, по прежним строкам сейчас `RejectReason`. Сама цитата верна дословно; нота не битая, битым стал только номер (класс строки бэклога 219). (б) **§3 назвал живое САМОПРОТИВОРЕЧИЕ канона и не дал ему носителя:** правка банка «takes effect on the NEXT run» (`openapi.yaml:504`) против «finished work is not bought twice» плюс подъём потолка только у ПРИОСТАНОВЛЕННОЙ книги (`:590`) — у дочитанной книги следующего прогона купить нечем, поэтому принятая правка умирает молча. Ни в едином бэклоге, ни в регистре платформы строки не было (проверено грепом по обеим фразам) — **заведена строка бэклога 241** 30.08. ⚠ Класс — «названо в теле ноты и не получило карриера»; норма приёмки требует строку ТЕМ ЖЕ лендингом, здесь она не легла два дня. > ⚠ **Эррата 30.08-а (D39.176 п.5, `PD-421`) — ошибка ОРКЕСТРАТОРА №20, пойманная ВЛАДЕЛЬЦЕМ.** Пункт вынесен владельцу с формулировкой «открытая вкладка теряет сессию примерно через час» — **неверно, и ровно тем классом, от которого предостерегает записка передачи: вывод из соседнего числа вместо проверки.** Час — это ПЕРИОДИЧНОСТЬ `SweepSessions`, а срок жизни сессии другой: `SessionIdleTTL` = **14 суток**, `SessionMaxAge` = **30** (`platform/internal/config/config.go:345-346`=`defaultSessionIdle`), причём окно бездействия скользит на любом аутентифицированном запросе (`platform/internal/auth/middleware.go:66`=`a.Sessions.Touch(r.Context(), digest`; поток `Touch` не зовёт — грепом 0, и это не важно при окне в две недели). Реальное следствие `PD-421` — то, что и записано в её теле: сессия, протухшая по бездействию И подметённая, гасит поток с опозданием до часа, то есть после ДВУХ НЕДЕЛЬ без единого действия. **Вопрос СНЯТ с листа владельца как несущественный; правка в паке P12 не нужна, строка остаётся `info`.** Владелец вопросом «ты уверен или придумываешь?» поймал ошибку до того, как она стала заказом на работу. diff --git a/docs/architecture/15-money-path.md b/docs/architecture/15-money-path.md index 47b74068..79b1adf6 100644 --- a/docs/architecture/15-money-path.md +++ b/docs/architecture/15-money-path.md @@ -19,9 +19,9 @@ - **Гейт потолков — ПЕР-ВЫЗОВНЫЙ** (`Reserve` на каждый свежий attempt). На границе юнита сидел РЕПЭЙР-суб-бюджет — ужесточён до пер-вызовного с ценой вызова (строка 135 закрыта D39.131); эскалационный кап хоп НЕ прицениваает — перелёт ≤1 хопа, задокументирован и запинен (диспозиция D39.131 п.2д, реопен — живой инцидент). ⚠ **ТРЕТЬЕ семейство, которого перечень не знал (доп. ревизией 02.09): банк-роли несут СОБСТВЕННЫЕ КНИГО-ШИРОКИЕ бюджеты** — `gates.terminology.budget_usd` и `gates.terminology.classify_budget_usd`. Именно оно резало ОПЛАЧЕННУЮ работу на холодном прогоне 31.08 (D39.182 §4: инцидент был на классификаторе). С 31.08 (D39.182) план прохода режется ценой партии ДО первого вызова, а не обрывается посередине; усечение ВИДНО в отчёте — поля `BatchesDropped` и `ClassifyBatchesDropped` (`backend/internal/pipeline/terminologist.go`, греп `BatchesDropped`). ⚠⚠ **И сами цифры этих суб-бюджетов в книжных конфигах КАЛИБРОВАНЫ ПОД ИЮЛЬСКИЕ ЦЕНЫ** (тот же множитель ×4.47, D39.179 п.1): на холодном прогоне 31.08 `classify_budget_usd` 0.02 оборвал классификатор дважды, а поднятый до 0.08 `escalation.budget_usd` был пробит фактом до 0.103305. То есть суб-бюджеты режут ОПЛАЧЕННУЮ работу не по замыслу, а по протухшей калибровке. - **leftover-reserved зануляется write-open.** `store.Open` (путь записи, каждый `translate`) выполняет `recoverReservations` (`backend/internal/store/store.go:110`=`s.recoverReservations(ctx)`; сама функция — `backend/internal/store/store.go:278-279`=`UPDATE spend SET reserved_usd = 0`) — файл владеется одним процессом, значит любой reserved на открытии принадлежит несеттлённому прогону. `OpenReadOnly` этого прохода намеренно НЕ делает (`backend/internal/store/store.go:124`=`does not run that pass`) ⇒ **reserved, увиденный read-only `status` В МОМЕНТ СПАВНА, — остаток мёртвого процесса** (несущий факт формулы PD-158, см. §3). ⚠ Но НЕ «всегда»: `OpenReadOnly` построен ровно затем, чтобы `status` работал ВО ВРЕМЯ живого прогона (`backend/internal/store/store.go:122`=`allows concurrent readers while a writer is live`), и конкурентный `status` покажет ЖИВУЮ резервацию между `Reserve` и settle. Узко формулирует и сам код: «after a run crashes, reserved_usd stays non-zero until the next WRITE command» (`backend/internal/store/store.go:130`=`after a run crashes, reserved_usd stays non-zero`), и платформа — «at spawn there is no other writer … so anything reserved is by construction a leftover, never a live promise» (`platform/internal/runs/spawn.go`, греп `never a live promise`). - **`--max-units` — ОБЪЁМНЫЙ потолок прогона, ортогональный денежному** (D39.165 §1б, принят D39.170). Ограничивает не деньги, а РАБОТУ: не больше N выходных ЮНИТОВ (гранулярность `units_total` манифеста — та же, в которой платформа продаёт главы) возьмут СЛОТ гранта в этом прогоне; юниты, отданные за $0 (резюм, ре-пин), ретраи и эскалации внутри юнита потолок не тратят. ⚠ **«Слот» ≠ «оплата», и с 03.09 это РАЗНЫЕ числа** (пак «число согласия», строка бэклога 232): юнит, который прежний прогон НАЧАЛ и не отгрузил, дописывается ВНЕ гранта, поэтому грант N оплачивает ДО 2N выходных юнитов — замерено приёмкой на живом раннере (грант 2 → `Paid()=4`, шесть вызовов провайдера). Единственный денежный бонд здесь — `--ceiling-usd`. ⛔ **Семантика переноса НЕ ратифицирована — слово владельца 03.09 «подумаем на этот счёт»**, и до его решения читать это как замеренный факт, а не как норму. Принимает только `translate`. **Остановка по объёму — ЗАВЕРШЕНИЕ (exit 0), не пауза:** словарь кодов выхода не расширялся и нового значения `Finished.Outcome` тоже нет — ⚠ **но с 31.08 признак едет ЧИСЛАМИ в кадре `finished` шва, а не только прозой** (D39.181 п.2, закон раскрытия: прозаическая строка отчёта до потребителя потока не доезжала): носитель — `Finished.Volume`, леджер доставки. Различение при этом живёт и в отчёте прогона, и в логе; отчёт разводит ДОСТАВКУ и ПЕРЕ-ДЕЛКУ. Носитель — `backend/internal/pipeline/volume.go:13`=`the VOLUME ceiling — the run's second stop`; словарь флага дословно — `backend/cmd/tmctl/invocation.go:143`=`Stopping on it is a COMPLETION (exit 0), not a pause`. ⚠ Проводка в платформу ГЕЙЧЕНА (`PD-422`): единственный писатель признака движения банка — дверь правок, рост АВТО-банка от майнинга флага не ставит. -- **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment…» (`backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`; ⚠-коммент `backend/cmd/tmctl/main.go:260`=`--ceiling-usd is NOT a per-run budget`; многоточие закрывает обрыв цитаты — во флаге дальше стоит «, and must be > 0»). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:495`=`BookUSD: r.bookCeilingUSD()` и `backend/internal/pipeline/stagerun.go:559`=`overrides the book's ceilings.book_usd` (стоп = `errReserveCeiling`, `backend/internal/pipeline/escalation.go:49`=`var errReserveCeiling`). +- **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment…» (`backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`; ⚠-коммент `backend/cmd/tmctl/main.go:260`=`--ceiling-usd is NOT a per-run budget`; многоточие закрывает обрыв цитаты — во флаге дальше стоит «, and must be > 0»). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:571`=`BookUSD: r.bookCeilingUSD()` и `backend/internal/pipeline/stagerun.go:653`=`overrides the book's ceilings.book_usd` (стоп = `errReserveCeiling`, `backend/internal/pipeline/escalation.go:49`=`var errReserveCeiling`). - **Потолки — wiring, не семантика:** `Ceilings` намеренно исключены из `BriefHash` («Wiring fields (paths, ceilings, db) deliberately excluded», `backend/internal/config/book.go:345`=`Wiring fields (paths, ceilings, db) deliberately excluded`; канон — `backend/internal/config/book.go:369`=`canon := struct {`) ⇒ смена ДЕНЕЖНОГО потолка не двигает ни снапшот, ни ре-билл (D39.110 п.2б). ⚠ Верно ровно про ДЕНЬГИ: потолок СЕГМЕНТАЦИИ `edit_ceiling_out` в снапшот ВХОДИТ и меняет границы чанков (`backend/internal/pipeline/snapshot.go:34`=`EditCeilingOut int`). -- **`status --json` отдаёт фигуры платформе:** `committed_usd`, `reserved_usd`, `book_ceiling_usd`, `ceiling_pct` = 100·(committed+reserved)/book_ceiling (`backend/internal/pipeline/status.go:311`=`book_ceiling_usd,omitempty`; арифметика `ceiling_pct` — `backend/internal/pipeline/status.go:869`=`100 * (committed + reserved)`). Дневной фигуры в status нет (PD-157 — `platform/docs/DEFECT_REGISTER.md`, греп `PD-157`). +- **`status --json` отдаёт фигуры платформе:** `committed_usd`, `reserved_usd`, `book_ceiling_usd`, `ceiling_pct` = 100·(committed+reserved)/book_ceiling (`backend/internal/pipeline/status.go:324`=`book_ceiling_usd,omitempty`; арифметика `ceiling_pct` — `backend/internal/pipeline/status.go:918`=`100 * (committed + reserved)`). Дневной фигуры в status нет (PD-157 — `platform/docs/DEFECT_REGISTER.md`, греп `PD-157`). - **Леджер = НИЖНЯЯ граница** (строка 78 бэклога, `docs/BACKLOG.md`, греп `Леджер денег = НИЖНЯЯ граница`): «2xx body decode failed (call IS billed)» — провайдер списал, попытка в `request_log` не попадает; живой замер D39.86 — 3 вызова из 14, неизвестность $0.015111 при леджере $0.114378; движок сеттлит оценку и печатает `estimated-cost rows: 3`. Канал виден, фикс — money-паком (дизайн GENERALITY_PHASE2 §5.6). - **Согласие на пере-оплату — ДВИЖКОВЫЙ гейт**. Прогон, который пере-покупает уже оплаченные юниты дороже порога, ОСТАНАВЛИВАЕТСЯ и требует явного согласия. Порог по умолчанию ратифицирован D20.2-Q2: `min($0.50, 5% × ProjectedBookUSD)`. Носитель — `backend/internal/pipeline/rebill.go`, греп `rebillConsentThreshold`; единственная ручка книги — `rebill_consent_usd` (валидатор `backend/internal/config/book.go`, греп `rebill_consent_usd`; 0 = ратифицированный дефолт). ⚠ Порог считается БЕЗ контура банк-ролей (D39.182 §3), то есть срабатывает позже, чем подсказывает полная проекция.