> ⚠ **ЭТО ХЕНДОФФ-ПРОМТ ДВИЖКОВОЙ СЕССИИ. Он живой и исполняется.** Прочитай `CLAUDE.md`, потом этот > файл целиком, потом — его карту чтения (§3) и больше ничего, пока не появится вопрос. > ⛔ **ПАК ПАРНЫЙ.** Твоя половина не лендится в одиночку: у платформы есть своя, и они принимаются > вместе (`D39.234` п.2). Что это значит практически — §2 и §13. # Пак бэкенда: «ДВЕ ОСТАНОВКИ» ## 1. Какая проблема и что решит твой результат Человек, который остановил перевод книги, сегодня рвёт всё, что летит к провайдерам прямо сейчас. Оплаченные генерации, которые провайдер уже начал, обрываются; движок честно списывает за них оценку, помечает позиции и на продолжении делает их заново — то есть **платит второй раз за ту же работу**. Владелец назвал это прямо: «нахрена обрывать соединение и запрос и посылать заново и терять до трети стоимости». 10.09 он ратифицировал форму (`D39.234` п.1): **остановок должно быть две.** - **МЯГКАЯ** — первое нажатие. Новых единиц работы не раздаём, **начатое доигрываем**, выходим сами. Ничего не рвётся, поэтому ни одна позиция не получает пометки «остановлено» и никто не платит дважды. - **ЖЁСТКАЯ** — второе нажатие. Ровно сегодняшнее поведение: отмена контекста, сеттл оценок, пометки `cancelled`. - Третье нажатие — дефолт процесса. **Твой результат:** у человека появляются обе, они различимы машинно, и платформа их различает. ⚠ **Это не «добавить фичу», а привести дерево к тому, что мы уже обещали.** Контракт `docs/architecture/14-api-contract/openapi.yaml:762` говорит «finished work is kept and not paid for again», а платформа в собственном коде пишет, что движок «finishes the in-flight chunk before exiting» (`platform/internal/runner/runner.go:56-58`). Оба утверждения сегодня ЛОЖНЫ. Ты делаешь их истинными. **Носители в трекере:** строки **381** (две остановки), **382** (пометка «оценка» не едет по шву), **377** (оплаченный обрыв, за которым ретрай успел, исчезает бесследно), **372** (кто отменил — человек или упавший сосед), **375** (выгрузка читает остановленную позицию как выпавший текст), **379**(а) (пин на стенных часах). Их тела — в `docs/BACKLOG.md`, грепом по номеру. ## 2. Зона и git Твоя зона записи — **`backend/`**. Ничего вне неё не трогаешь: `platform/` делает своя сессия, `docs/` — оркестратор. **Коммитит только оркестратор**: дерево готовишь и передаёшь на лендинг. Свои итоги, вопросы и расхождения пиши в СВОЮ секцию `docs/PROGRESS.md` — это единственное исключение из «`docs/` — зона оркестратора». ⛔ **Пара.** Платформенная половина (`mode` у `/stop`, грейс, путь второго сигнала, чтение оценочных строк) делается параллельно другой сессией. Ты её кода не пишешь и не правишь. Но твой кадр событий — её вход, поэтому **форму кадра ты обязан объявить в отчёте раньше, чем закончишь**: оркестратор сведёт её с платформенной половиной. Если тебе нужно от платформы поведение, которого нет, — это вопрос оркестратору, не правка. ## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам 1. `backend/internal/pipeline/waverun.go` — целиком. Здесь живёт мягкий ГЛАГОЛ, который ты будешь поднимать (`latched`, строки ~353–363), жёсткий (`cancel()`, ~404 и ~414) и обе пост-обработки волн. 2. `backend/internal/pipeline/stagerun.go` — от `runStage` до конца денежного гейта: пер-вызовная резервация `r.gate.admit(...)` (~570), ожидание `waitForSettle` (~607), хартбит «still waiting for the provider» (~990). 3. `backend/internal/pipeline/reservegate.go` — тип `waitOutcome` и его исходы (~113–128). 4. `backend/cmd/tmctl/main.go` — словарь кодов выхода (~96–104), `signal.NotifyContext` (~207), `exitCode` (~115–130); и `backend/internal/pipeline/events.go` — `terminal()` (начало ~479, ветки исходов ~505–530: их ПОРЯДОК там сам объявлен утверждением, читай с начала функции). 5. `backend/internal/runevents/runevents.go` — кадр `Finished` (~255+) и **комментарий к полю `Volume`**: он объясняет, ПОЧЕМУ у объёмного потолка нет своего `outcome`, и это твой прецедент. Всё остальное — грепом по мере вопросов. Тела нот: `docs/architecture/05-decisions-log.md`, номер грепается `^## D<номер>`; статус одним хопом — реестр `docs/architecture/05-decisions-index.md`. ⚠ ПОДНОМЕР вида `D39.234` п.1 собственного тела не имеет — это ПУНКТ внутри `## D39.234`. ## 4. Состав и разметка свободы ### 4.1 Типизированный сентинел остановки — **делай РОВНО так** Мягкая остановка **контекст не отменяет**. Значит `errors.Is(err, context.Canceled)` на ней ЛОЖНО, и без своего типа она провалится в ветку «ошибок нет» — то есть прогон с недоделанными единицами уедет как **тихий успех**. Это ровно ловушка, которую `waverun.go` уже называет своими словами («a draft-only run reporting empty translations as exit-0 success»). ⇒ заведи сентинел (имя твоё, `*StopRequested` — предложение) со **своим слотом** в `fail()` волны, рядом с тем, что уже есть у `*CeilingHalt`. Ранжирование при столкновении — твоё решение, но объясни его в коде: паника старше остановки, остановка старше обычной ошибки — или иначе, если у тебя есть довод. ### 4.2 Где именно мягкая остановка отказывает — **делай РОВНО так** **Точек отказа ДВЕ, и обе обязательны.** ⛔ **Исправлено по ревью до выдачи: одного гейта НЕДОСТАТОЧНО, и это замерено.** **(1) Пер-вызовный денежный гейт** — `r.gate.admit(...)` (`backend/internal/pipeline/stagerun.go:570`). Он отсекает НОВЫЕ вызовы: элемент волны — это вся последовательность стадий, то есть несколько платных вызовов подряд, и без гейта рабочий, уже держащий элемент, доиграет всю. **(2) Транспортная петля ретраев** — `retryLoop` (`backend/internal/llm/httpllm.go:187`). ⛔ Без неё ратифицированные ~20 минут НЕ ПОЛУЧАЮТСЯ: резервация берётся ОДИН раз на `runAttempt` (`stagerun.go:477`, `admit` на `:570`), а `client.Complete(ctx, …)` (`:699`) держит под ней ВСЮ цепочку ретраев — это говорит собственный комментарий кода: «This timer wraps client.Complete — **the WHOLE retry chain**» (`stagerun.go:984`). ⇒ 503 на первой попытке → бэкофф → вторая попытка ПОД ТОЙ ЖЕ резервацией, гейта никто не спросит, и ожидание становится ЦЕПОЧКОЙ (~64 мин), а не попыткой (~20). `retryLoop` сегодня между попытками смотрит только `ctx.Err()` (`httpllm.go:197`) — мягкий стоп он увидеть не может. ⇒ **транспорт обязан видеть мягкую остановку МЕЖДУ попытками и в бэкоффе**: сигнал «новых попыток не начинать», который читается только на границе попытки и НЕ трогает летящий запрос. Форма твоя (лишний параметр профиля, поле клиента, значение в контексте — выбери и обоснуй), но ограничение обязано стать ЗАПИНЕННЫМ, а не подразумеваемым. ⭐ **Владелец ратифицировал именно ~20 минут** (`D39.234` п.1б): «Приемлимо». Цепочку ретраев не доигрываем — ретрай это НОВОЕ решение о покупке. ⚠ Фидер волны при этом тоже латчится (новых единиц не начинаем) — но это следствие, а не механизм. ### 4.3 Прогонный источник латча — **делай РОВНО так по форме, устройство твоё** Мягкий глагол уже построен и работает: `latched` в `runWave`, дословно «a different verb from cancel(): closing it makes the feeder stop handing out indices — nothing new begins — while every worker already holding an item runs it to completion under a context nobody cancelled». ⛔ Но он **локален одному вызову** `runWave`, а вызовов два (черновая волна и редакторская). Кнопке нужен ПРОГОННЫЙ источник, который каждая волна и каждый гейт спрашивают. Это подъём существующего на этаж выше, а НЕ новый механизм (⚠ испр. до выдачи: вызовов `runWave` ровно ДВА — `waverun.go:140` и `:285`; по главам волны НЕТ, прежняя редакция этого пункта учила неверной топологии) — если ты пришёл к выводу, что нужен новый, напиши почему, прежде чем писать код. ### 4.4 Пост-обработка ОБЕИХ волн — по образцу потолка, не по образцу отмены — **делай РОВНО так** Сегодня в `waverun.go` (~161–173) ветка «ошибка волны» разделена надвое: `*CeilingHalt` уходит через `r.reportEvicted(...)` + `r.moneyStoppedResult(...)`, любая другая ошибка — `return nil, err` мимо всего. Мягкая остановка обязана идти по ПЕРВОМУ пути. Причина названа там же в коде и она про деньги: черновая волна после остановки НЕ должна идти ни в майнинг банка, ни в редакторскую волну, потому что редакторская находит незапущенного члена с нулевым результатом и **платит за редактуру единицы с дырой**. Тот же класс уровнем выше: пере-сеять банк из половины книги значит сдвинуть снапшот редакторской волны для всех последующих прогонов. Редакторская волна — свой путь (`foldUnitOutcomes` + `noteMoneyStop`, ~299–318 и ~928–933). ### 4.5 Банковые проходы идут ВНЕ волны — **делай РОВНО так** `runTerminologist` (`backend/internal/pipeline/terminologist.go:348`, зовётся из `mining.go:166`) и `runBankRoleBatches` (`terminologist.go:907`, зовётся из `terminologist.go:465` и `:666`). ⚠ **Испр. до выдачи: прежняя редакция утверждала, будто латч их не остановит, потому что они идут мимо волны — это ЛОЖЬ.** Они идут через `r.runAttempt(...)` (`terminologist.go:819`), то есть через тот же гейт `r.gate.admit`. Гейт их остановит — но ОШИБКОЙ посреди паса, а не усечением. ⇒ **решение, которое принимаю я, а не ты:** партии, уже купленные к моменту мягкой остановки, **НЕ применяются**, пас выходит остановкой ДО применения. Довод тот же, что в §4.4: пере-сев банка из половины двигает снапшот редакторской волны для всех последующих прогонов, а половина паса — это половина банка. Деньги за купленные партии уже потрачены и остаются в леджере; терять их обидно, но тихо сдвинутый снапшот дороже. У паса есть СВОЙ попартийный приём (`terminologist.go:940–970`), где он уже умеет решать «сколько партий беру» и говорить об этом вслух — это и есть место, где мягкая остановка говорит «новых партий не начинаем». ⚠ Читай там комментарий целиком прежде, чем править: он объясняет, почему этот пас УСЕКАЕТ, а не ОТКАЗЫВАЕТ, и почему отказ классом `Refusal` был бы враньём платформе про деньги. ### 4.6 Эскалация вторым сигналом — **делай РОВНО так** ⛔ **Сегодня второй Ctrl-C не делает НИЧЕГО, и это надо знать до того, как ты начнёшь.** `signal.NotifyContext` (`main.go:207`) поднимает горутину, которая после ПЕРВОГО сигнала выходит; регистрация снимается только `defer stop()` (~208); канал ёмкостью 1. ⇒ второй сигнал падает в переполненный буфер и никем не читается, процесс не умирает. То есть «второе нажатие ужесточает» — **новое поведение, которое ты пишешь**, а не восстановление дефолта. Форма ратифицирована (`D39.234` п.1а): первый сигнал — мягкая, второй — жёсткая (сегодняшний путь), третий — дефолт процесса. Устройство твоё; `NotifyContext` для этого мал, и это нормально. ⚠ **Платформа шлёт ровно ОДИН SIGTERM** (`platform/internal/runner/runner.go`, `systemctl stop`). Значит её «остановить сейчас» — это второй сигнал, который она пошлёт отдельно. Твоя часть — чтобы второй сигнал делал жёсткую остановку независимо от того, человек его послал или платформа. ### 4.7 Кадр `finished`: числа, а не новое слово — **делай РОВНО так** ⛔ **Коды выхода НЕ ТРОГАТЬ. Оба стопа — 5.** Полоса заморожена («a fresh code would be a word added to a ratified dictionary»), и на 5 завязана логика платформы: `platform/internal/ingest/exit.go:34` = `ExitStopped = 5`. ⛔ **И нового значения `Outcome` тоже НЕ заводить.** Прочитай комментарий к полю `Finished.Volume` (`runevents.go:255+`) — там разобран ровно этот соблазн и объяснено, почему он неверен: словарь `Outcome` читается из ОБОИХ каналов, каждое значение маппится в код выхода, и значение, живущее только в потоке, заставило бы два канала сообщать РАЗНЫЙ исход одного прогона. **У чисел словаря нет, поэтому факт едет числами** — присутствие поля и есть признак. ⇒ различие «мягко / оборвано» едет ПОЛЕМ с числами. Форму предложи сам и объяви её в отчёте ДО того, как закончишь (§2): оркестратору её сводить с платформенной половиной. Мой набросок, спорь с ним — `stop: {mode: soft|hard, in_flight_finished: N, in_flight_cut: M, cut_estimated_micro_usd: X}`. ### 4.8 Строка **377** — оплаченный обрыв, за которым ретрай УСПЕЛ, исчезает бесследно `backend/internal/llm/httpllm.go`, ~189–191: на успешном ретрае `if err == nil { return resp, nil }`, и накопленный `owedCut` ВЫБРАСЫВАЕТСЯ. Провайдер спрошен дважды (первый вызов доставлен, подтверждён и оборван), вызывающий получает чистый успех и о первом не узнаёт ничего — ни строки леджера, ни оценки, ни пометки. ⭐ **Владелец велел делать её ЭТИМ ЖЕ заходом** (`D39.234` п.1в), и довод не в сумме. `D39.230` п.2 оставил открытым вопрос «биллит ли вендор оборванное» и держит его на сверке инвойса вендора с суммой оценочных строк. Невидимая вторая генерация ломает эту сверку по построению: сверять нечем. Форма починки твоя. ⚠ Ни одна из девяти фикстур этого пути не покрывает (`TestABrokenConnectionAfterDeliveryIsRetriedOnceAndOnlyOnce` роняет ОБЕ попытки) — значит фикстуру пишешь новую, и она обязана доказать, что второй запрос УСПЕЛ. ### 4.9 Строка **375** — третье состояние единицы — **делай РОВНО так** Выгрузка сегодня читает строку `cancelled` как ВЫПАВШЕГО ЧЛЕНА редакторской единицы (`memberDrops`, `backend/internal/pipeline/status.go:534`, живой вызов один — `backend/internal/pipeline/export.go:307`) и сообщает читателю «из единицы пропал текст», тогда как пропала лишь незаконченная работа. ⛔ **Простого фильтра НЕДОСТАТОЧНО.** ⚠ Испр. до выдачи: прежняя редакция называла это «замерено движковой зоной» — носителя такого замера НЕТ (наряд ставит по этой позиции «пинг», в `docs/PROGRESS.md` следов нет). Это рассуждение, а не замер, и доказать сценарий фикстурой — твоя работа. Если применить `resolvedForResume` (`cutcall.go:253`) и на этом остановиться, единица уйдёт `ok`, МОЛЧА не досчитавшись текста члена: `DroppedMembers` = 0, `DroppedReason` пуст. Это обмен ЛЖИ на МОЛЧАНИЕ, и верно ни то ни другое. ⇒ нужен ТРЕТИЙ признак: «единица неполна ИЗ-ЗА ОСТАНОВКИ». ⭐ Попутно фильтр чинит артефакт порядка: внутренний цикл оставляет ПОСЛЕДНЮЮ флагнутую черновую строку, строки приходят `ORDER BY … stage`, и при двух черновых стадиях `cancelled` может перебить содержательную причину. ⛔ **И вот ЗДЕСЬ прежняя редакция ошибалась опаснее всего: случай «`cancelled` затёр настоящую причину» ВОЗМОЖЕН.** Апсерт по `(book_id, chapter, chunk_idx, stage)` (`backend/internal/store/chunkstatus.go:93`) не добавляет строку, а ЗАМЕНЯЕТ её. Сценарий (выведен по коду, фикстурой не гонялся — прогони сам): единица c-lite, где член выпал по `length`, а редактор отработал `ok` по остатку → redrive черновика этого члена (`ResetChunkStages` сбрасывает только флагнутую стадию, `ok`-стадии никогда) → мягкой остановки над ним нет, а ЖЁСТКАЯ пишет `cancelled` поверх `length` → и фильтр §4.9 спрячет НАСТОЯЩИЙ дроп. ⇒ починок ДВЕ: третий признак «неполна из-за остановки» И запрет `cancelled` затирать флагнутую строку (колонка `first_flag_reason` для этого уже есть). Форма твоя; сценарий обязан быть предъявлен фикстурой. ### 4.10 Строка **372** — четвёртая причина аборта ожидания — **делай РОВНО так** `waitOutcome` (`reservegate.go`) сегодня разводит исходы ожидания, и его собственный комментарий рассказывает, какая ошибка на этом месте уже была совершена: с `bool` «ничего не летит» и «контекст умер» читались одинаково, и прогон, остановленный человеком, публиковал `ceiling`-событие с нехваткой — то есть просил у платформы денег за остановку, к деньгам отношения не имевшую. Мягкая остановка — ЕЩЁ ОДНА причина, и вместе с ней решается 372: «кто отменил — человек или упавший сосед» превращается в «мягко / жёстко / сосед». ⚠ **Мелочь, которую заметь и почини заодно:** комментарий над типом говорит «THREE values», а констант там четыре — `waitNotAllowed` добавили позже и текст не поправили. Ты добавляешь пятую. ### 4.11 Строка **382** — пометка «это оценка» не едет по шву — **делай РОВНО так по своей половине** Владелец разрешил списывать по ОЦЕНКЕ при УСЛОВИИ, что пометка стоит (`D39.230` п.1). В движке она есть: `backend/internal/pipeline/status.go:321` — `estimated_rows` / `estimated_usd` в `status --json`. Дальше она обрывается: в кадрах `backend/internal/runevents/runevents.go` слова `estimated` **0 хитов** (контроль: `committed` в том же файле — **6**; денежный кадр несёт один `committed_micro_usd`, ~244). Платформа её не читает и прямо просит: `platform/internal/pgstore/credits.go:235` — «publish the count and sum of estimated-price rows beside committed_usd, which is what would let this side say "at most Y"». ⇒ **твоя половина — довезти счёт и сумму оценочных строк до кадра.** Чтение — платформенная половина. Форму согласуй тем же объявлением, что и §4.7: это один шов, и два поля на нём не должны спорить. ### 4.12 Строка **379**(а) — пин, решаемый окном стенных часов — **бери, раз всё равно трогаешь** `backend/internal/pipeline/cutcall_test.go:402` — две строки таблицы девяти («after 2xx · cancelled by the operator» и «before headers · cancelled by the operator») разводятся `<-srv.arrived` + `time.Sleep(50 * time.Millisecond)` против `attempt_s: 1`. По какую сторону границы заголовков сел `cancel()`, не утверждается ничем; на мутанте это давало **2 красных из 8**. Ты в любом случае перестраиваешь семантику остановки, значит эти строки твои. Фикстура обязана строиться так, чтобы предмет был НЕИЗБЕЖЕН, а не вероятен. ### 4.13 Оператору — сказать, чего он ждёт — **реши сам, но молчать нельзя** ⚠ **Испр. до выдачи: «не печатает НИЧЕГО» было буквально неверно** — при отмене пайплайн пишет WARN (`backend/internal/pipeline/cutcall.go:214`), и `tmctl` печатает ошибку на выходе. Настоящих дыры две, и вторая дороже: **(1)** в момент сигнала подтверждения нет — человек нажал и не понял, случилось ли что-нибудь, а через двадцать секунд нажмёт снова и получит жёсткую, потеряв ровно те деньги, ради которых мягкая заводилась; **(2)** ⛔ **на стоп-пути НЕ рендерится леджер**: частичный результат `translate` показывает только для подписного стопа и потолка (`backend/cmd/tmctl/main.go`, греп `WaveSignatureStop` и `CeilingHalt`), а результат остановленного прогона выбрасывается. Мягкая остановка по §4.4 результат ПРОИЗВОДИТ (`moneyStoppedResult`) — и до оператора он не доедет. ⇒ при первом сигнале печатается строка вида «остановка запрошена: N вызовов в полёте, самый долгий дедлайн Xs; ещё раз — оборвать их (спишется оценка)». Опора готова: хартбит «still waiting for the provider on a call in flight» (`stagerun.go:990`) уже знает и число, и дедлайн. ### 4.15 Пять мест, которые ревью назвало «без них не сойдётся» — **делай РОВНО так** **(а) ⛔ ГРАНИЦЫ ФАЗ, а не только волны.** §4.4 покрывает остановку, пришедшую ВО ВРЕМЯ волны. Пришедшая МЕЖДУ фазами уводит прогон платить за редактуру: источник обязаны спрашивать переходы — после черновой волны → перед майнинг-стопом → перед подписным стопом → перед редакторской волной, включая авто-продолжение с пере-севом (`backend/internal/pipeline/mining.go`, греп `resnapshot`/пере-сев). **(б) `res.Volume = nil` на мягкой остановке** — как это делает потолок. Плановые счётчики после остановки посреди работы врут; довод дословно — в комментарии к полю `Finished.Volume`. **(в) Статус джоба при отказе гейта.** Сегодня отказ даёт `failed` (`backend/internal/pipeline/stagerun.go`, греп `mandatory`/статус после `admit`). Для мягкой остановки это НЕВЕРНОЕ слово: ничего не сломалось. **(г) Столкновение мягкой остановки с падением соседа — решение приняло за тебя.** Инфра-ошибка соседнего рабочего зовёт `cancel()` (`waverun.go`, ветка `fail`) и рвёт ровно те вызовы, которые мягкая остановка берегла. **Это приемлемо** — провайдер сломан, беречь нечего, — но исход прогона тогда `failed` С пометками `cancelled`, и так и должно быть. Не строй защиту от этого; запинь, что так и есть. **(д) Пин на резюм после МЯГКОЙ остановки:** ноль платных вызовов по уже отвеченным стадиям, ничего не пере-делывается. Тривиально по построению — и ровно поэтому должно быть проверено, а не подразумеваться. ### 4.14 Чего в паке НЕТ — **не делай**, и это ОБЪЯВЛЕННЫЕ сужения - **Платформенная половина** — чужая зона и чужая сессия. Ты не правишь `platform/` вообще. - **Коды выхода и словарь `Outcome`** — заморожены (§4.7). - **Продуктовый вопрос «сколько ждать по умолчанию»** — решён владельцем (~20 мин, одна попытка). Не пере-открывай; если у тебя есть довод против — это вопрос оркестратору, а не правка. - **Строки 373 и 376** (инертный дедлайн на HTTP/2; доставленный обрыв до заголовков книжится нулём) — в паке НЕТ. У 376 направление недосчёта ратифицировано (`D39.196` п.2а), у 373 нужен пир, не дренящий тело. - **Вердикт главы** — трогать НЕ надо: замером 10.09 установлено, что остановка его уже не портит (`D39.233` п.2 — там замер; пин `TestAStoppedPositionIsNotADecidedUnit` зелён). ## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается Это норма проекта и решение владельца, не пожелание. Самоотчёт «проверено» без исполнения здесь регулярно оказывался ложным и стоил денег. 1. **Baseline СВОИМ прогоном ДО первой правки:** `make battery` — запиши MAKE-EXIT, число `ok`, число FAIL, «no test files» и **число скипов поимённо**. ⛔ Прогон без `-v` прячет скипы: однажды 431 скип проехали как зелень, и среди них были все пины предмета. 2. **`python3 docs/scripts/counts.py --check`** — до и после. 3. **`make mutations`** — снимается отдельно; число внеси в итоговый отчёт вместе с составом. 4. **Мутируй СВОИ новые тесты.** Линза качества трижды находила дыры в тестах, которые их автор считал сильными. 5. ⛔ **Мутация засчитывается по ТЕКСТУ падения, а не по факту красноты.** Читай текст: говорит ли он про сломанное тобой. Правый вердикт по неправой причине — дыра, а не поимка. 6. ⛔ **Пин, ФЛЕЙКОВЫЙ на мутанте, измеряет пустой сценарий.** Денежный пин гоняется не один раз (8 прогонов — рабочее число этой смены), и фикстура строится так, чтобы предмет был НЕИЗБЕЖЕН. 7. ⛔ **Пин может удовлетворяться ЧУЖОЙ уликой, и это не флейк, а детерминированная пустота.** На мутанте спрашивай не «покраснело ли», а ЧТО ИМЕННО удовлетворяло утверждение. 8. ⛔ **Отрицательный замер обязан ДОКАЗАТЬ, что спросил существующее:** рядом с нулём ПЕЧАТАЕТСЯ контрольная величина. Не «проверено с контролем», а число. 9. ⛔ **Копия под мутацию защищается ПОСТРОЕНИЕМ** (`test -f go.mod` + сверка `pwd`), а не `set -e`: 10.09 `cd` в несозданный каталог провалился, `set -e` не удержал, и мутация ушла в НАСТОЯЩЕЕ дерево. **Адверсариальный проход по СВОЕЙ готовой работе — отдельным заходом, свежим взглядом.** Что в ЭТОМ паке уязвимо, направление даю: - **Гонки.** Мягкая остановка — это состояние, которое читают несколько горутин. Латч в `runWave` сделан КАНАЛОМ, а не флагом, и в коде объяснено почему (фидер БЛОКИРУЕТСЯ на отправке ровно в том состоянии, в котором остановка и наступает). Твой прогонный источник обязан удержать то же свойство. - **Тихий успех.** Главный риск пака (§4.1). Спроси у своей работы: может ли прогон с недоделанными единицами уехать с кодом 0? - **Двойной учёт.** Мягкая остановка не платит — значит ни одна пометка `cancelled`, ни один сеттл оценки на ней возникать НЕ должны. Если возникают — ты построил жёсткую под мягким именем. - **Тест, который измеряет время вместо предмета** (§4.12) — не повтори его в новых фикстурах. ## 6. Оси ревью — характер «Код», плюс деньги и шов Ось вправе заменить с аргументом. По умолчанию: **деньги** (кто и когда платит), **шов** (что видит платформа), **конкурентность** (латч и его читатели), **словарь исходов** (коды выхода и `Outcome` неприкосновенны), **тесты** (не измеряют ли они соседнюю величину). ## 7. Записка-план ДО первой правки Напиши в свою секцию `docs/PROGRESS.md` порядок работы и почему он такой, ДО того как тронешь код. Рекомендованный: §4.1 сентинел (без него ни один другой пункт не проверяется) → §4.3 источник → §4.2 точка отказа → §4.4/§4.5 пост-обработка → §4.6 эскалация → §4.7 кадр → дальше строки. ⛔ **ДВА ЛЕНДИНГА, линия объявлена заранее.** Пак большой: транспорт, пайплайн, `tmctl`, кадр событий плюс конкурентность. Резать его на две СЕССИИ нельзя — §4.2 и §4.8 правят одну функцию `retryLoop`. Поэтому одна сессия, но **два лендинга**: первый — §4.1–4.7, §4.10, §4.12, §4.13, §4.15 и кадр; второй — §4.8 (строка **377**) и §4.9 (строка **375**). ⚠ **Кончившийся контекст после первого лендинга — штатный исход, а не провал:** остаток уходит новым промтом новой сессии, и это заранее разрешено. Владелец велел делать 377 «этим же заходом» — речь была о ПОРЯДКЕ работ, а не о размере одного лендинга. ## 8. Заявление = команда Любое «сделано / проверено / закрыто» в отчёте сопровождается КОМАНДОЙ, которой это можно воспроизвести, и её выводом. Утверждение без команды не считается сделанным. ## 9. Эхо-протокол старта Первым сообщением верни: **(а)** одной фразой — в чём разница между двумя остановками; **(б)** какой пункт §4 ты считаешь самым опасным и почему; **(в)** что в этом паке ты считаешь неверным или недоказанным (право сказать это у тебя есть и им пользуются). ## 10. Что НЕ удалось — обязательная секция отчёта Пустой она не бывает. Если пуста — ты не искал. ## 11. Канал вопросов и право сказать «этого делать не надо» Вопрос — секцией в свою часть `docs/PROGRESS.md`; оркестратор читает. ⚠ Слово, живущее только в переписке, умирает вместе с сессией: этой смене это стоило трёх потерянных диспозиций (`D39.231` п.1). Всё, что должно пережить твою сессию, пишется в репозиторий. Если считаешь, что пункт пака неверен, — скажи, а не обходи молча. Два пункта прошлого пака оказались неверны, и оба были ошибками оркестратора. ## 12. Критерий завершённости — по нему тебя примут 1. Две остановки существуют, различимы человеком и машиной, и мягкая **не создаёт ни одной пометки `cancelled`** — это проверяется тестом, а не рассуждением. 2. Прогон с недоделанными единицами не может уехать как успех. 3. Ожидание мягкой остановки ограничено ОДНОЙ летящей попыткой, и это ограничение запинено. 4. Второй сигнал даёт сегодняшнюю жёсткую остановку, **третий — дефолт процесса** (проверяется, а не подразумевается). 5. Кадр `finished` несёт числами, мягко или жёстко всё кончилось, и счёт с суммой оценочных строк. Форма объявлена в отчёте ДО сдачи. 6. Строки **377**, **375**, **372**, **379**(а) закрыты, каждая с предъявленной уликой (377 и 375 — вторым лендингом, §7). 6-бис. Резюм после мягкой остановки делает ноль платных вызовов по отвеченным стадиям — запинено (§4.15д). 7. Батарея зелёная целиком (все пакеты, скипы названы), `counts.py --check` зелёный, мутационный прогон снят с числом. 8. Отчёт: что не удалось, что ты изменил против пака и почему, какие числа сняты какой командой. ## Деньги Пак **$0**: платных вызовов не требует и не разрешает. Всё проверяется фикстурами. Если тебе кажется, что нужен живой провайдер, — это вопрос оркестратору, а не решение сессии. ## Отчёт Своя секция `docs/PROGRESS.md`. Числа — командами. Ничего не коммить.