30 KiB
Бэкенд-пак: КУПЛЕНО — ЗНАЧИТ ДОСТАВЛЕНО. Юнит, который не доделается НИКОГДА
Выдан оркестратором №23 (сессия
textmachine-11) 11.09. Пак $0 — платных вызовов в нём нет. Предмет решён владельцем 05.09 (D39.204); ты строишь решённое, а не выбираешь между вариантами.
1. Какая проблема и что решит твой результат
Читатель заплатил за главу. Движок купил первую попытку, получил флагуемый результат, пошёл за второй — и
вторую ему не продали: потолок книги или дня не вмещает удвоенный бюджет регенерации
(maxTokensForAttempt удваивает на каждой, значит попытка 1 резервирует вдвое больше попытки 0).
Дальше происходит вот что, и это проверяется одной строкой: backend/internal/pipeline/stagerun.go:204=infra failure; the cancelled-position mark — ЛЮБАЯ ошибка попытки уходит наверх из цикла, РАНЬШЕ UpsertChunkStatus.
Следствия два, и оба стоят денег читателя:
- Юнит не доделается НИКОГДА. Чекпойнт попытки 0 есть (реплей за $0), строки
chunk_statusНЕТ ⇒ читающие модели видят юнит какpending, экспорт не отдаёт ничего. Каждый резюм реплеит попытку 0 за $0 и умирает на попытке 1. Прогон уходитexit 4при ПОЛНОСТЬЮ доставленных купленных юнитах. - Волну нельзя продолжить без ДЫРЫ в непрерывном префиксе — то есть за это платят и соседние юниты, которые в бюджет влезали (разброс резервов внутри волны замерен ×2.976).
Решение владельца (D39.204, и оно НЕ обсуждается): отказ резервации на РЕТРАЕ деградирует во ФЛАГ,
причина которого ОТЛИЧНА от исчерпания регенераций; текст попытки 0 оплачен и отгружается; волна идёт
дальше. ⛔ Вариант «минимальная покупка ≥ step_max_usd» СНЯТ владельцем — он стоил бы покупателю
входного порога в пять глав. Не реализуй его и не предлагай.
⚠ И канон здесь сильнее, чем кажется: D2 п.2 уже ТРЕБУЕТ тегировать flag_reason. Значит новая
причина — не добавка к контракту, а его ИСПОЛНЕНИЕ. (Ссылка «смена D2.3», гулявшая по бэклогу, неверна
дважды: D2 и так кончается флагом, а D2.3 — про детектор вырождения перед удвоением.)
2. Зона записи и git
- Зона —
backend/, и только она.platform/не трогать: её половина едет ОТДЕЛЬНЫМ промтом, но ОДНИМ АКТОМ с твоим (см. §4.3). - Ты не коммитишь. Готовое дерево передаёшь мне, я лендлю.
git add -Aзапрещён каноном. - Свою секцию итогов пиши в
docs/PROGRESS.md— это единственное исключение из «docs/— зона оркестратора». Чужие незакоммиченные файлы в дереве не трогай.
3. Карта чтения — ЗАКОН, дальше только по её ссылкам
CLAUDE.md— целиком.- Тело
D39.204вdocs/architecture/05-decisions-log.md(решение владельца) иD2п.2 — он в закрытой эре, тело вdocs/archive/architecture/05-decisions-D1-D38.md, греп^## D2\.. ⚠ Подномер собственного тела не имеет:D2.3— это ПУНКТ 3 телаD2. - Ряд 291 в
docs/BACKLOG.md— там весь разбор, включая опровергнутую посылку прошлого оркестратора: «ретрай оставляет ФЛАГОВАННЫЙ юнит» — ЛОЖЬ, зона опровергла по строкам. Не наступи второй раз. backend/internal/pipeline/stagerun.go(цикл попыток иrunAttempt),escalation.go(errReserveCeilingи кто на нём УЖЕ деградирует),disposition.go(словарьFlagReason).docs/architecture/12-go-style-notes.md— нормативы стиля, только больные места.
Код-факты добывай сам; file:line выше — отправные точки. Код первичен.
4. Состав
4.1 Деградация — делай РОВНО так в ЧАСТИ РЕШЕНИЯ, сам решай КАК
Ровно так (решено владельцем, отступление = пинг):
- отказ резервации на попытке ≥1 при существующем оплаченном результате попытки 0 ⇒ флаг, а не ошибка наверх;
- у флага СВОЯ причина, отличная от исчерпания регенераций: читатель и оператор обязаны отличать «мы перепробовали всё» от «денег не хватило на вторую попытку»;
- волна идёт дальше.
⛔ ЧЕГО В ЭТОМ СПИСКЕ БОЛЬШЕ НЕТ И ПОЧЕМУ — читай, это главное место пака. Первая редакция промта
требовала, вслед за 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) — у юнита появляется статус, значит дыры в непрерывном префиксе нет. Похоже на правду; предъяви замером.
4.2 Пин, ради которого пак существует
«Купленное доставлено ⇒ не exit 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 Платформенная половина — твоё дело назвать, не построить
Карта заметок платформы РУКОПИСНАЯ (platform/internal/ingest/notes.go) и уезжает независимо от
словаря движка. Цена несделанного замерена: PD-246 — минор 0.10.0 сутки жил ЛОЖНЫМ на проводе, причина
доезжала читателю как unspecified, и поймала это зона своей пере-проверкой ПОСЛЕ слов оркестратора
«приёмка закончена».
⇒ отдельным пунктом отчёта назови мне: точное значение новой причины на проводе · что платформа обязана
записать прогону, который кончился флагом вместо exit 4 · какой её пин это стережёт. Я передам это
платформенной сессии, и акт будет один на две половины.
4.4 Приложение — ДВА пина, краснеющих по УДАЧЕ (ряд 379, ОБЕ половины)
Отдельная от §4.1 работа; делай её ПОСЛЕ основной. ⛔ И мой диагноз здесь был НЕВЕРЕН — опровергнут замером, а не мнением. Читай, чем именно, иначе повторишь мою ошибку.
Половина (б). Запись каталога 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).
Два независимых замера на посадке (снятие answered &&), и они расходятся: мой — 7 красных из 8
(снят на нагруженной машине, рядом шла батарея); опровергателя на спокойной — 8 из 8, а на сорока
процессах 39 FAIL / 1 PASS. ⇒ пин краснеет по УДАЧЕ, это подтверждено обоими; частота зависит от
нагрузки, и поэтому «восемь красных подряд» не является критерием приёмки: сегодня, без единой правки,
восьмёрка выпадает примерно в 82 случаях из 100.
⛔ А вот ПРИЧИНА не та, что я написал. Я заказывал «сделать приход байта ответа НЕИЗБЕЖНЫМ до обрыва».
Зонд опровергателя (40 итераций, печать того, что реально получил клиент) дал 39 × обрыв с отказом
записи и 1 × ПОЛНЫЙ 401, и ни разу — «ни куска, ни статуса». То есть байт приходит практически всегда,
а мутант выживает в ДРУГОМ случае: клиент успевает вычитать 401 ЦЕЛИКОМ, Do возвращает не ошибку
записи, а ОТВЕТ, и delivered() не спрашивают вовсе — ошибка уходит статусной веткой. Моё лекарство
било по половине, которая и так держится, и увеличивало вероятность ровно того исхода, в котором мутант
выживает. (И «~6 МБ тела» тоже неверно: 11.0 МБ — Go не эскейпит кириллицу в JSON.)
Задача — делай РОВНО так в части ЧТО, способ твой:
- Тест утверждает СВОЮ ПОСЫЛКУ и печатает её: что
Doвернул ошибку ЗАПИСИ и что трасса видела первый байт. Сценарий не состоялся ⇒ тест КРАСНЫЙ, а не зелёный «мимо предмета». Сегодня он зелен и на полном401, то есть на другом предмете. - Прямой табличный пин на сам
delivered()— он убивает посадкуanswered &&детерминированно, без гонки вообще. - ⚠ У фикстуры
MaxAttempts: 3— до трёх бросков кости на одинComplete; границу утверждай ПО ПОПЫТКЕ, а не по итогу вызова.
Половина (а) — её первая редакция промта не заказывала вовсе. Тот же ряд 379 несёт вторую половину:
две строки таблицы девяти в backend/internal/pipeline/cutcall_test.go:402=time.Sleep(50 * time.Millisecond)
разводятся ОКНОМ СТЕННЫХ ЧАСОВ, и по какую сторону границы заголовков сел cancel(), не утверждается
ничем — замер приёмки дал 2 красных из 8. Ряд закрывается ТОЛЬКО вместе с этой половиной.
⛔ Критерий приёмки берётся из самого ряда 379, он сильнее моего: «каждый из двух пинов ПЕЧАТАЕТ величину, доказывающую пройденную границу, и ПАДАЕТ, когда сценарий не состоялся».
5. Где этот пак мягкий — четыре места, назвал я, веер и глубину выбираешь ты
Мандат самопроверки — исполнением; субагенты разрешены явно. Адверсариальный проход по СВОЕЙ ГОТОВОЙ работе обязателен.
- ⛔ Пин, утверждающий только молчание, вакуумен ровно там, где сообщение ложно. «Ошибка не вышла наверх» — утверждение о молчании. Фикстура обязана быть такой, где новая причина ОБЯЗАНА прозвучать, и тест утверждает её ИМЯ.
- ⛔ Фикстура обязана сделать отказ резервации НЕИЗБЕЖНЫМ на попытке ≥1 и НЕВОЗМОЖНЫМ на попытке 0. Денежный пин, флейковый на мутанте, измеряет пустой сценарий: у нас уже был пин, дававший 2 красных из 8, потому что деньги были ВЕРОЯТНЫ, а не неизбежны. Гоняй свой не один раз.
- ⛔ Посадка обязана бить в тот же слой, что стережёт пин. За прошлый пак мутация трижды оказалась не про то: недостижимое условие в фикстуре · не собирающаяся посадка (НЕизмеренная — хуже выжившей) · код против гейта над данными.
- ⛔ Новая причина — факт ПРОВОДА, а не словаря. Тест, проверяющий только, что константа добавлена в
FlagReason, не доказывает, что читатель её увидит. Скажи, чем предъявлена дорога от флага до отчёта/экспорта.
⚠ И общее, стоившее трёх смен подряд: зелёная батарея плюс полный мутационный каталог сходимостью НЕ являются. В последних трёх паках направленный второй читатель находил 6, 7 и 9 дефектов при полной зелени.
⭐ Адверсариальная стойка — способ смотреть, не правило пака
- ⛔ Автор и ревьюер — разные роли, даже когда это один ты. Перечтение своей работы рубежом не считается: поднимай читателя, которому НАЗВАНО, где мягко.
- ⛔ Спрашивай у аномалии, о ЧЁМ она — о предмете или о твоём приборе.
- ⛔ Верный результат при неверном методе не краснеет нигде. «Сошлось» — не доказательство.
- ⛔ Утверждение о молчании вакуумно без фикстуры, где оно ОБЯЗАНО прозвучать.
- ⛔ Заимствованное число проверяется не на существование, а на ТУ ЛИ КЛЕТКУ: та же роль, та же модель, та же стадия, тот же режим?
- ⭐ Главный вопрос отчёта — не «что не получилось», а «что ты знаешь и не сказала».
⭐ Находки ВНЕ заказа — отдельный заказ владельца
Слово владельца 11.09: находить проблемы даже там, где мы не ждём. Задавай себе в каждой фазе вопрос, у которого нет заранее известного ответа: что здесь стоит дороже, работает хуже или ведёт себя страннее, чем должно бы, — и о чём никто не спрашивал? Ради чего он заведён: удорожание из-за падающих запросов на НАШИХ настройках никто не искал — просто кто-то посмотрел в леджер без гипотезы и увидел 25.4 % цены книги в выброшенных попытках, при зелёных тестах и молчащих гейтах.
Докладывай отдельной секцией: криты · мажоры · регрессии · баги · жёсткие точки улучшения — каждое
аргументированно, с носителем (file:line, число, команда) и с ценой в деньгах, тексте или доверии
читателя. ⚠ Секция обязана быть непустой ИЛИ нести строку «искал вот так, не нашёл» с перечислением мест:
«находок нет» и «не смотрел» в отчёте выглядят одинаково.
6. Предметные оси ревью
Выбери 1–3 и назови какие. Мой приор: деньги под гонкой · терминальное состояние прогона (что видит платформа и что видит человек) · обратная совместимость словаря причин.
7. Записка-план ДО работы
Перед первой правкой — записка: что меняешь, чем предъявишь, где ждёшь сопротивления. Комплектность против заказа сверяй механически.
8. Заявление = команда
Каждое число и каждая категорика отчёта — с командой, которой получены. Дифф ^func Test — исполнением,
не памятью. Приёмка пере-снимает.
9. Эхо-протокол старта
Первым действием — ≤10 строк СВОИМИ словами: что понял · что считаешь опасным · что считаешь неверным. Не пересказ: дословный пересказ подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним.
Адрес бери из /tmp/textmachine-channel — блок с role=оркестратор, — а не из этого промта: имя сессии
не переживает рестарт окружения. Перед отправкой сверься с ListAgents: файл переживает смерть сессии, а
ListAgents — нет.
10. Что НЕ удалось — обязательная секция отчёта
И вторая её половина: «где прибор слеп и я это знаю» — что невидимо · почему не чинил · чем закрывается. ⭐ Норма прошлой смены: «в коде названо, в отчёте нет — значит для следующей смены НЕ названо».
11. Канал вопросов и право отказаться
Конфликт промта с кодом или доками — пинг мне, не интерпретация. Право сказать «этого делать не надо» с аргументом у тебя есть и им пользовались: в двух случаях из трёх правы были сессии, а не я.
⛔ Тесты и гейты под зелень не подгонять. Правка, вызванная ЗАКАЗАННОЙ сменой поведения, — обслуживание, и протухший тест держать не нужно; но она ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось, какой тест это описывал, куда уехала гарантия.
12. Прямой канал
Механизм — CLAUDE.md §«Связь между сессиями». Впиши свой блок в /tmp/textmachine-channel ПЕРВЫМ
действием. Нужной роли нет ⇒ канала нет, и это нормальный случай: НЕ опрашивай сессии подряд.
13. Критерий завершённости
У каждого пункта заказа — исход (сделано · не делаю с доводом · пинг) · круги СОШЛИСЬ (последний не дал
НОВЫХ находок; прежние закрыты таблицей «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО») · таблица мутаций
полная, выжившие названы, и КАЖДАЯ новая запись помечена battery (прошлый пак этого не сделал, и его
«21/21 RED» оказался разовым прогоном смены, а не гейтом проекта) · числа сняты ПОСЛЕ последней правки ·
всё живое в ДЕРЕВЕ, а не в письме · явное «работа завершена, править не планирую». Без последнего пак
считается идущим.