Return the refused retry to the standard stop instead of inventing an outcome: the position gets a mark, the frame already carries the shortfall, and a top-up resume finishes it

This commit is contained in:
heaven 2026-09-11 22:16:37 +03:00
parent baa06cef7b
commit b0cb7a40f2

View file

@ -56,110 +56,80 @@
## 4. Состав
### 4.1 Деградация — **делай РОВНО так в ЧАСТИ РЕШЕНИЯ, сам решай КАК**
### 4.1 ⛔ КОНСТРУКЦИЯ ПЕРЕ-РЕШЕНА ВЛАДЕЛЬЦЕМ 11.09 — читай это ПЕРЕД кодом
**Ровно так (решено владельцем, отступление = пинг):**
- отказ резервации ⇒ **флаг**, а не ошибка наверх — ⛔ **но предикат НЕ «индекс попытки ≥1»: эта
формулировка была моей, и она теряла бы юниты НАВСЕГДА** (поймала исполняющая сессия, я пере-проверил
кодом). Индекс попытки ПЕРЕ-ПРИСВАИВАЕТСЯ прогулкой по сожжённым ключам
(`backend/internal/pipeline/stagerun.go:201`=`attempt = att.attempt`; правило —
`stagerun.go:519`=`BURNED KEYS ARE WALKED OVER HERE`), поэтому после остановленного прогона ПЕРВАЯ
свежая покупка позиции уже идёт под индексом ≥1, а в руках нет ни текста, ни классификации. Флаг там
написал бы вердикт юниту, которого ни разу не перевели, — и поскольку `resolvedForResume` истинна для
любого флага, кроме `cancelled` (`cutcall.go:293`=`FlagReason(cs.FlagReason) != FlagCancelled`), юнит
стал бы терминальным вместе со сожжёнными деньгами. ⇒ **предикат: «в руках есть ОПЛАЧЕННАЯ
КЛАССИФИЦИРОВАННАЯ попытка»** (есть `judged`/`last`), а не «индекс ≥1»;
- у флага **СВОЯ причина**, отличная от исчерпания регенераций: читатель и оператор обязаны отличать «мы
перепробовали всё» от «денег не хватило на вторую попытку»;
- волна **идёт дальше**.
**Слово владельца дословно:** «Надо о таком просто сообщать в виде метаданты, которая сейчас и пишется
предупреждением, и иметь возможность поднять просто потолок и возобновить прогон, так как **это ситуация
всё же СТАНДАРТНАЯ СО СТОПОМ, просто он произошёл по какой-то другой причине**».
**ЧЕГО В ЭТОМ СПИСКЕ БОЛЬШЕ НЕТ И ПОЧЕМУ — читай, это главное место пака.** Первая редакция промта
требовала, вслед за `D39.204` п.4, чтобы «текст попытки 0 **отгружался**». **Это невыполнимо как написано,
и поймал это опровергатель, а не я.** В цикл ретрая попадают ТОЛЬКО флагованные попытки ретраебельных
причин — `length`/`empty` плюс эхо-реролл (`backend/internal/pipeline/disposition.go`, греп `retryable`).
Значит «оплаченный текст попытки 0» **по построению** обрезан, пуст либо является эхом исходника. А
флагованная диспозиция (кроме `FlagSanitizerStripped`) оставляет пустой финальный текст, следующая стадия
скипается, и экспорт не отдаёт ничего — `backend/internal/pipeline/holes.go` называет это `withheld` прямо:
«contaminated output never ships» (`D2`).
**Это отменяет обе половины прежнего заказа — и моего, и `D39.204` п.4.** Ни «деградировать во флаг», ни
«волна идёт дальше» больше НЕ строится. Прежний план заводил новый исход; новый — **возвращает ситуацию в
УЖЕ ПОСТРОЕННЫЙ штатный путь остановки**, из которого она сегодня выпадает.
⇒ **настоящий выигрыш пака формулируется так, и он не меньше прежнего, а честнее:**
**невидимая дыра `pending`** (строки нет · юнит заклинен · `exit 4`) **→ НАЗВАННАЯ дыра `withheld`**
(строка есть · причина денежная и названа · волна идёт · прогон кончается штатно).
⛔ **ОТВЕТ ВЛАДЕЛЬЦА ПОЛУЧЕН 11.09, и он снимает половину вопроса: «пока платит за всё ЮЗЕР, потом будем
решать эту проблему».** ⇒ **ТВОЙ ПАК НЕ МЕНЯЕТ, КОМУ ЧТО ВЫСТАВЛЯЕТСЯ, НИ В КАКУЮ СТОРОНУ.** Никакого
механизма «платит продукт» не строить; существующее биллинговое поведение не трогать даже там, где оно
выглядит непоследовательным. Предмет пака — **ВИДИМОСТЬ дыры, а не её цена.**
**Что это значит механически (моё чтение; спорь, если код скажет иначе):**
⚠ И назови это в отчёте отдельной строкой, чтобы следующая смена не открывала заново: объёмный леджер
СЕГОДНЯ считает флагованный юнит НЕ доставленным, то есть грант за него не съеден — это **расходится** со
словом «платит за всё юзер» и существует ДО твоего пака. **Не чини, не оптимизируй, просто НАЗОВИ**:
владелец объявил вопрос отложенным.
1. **Прогон ОСТАНАВЛИВАЕТСЯ, как при любом отказе по потолку.** Не продолжается с флагом. ⭐ И это заодно
снимает то, чем ряд 291 пугал: «продолжить волну нельзя без ДЫРЫ в непрерывном префиксе» — при остановке
дыры не возникает вовсе.
2. **Отказ ретраю публикует ту же метадату, что и любая остановка по потолку** — кадр
`Ceiling{Halted, Scope, ShortfallMicroUSD}` (`backend/internal/runevents/runevents.go`, греп
`ShortfallMicroUSD`). ⭐ **Он уже несёт ровно то, что владелец просит:** «пополни хотя бы на столько, и
тот же вызов будет допущен» — и это **ратифицировано им же, `D39.203`**. Строить новый носитель не надо.
**И твоя находка №2 этим РАЗРЕШАЕТСЯ, а не отменяется:** ты была права, что `ceilingReached`
идемпотентен по прогонному флагу и съел бы событие настоящей остановки — но теперь съедать НЕЧЕГО: эта
остановка И ЕСТЬ настоящая. Публикуй через штатный путь.
3. ⛔ **НАСТОЯЩИЙ ДЕФЕКТ — УЖЕ, чем я написал, и он назван в самом дереве.** Механизм «отметить
остановленную позицию» построен ИМЕННО против невидимых дыр:
`backend/internal/pipeline/stagerun.go:142`=`THE MARK FOR A STOPPED POSITION IS ATTACHED TO EVERY EXIT`,
и его комментарий приводит замер прежнего инцидента — «прогон, остановленный на эскалационном хопе,
рассчитал деньги и не оставил строки вовсе: `committed=0.001176`, `chunk_status_rows=0`». **Но метка —
no-op для всего, что не «доставленный вызов, который остановил человек»** (`recordCancelledStage`).
Отказ по потолку под это не подпадает ⇒ юнит остаётся без строки. **Вот дыра, и вот что ты закрываешь.**
4. **Поднял потолок → резюм ДОДЕЛЫВАЕТ юнит.** Значит строка, которую ты пишешь, обязана быть
НЕ-ТЕРМИНАЛЬНОЙ для резюма — по образцу `FlagCancelled`, единственной причины, у которой
`resolvedForResume` ложен (`backend/internal/pipeline/cutcall.go:293`=`FlagReason(cs.FlagReason) != FlagCancelled`),
и ложен именно потому, что это «оплачено, но не сделано». Твой случай той же природы.
⚠ Отгрузка обрезанного текста остаётся невыполнимой по `D2` независимо от того, кто платит. Сам не строй.
**Делай РОВНО так:** остановка штатная · метадата через уже построенный кадр с недостачей · **юнит получает
строку, а не пустоту** · строка НЕ терминальна для резюма · после пополнения резюм доделывает.
**И ВТОРОЕ, без чего пак сделает читателю хуже: ДЕНЬГИ ОБЯЗАНЫ ОСТАТЬСЯ ВИДНЫ.** Сегодня отказ ретрая
публикует денежный факт: событие `ceiling` + защёлка + `exit 4`, и платформа пишет прогону `paused` и
просит пополнить. После деградации этот путь исчезает: терминальные ветки `flagged` и `clean` несут только
объём, а `Money` висит на ошибочных (`backend/internal/pipeline/events.go`, греп `terminal`). **Читателю
кончились деньги — и никто ему этого не говорит.** ⛔ **НО НЕ ЧЕРЕЗ `ceilingReached` — развилку разрешила исполняющая сессия, и её довод сильнее моего
первоначального «публикуй».** Я пере-проверил кодом: событие идемпотентно по ПРОГОННОМУ флагу
(`backend/internal/pipeline/events.go:454`=`already := e.ceilingSaid`), поэтому отказ ретраю **СЪЕЛ БЫ
событие настоящей остановки** позже в том же прогоне, и платформа осталась бы с остановкой без причины.
Плюс `Halted: true` переводит идущий прогон в паузу НА ХОДУ. ⇒ **делай РОВНО так: денежный факт виден
НОВОЙ ПРИЧИНОЙ на проводе юнита плюс операторской строкой с недостачей (`shortfall`); событие
`ceiling`-остановки НЕ публикуется.** Довод записывается и в код, и в отчёт.
**Решаешь сам и аргументируешь:** как назвать причину в строке юнита (или взять существующую) · где ставится
метка · как отличить «остановлено на ретрае» от «остановлено на попытке 0» в носителе, и надо ли отличать.
**И зеркало, которое твой пак делает ложным В СТОРОНУ ОТСУТСТВИЯ:** `backend/internal/runevents/runevents.go:289`=`ITS PRESENCE RULE IS`
говорит «правило присутствия — „потолок был ДОСТИГНУТ“, а не „исход = ceiling“». После пака потолок бывает
достигнут, а `Money` нет. ⇒ сузь прозу до «ОСТАНОВИЛ прогон» **в своей зоне** и **назови мне зеркала вне
её** (платформенный приёмник событий, канон 14) — я отнесу. Чужое не правь.
**Попытку 0 не трогать:** её отказ и сегодня останавливает книгу штатно — ратифицировано.
**Решаешь сам и аргументируешь:** имя причины · где ставится деградация. ⛔ **И вот ШТАТНЫЙ носитель,
который первая редакция промта пропустила:** у `runAttempt` уже есть параметр **`mandatory`** — «позволено
ли отказанной резервации ЖДАТЬ освободившихся денег»; волна зовёт его с `true`, а эскалационный хоп,
под-шаг ремонта и терминология зовут с `false` и потому деградируют (`backend/internal/pipeline/stagerun.go`,
греп `mandatory`; правило — `reservegate.go`, греп `waitNotAllowed`). Это ровно тот класс, под который
механизм построен. ⚠ **Но у него есть цена, и её надо назвать прежде, чем брать:** `mandatory=false`
отнимает не только остановку, но и ПРАВО ЖДАТЬ — ретрай деградирует даже там, где деньги освободились бы
через миллисекунду сеттлом соседа. **Остановка и ожидание — два решения на одном флаге.**
**КТО ПЛАТИТ — НЕ ПРЕДМЕТ ЭТОГО ПАКА.** Слово владельца 11.09: «пока платит за всё ЮЗЕР, потом будем
решать эту проблему». Механизм «платит продукт» не строить; существующее биллинговое поведение не трогать
ни в какую сторону, даже где оно выглядит непоследовательным. ⚠ И отдельной строкой отчёта НАЗОВИ (не чини):
объёмный леджер сегодня считает флагованный юнит НЕ доставленным, то есть грант за него не съеден — это
расходится со словом владельца и существует ДО твоего пака. Он объявил вопрос отложенным.
**Попытку 0 не трогать:** её отказ по-прежнему ОСТАНАВЛИВАЕТ книгу — ратифицировано, менять нельзя.
**Гипотеза, которую надо ПРОВЕРИТЬ, а не принять:** деградация сама по себе закрывает и следствие (2) —
у юнита появляется статус, значит дыры в непрерывном префиксе нет. Похоже на правду; предъяви замером.
**Отгрузка обрезанного текста не строится**`D2` запрещает отгружать загрязнённое, и это не зависит от
того, кто платит.
### 4.2 Пин, ради которого пак существует
**«Купленное доставлено ⇒ не `exit 4`».** Прогон, где каждый купленный юнит доставлен, а один из них
доставлен ФЛАГОВАННЫМ из-за нехватки денег на ретрай, **обязан кончиться `exit 2`** (флаги есть, но прогон
завершён), **а не `exit 4`**. ⚠ Не «успехом»: `exit 0` требует, чтобы флагов не было вовсе, а флаг здесь
есть по построению — это и есть честный исход.
**«ОСТАНОВКА, КОТОРАЯ ЛЕЧИТСЯ ПОПОЛНЕНИЕМ».** Три утверждения, и они про разное:
**И заточка, которую дала исполняющая сессия, — она идёт В АКТ:** лечение клина НЕ зависит от того,
кончился прогон кодом 2 или 4. Даже когда потолок исчерпан совсем и попытка 0 следующего юнита законно
останавливает книгу, у деградированного юнита **всё равно появляется строка** — то есть следствие (1)
закрыто и в окне `exit 4`. **Пин ниже стережёт более узкое окно, чем сам выигрыш**, и это надо сказать, а
не выдать окно пина за границу лечения.
1. **Юнит, на ретрае которого отказал потолок, ПОЛУЧАЕТ СТРОКУ.** Сегодня её нет — это и есть невидимая
дыра. Пин обязан печатать величину, доказывающую, что деньги были потрачены, а строка появилась: пустой
`chunk_status` рядом с ненулевым `committed` — ровно тот замер, которым дерево описывает прошлый
инцидент.
2. **Остановка несёт НЕДОСТАЧУ.** `ShortfallMicroUSD` ненулевой и таков, что пополнение на него допускает
тот же вызов. ⚠ Пин на «событие вообще есть» вырожден — утверждай ЧИСЛО.
3. **Пополнил потолок → резюм ДОДЕЛАЛ юнит.** Два прогона в одном тесте: без пополнения резюм не делает
свежих платных вызовов и не уходит в бесконечный цикл; после пополнения — доделывает и отдаёт текст.
⛔ Это и есть главный пин пака: сегодня второй прогон умирает вечно.
**И назови СУЖЕНИЕ, иначе будешь ловить недостижимое:** утверждение верно в ОКНЕ, когда остатка потолка
хватает на попытку 0 каждого следующего юнита. Кончился потолок совсем — попытка 0 следующего юнита
обязательна, отказ по ней штатно останавливает книгу, и `exit 4` возвращается ЗАКОННО. Фикстуру строй
именно на этой развилке.
⛔ **ПИН НА РЕЗЮМ — И ЗДЕСЬ ПЕРВАЯ РЕДАКЦИЯ ПРОМТА ЗАПИНИЛА БЫ ОТМЕНУ РАТИФИЦИРОВАННОГО, не заметив.**
Я требовал «после такого прогона резюм не делает НИ ОДНОГО свежего платного вызова». Но денежный флаг —
единственная причина, которую ЛЕЧАТ ДЕНЬГАМИ: сегодня «поднял потолок → резюм → читатель получил хороший
перевод» ратифицировано (`backend/internal/pipeline/escalation.go`, греп `durably pauses and resumes`,
`D4`). Мой пин сделал бы юнит навсегда деградированным, лечимым только редрайвом.
**делай РОВНО так: денежная причина ПЕРЕ-АТАКУЕМА на резюме** — как `FlagCancelled`, единственная
причина, у которой `resolvedForResume` ложен именно потому, что это «оплачено, но не сделано»
(`backend/internal/pipeline/cutcall.go`, греп `resolvedForResume`). Пинь ДВЕ вещи: **без поднятия потолка
резюм не делает свежих платных вызовов и не умирает** · **после поднятия потолка резюм ДОДЕЛЫВАЕТ юнит**.
**Про коды выхода.** Прежняя редакция промта требовала `exit 2` вместо `exit 4`. **Снято:** при штатной
остановке код остаётся тем, каким он и был для остановки по потолку. Предмет пака — не смена кода выхода, а
то, что за ним стоит: **строка вместо пустоты и лечимость пополнением.**
**Два гейта поймают тебя сами — назову, чтобы не терять круг:** `TestEveryFlagReasonIsRanked` потребует
завести новую причину в `flagSeverity` (`backend/internal/pipeline/status.go`), а
завести новую причину в `flagSeverity` (`backend/internal/pipeline/status.go`), если ты её заводишь, а
`TestEveryOperatorMessageIsCatalogued` — внести новое сообщение в
`backend/internal/pipeline/testdata/operator-messages.txt`.
`backend/internal/pipeline/testdata/operator-messages.txt` (сегодня в каталоге 134 строки, из них о потолке
уже говорят четыре — посмотри их прежде, чем писать пятую).
### 4.3 ⛔ ДЕФЕКТ, КОТОРЫЙ ТВОЙ ПАК НАКОРМИТ — и он НЕ латентный. Чинить ЗДЕСЬ