diff --git a/docs/BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md b/docs/BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md index 9c47f8f0..bec013d4 100644 --- a/docs/BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md +++ b/docs/BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md @@ -60,12 +60,42 @@ ошибка наверх; - у флага **СВОЯ причина**, отличная от исчерпания регенераций: читатель и оператор обязаны отличать «мы перепробовали всё» от «денег не хватило на вторую попытку»; -- текст попытки 0 **отгружается**; - волна **идёт дальше**. -**Решаешь сам и аргументируешь:** имя причины · где именно ставится деградация (в цикле `runStage` или -внутри `runAttempt` — у обоих есть довод, назови свой) · как при этом НЕ тронуть попытку 0: её отказ -по-прежнему ОСТАНАВЛИВАЕТ книгу, это ратифицировано и менять нельзя (`stagerun.go`, греп `mandatory`). +⛔ **ЧЕГО В ЭТОМ СПИСКЕ БОЛЬШЕ НЕТ И ПОЧЕМУ — читай, это главное место пака.** Первая редакция промта +требовала, вслед за `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`). + +⇒ **настоящий выигрыш пака формулируется так, и он не меньше прежнего, а честнее:** +**невидимая дыра `pending`** (строки нет · юнит заклинен · `exit 4`) **→ НАЗВАННАЯ дыра `withheld`** +(строка есть · причина денежная и названа · волна идёт · прогон кончается штатно). +⚠ **Отгрузка обрезанного текста — ОТДЕЛЬНОЕ продуктовое решение, и я задал его владельцу.** Придёт «да» — +доедет релеем отдельным сообщением, и ты подтвердишь его эхом. Сам не строй. + +⛔ **И ВТОРОЕ, без чего пак сделает читателю хуже: ДЕНЬГИ ОБЯЗАНЫ ОСТАТЬСЯ ВИДНЫ.** Сегодня отказ ретрая +публикует денежный факт: событие `ceiling` + защёлка + `exit 4`, и платформа пишет прогону `paused` и +просит пополнить. После деградации этот путь исчезает: терминальные ветки `flagged` и `clean` несут только +объём, а `Money` висит на ошибочных (`backend/internal/pipeline/events.go`, греп `terminal`). **Читателю +кончились деньги — и никто ему этого не говорит.** ⇒ **делай РОВНО так:** прогон, отказавший только РЕТРАЮ, +**по-прежнему публикует денежный факт** — либо явно НЕ публикует, и тогда довод записан в отчёте и в коде. +⚠ И рядом живёт комментарий, который твой пак сделает ложным, если факт пропадёт: `runevents.go`, греп +`ITS PRESENCE RULE` — «правило присутствия — „потолок был достигнут“, а не „исход = ceiling“». + +**Решаешь сам и аргументируешь:** имя причины · где ставится деградация. ⛔ **И вот ШТАТНЫЙ носитель, +который первая редакция промта пропустила:** у `runAttempt` уже есть параметр **`mandatory`** — «позволено +ли отказанной резервации ЖДАТЬ освободившихся денег»; волна зовёт его с `true`, а эскалационный хоп, +под-шаг ремонта и терминология зовут с `false` и потому деградируют (`backend/internal/pipeline/stagerun.go`, +греп `mandatory`; правило — `reservegate.go`, греп `waitNotAllowed`). Это ровно тот класс, под который +механизм построен. ⚠ **Но у него есть цена, и её надо назвать прежде, чем брать:** `mandatory=false` +отнимает не только остановку, но и ПРАВО ЖДАТЬ — ретрай деградирует даже там, где деньги освободились бы +через миллисекунду сеттлом соседа. **Остановка и ожидание — два решения на одном флаге.** + +⛔ **Попытку 0 не трогать:** её отказ по-прежнему ОСТАНАВЛИВАЕТ книгу — ратифицировано, менять нельзя. ⚠ **Гипотеза, которую надо ПРОВЕРИТЬ, а не принять:** деградация сама по себе закрывает и следствие (2) — у юнита появляется статус, значит дыры в непрерывном префиксе нет. Похоже на правду; предъяви замером. @@ -73,10 +103,29 @@ ### 4.2 Пин, ради которого пак существует **«Купленное доставлено ⇒ не `exit 4`».** Прогон, где каждый купленный юнит доставлен, а один из них -доставлен ФЛАГОВАННЫМ из-за нехватки денег на ретрай, **обязан кончиться успехом**, а не кодом 4. +доставлен ФЛАГОВАННЫМ из-за нехватки денег на ретрай, **обязан кончиться `exit 2`** (флаги есть, но прогон +завершён), **а не `exit 4`**. ⚠ Не «успехом»: `exit 0` требует, чтобы флагов не было вовсе, а флаг здесь +есть по построению — это и есть честный исход. -⛔ И **пин на саму незаклиниваемость**: после такого прогона резюм не делает НИ ОДНОГО свежего платного -вызова по этому юниту и не умирает. Сегодня он умирает вечно. +⛔ **И назови СУЖЕНИЕ, иначе будешь ловить недостижимое:** утверждение верно в ОКНЕ, когда остатка потолка +хватает на попытку 0 каждого следующего юнита. Кончился потолок совсем — попытка 0 следующего юнита +обязательна, отказ по ней штатно останавливает книгу, и `exit 4` возвращается ЗАКОННО. Фикстуру строй +именно на этой развилке. + +⛔ **ПИН НА РЕЗЮМ — И ЗДЕСЬ ПЕРВАЯ РЕДАКЦИЯ ПРОМТА ЗАПИНИЛА БЫ ОТМЕНУ РАТИФИЦИРОВАННОГО, не заметив.** +Я требовал «после такого прогона резюм не делает НИ ОДНОГО свежего платного вызова». Но денежный флаг — +единственная причина, которую ЛЕЧАТ ДЕНЬГАМИ: сегодня «поднял потолок → резюм → читатель получил хороший +перевод» ратифицировано (`backend/internal/pipeline/escalation.go`, греп `durably pauses and resumes`, +`D4`). Мой пин сделал бы юнит навсегда деградированным, лечимым только редрайвом. +⇒ **делай РОВНО так: денежная причина ПЕРЕ-АТАКУЕМА на резюме** — как `FlagCancelled`, единственная +причина, у которой `resolvedForResume` ложен именно потому, что это «оплачено, но не сделано» +(`backend/internal/pipeline/cutcall.go`, греп `resolvedForResume`). Пинь ДВЕ вещи: **без поднятия потолка +резюм не делает свежих платных вызовов и не умирает** · **после поднятия потолка резюм ДОДЕЛЫВАЕТ юнит**. + +⚠ **Два гейта поймают тебя сами — назову, чтобы не терять круг:** `TestEveryFlagReasonIsRanked` потребует +завести новую причину в `flagSeverity` (`backend/internal/pipeline/status.go`), а +`TestEveryOperatorMessageIsCatalogued` — внести новое сообщение в +`backend/internal/pipeline/testdata/operator-messages.txt`. ### 4.3 Платформенная половина — **твоё дело назвать, не построить** @@ -89,20 +138,47 @@ записать прогону, который кончился флагом вместо `exit 4` · какой её пин это стережёт. Я передам это платформенной сессии, и **акт будет один на две половины**. -### 4.4 Приложение — фикстура, краснеющая по УДАЧЕ (ряд 379(б)) +### 4.4 Приложение — ДВА пина, краснеющих по УДАЧЕ (ряд 379, ОБЕ половины) -Отдельная, маленькая и не связанная с §4.1 работа; делай её ПОСЛЕ основной. +Отдельная от §4.1 работа; делай её ПОСЛЕ основной. ⛔ **И мой диагноз здесь был НЕВЕРЕН — опровергнут +замером, а не мнением. Читай, чем именно, иначе повторишь мою ошибку.** -Запись каталога `CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write` стережёт самую дорогую половину -денежного бита `delivered()` (`backend/internal/llm/attemptcut.go:189`=`answered && t.firstByte.Load()`). **Замер оркестратора 11.09:** на -чистом дереве пин `TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite` даёт **8 зелёных из 8**, на -своей посадке — **7 КРАСНЫХ ИЗ 8, один ЗЕЛЁНЫЙ**. Текст падения верный и про предмет; дыра в ФИКСТУРЕ: -она шлёт ~6 МБ тела, сервер читает строку запроса, отвечает `401` и рвёт связь `SetLinger(0)` — и успеет ли -транспорт записать `GotFirstResponseByte` раньше, чем `Do` вернёт ошибку записи, **решает гонка, а не -построение**. ⇒ прежние RED этой записи были удачей. +**Половина (б).** Запись каталога `CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write` стережёт самую +дорогую половину денежного бита `delivered()` +(`backend/internal/llm/attemptcut.go:189`=`answered && t.firstByte.Load()`): провайдер ОТКАЗЫВАЕТ и рвёт +коннект, пока наше тело ещё пишется — байт ответа есть, статуса в руках нет, и чтение «байт = доставка» +оплачивает сметой каждый отказ (замер прежнего пака: 22 отказа из 25, один на $0.80). -**Задача:** перестроить фикстуру так, чтобы приход байта ответа был НЕИЗБЕЖЕН до обрыва, и предъявить -**8 из 8 красных на посадке при 8 из 8 зелёных на дереве**. Способ выбираешь сам. +**Два независимых замера на посадке (снятие `answered &&`), и они расходятся:** мой — **7 красных из 8** +(снят на нагруженной машине, рядом шла батарея); опровергателя на спокойной — **8 из 8**, а на сорока +процессах **39 FAIL / 1 PASS**. ⇒ пин краснеет по УДАЧЕ, это подтверждено обоими; частота зависит от +нагрузки, и поэтому «восемь красных подряд» **не является критерием приёмки**: сегодня, без единой правки, +восьмёрка выпадает примерно в 82 случаях из 100. + +⛔ **А вот ПРИЧИНА не та, что я написал.** Я заказывал «сделать приход байта ответа НЕИЗБЕЖНЫМ до обрыва». +Зонд опровергателя (40 итераций, печать того, что реально получил клиент) дал **39 × обрыв с отказом +записи и 1 × ПОЛНЫЙ `401`**, и ни разу — «ни куска, ни статуса». То есть байт приходит практически всегда, +а мутант выживает в ДРУГОМ случае: клиент успевает вычитать `401` ЦЕЛИКОМ, `Do` возвращает не ошибку +записи, а ОТВЕТ, и `delivered()` не спрашивают вовсе — ошибка уходит статусной веткой. **Моё лекарство +било по половине, которая и так держится, и увеличивало вероятность ровно того исхода, в котором мутант +выживает.** (И «~6 МБ тела» тоже неверно: 11.0 МБ — Go не эскейпит кириллицу в JSON.) + +**Задача — делай РОВНО так в части ЧТО, способ твой:** +1. **Тест утверждает СВОЮ ПОСЫЛКУ и печатает её:** что `Do` вернул ошибку ЗАПИСИ и что трасса видела + первый байт. **Сценарий не состоялся ⇒ тест КРАСНЫЙ**, а не зелёный «мимо предмета». Сегодня он зелен и + на полном `401`, то есть на другом предмете. +2. **Прямой табличный пин на сам `delivered()`** — он убивает посадку `answered &&` детерминированно, без + гонки вообще. +3. ⚠ У фикстуры `MaxAttempts: 3` — до трёх бросков кости на один `Complete`; **границу утверждай ПО + ПОПЫТКЕ**, а не по итогу вызова. + +**Половина (а) — её первая редакция промта не заказывала вовсе.** Тот же ряд 379 несёт вторую половину: +две строки таблицы девяти в `backend/internal/pipeline/cutcall_test.go:402`=`time.Sleep(50 * time.Millisecond)` +разводятся ОКНОМ СТЕННЫХ ЧАСОВ, и по какую сторону границы заголовков сел `cancel()`, не утверждается +ничем — замер приёмки дал **2 красных из 8**. **Ряд закрывается ТОЛЬКО вместе с этой половиной.** + +⛔ **Критерий приёмки берётся из самого ряда 379, он сильнее моего:** «каждый из двух пинов **ПЕЧАТАЕТ +величину, доказывающую пройденную границу, и ПАДАЕТ, когда сценарий не состоялся»**. ## 5. Где этот пак мягкий — четыре места, назвал я, веер и глубину выбираешь ты