431 lines
47 KiB
Markdown
431 lines
47 KiB
Markdown
> ⚠ **ЭТО ХЕНДОФФ-ПРОМТ ДВИЖКОВОЙ СЕССИИ. Он живой и исполняется.** Прочитай `CLAUDE.md`, потом этот
|
||
> файл целиком, потом — его карту чтения (§3) и больше ничего, пока не появится вопрос.
|
||
> ⛔ **ПАК ПАРНЫЙ.** Твоя половина не лендится в одиночку: у платформы есть своя, и они принимаются
|
||
> вместе (`D39.234` п.2). Что это значит практически — §2 и §12.
|
||
|
||
# Пак бэкенда: «ДВЕ ОСТАНОВКИ»
|
||
|
||
## 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`, грейс, путь второго сигнала, чтение оценочных
|
||
строк) делается параллельно другой сессией. Ты её кода не пишешь и не правишь.
|
||
|
||
⛔ **ФОРМУ КАДРА НЕ ПРОЕКТИРУЕШЬ — она уже ЗАФИКСИРОВАНА нотой `D39.235` п.1**, до начала работ и именно
|
||
затем, чтобы две зоны не решили по-разному. Читаешь её и делаешь РОВНО так (§4.7). Если считаешь форму
|
||
неверной — это вопрос оркестратору ДО кода, а не правка.
|
||
|
||
## 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`, и это твой прецедент.
|
||
6. ⛔ **Тело ноты `D39.235` п.1** — `docs/architecture/05-decisions-log.md`, греп `^## D39.235`. Это ФОРМА
|
||
КАДРА, зафиксированная до начала работ, и она обязательна к исполнению буквой. Шестая позиция карты
|
||
намеренно: без неё ты спроектируешь то, что уже решено, и разойдёшься с платформенной половиной.
|
||
|
||
Всё остальное — грепом по мере вопросов. Тела нот: `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.15(г) — прочти его прежде, чем выбирать.
|
||
|
||
### 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: пере-сев банка из
|
||
половины двигает снапшот редакторской волны для всех последующих прогонов, а половина паса — это
|
||
половина банка. ⚠ И ничего не теряется: уже купленные партии лежат чекпойнтами и следующий прогон реплеит их за $0
|
||
(`backend/internal/pipeline/terminologist.go`, греп `Already-paid batches cost nothing`). Беречь
|
||
частичный результат применением — не бережливость, а тихий сдвиг снапшота.
|
||
|
||
У паса есть СВОЙ попартийный приём (`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` читается из ОБОИХ каналов, каждое значение маппится в код выхода, и значение, живущее только
|
||
в потоке, заставило бы два канала сообщать РАЗНЫЙ исход одного прогона. **У чисел словаря нет, поэтому
|
||
факт едет числами** — присутствие поля и есть признак.
|
||
|
||
⇒ различие «мягко / оборвано» едет ПОЛЕМ С ЧИСЛАМИ, и **форма зафиксирована `D39.235` п.1 — делай РОВНО так:**
|
||
|
||
- `Finished.Stop{ mode: "soft"|"hard", in_flight_finished, in_flight_cut, cut_estimated_micro_usd }`.
|
||
⛔ **Правило присутствия — «есть, если остановку ЗАПРОСИЛИ», а НЕ «если исход stopped».** Прецедент
|
||
дословно у соседнего поля: `Money` present iff a ceiling was REACHED, not iff the outcome is ceiling.
|
||
Это важно ровно в случае §4.15(г): остановку запросили, а прогон уехал `failed` из-за соседа — поле
|
||
обязано БЫТЬ.
|
||
- `estimated{ rows, micro_usd }` едет **на ОБОИХ кадрах** — и на кумулятивном `spend`, и в `Money`
|
||
терминального. Довод: платформа отвечает пользователю «не больше Y» ВО ВРЕМЯ прогона, а число,
|
||
появляющееся только в конце, на этот вопрос не отвечает.
|
||
- `Money` на исходе `stopped` едет **всегда, когда счётчики есть**. Сегодня он `nil` не только без
|
||
потолка, но и когда волн ещё не было (`backend/internal/pipeline/events.go`, греп `moneyLedger`):
|
||
стоп до волн — ингест, сид — счётчиков не имеет, и это законная пустота, а не умолчание.
|
||
- **Минор словаря потока `1.3 → 1.4`** (`backend/internal/runevents/runevents.go`, греп `StreamVersion`).
|
||
⚠ Платформа поднимает СВОЮ константу сама — это её половина, не твоя.
|
||
|
||
### 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"».
|
||
|
||
⇒ **твоя половина — довезти счёт и сумму оценочных строк до кадров.** Чтение — платформенная половина.
|
||
Форма — та же `D39.235` п.1, что в §4.7: `estimated{rows, micro_usd}` на `spend` И в `Money`. Ничего не
|
||
согласовывай отдельно: это один шов, и он уже описан нотой.
|
||
|
||
⛔ **ОДНО ОПРЕДЕЛЕНИЕ, ДВА ЧИТАТЕЛЯ — ЗАПИНЬ ИХ РАВЕНСТВО.** Величина «сколько из списанного — оценка»
|
||
поедет двумя каналами: кадром (эмиттер копит на лету) и `status --json`, который считает её ИЗ СТРОК
|
||
`request_log` (`backend/internal/pipeline/status.go`, греп `estimatedSpend`). Платформа по `D39.235` п.1(г)
|
||
обязана читать ОБА — и если на одном сторе они разойдутся, пользователю скажут два разных «не больше Y».
|
||
⇒ тест: на одном и том же сторе `Money.estimated` терминального кадра РАВЕН паре `estimated_rows`/
|
||
`estimated_usd` из `status --json`. Это ровно та дисциплина «одно определение», которой §4.9 требует от
|
||
`memberDrops`; здесь она нужна не меньше.
|
||
|
||
### 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`, греп `bank-mining/auto-continue`).
|
||
|
||
**(б) `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 одним из трёх слов: МЕХАНИЗМ · ПИН · ВТОРОЙ ЛЕНДИНГ.** Подпунктов
|
||
пятнадцать, но работ там не пятнадцать: часть — однострочники и пины. Разметка нужна не для порядка, а
|
||
чтобы ОТЧЁТ первого лендинга сверялся со СПИСКОМ, а не с памятью. В прошлом паке этой серии ровно такая
|
||
разметка наряда дала «36 позиций, исходов не из трёх — 0»; без неё сессия сдаёт то, что помнит.
|
||
Рекомендованный: §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. Кадры несут числами, мягко или жёстко всё кончилось, и счёт с суммой оценочных строк — **в форме
|
||
`D39.235` п.1, буква в букву**, включая правило присутствия `Stop` и минор `1.3 → 1.4`.
|
||
6. Строки **377**, **375**, **372**, **379**(а) закрыты, каждая с предъявленной уликой (377 и 375 — вторым лендингом, §7).
|
||
6-бис. Резюм после мягкой остановки делает ноль платных вызовов по отвеченным стадиям — запинено (§4.15д).
|
||
7. Батарея зелёная целиком (все пакеты, скипы названы), `counts.py --check` зелёный, мутационный прогон
|
||
снят с числом.
|
||
8. Отчёт: что не удалось, что ты изменил против пака и почему, какие числа сняты какой командой.
|
||
|
||
## Деньги
|
||
|
||
Пак **$0**: платных вызовов не требует и не разрешает. Всё проверяется фикстурами. Если тебе кажется,
|
||
что нужен живой провайдер, — это вопрос оркестратору, а не решение сессии.
|
||
|
||
## Отчёт
|
||
|
||
Своя секция `docs/PROGRESS.md`. Числа — командами. Ничего не коммить.
|