textmachine/docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md

362 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Пак бэкенда: «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ»
## 1. Какая проблема и что решит твой результат
Движок отправляет запрос провайдеру, тот принимает его и генерирует — а мы по своему же дедлайну
перестаём ждать и рвём соединение. Провайдер об этом не знает и работу не отменяет. Дальше движок
**выбрасывает единственную улику**: `internal/llm/httpllm.go:409``data, _ := io.ReadAll(...)`, ошибка
чтения уходит в `_`, а обрыв распознаётся по РАЗМЕРУ. Тело, оборванное нашим дедлайном, маленькое ⇒ не
отличимо от целого ⇒ не разбирается как JSON ⇒ объявляется «оплаченным 2xx с нечитаемым телом» и
**ретраится** (`:433` терминален только для >16 МиБ; `:365` даёт один повтор).
**Цена — один замер и одно наблюдение, и разница между ними важна.** `docs/experiments/23-editor-tier.md` §5: латентности ровно `2×attempt_s`,
**$0.475893 = 23.8 %** цены руки — это САМИ четыре строки; юниты целиком, вместе с их попытками 0, —
**$0.689215 = 34.5 %**. ⛔ Источник пишет дословно «две законные цифры, и путать их нельзя» и регистрирует
эту путаницу СВОИМ дефектом №11; не повторяй её, называя одно число двумя долями. Живой прогон 08.09: два редакторских вызова,
`unexpected end of JSON input`, **$0.407606 из $1.259217 = 32 % бюджета**. ⚠ **Что это ИМЕННО таймауты —
не измерено, а выведено по совпадению подписи** с замером выше: сырого леджера того прогона в дереве нет
(`.db` новее 07.09 — 0 при 163). Не опирайся на второе число как на улику класса.
**Корень — НЕ транспорт, а дедлайн против оси регенерации.** DeepSeek держит 7576 tok/s (n=139). Грант
редактора 16 000 ≈ 213 с; реген удваивает его до 32 000 ≈ **427 с** и НЕ трогает время, а `attempt_s`
равен 240. Потолок по времени 1721 тыс. токенов; максимум, когда-либо доставленный регеном, — **19 354 =
60 % гранта**, и **13 доставок из 17 уложились бы в исходные 16 000**. ⇒ ось эскалации сегодня фиктивна:
резервация бронирует недостижимое и съедает headroom под книжным потолком (строка **369**).
**Что покупает твой результат.** (1) Мы перестаём покупать одну генерацию дважды. (2) Оператор и читатель
видят, какие деньги — оценка, а не факт. (3) Эскалация становится настоящей: правильный дедлайн делает
выданный бюджет достижимым.
## 2. Зона и git
**Зона записи — `backend/`** плюс своя секция в `docs/PROGRESS.md` (это ЕДИНСТВЕННОЕ исключение из
«`docs/` — зона оркестратора»). Чужие зоны (`platform/`, `frontend/`, `eval/`, `books/`) не трогаешь ни
строкой; расхождения — пингом. **Ты НЕ коммитишь**: дерево готовишь и передаёшь на лендинг. Никаких
`git add -A`, `reset --hard`, переписывания истории. `.env` не читать никогда.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `docs/BACKLOG.md` — строки (⚠ «строка N» здесь — **ID в первой колонке, а не номер строки файла**: `sed -n '360p'` даст пустоту, греп `^| 360 |` — нужный ряд) **360** (несёт СОШЕДШУЮСЯ конструкцию лечения целиком), **369** (ось
эскалации), **78** (движковая половина `PD-441`: публикация оценочных строк). Греп `^| 360 |`.
2. `docs/experiments/00-provider-quirks.md`**канон велит читать ПЕРЕД правкой адаптеров и вызовов.**
⚠ Класса, который ты чинишь, там НЕТ (замер: `early-200`/«empty lines»/«10 minutes» — 0 хитов) — это
пробел, и он пингом полигону, не твоя правка.
3. `docs/research/21-llm-transport-survey.md` — §Q6 и **ЭРРАТА ОРКЕСТРАТОРА 08.09 в шапке**: обзор неверен
дважды, и обе половины вместе полтора месяца держали неверный дедлайн. Читай эррату ПЕРЕД §Q6.
4. `docs/architecture/15-money-path.md` — денежный путь; леджер есть НИЖНЯЯ граница.
5. `backend/cmd/tmmutate/main.go` — шапка целиком (контракт мутационного инструмента) + `backend/Makefile`,
цели `battery` и `mutations`.
Ратифицированное, нужное по существу, вложено сюда и похода в журнал решений НЕ требует:
-**`D39.230` п.1 — СЛОВО ВЛАДЕЛЬЦА, и у него есть УСЛОВИЕ.** Оценка за оборванный нами вызов
списывается с читателя. **Но пометка — часть решения, а не украшение:** строка обязана нести свой класс
и флаг `estimated=1`, а счёт и сумма оценочных строк — публиковаться машинно-читаемо рядом с
`committed_usd`. Списать с человека непроверенную оценку без видимой пометки — НЕ то, на что дано слово.
- **`D39.230` п.2** — платная проба вендорского баланса ОТКЛОНЕНА владельцем. ⇒ вопрос «биллит ли вендор
оборванное» осознанно ОТКРЫТ; оценка есть верхняя граница под гипотезой «вендор досчитывает». Проб не
проектировать и не предлагать.
- **`D39.230` п.3** — ожидание провайдера до ~20 минут РАЗРЕШЕНО. Сверху не убьёт ничто: `RuntimeMaxSec`/
`WatchdogSec` — 0 хитов при 16 файлах с systemd-свойствами, у движка дедлайна нет.
- **`D39.216`** — где форма не тянет, её развязывают или переписывают; «дешевле подпереть» доводом НЕ
является; второй носитель одного знания не заводится (п.3б).
- **`D39.183`** — запрет править тесты «чтобы прошло» про МОТИВ; правка от ЗАКАЗАННОЙ смены поведения —
обслуживание, но объявляется отдельным пунктом отчёта.
- **`D39.202`** — прибор, честно отвечающий на СВОЙ вопрос, читается как ответ на другой. Ноль и «не
смотрели» неразличимы без контрольной величины.
## 4. Состав и разметка свободы
### 4.1 Признак: граница денег — ДОСТАВКА запроса, а не статус-строка — **делай РОВНО так**
⛔ **Доктрина «2xx ⇒ провайдер уже списал» (`httpllm.go:428`, `stagerun.go:610`) ЛОЖНА для нашего боевого
провайдера.** DeepSeek документирует ранний `200` с пустыми строками, пока запрос ждёт планирования, и
закрытие соединения, если инференс не начался за 10 минут; в наших логах пустое тело при `200` видно буквально
(`response="\n"`, **8 строк из 617** «llm exchange» под `books/gu-zhenren/` — счёт мой). ⚠ **Но НЕ бери эту строку как
улику self-cut: она пришла на 91-й секунде при дедлайне 240 — ушли не мы.** Настоящие self-cut в том же
файле лежат ровно на **240.0 с** — четыре трейса в `probe-4axes/logs/p7-off.log` ⚠ (счёт опровергателя
приёмки, оркестратором НЕ пере-снят; пере-сними сама, если будешь на них опираться). Разница и есть предмет
пака: одна подпись, два разных класса, и различает их только признак §4.1. ⇒ `200` значит «принят», не «сгенерирован», и ветвь `No 2xx ever arrived`
на DeepSeek недостижима по построению — все наши обрывы прятались под `decode_error`.
**Единственный факт, общий для всех этих случаев: запрос ДОСТАВЛЕН, и ушли МЫ.** Он фиксируется без байта
на проводе — `httptrace.ClientTrace{WroteRequest, GotFirstResponseByte}` в `attempt()`. Различение
проверено исполнением на клиенте той же сборки:
| случай | `WroteRequest` | `GotFirstResponseByte` |
|---|---|---|
| доставлен, заголовки держат, наш дедлайн | `true` | `false` |
| TLS-рукопожатие висит — НЕ доставлен | `false` | `false` |
| ранний `200` + `"\n"`, наш дедлайн | `true` | `true` |
**Заказ:** (а) ошибка `ReadAll` больше не выбрасывается; (б) заводится типизированная
`AttemptTimeoutError{Delivered, AfterHeaders, BytesRead, WhitespaceOnly, Elapsed}`; (в) исходы —
**перечислены ниже поимённо, свой набор не собирай**. ⛔ Обёртывание обязано СОХРАНИТЬ
`errors.Is(err, context.Canceled)` для кодов выхода — иначе стоп прогона получит чужой exit-код.
**ПОСЛЕ 2xx — пять исходов:**
| исход | признак | вызовов | деньги | чанк |
|---|---|---|---|---|
| целое, JSON ок | `readErr == nil` | 1 | usage | как есть |
| целое, JSON битый | `readErr == nil`, разбор упал | **2** (кап 1, как сегодня) | оценка | флаг |
| **self-cut** | `readErr != nil`, дедлайн `attemptCtx`, родитель ЖИВ | 1 | `settleUSDForCutCall` | флаг; резюм — **0** вызовов |
| **отмена РОДИТЕЛЕМ** (стоп, Ctrl-C) | `readErr != nil`, `ctx.Err() != nil` | 1 | `settleUSDForCutCall` | ⛔ своя причина `cancelled`, НЕ общий флаг «нужен человек»; резюм — 1 вызов на ТОМ ЖЕ бюджете |
| разрыв соединения | `readErr != nil` иначе (`ErrUnexpectedEOF`, h2 stream) | 2 | `settleUSDForCutCall` | на исчерпании — **инфра-пауза, НЕ флаг** |
**ДО заголовков — четыре:** self-cut доставленный (1 вызов, флаг, резюм 0) · отмена родителем
доставленного (1, деньги те же, резюм 1 на том же бюджете) · разрыв после доставки (2, деньги те же,
инфра-пауза, резюм 1) · **НЕ доставлен** (до `max_attempts`, **$0**, освобождение резерва, резюм 1 — это
первая покупка, а не перекупка).
**`TestBilledDecodeErrorReBillCap` не меняется** — у него `readErr == nil`, это второй исход таблицы.
### 4.2 Диспозиция: не ретраить то, что оборвали мы — **делай РОВНО так**
- **Доставлен + наш дедлайн** (с заголовками и без) — класс `attempt_timeout`, **чекпойнт + флаг, БЕЗ
ретрая**, сумма — через `settleUSDForCutCall`.
- **НЕ доставлен** (дедлайн до `WroteRequest`) — единственный случай, где «ничего не куплено» верно по
построению: остаётся как сегодня (retryable, на исчерпании освобождение резерва, инфра-пауза).
-**ОТМЕНА РОДИТЕЛЕМ доставленного вызова** (стоп прогона, Ctrl-C) — **это тот самый случай, с которого
начался заказ (строка 331), и диспозиция у него ДРУГАЯ.** Деньги — та же `settleUSDForCutCall` (ровно то, что просит
`PD-441` — по-русски: «сеттлить оценку и при отмене вызова, который успел уйти, различая по ФАКТУ ухода»), но чанк получает **СВОЮ причину `cancelled`, а не общий флаг «нужен человек»**: вызов был здоров, остановил его ЧЕЛОВЕК. ⛔ **Это НЕ «флага нет».** Дыры без пометки не должно быть ни в один момент: пока прогон не резюмирован, диспозиция чанка не `ok`, и экспорт показывает `flagged(cancelled)` — читается как «остановлено, резюм пере-сделает», а не как «нужен человек». Флаг, врущий о ПРИЧИНЕ, запрещён отдельно (`D39.93` п.2); невидимая дыра запрещена тем более. ⛔ **И резюм обязан пере-делать
его на ТОМ ЖЕ `max_tokens`** — `maxTokensForAttempt` удваивает бюджет по оси попыток
(`disposition.go:393-401`, `base << uint(attempt)`; петля берёт его из НОМЕРА попытки,
`stagerun.go:147`), и здесь это ловушка: удвоение купит вдвое больший вызов за здоровье, которого никто
не терял.
⛔⛔ **КАК это построить — задано, потому что наивный путь НЕВОЗМОЖЕН, и это проверено.** В сторе ровно
три функции (`Reserve`, `Release`, `SettleWithCheckpoint`), деньги нельзя записать БЕЗ чекпойнта, а
чекпойнт — ключ резюма (`ON CONFLICT (request_hash) DO NOTHING` ⇒ «Duplicate settle: book no new
spend»). ⇒ «заплатить и пере-сделать на том же ключе» несовместимо по построению.
**Развязка — в ПЕТЛЕ, а не в сторе:** индекс попытки и счётчик УДВОЕНИЙ разводятся.
`maxTokens = base << escalations`, где `escalations` растёт только на `length`/`empty`; кап регенерации
считает УДВОЕНИЯ, а не индекс. Тогда попытка N+1 берёт тот же бюджет, но НОВЫЙ хеш (`Attempt`
самостоятельное измерение) ⇒ свежие `reserve → call → settle`, `ON CONFLICT` недостижим, инвариант
`committed == sum(checkpoints)` цел, стор не тронут. ⛔ **Стор и его инварианты не трогать** — упрёшься,
это пинг.
⚠ И проверь побочное: `priceprojection.go` считает проекцию через `maxTokensForAttempt` («RETRIES ARE IN
IT, AND THAT IS THE POINT») — смена оси двигает СМЕТУ. Двинулась — назови в отчёте числом. ⚠ **Видимое следствие, назови его в отчёте:** стоп прогона
теперь ПЛАТИТ оценку за каждый летящий вызов (в редакторской волне это до восьми × ~$0.13). Слово
владельца это покрывает («оборванный НАМИ» — мы рвём по его команде), но молча вводить это нельзя.
- **Разрыв соединения ПОСЛЕ доставки** — один ретрай (кап ключуется на «доставлен», а не на «2xx»), на
исчерпании **инфра-пауза, не флаг**: серия таких — мёртвый провайдер, и довод нулевой ветви про
флаг-шторм здесь верен. ⛔ **Но деньги назвать обязательно, иначе граница течёт через эту дверь:**
сегодняшний инфра-путь зовёт `releaseReservation` и даёт **$0**, а запрос был ДОСТАВЛЕН ⇒ идёт
`settleUSDForCutCall` + класс `connection_lost`, и ТОЛЬКО потом пауза; резюм пере-делает на том же
бюджете. Для post-header разрыва это сегодняшнее число, для pre-header — новое, и объявляется
заказанной сменой поведения.
⚠ Довод против «оставить ретрай, он же 3 раза из 7 спасал»: правильный дедлайн даёт то же самое без
монетки по цене полного счёта. Клапан на будущее — ДАННЫЕ (`tok_s_floor`), не код.
### 4.3 ⛔ ТРЕТЬЯ УТЕЧКА, которой нет даже в строке 331 — **делай РОВНО так**
Сегодняшняя нулевая ветвь (`stagerun.go:634`) зовёт `releaseReservation` и **чекпойнта НЕ пишет**, ставя
стадию `failed`**резюм ПОКУПАЕТ ВЫЗОВ ЗАНОВО**. Убрать ретрай и оставить это значит закрыть класс
наполовину: тот же платный вызов уходит другой дверью.
**Заказ:** доставленный self-cut получает чекпойнт, и резюм его находит. **Предъявление именно этим:** тест
утверждает, что после self-cut резюм на том же снапшоте **не делает НИ ОДНОГО HTTP-вызова**. Мутация:
убрать запись чекпойнта — тест обязан покраснеть текстом про свежий вызов, а не про деньги.
### 4.4 Дедлайн считается от бюджета — **делай РОВНО так по форме, числа реши сам и обоснуй**
`timeouts` получает `tok_s_floor`, `queue_slack_s`, `attempt_max_s`; дедлайн вызова =
`clamp(queue_slack_s + max_tokens/tok_s_floor, attempt_s, attempt_max_s)`, считается в `attempt()` из
`maxTokens` запроса. **`attempt_s` становится ПОЛОМ** ⇒ ни один существующий конфиг не теряет времени.
Дефолт пола — **35.5 tok/s** (128 000/час, единственное опубликованное вендором число).
**Почему этот дефолт правильный, и это же твой тест:** он ПРОИЗВОДНЫЙ, и на черновике (8 496 токенов)
даёт **238.95 с** — сегодняшние 240 с точностью до секунды. То есть нынешнее значение верно ровно для
черновика и было перенесено на редактора слепо. ⛔ **Тест-тождество обязан пинить ФОРМУЛУ, а не число, и вот операционная форма — иначе его исполнят
формально.** `assert derive(8496) == 238.95` негоден: это литерал, произведённый формулой, и через полгода
его «починят» под круглые 240. Требуется: ожидание **вычисляется в тесте** из двух вендорских констант
(час, 128 000) и гранта; **литералов `238`/`239`/`240`/`900` в тест-файле НЕТ** — предъяви грепом с
контрольной величиной; не меньше ДВУХ грантов (8 496 и 32 000); отношение удвоения
`derive(2N) slack == 2·(derive(N) slack)`; края `clamp`; пустое и нулевое поле дают ДЕФОЛТ, а не
деление на ноль и не нулевой дедлайн.
**Твоё — ЧИСЛА. Моё — ПРАВИЛО их вывода, и оно РОВНО так:**
- `tok_s_floor` — НИЖЕ p10 наблюдаемой скорости модели по ≥100 строкам `request_log`, округление вниз
объявлено (DeepSeek: p10 ≈ 67 ⇒ 50);
- `queue_slack_s` — документированное ВЕНДОРОМ максимальное ожидание до инференса (DeepSeek — 600).
⛔ Меньше — только с доводом и ПИНГОМ: владелец сказал «готов ждать» именно про ~20 минут, и меньший слак
меняет принятый им размен молча, в другую сторону;
- `attempt_max_s` — слак + максимальный грант С УДВОЕНИЕМ / пол, округлённый вверх (DeepSeek:
600 + 32000/50 = 1240).
**И два механизма, без которых «новый провайдер без правки Go» не работает:** WARN на старте для
провайдера БЕЗ `tok_s_floor`, чей стадийный грант с учётом удвоения выше ~8.5k («дефолт 35.5, дедлайн для
32 000 = 900 с, задай после первого прогона»), и валидационный тест `models.yaml`, ТРЕБУЮЩИЙ поле для таких
провайдеров.
**Снапшот не двигается, и проверено это ПРЯМО, а не соседством:** `buildSnapshotID`
(`internal/pipeline/snapshot.go`) перечисляет входы поимённо, и таймаутов среди них нет — читай сам список,
а не грепай близость слов. ⛔ Прежняя редакция этого пака писала «`AttemptS` — 0 хитов из 120 файлов со
`snapshot`»: замер был неверен в ЧИСЛИТЕЛЕ (хит 1 — `config/models.go`) и в ЗНАМЕНАТЕЛЕ (файлов 145), а
вывод верен — правый вердикт по неправой причине, `D39.202`. Если твоя правка список входов меняет — СТОП
и пинг: перекупка книг словом владельца не покрыта.
### 4.5 Пометка — УСЛОВИЕ решения владельца — **делай РОВНО так**
Класс `attempt_timeout` и `estimated=1` едут в чекпойнт и `request_log`; `classify()` узнаёт класс на
резюме; `paidtail` включает его в LostUSD. **И сверх того — то, ради чего дано слово:** счёт и сумма
оценочных строк публикуются **машинно-читаемо рядом с `committed_usd`** (`D39.230` п.1; это же движковая
половина `PD-441`, строка **78**). ⚠ **Поверхность назови точно: это тот JSON, который ЧИТАЕТ платформа
(`tmctl status --json`), а не новый файл.** Формат — пингом платформе, не решением за неё. ⚠ Сегодня флаг `estimated` виден только человеку в `tmctl render.go`;
платформа его не читает — ей нужно число, а не строка экрана.
### 4.6 Оператор не должен принимать ожидание за висяк — **реши сам, бери если по пути**
С разрешённым ожиданием до ~20 минут INFO-лог говорит «calling model» и потом молчит. Дешёвая строка «жду
провайдера: прошло X из Y» раз в N минут снимает это. ⚠ И назови в отчёте, что `waitForSettle` теперь ждёт
до самого длинного летящего вызова — это принятая цена решения владельца, но она должна быть названной.
### 4.6-бис Мандат чистоты — **делай РОВНО так**
Пак ПРОЕКТИРУЕТ: новый тип ошибки, три новых поля конфига, новая денежная семантика. ⇒ `D39.216` действует
буквой: где нынешняя форма не выдерживает заказ, её **развязывают или переписывают**, а не обкладывают
заплатой; «дешевле подпереть» доводом НЕ является; второй носитель одного знания не заводится (п.3б).
Отступление законно, но объявляется С ЦЕНОЙ: почему заплата дешевле развязки и что именно она откладывает.
⚠ Нота требует этой строки в КАЖДОМ паке с проектированием и называет её отсутствие дефектом промта — она
здесь не для полноты.
### 4.6-трис ⚠ **ИСПР. 10.09 ПО ИТОГУ ПРИЁМКИ — ДВА МЕСТА ЭТОГО ПАКА ОКАЗАЛИСЬ НЕВЕРНЫ, И ОБА МОИ**
Приёмка предъявила два расхождения между буквой пака и тем, что ратифицировано. Исправляю здесь, а не в
письме, чтобы читатель пака не мерился ложной буквой.
1.**§4.2 писал: «доставлен + наш дедлайн (с заголовками И БЕЗ) ⇒ сумма через `settleUSDForCutCall`».**
Это отменено МОИМ ЖЕ поздним заказом — разведением предикатов: деньги считаются по
`answered && afterHeaders`, а до заголовков доставленный обрыв книжится **нулём**. Довод был замером и
он в силе: 3 из 25 отменённых в полёте сеттлили оценку за запрос, которого не видел ни один хендлер, —
переплата ЧИТАТЕЛЯ, единственное запрещённое `D39.196` п.2а направление. ⇒ **буква §4.2 про «и без»
недействительна; действует разведение предикатов.** Остаток (доставленный обрыв до заголовков уходит в
ноль) — строка бэклога, не дефект исполнения.
2.**§4.7 писал: «Anthropic-адаптер — ВНЕ пака ЦЕЛИКОМ».** Отменено моей же ратификацией `D39.231` п.2:
одна строка `CheckRedirect` внесена, потому что через редирект уходил `x-api-key`, а `net/http`
кастомный заголовок через смену хоста не срезает. Приёмка права, что это выход за объявленное сужение —
и права также в том, что цена (тест и мутация) ровно та, которой сужение и мотивировалось. ⇒ **сужение
§4.7 действует на всё, КРОМЕ утечки учётных данных.**
### 4.7 Чего в паке НЕТ — **не делай**, и это ОБЪЯВЛЕННЫЕ сужения
- **Стриминг** — не спас бы ни одно из 19 замеренных событий; цена: `stream:true` в теле ⇒ `RequestHash`
перекупка книги, непроверенная байт-идентичность склейки, новый парсер, потеря usage-fidelity. Trip-wire
обзора переписан: доля self-cut в спенде редактора ПОСЛЕ починки дедлайна.
- **Платная проба вендорского баланса** — ОТКЛОНЕНА владельцем (`D39.230` п.2). Не проектировать.
- **Адаптивный пол из леджера на лету** — прячет число от оператора, тащит состояние прогона в дедлайн.
- **`max_attempts` и бэкофф** — другая ось, не трогать.
- **Порог по латентности** («≈2×attempt_s») — совпадение; протухнет молча при смене `attempt_s`.
- **Idle-timeout в non-streaming** — схлопывается в общий бюджет (§Q6 прав).
- **Разбор обрывков тела.**
- **Оценка по времени `elapsed × rate`** — догадка на догадке; оправдана только исходом пробы, которой не будет.
- **Денежная сумма как YAML-ручка** — политику нельзя переключать молча конфигом; она за одной функцией.
- **Строка 368 (`est_tokens` занижена в 5 раз)** — отдельный носитель, другой предмет. Наткнёшься —
наблюдением в отчёт, не чини: два денежных класса в одном лендинге мы сегодня уже разводили.
- **Anthropic-адаптер** (`provider_anthropic.go`) — ВНЕ пака целиком: поверхность deprecated (из стека
убрана 04.07), боевой цены ноль, а тесты и мутации она бы добавила. Уходит строкой бэклога.
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается
1. **Каждая гарантия предъявляется ИСПОЛНЕНИЕМ на фейк-сервере**, а не чтением. Пять исходов после 2xx и
четыре до заголовков — **ДЕВЯТЬ фикстур**, и у каждой утверждается ЧИСЛО HTTP-вызовов, а не только исход.
**Счёт вызовов берётся у ФЕЙК-СЕРВЕРА** (сколько запросов он принял), а не счётчиком клиента: клиент
считает свои намерения, сервер — факты.
⛔⛔ **И к КАЖДОЙ доставленной-и-потерянной фикстуре — ПАРНОЕ утверждение о РЕЗЮМЕ**, иначе конструкция
вырождается в противоположную сторону: сессия может «пере-закрыть» — не пере-делывать НИЧЕГО
доставленного — и остаться зелёной. Ожидания: self-cut → резюм **0** вызовов; отмена родителем и
исчерпанный разрыв → **ровно 1 вызов на ТОМ ЖЕ `max_tokens`**; не доставлен → 1.
**Поля утверждаются ДВУСТОРОННЕ, иначе они декоративны:** `WhitespaceOnly``true` на теле `"\n"` и
`false` на обрывке JSON; `Delivered`обе стороны. И денежное утверждение на каждой строке:
`estimated=1`, `cost_usd == estimate`, класс на месте; у «не доставлен» — `reserved_usd` вернулся в 0.
2. **Мутации — на копии, рецепт УЖЕ в цели `mutations`** (`backend/Makefile`); своего однострочника не пиши.
Краснота читается ПО ТЕКСТУ падения: говорит ли он про сломанное тобой.
3. **Мутируй и СВОИ новые фикстуры.** ⛔ Особая осторожность: у тебя условные сообщения (пометка
`estimated`, WARN про пол провайдера) — на каждое нужна фикстура, где оно ОБЯЗАНО прозвучать, И вторая,
где обязано молчать. Тест, утверждающий только молчание, вакуумен.
4. **Субагенты РАЗРЕШЕНЫ ЯВНО** — и на охват, и на адверсариальный проход по ГОТОВОЙ работе.
**Направление — что уязвимо ИМЕННО в этом паке** (норма куплена замером 08.09: направленный проход
нашёл шесть дефектов пака на ЗЕЛЁНОЙ батарее): **(1)** фикстуры, где «доставлен» и «не доставлен»
случайно совпали — весь пак стоит на этом различении; **(2)** путь резюма: чекпойнт написан, но
`classify()` его не узнаёт ⇒ перекупка возвращается тихо; **(3)** `errors.Is(context.Canceled)` после
твоего обёртывания — коды выхода ломаются молча, И одновременно `errors.As` обязан НАХОДИТЬ твой тип:
два утверждения в одной фикстуре, не в разных; **(4)** исход «отмена родителем» — его легко потерять,
потому что он единственный, где деньги списываются, а флага НЕТ.
5. **Проверяемый артефакт** — таблица «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО» плюс столбец
**«закрыт в N носителях из M»**, где M посчитан командой.
6. **BASELINE НА ВХОДЕ сними САМ** и сверь с моим: `make battery` → 19 `ok` · 0 FAIL · 4 «no test files» ·
4 скипа; `make mutations`**65 записей · 0 выживших · свип `0 of 276`**; `python3
docs/scripts/counts.py --check` → «Литералы сходятся». ⛔ **Печатай счёт СКИПОВ рядом с числом `ok`**
норма куплена 08.09: «20 пакетов ok» и «20 ok при 431 скипе» неразличимы, и приёмка так приняла механизм,
ни разу его не коснувшись. Красное на входе — находка и пинг, а не твоя работа.
## 6. Оси ревью — характер «Код», плюс деньги и шов (ось вправе заменить с аргументом)
Тест утверждает не то, что думает (`D39.171`): часы как способ синхронизации там, где предмет НЕ время, и
ассерт по подстроке в общем буфере. Обе формы дают и ложное красное, и **тихо-зелёное** — оно дороже.
**Деньги:** любое число из СЫРОГО леджера, а не из агрегата; леджер — нижняя граница.
**Шов:** платформа биллит пользователю `committed_usd` и просила у движка публикацию оценочных строк
(`PD-441`). Инвентарь её каналов — чтением ЧУЖОГО кода, не по памяти; правок в `platform/` НЕ делать.
**Индустриальный первоисточник прежде велосипеда:** `httptrace` — stdlib; если решишь иначе, отвергни с доводом.
## 7. Записка-план ДО работы
До первой правки — короткая записка в свою секцию журнала: что берёшь, в каком порядке, что считаешь
рискованным. После работы — сверка комплектности против §4 МЕХАНИЧЕСКИ (таблицей), не перечитыванием.
## 8. Заявление = команда
**Эрудиция НЕ улика:** утверждение о коде, провайдере или чужой зоне грунтуется `file:line`, цитатой или
замером — либо несёт пометку **«мнение»**. Каждое число и категорика — с командой, которой получены. Отрицательный замер печатается ВМЕСТЕ с
контрольной величиной. Сверх этого: **перед отчётом сверь каждый клейм с результатом инструмента ЭТОЙ
сессии**, а не с памятью и не с этим промтом — адреса §4 сняты 08.09 и могли уехать. **Последний абзац
отчёта оказался планом или обещанием? сделай это СЕЙЧАС.** ⛔ Править тест/гейт, чтобы он прошёл, —
НЕДОПУСТИМО; исключение — правка от ЗАКАЗАННОЙ смены поведения, и она объявляется отдельным пунктом.
Сессия длинная — поставь себе интервальную самоверификацию субагентом против ЯВНЫХ критериев §13.
## 9. Эхо-протокол старта
ДО работы — ≤10 строк **СВОИМИ словами**: скоуп · инварианты · что НЕ делаешь. Дословный пересказ промта
подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним. Эхо — оркестратору по адресу
из прямого канала (§12); канала нет — эхо первой секцией отчёта, работа продолжается.
## 10. Что НЕ удалось — обязательная секция отчёта
Что не вышло, что не проверено, где сомневаешься. «Не проверено» и «опровергнуто» — РАЗНЫЕ исходы.
## 11. Канал вопросов и право сказать «этого делать не надо»
Конфликт промта с кодом или доками — **пинг, а не интерпретация**: конструкция этого пака сошлась в
консилиуме двух моделей, но её проверяли чтением, а не твоим прогоном. Считаешь пункт неверным — скажи с
доводом, довод сильнее моей подписи. Решение владельца требуется, если работа упрётся в перекупку книг, в
скоуп или в продукт.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши СВОЙ блок в `/tmp/textmachine-channel` первым делом,
чужие блоки не трогай. ⚠ Адрес из файла не доказывает, что сессия жива: сверься с `ListAgents`. Нужной роли
нет или имя не отвечает ⇒ **канала нет, и это НОРМАЛЬНЫЙ случай: НЕ опрашивай сессии подряд** — вопрос
секцией в отчёт, работа продолжается.
## 13. Критерий завершённости — по нему тебя примут
Работу доводишь до конца САМ; приёмка идёт последней и вторым кругом твоей самопроверки не служит.
Готово — это когда:
- у КАЖДОГО пункта §4 есть исход: сделано · не делаю с доводом · пинг;
- круги СОШЛИСЬ: последний проход не дал НОВЫХ находок, прежние закрыты таблицей;
- **каждый класс закрыт со ЗНАМЕНАТЕЛЕМ «N из M»**, и M посчитан командой;
- ДЕВЯТЬ фикстур §5.1 существуют, утверждают ЧИСЛО вызовов у СЕРВЕРА и несут ПАРНОЕ утверждение о резюме;
пин резюма (§4.3) предъявлен мутацией; поля утверждены двусторонне;
- тест-тождество §4.4 пинит формулу, а не константу;
- числа сняты ПОСЛЕ последней правки, со счётом скипов рядом с числом `ok`;
- **таблица мутаций ПОЛНАЯ, и выжившие названы поимённо** (выжившая мутация — находка, а не шум);
- **дифф `^func Test` снят ИСПОЛНЕНИЕМ, а не памятью**, и назван прибор, которым считан;
- всё живое — в ДЕРЕВЕ, а не в письме;
- сказано явно: **«работа завершена, править не планирую»**. Без этой фразы пак считается идущим.
## Деньги
**Пак $0.** Платных вызовов провайдерам НОЛЬ: все девять исходов предъявляются на `httptest`-сервере,
формула дедлайна — арифметикой, резюм — фикстурой. ⛔ Платная проба вендорского баланса ОТКЛОНЕНА
владельцем (`D39.230` п.2) — не проектировать и не предлагать. Кажется, что нужен платный прогон, — это
стоп и пинг, а не вызов.
## Отчёт
Своя секция в `docs/PROGRESS.md`: исход по каждому пункту §4 · таблица «находка → что сделано → чем
предъявлено» · столбец «закрыт в N из M» · числа со счётом скипов · заказанные смены поведения отдельными
пунктами · пинги (квирк-реестр — полигону; публикация оценочных строк — платформе) · §10 «что не удалось» ·
вопросы оркестратору.