From 8fb03362707b2fbb3393d424b1577f4bf3169d9c Mon Sep 17 00:00:00 2001 From: heaven Date: Thu, 10 Sep 2026 21:54:08 +0300 Subject: [PATCH] Issue the paired packs that give a person a stop which lets running work finish, with the frame they both write to fixed beforehand and the contract corrected where it demanded a retired field. --- docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md | 400 ++++++++++++++++++ docs/BACKLOG.md | 3 +- docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md | 233 ++++++++++ docs/PROGRESS.md | 5 +- docs/README.md | 2 + docs/architecture/05-decisions-index.md | 3 +- docs/architecture/05-decisions-log.md | 24 +- docs/architecture/09-target-architecture.md | 2 +- docs/architecture/14-api-contract/README.md | 2 +- .../architecture/14-api-contract/openapi.yaml | 21 +- 10 files changed, 679 insertions(+), 16 deletions(-) create mode 100644 docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md create mode 100644 docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md diff --git a/docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md b/docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md new file mode 100644 index 00000000..599bf0ae --- /dev/null +++ b/docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md @@ -0,0 +1,400 @@ +> ⚠ **ЭТО ХЕНДОФФ-ПРОМТ ДВИЖКОВОЙ СЕССИИ. Он живой и исполняется.** Прочитай `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`. Числа — командами. Ничего не коммить. diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 913e2ccc..68728c8c 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -3,7 +3,7 @@ > **Единственный трекер проекта.** Здесь живут строки, на которые доки, промты и D-ноты ссылаются словами «строка N» / «строка бэклога N»: **ID строки стабилен навсегда**, не перенумеровывается и не переиспользуется (D39.80). Правки — только через оркестратора; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено. > ⚠ **Это НЕ бэклог зоны `docs/`, а бэклог ПРОЕКТА.** У зон свои, с другими неймспейсами ID, и единый их строк не принимает (D39.84): платформа — [../platform/BACKLOG.md](../platform/BACKLOG.md) (`П-N`) и её регистр дефектов `platform/docs/DEFECT_REGISTER.md` (`PD-N`); фронт — [../frontend/docs/BACKLOG.md](../frontend/docs/BACKLOG.md) (`Ф-N`). > ⚠ **Состояние проекта — не здесь.** Очередь, курс, CURRENT-STATE, состояние паков и живая хроника — шапка [PROGRESS.md](PROGRESS.md). Здесь только долг и его диспозиции. -> - **СЧЁТ ОЧЕРЕДИ на 10.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **269** строк · зона бэкенд **131** строго / **178** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0** (236 закрыта D39.175; **371** — актом D39.233, **360** — актом D39.232, обе 10.09), платные прогоны разблокированы · «скоро» **124** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). +> - **СЧЁТ ОЧЕРЕДИ на 10.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **268** строк · зона бэкенд **130** строго / **177** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0** (236 закрыта D39.175; **371** — актом D39.233, **360** — актом D39.232, обе 10.09), платные прогоны разблокированы · «скоро» **124** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). > - ⚠ **Переезд 06.09 (D39.218):** таблица вынесена из `PROGRESS.md` ДОСЛОВНО и ни один ID не сдвинут — **с единственным исключением: две ячейки (строки 220 и 306) тронуты РОВНО в номере якоря `docs/scripts/counts.py:566`→`:569` и `:592`→`:595`**, потому что цель сдвинул этот же коммит; текст ячеек не менялся, длины равны. В `PROGRESS.md` на прежнем месте оставлен заголовок-указатель, поэтому ссылки «секция „Бэклог“» доезжают в один хоп. ## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора) @@ -97,7 +97,6 @@ | 162 | **Структура глав, Этап 2**: in-band метки глав в потоке перевода (третий эшелон детекта + коррекция структуры по ходу черновой волны, §3а п.3) + канон-гейт названий ($0) + миграционные карты якорей (same/moved/split/merged/gone/new) + калибровочный корпус детекта в CI ⚠ Форма владельца 30.08: черновая модель отвечает МЕТАДАТОЙ в конец ответа, если посчитала строку фрагмента авторской разметкой главы — разметка едет попутно с черновиком без отдельного платного прохода; доверие — через фиксацию в манифесте и подтверждение спорных. | бэкенд + полигон | когда-нибудь (после Этапа 1) | пак + корпус | research/27 §5/§6 Этап 2 | | 163 | **Версии модулей x/text|x/net двигают нарезку МИМО всех ключей** (NFC в NormalizeSource — таблицы модуля, не тулчейна; epub-токенайзер): бамп `go get -u` тихо меняет нарезку при свежих на вид манифесте/чекпойнтах/снапшоте — класс всей чекпойнт-плоскости, унаследован манифестом; вплести версии модулей (debug.ReadBuildInfo) в NormVersion/chunkerVersion либо явный запрет бампа без `--resnapshot` | бэкенд | когда-нибудь (до второго прогона книги) | малое касание | **D39.122**, приёмка (охотник №1) | | 164 | **Rebill-проекция `status` с nil baseMemory завышает RebillUSD**: read-путь не материализует base-банк → renderedContentHashes даёт пустую инъекцию → repin недоопознан, строка считается re-pay (консервативная сторона, но противоречит комменту «status can never quote a different number than translate»); материализовать base-банк на read-пути либо смягчить клейм | бэкенд | когда-нибудь | малое касание | **D39.122**, приёмка (охотник №1); preexisting | -| 166 | ⚠ **ДВИЖКОВАЯ ПОЛОВИНА ПОСТРОЕНА 05.09** — `expected_usd` по главам, `book_once_usd`, `step_max_usd` в `manifest --json`/`status --json` (лендинг `81a89e9`, акт D39.206). ⛔ **ПЛАТФОРМЕННАЯ ПОЛОВИНА ТОЖЕ ПОСТРОЕНА (пере-снято 07.09 той же командой, что доказывала обратное: 19 файлов, контроль 0); механика — `architecture/15-money-path.md`, греп `Hold(expected, stepMax`.** ⇒ строка КАНДИДАТ НА ЗАКРЫТИЕ, но закрывается лендингом платформенного пака, а не сейчас: его дерево в полёте и трогает `pricing`. **Движковая поверхность оценки $/глава** (ставка-константа платформы $0.03 из exp08 v2 — временная мера беты, П-10 зоны платформы; честная оценка по книге/паре — движковая) | бэкенд | когда-нибудь | отдельный пак | D39.123 | | 168 | **Состав волн пайплайна = пар-конфиг (данные), не общий хардкод**: вопрос владельца 10.08 «нормально ли под китайский строить весь бэкенд и конфиги?» — жильцы ролей и промпты уже конфиг/langpacks, но ТОПОЛОГИЯ волн (перепис · точечный edit-контур · без второго прохода) одна на всех пар; если фаза Д покажет разных победителей по парам (H-3/H-4: en-черновик+edit против zh-связки), состав волн уезжает в пар-конфиг данными; ревью-вопрос общности §0 `12-go-style-notes.md` | бэкенд | когда-нибудь (после данных фазы Д) | замер (фаза Д) → решение владельца → малое касание | слово владельца 10.08, промт фазы Д | | 170 | **`tmctl init` — движок сам заводит книгу** (форма В развилки интейка): платформа зовёт $0-команду, движок пишет СВОЙ `book.yaml` — требования к полям живут там же, где проверяются (`config/book.go`); до неё бета живёт формой Б (рендер из деплой-шаблона платформой при создании книги, ратификация D39.130) | бэкенд | когда-нибудь (после беты формы Б) | малый пак | приёмка P5, **D39.130** | | 172 | **Остаток после пере-пина DeepSeek (пин ИСПОЛНЕН и ПРИНЯТ — D39.137, `76049bb`):** (а) **схемное решение владельца по time-based** — доклад §5 отчёта вендор-сессии (рекомендация: пик оставить; scheduler-вариант строки 60 не начинать до ответа вендора «по какой отметке времени тарифицируется вызов» — вендором НЕ задокументировано — и замера доли пика; дешёвый рычаг: операционное правило «прогоны не стартуют в 01–04/06–10 UTC» — почти всё ×2 без строчки Go; гибрид «резерв пиком, сеттл по окну факта» назван) · (б) пере-калибровка ставки $0.03/глава (166/П-10; множитель счёта замерен) · (в) exp08 v3 (85) обязателен до budget_usd · (г) **вахта весов V4-Pro-0813** (класс D39.61): риг готов, порог ратифицированной ВЫБОРОЧНОЙ формы («≥4/5 прогонов N=5 достигают 6/6», D39.136 п.6б) **в риге ИСПОЛНЕН** паком честности — арифметика вынесена в `live_reprobe_threshold_test.go`, сам риг применяет её же. ⚠ **ИМЯ СТРОКИ ВВОДИТ В ЗАБЛУЖДЕНИЕ (испр. №18 17.08, сверено кодом):** названный риг меряет роль КЛАССИФИКАТОРА, а она ходит на **flash** (стендовый `pipeline.yaml:64` + артефакт прошлого прогона `model = deepseek-v4-flash`), тогда как сменившиеся веса — у **pro**, а pro у нас = РЕДАКТОР (`configs/pipeline-c1.yaml:62,76`). Владелец 17.08 санкционировал ТОЛЬКО вариант (а) — пере-съём классификаторного гейта, потолок $0.20, исполняется п.5 пака честности. **По этому прогону строку НЕ закрывать:** вопрос «сменились ли веса редактора» остаётся открытым, дешёвый вход к нему — данные фазы Д, где pro гоняется именно редактором (спросить полигон прежде, чем покупать новый замер) | владелец → бэкенд/полигон | скоро (схема — на владельце; вахта — после 16.08) | СТОП-решение (схема) + замер (вахта) | вендор 13.08, **D39.137**, строка 60, D30.4 | diff --git a/docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md b/docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md new file mode 100644 index 00000000..3d01026f --- /dev/null +++ b/docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md @@ -0,0 +1,233 @@ +> ⚠ **ЭТО ХЕНДОФФ-ПРОМТ ПЛАТФОРМЕННОЙ СЕССИИ. Он живой и исполняется.** Прочитай `CLAUDE.md`, потом +> этот файл целиком, потом — его карту чтения (§3) и больше ничего, пока не появится вопрос. +> ⛔ **ПАК ПАРНЫЙ.** Твоя половина не лендится в одиночку: движковая делается параллельно другой +> сессией, и обе принимаются вместе (`D39.234` п.2). Что это значит практически — §2. + +# Пак платформы: «ДВЕ ОСТАНОВКИ» + +## 1. Какая проблема и что решит твой результат + +Пользователь, остановивший перевод книги, сегодня рвёт всё, что летит к провайдерам прямо сейчас: +оплаченные генерации обрываются, движок списывает за них оценку и на продолжении делает их заново. +Владелец 10.09 ратифицировал форму (`D39.234` п.1): остановок **две** — мягкая (новое не начинаем, +начатое доигрываем, ничего не рвём) и жёсткая (сегодняшнее поведение). Первое нажатие — мягкая, второе +— жёсткая. + +**Твоя зона держит три из четырёх вещей, без которых это не заработает**, и одну, которую сама же +просила. + +⚠ **Твой код уже НАПИСАН так, будто мягкая остановка есть.** `platform/internal/runner/runner.go:56-58` +— «the engine … finishes the in-flight chunk before exiting»; `:163-164` — «SIGTERM to the engine alone +so it can finish the chunk it is paying for». Сегодня это ЛОЖНО, движок рвёт. После пака станет правдой. +⇒ комментарии не «чинить под дерево», а сверить с тем, что реально построишь. + +**Носители в трекере (единый бэклог, `docs/BACKLOG.md`):** строки **381** (две остановки, с твоими +адресами) и **382** (пометка «оценка» не едет по шву). Зонные строки заводи по своим правилам. + +## 2. Зона и git + +Твоя зона — **`platform/`**. `backend/` правит движковая сессия, `docs/` — оркестратор. **Коммитит +только оркестратор.** Итоги, вопросы и расхождения — ЗОННЫЙ журнал `platform/docs/platform-PROGRESS.md` +(в `docs/PROGRESS.md` платформа не пишет). + +⛔ **Пара.** Форму кадра событий («мягко или жёстко» + счёт и сумма оценочных строк) объявляет ДВИЖКОВАЯ +половина. Ты её потребитель. **Не проектируй кадр в одиночку**: если тебе нужно поле, которого движок не +объявил, — это вопрос оркестратору, он сведёт обе половины. Читай пинг №23 в своём журнале: там четыре +адреса, снятые оркестратором, и они — вход в эту работу. + +## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам + +1. `platform/docs/platform-PROGRESS.md` — **пинг оркестратора №23 (10.09)** сверху: четыре адреса и что + с каждым делать. +2. `platform/internal/runner/runner.go` — целиком: `stopGrace` (~59), свойства юнита (~160–170), + `Stop` (~194). +3. `platform/internal/runs/reconcile.go` — как остановка сводится с исходом: `StopRequestedAt` + (~558, ~575, ~591), `finishStopped` (~678), `stoppedOnRequest` / `interruptedBySomeoneElse` + (~1018, ~1112–1134), и комментарий ~1534. +4. `platform/internal/ingest/exit.go` — словарь кодов выхода, `ExitStopped = 5` (~31–34). +5. `docs/architecture/14-api-contract/` — `/runs/{runId}/stop` (`openapi.yaml:754+`) и его описание в + README контракта. + +Тела нот: `docs/architecture/05-decisions-log.md`, номер грепается `^## D<номер>`; статус одним хопом — +`docs/architecture/05-decisions-index.md`. ⚠ ПОДНОМЕР (`D39.234` п.1) своего тела не имеет — это ПУНКТ +внутри `## D39.234`. + +## 4. Состав и разметка свободы + +### 4.1 ⛔ ГРЕЙС МЕНЬШЕ ЗАКОННОГО ОЖИДАНИЯ — это первое, и без него пак вреден + +`platform/internal/runner/runner.go:59` — `stopGrace = 10 * time.Minute` (600 с), он же уезжает в +`TimeoutStopSec` свойством юнита. Законное ожидание ОДНОГО вызова у движка — до `attempt_max_s` +**1240 с** (`backend/configs/models.yaml`, deepseek), и владелец это ожидание ратифицировал +(`D39.230` п.3; потолок подтверждён `D39.234` п.1б — «~20 минут приемлемо»). + +⇒ **в ту секунду, когда SIGTERM станет мягким, systemd начнёт убивать ЗАКОННОЕ ожидание.** А SIGKILL не +оставляет ни пометки `cancelled`, ни сеттла оценки: деньги у провайдера списаны, следа в леджере нет — +то есть ровно та потеря, ради устранения которой пак и делается. + +**Делай РОВНО так:** грейс обязан покрывать потолок ожидания ПЛЮС время жёсткой фазы. Входы формулы +называю я: `max(attempt_max_s)` по всем провайдерам `backend/configs/models.yaml` (сегодня 1240 с) + +время жёсткой фазы + запас. Само число выбери сам и **обоснуй его в отчёте арифметикой**, а не круглостью: откуда взял потолок, сколько заложил на жёсткую +фазу, что будет, если провайдер сменит `attempt_max_s`. ⚠ Отдельно скажи, как число не протухнет: сегодня +оно живёт константой в твоей зоне, а зависит от конфига в чужой. + +### 4.2 `mode` у `/stop` — **делай РОВНО так по существу, форму согласуй** + +Сегодня у продукта один глагол: `POST /runs/{runId}/stop` (`docs/architecture/14-api-contract/openapi.yaml:754`), +и он ведёт к `systemctl --user stop` (`runner.go:194`), то есть к ОДНОМУ SIGTERM. + +После пака один SIGTERM = мягкая остановка. Значит продукту нужен способ сказать «останови СЕЙЧАС». + +⇒ у `/stop` появляется режим, и форму я называю сразу, чтобы не гонять её третий круг: **`mode: soft|hard`, +дефолт `soft`.** Дефолт мягкий — потому что пользователь, который просто нажал «стоп», не должен терять +деньги. ⚠ Контракт — ратифицированный носитель; правку контракта вносит оркестратор, не твоя рука: +пришли точную формулировку описания в отчёте, я её ратифицирую минором. + +### 4.3 Путь второго сигнала — **делай РОВНО так** + +Жёсткая остановка = ВТОРОЙ сигнал движку. `systemctl stop` для этого не годится: он уже потрачен на +первый и запустит свой грейс. + +Форма, которую рекомендует консилиум: `systemctl kill --signal=SIGTERM` по тому же юниту (юнит не +трогая), то есть просто ещё один SIGTERM процессу. Проверь это сам на своей машине — рекомендация не +замер. + +⛔ **Кто жмёт второй раз — решено, и в пак идёт ТОЛЬКО (а): пользователь, явным «останови сейчас».** +⚠ Испр. до выдачи: прежняя редакция выносила это сессии как развилку и приводила довод за (б) — +автоэскалацию платформой. Довод снят и вот почему: (б) означает, что платформа САМА решает оборвать +оплаченные вызовы, списать за них оценки и купить их заново на резюме — без слова пользователя. Это +противоречит канону владельца («деньги видимы с первого вызова, явное согласие на пере-оплату») и его +же слову «готов ждать» (`D39.230` п.3). Для живучести (б) и не нужна: мягкое ожидание движка само +ограничено дедлайном попытки, а SIGKILL по грейсу — последний рубеж. Если ты считаешь иначе — это +вопрос ВЛАДЕЛЬЦУ через оркестратора, а не решение сессии. + +### 4.4 Читать счёт и сумму оценочных строк — **делай РОВНО так** + +⭐ **Это то, что твоя же зона просила, и оно наконец приедет.** +`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"». + +Движок эти числа считает и печатает (`backend/internal/pipeline/status.go:321` — `estimated_rows` / +`estimated_usd`), но по шву они не едут: в кадрах `backend/internal/runevents/runevents.go` слова +`estimated` **0 хитов** (контроль: `committed` в том же файле — **6**; денежный кадр несёт один +`committed_micro_usd`, ~244). С твоей стороны читателя тоже нет: в `platform/internal` вне тестов +`estimated` — **2 хита, оба комментарии** (контроль: `committed_micro` — 1 хит; go-файлов вне тестов +прибор прочёл **81**). + +Движковая половина везёт их в кадр. **Твоя — прочитать и довести до пользователя.** Зачем это нужно +владельцу дословно: он разрешил списывать по ОЦЕНКЕ при УСЛОВИИ, что пометка стоит (`D39.230` п.1) — +условие сегодня исполнено на одном канале из двух и потребителя не имеет. Пользователь должен видеть не +только «списано X», но и «из них оценка Y». + +### 4.5 Протухшая проза про движок — **почини, раз ты здесь** + +`platform/internal/runs/reconcile.go:1534` и `platform/internal/pgstore/runs.go:320` пишут «the engine +catches SIGTERM and exits 1». Движок на пойманном сигнале отдаёт **5** (`backend/cmd/tmctl/main.go:100`), +и твой собственный код это знает (`platform/internal/ingest/exit.go:34` = `ExitStopped = 5`, +`reconcile.go:1134` сверяется именно с ним). Врут только комментарии — но их цитируют доки, и ложь +уезжает дальше. Тот же текст — в `platform/internal/runs/control_test.go:201` (⚠ адрес испр. до выдачи: файла +`platform/internal/runner/control_test.go` не существует). + +### 4.7 ⛔ САМОЕ ДОРОГОЕ: `Stop` БЛОКИРУЕТСЯ НА ВЕСЬ ГРЕЙС — замерено исполнением + +`Runner.Stop` зовёт `systemctl --user stop` **без `--no-block`** (`platform/internal/runner/runner.go:195`), +а `runCommand` собственного тайм-аута не имеет (`:106-112`). Пока стоп ЖЁСТКИЙ, юнит умирает за секунды и +этого не видно. С мягким SIGTERM `stop` будет ждать столько, сколько живёт движок. + +**Замер (пробник на транзиентном юните, игнорирующем SIGTERM, `TimeoutStopSec=15`):** +`systemctl --user stop` вернулся через **15,21 с** — то есть по SIGKILL; `stop --no-block` — через +**0,019 с**, юнит в `deactivating`. + +⇒ **два прямых следствия, оба ломают продукт в первый же день:** +- в HTTP-глаголе `/stop` при `WriteTimeout: 30 s` (`platform/cmd/tmplatformd/main.go`, греп `WriteTimeout`) + пользователь НЕ получает `202`, который контракт обещает (`docs/architecture/14-api-contract/openapi.yaml`, + греп `The `202` does not mean the run has stopped`); +- свип реконсайлера (`platform/internal/runs/reconcile.go`, вызовы `Stop`) встанет до двадцати минут на + одном прогоне. + +**Делай РОВНО так:** `--no-block`. Реконсайлер и так пере-выдаёт стоп на следующем проходе — это написано +в комментарии над его `Stop`. ⚠ И пере-сними мой замер своей рукой, прежде чем опираться: он снят не на +вашем юните. + +⭐ **Заодно закрыт вопрос второго сигнала (§4.3):** `systemctl kill --signal=…` на юните в состоянии +`deactivating` ДОХОДИТ до процесса, состояние не меняется. Рекомендация — `--kill-whom=main`. + +### 4.8 Ещё два места, названные ревью — **делай РОВНО так** + +**(а) Dev-супервизор.** `platform/internal/ingest/supervisor.go` шлёт SIGINT и держит `WaitDelay` **30 с** +(греп `WaitDelay`). В dev-режиме мягкая остановка будет убита через полминуты. Либо поднять, либо явно +объявить «dev не поддерживает мягкую остановку» — но не молчать. + +**(б) ЗАПАСНОЙ КАНАЛ для чисел.** У прогона, убитого по грейсу, кадра `finished` НЕТ ВООБЩЕ (его +отсутствие само по себе значимо — см. шапку `Finished` в `backend/internal/runevents/runevents.go`). При +этом `status --json` движка несёт `estimated_rows`/`estimated_usd` всегда. ⇒ платформа обязана читать ОБА +канала, и это твоя половина, а не движковая. + +### 4.6 Чего в паке НЕТ — **не делай** + +- **Движковая половина** — чужая зона. Ты не правишь `backend/` вообще, включая тесты. +- **Коды выхода** — заморожены: оба стопа остаются **5**. Различие «мягко / оборвано» приезжает ЧИСЛАМИ + в кадре, а не новым кодом и не новым значением `outcome` (довод — в комментарии к полю + `Finished.Volume`, `backend/internal/runevents/runevents.go`, прочитай его прежде чем спорить). +- **Правка контракта своей рукой** — через оркестратора (§4.2). +- **Квоты и фри-тир** — их нет и не проектируется (`D39.176` п.1). + +## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается + +Норма проекта и решение владельца. Самоотчёт «проверено» без исполнения здесь регулярно оказывался +ложным. + +1. **Baseline СВОИМ прогоном ДО первой правки** — зелень зоны целиком, скипы названы поимённо. +2. ⛔ **Отрицательный замер обязан ДОКАЗАТЬ, что спросил существующее:** рядом с нулём ПЕЧАТАЕТСЯ + контрольная величина. Не «проверено с контролем», а число. +3. **Грейс проверяется ИСПОЛНЕНИЕМ, а не чтением конфига.** Сценарий, который обязан быть в отчёте: + юнит с новым грейсом, процесс, который не выходит N секунд, — и что реально сделал systemd. ⚠ Это + единственная часть пака, где ошибка молча уничтожает деньги пользователя. +4. **Второй сигнал — тоже исполнением:** что именно приходит процессу и в каком порядке. +5. **Адверсариальный проход по СВОЕЙ готовой работе**, отдельным заходом. Направление — что здесь + уязвимо: (а) гонка «пользователь нажал второй раз, пока идёт первый»; (б) остановка юнита, который + уже мёртв (у `Stop` для этого есть намеренная ветка — не сломай её); (в) реконсиляция: не читается ли + мягко остановленный прогон как «упал» или как «дошёл до конца книги»; (г) идемпотентность `/stop`. + +## 6. Записка-план ДО первой правки + +В зонный журнал: порядок работы и почему такой. Рекомендованный: §4.1 грейс (без него остальное вредно) +→ §4.3 второй сигнал → §4.2 режим → §4.4 чтение чисел → §4.5 проза. + +## 7. Заявление = команда + +Любое «сделано / проверено» сопровождается КОМАНДОЙ и её выводом. Утверждение без команды не считается. + +## 8. Эхо-протокол старта + +Первым сообщением верни: **(а)** одной фразой — что произойдёт, если заландить твою половину без +движковой, и что если наоборот; **(б)** какое число ты считаешь самым опасным в этом паке; **(в)** что +здесь неверно или недоказано — право сказать это у тебя есть. + +## 9. Канал вопросов и право сказать «этого делать не надо» + +Вопрос — секцией в `platform/docs/platform-PROGRESS.md`. ⚠ Слово, живущее только в переписке, умирает +вместе с сессией — этой смене это стоило трёх потерянных диспозиций (`D39.231` п.1). Всё, что должно +пережить твою сессию, пишется в репозиторий. + +## 10. Что НЕ удалось — обязательная секция отчёта + +Пустой она не бывает. + +## 11. Критерий завершённости + +1. Грейс покрывает законное ожидание движка, число обосновано арифметикой и проверено исполнением. +2. У продукта есть обе остановки (`mode: soft|hard`, дефолт `soft`); путь второго сигнала работает и показан исполнением. +2-бис. `Stop` не блокируется на грейс — показано исполнением, своим замером, а не моим (§4.7). +3. Мягко остановленный прогон не читается реконсиляцией ни как падение, ни как дочитанная книга. +4. Счёт и сумма оценочных строк доходят до пользователя. +5. Протухшая проза про движок исправлена. +6. Зелень зоны целиком, скипы названы; отчёт: что не удалось, что изменил против пака и почему. + +## Деньги + +Пак **$0**: платных прогонов не требует и не разрешает. + +## Отчёт + +`platform/docs/platform-PROGRESS.md`. Числа — командами. Ничего не коммить. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index fb410eb4..15662a69 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-09-10, голова D39.234 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №23** (единственный носитель — здесь; роль передана 06.09, №22 закрыт нотой передачи D39.217; ⚠ испр. 06.09: коммит передачи `c992ee5` бампнул голову и НЕ тронул номер — гейта на номер очереди нет вовсе, `counts.py` сверяет только голову, поэтому носитель разошёлся молча). ⚠ **ЗАКАЗ ВЛАДЕЛЬЦА 01.09 — ИСПОЛНЕН, испр. 08.09 (висел как «первым» неделю после исполнения):** (0а) ревизия документации на протухшее ОТРАБОТАНА 01.09 воркфлоу `docs-staleness-revision-A` (15 срезов), провенанс находок — `D39.185`; (0б) планы доработок в бэкенд и платформу — исполняются ПАКАМИ, за 07–08.09 закрыты два движковых (`D39.225`, `D39.226`); (0в) вынос неактуального в архив идёт батчами `DOC_CLEANUP_PLAN.md` (Б14/Б15/Б17 живы). ⇒ строка ниже — не заказ, а история: (0а) ревизия документации на ПРОТУХШЕЕ — по всем зонам; (0б) планы доработок в БЭКЕНД и ПЛАТФОРМУ; (0в) вынос неактуального в АРХИВ (`docs/archive/`, `platform/docs/archive/`) — за сутки 31.08 закрыто много, и часть носителей стала историей. ⚠ Трезвость по масштабу (пере-считано 02.09): **94** живых дока в `docs/` (пере-счёт 04.09: `find docs -name '*.md' -not -path 'docs/archive/*' | wc -l`), 8 в `platform/docs`, 9 в `backend/docs` — ⚠ из 94 ВРЕМЕННЫЙ остался ОДИН (`DOC_CLEANUP_PLAN`, живой до закрытия батчей Б14/Б15/Б17); два прежних временных уехали в `archive/reports/` 02.09; счёт бэклога — бюллетенем ниже, открытых рядов регистра платформы — 109 (major 1), всего рядов 465 (пере-счёт `python3 docs/scripts/counts.py`; с 04.09 оба числа под гардом `--check`, прежние 95/3 разошлись молча) ⚠ (ревизией 02.09 ряды **154** и **157** переведены из «скоро» в «когда-нибудь»: их гейтом стоял первый холодный прогон, он ОТРАБОТАЛ 31.08 и оба предусловия оказались другими — разбор в самих ячейках, ни одна НЕ закрыта). ⚠ Числа доков `counts.py` НЕ сторожит — при переносе файлов пере-считывать руками командой `find docs -name '*.md' -not -path 'docs/archive/*' | wc -l`. ⚠ И предупреждение о МЕТОДЕ, купленное сменой №21: «выглядит протухшим» ≠ «протухло». Три факта оркестратора опровергнуты ЗАМЕРОМ сессий, а якоря `15-money-path.md` в девяти случаях из двенадцати РОДИЛИСЬ верными и сгнили дрейфом — то есть ревизия обязана быть исполнением, а не чтением. ⚠ **ОЧЕРЕДЬ, унаследованная от №21** (три лендинга 31.08 — секция «СОСТОЯНИЕ ПАКОВ» ниже): **(1) ~~РАЗРЫВ ЦИКЛА~~ ЗАМКНУТ ЖИВЬЁМ 04.09** — пользователь получил EPUB настоящего ПЛАТНОГО перевода ЧЕРЕЗ API, `epubcheck` 5.3.0 на СКАЧАННОМ файле 0/0/0/0 (книга `bk_SS5VES2JELESJSTR`, потрачено $0.278319 из гранта $0.60 при потолке пака $1.5, санкция D39.189). Дверь выдачи построена и проверена исполнением: `202`+`Location`, поллинг с `Retry-After`, Range 206 · второй клиент 200 · аноним 401 · чужая книга 404, TTL с GC, идемпотентность третьего создающего вызова. Все ТРИ сценария строки 216 предъявлены живьём (подпись банка · halt на потолке с exit 4 · `409 run_not_resumable/ceiling_reached` и лечение новым прогоном). +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-09-10, голова D39.235 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №23** (единственный носитель — здесь; роль передана 06.09, №22 закрыт нотой передачи D39.217; ⚠ испр. 06.09: коммит передачи `c992ee5` бампнул голову и НЕ тронул номер — гейта на номер очереди нет вовсе, `counts.py` сверяет только голову, поэтому носитель разошёлся молча). ⚠ **ЗАКАЗ ВЛАДЕЛЬЦА 01.09 — ИСПОЛНЕН, испр. 08.09 (висел как «первым» неделю после исполнения):** (0а) ревизия документации на протухшее ОТРАБОТАНА 01.09 воркфлоу `docs-staleness-revision-A` (15 срезов), провенанс находок — `D39.185`; (0б) планы доработок в бэкенд и платформу — исполняются ПАКАМИ, за 07–08.09 закрыты два движковых (`D39.225`, `D39.226`); (0в) вынос неактуального в архив идёт батчами `DOC_CLEANUP_PLAN.md` (Б14/Б15/Б17 живы). ⇒ строка ниже — не заказ, а история: (0а) ревизия документации на ПРОТУХШЕЕ — по всем зонам; (0б) планы доработок в БЭКЕНД и ПЛАТФОРМУ; (0в) вынос неактуального в АРХИВ (`docs/archive/`, `platform/docs/archive/`) — за сутки 31.08 закрыто много, и часть носителей стала историей. ⚠ Трезвость по масштабу (пере-считано 02.09): **94** живых дока в `docs/` (пере-счёт 04.09: `find docs -name '*.md' -not -path 'docs/archive/*' | wc -l`), 8 в `platform/docs`, 9 в `backend/docs` — ⚠ из 94 ВРЕМЕННЫЙ остался ОДИН (`DOC_CLEANUP_PLAN`, живой до закрытия батчей Б14/Б15/Б17); два прежних временных уехали в `archive/reports/` 02.09; счёт бэклога — бюллетенем ниже, открытых рядов регистра платформы — 109 (major 1), всего рядов 465 (пере-счёт `python3 docs/scripts/counts.py`; с 04.09 оба числа под гардом `--check`, прежние 95/3 разошлись молча) ⚠ (ревизией 02.09 ряды **154** и **157** переведены из «скоро» в «когда-нибудь»: их гейтом стоял первый холодный прогон, он ОТРАБОТАЛ 31.08 и оба предусловия оказались другими — разбор в самих ячейках, ни одна НЕ закрыта). ⚠ Числа доков `counts.py` НЕ сторожит — при переносе файлов пере-считывать руками командой `find docs -name '*.md' -not -path 'docs/archive/*' | wc -l`. ⚠ И предупреждение о МЕТОДЕ, купленное сменой №21: «выглядит протухшим» ≠ «протухло». Три факта оркестратора опровергнуты ЗАМЕРОМ сессий, а якоря `15-money-path.md` в девяти случаях из двенадцати РОДИЛИСЬ верными и сгнили дрейфом — то есть ревизия обязана быть исполнением, а не чтением. ⚠ **ОЧЕРЕДЬ, унаследованная от №21** (три лендинга 31.08 — секция «СОСТОЯНИЕ ПАКОВ» ниже): **(1) ~~РАЗРЫВ ЦИКЛА~~ ЗАМКНУТ ЖИВЬЁМ 04.09** — пользователь получил EPUB настоящего ПЛАТНОГО перевода ЧЕРЕЗ API, `epubcheck` 5.3.0 на СКАЧАННОМ файле 0/0/0/0 (книга `bk_SS5VES2JELESJSTR`, потрачено $0.278319 из гранта $0.60 при потолке пака $1.5, санкция D39.189). Дверь выдачи построена и проверена исполнением: `202`+`Location`, поллинг с `Retry-After`, Range 206 · второй клиент 200 · аноним 401 · чужая книга 404, TTL с GC, идемпотентность третьего создающего вызова. Все ТРИ сценария строки 216 предъявлены живьём (подпись банка · halt на потолке с exit 4 · `409 run_not_resumable/ceiling_reached` и лечение новым прогоном). > ⛔ **ПЯТЬ ПОТОКОВ РАБОТЫ — состояние на 07.09 (пере-снято лендингами смены №23).** > **(1) ПОЛИГОН — «ремонт прибора»** (`docs/POLYGON_INSTRUMENT_REPAIR_SESSION_PROMPT.md`, строки 319 · 300 · 265 · 143): > ⚠ **ЕДИНСТВЕННЫЙ ПОТОК, НЕ СДВИНУВШИЙСЯ ЗА СМЕНУ — сессия по промту так и не стартовала.** Блокирующая линза @@ -13,6 +13,7 @@ > `3d90943`). Строки 285 · 305 · 325 закрыты. ⚠ **Синхронный разрез приёма принят ПОСТРОЕННЫМ, НО НЕ ГОТОВЫМ** — > сработало правило остановки (9 major из 11 в одном механизме), десять пунктов несёт строка **332**; ложный > инвариант бюджета заландён осознанно (окно лжи 8 секунд при дефолте в 16 минутах, ломается идемпотентность). +> **(13) ДВА ПАРНЫХ ПРОМТА «ДВЕ ОСТАНОВКИ» ВЫДАНЫ, акт `D39.235`** — движковый и платформенный, порознь не лендятся. Форма кадра остановки ЗАФИКСИРОВАНА нотой ДО старта работ (две зоны, один шов). ⛔ Эррата к `D39.234` п.1б: мой механизм «отказ на гейте» ратифицированных ~20 минут не даёт — резервация держит всю цепочку ретраев. ⛔ Замером исполнением найдено, что платформенный `Stop` без `--no-block` блокируется на весь грейс и сломал бы `202` контракта. **Сплошной инвентарь живых доков по слову владельца: 19 мест починено**, контракт исправлен минором `0.13.1` (схема требовала отставленный член ⇒ каждый ответ `Run` был невалиден). > **(12) СЛОВО ВЛАДЕЛЬЦА ПО ОСТАНОВКАМ ПОЛУЧЕНО И РАТИФИЦИРОВАНО, акт `D39.234`:** первое нажатие — мягкая, второе — жёсткая; ждём одну летящую попытку (~20 мин), не цепочку; пак мягкой остановки и строку **377** делаем ОДНИМ заходом; коды выхода не трогаем. Пак движка ПАРНЫЙ платформенному. ⛔ Тем же актом сверены носители, которых `D39.232` не тронул: закрыты и убраны строки **78** · **331** · **360** · **369**; блокеров очереди в трекере теперь **0**. Инвентарь живых доков прогнан (94 файла): `15-money-path.md` чинен в трёх местах. > **(11) ДВА СТОПА — СЛОВО ВЛАДЕЛЬЦА НАКОНЕЦ ЗАПИСАНО, акт `D39.233`.** У человека одна кнопка, и она рвёт всё в полёте; мягкий глагол в движке построен, но дёрнуть его может только денежный потолок. ⛔ Контракт и платформа УЖЕ обещают мягкий стоп, которого нет, а платформенный грейс (600 с) меньше законного ожидания вызова (1240 с). ⛔ Пометка «это оценка» — условие слова владельца — по шву НЕ едет и потребителя не имеет (строка **382**). ✅ Замером закрыты строки **374** (вердикт главы остановка уже не портит) и **371**. Пак не заказан: ждёт слова владельца по форме (рекомендация — «повтор ужесточает», коды выхода не трогать). > **(10) ПАК «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ» — ✅ ПРИНЯТ И ЗАЛЕНДЕН 10.09, акт `D39.232`** (`3f05fab`, 28 путей, $0). Движок перестал платить дважды за одну генерацию. Числа пере-сняты оркестратором: батарея 19 ok · 0 FAIL · 4 скипа; каталог 276 → 360, батарея 65 → 149; тесты +67 / −0; снапшот не двинулся. ⛔ Приёмка ДО ратификации нашла пять выживших мутаций, все на носителях ВИДИМОСТИ денег — то есть на условии слова владельца. ⭐ Куплены четыре нормы об измерении и три класса вырожденных пинов. Остатки — строки **375**–**377** (**374** закрыта замером, акт `D39.233` п.2). @@ -53,7 +54,7 @@ > против самообслуживания · обязательный ВТОРОЙ круг селф-ревью (первая половина того же слова подтверждена как > РЕКОМЕНДАЦИЯ, `D39.220`; вторая не подтверждена, улика в её пользу — ниже в этом же листе). > ⚠ **Зоны параллельны по норме проекта; ограничение — пропускная способность ПРИЁМКИ оркестратора: сериализуй -> ЛЕНДИНГИ, не старты.** ⚠ Зеркало фронта — `0.2.3` против канона **`0.13.0`** (одиннадцать миноров), зона заморожена. +> ЛЕНДИНГИ, не старты.** ⚠ Зеркало фронта — `0.2.3` против канона **`0.13.1`** (одиннадцать миноров плюс корректирующий, `D39.235` п.6), зона заморожена. > ✅ **ПОЧИНЕНО 05.09 ЛЕНДИНГОМ `616a8e4` (пак «денежный стоп», акт приёмки **D39.206**; ⚠ испр.: раньше здесь стояла D39.203 — это нота ВЛАДЕЛЬЦА про «сколько добавить», а не акт) — ниже до-паковая археология, читать как историю.** Отказ резервации больше не роняет волну: он ЗАЩЁЛКИВАЕТ её (новое не стартует, допущенное дорабатывается), а обязательный вызов ЖДЁТ settle, когда settle может помочь. ⇒ **порог «книга короче десяти глав не переводится» СНЯТ**; остаточный пол — одна резервация (~$0.07 ≈ 3 главы), и он ВЫВЕДЕН, а не замерен (замер — $0.15-прогон пятиглавой книги, передан полигону). ⚠ Разбор эрратой — `architecture/15-money-path.md`, греп `ЭРРАТА 05.09`. ⚠ **Эта строка сутки стояла непоправленной ПОСЛЕ лендинга и по ней оркестратор предупредил владельца о снятой стене как о действующей** — канон, не обновлённый лендингом, врёт громче отсутствующего. **НО КНИГУ ЧЕРЕЗ API НЕЛЬЗЯ БЫЛО ДОВЕСТИ ДО КОНЦА — найдено прогоном 04.09 и воспроизведено дважды.** Прирост потолка на прогон = `chaptersLeft × $0.03` (ставка `DefaultPerChapter`, занижена ×4.47 — D39.179 п.1), и после первой готовой главы это $0.27, тогда как движок при `waves.workers: 4` резервирует под редакторский шаг ~$0.28 ⇒ **отказ резервации, прогон встаёт, книга не двигается**. Два прогона подряд дали одно и то же: `committed` не сдвинулся, холд вернулся. ⇒ **занижение ставки — не «шкала короче», а СТОП ПРОДУКТА: с какого-то момента каждая следующая покупка не покупает ничего.** Лечить придётся согласованно двумя сторонами (ставка платформы + резерв движка под шаг с параллельными воркерами), носитель — ряд регистра зоны, вес `major` **за недостижимость продукта, а не за деньги**: сам стоп НЕ ЖЖЁТ ничего — замер платформы 04.09 показал отмену ДО ухода запроса (латентность 20–31 мс, `model_actual` пуст, токенов ноль), и вся книга сходится до микро-доллара по трём источникам (`request_log` 0.278319 · `spend.committed_usd` 0.278318752 · платформенный `committed` 278319 микро). > ⛔ **ЗАТО HALT НА ПРОГОНЕ, КОТОРЫЙ ПРОДВИНУЛСЯ, ЖЖЁТ — отдельная находка того же прогона и свежая датированная точка строки 78 («леджер = НИЖНЯЯ граница»).** Четыре редакторских вызова ушли параллельно; один дошёл ($0.063404) и своим коммитом сорвал потолок, три других отменены в тот же миг, **отработав 120756, 156469 и 156470 мс** — и легли в леджер нулём. Незаписанное ≈ **$0.05–0.19 на ОДИН halt** против $0.278 за всю книгу, то есть механизм, защищающий от перерасхода, сам создаёт неучтённый расход. ⚠ Списание провайдером НЕ доказано (биллинга у нас нет) — утверждается ровно наблюдаемое: вызовы шли 2–2.6 минуты, их цены в леджере нет. > ⇒ **Дыра НЕ в принципе, а в покрытии, и это делает её заказом, а не наблюдением** (разбор оркестратора 04.09 по коду): консервативная оценка уже построена и ратифицирована — `backend/internal/pipeline/stagerun.go`, греп `settling the reservation estimate to keep the ceiling honest` (pack-13 point-9), — но её условие требует ОТВЕТА провайдера (`resp.Usage`), а отменённый в полёте вызов ответа не получает и уходит другим путём. Направление лечения: сеттлить оценку при отмене вызова, который успел уйти; различитель — факт ухода (латентность кандидат, порог обязан быть обоснован замером, а не назначен). Половина ДВИЖКОВАЯ. ⚠ Прежняя формулировка пункта — ниже, она о состоянии ДО 04.09. Движок пишет файл с 30.08 (`tmctl build` — EPUB 3 + чистый txt, D39.175), и холодный прогон 31.08 предъявил это живьём: собранный EPUB настоящего перевода прошёл `epubcheck` 0/0/0/0 и передан владельцу. ⚠ **ЭТОТ АБЗАЦ ЗАКРЫТ 04.09 и оставлен как история:** дверь выдачи ПОСТРОЕНА и заленджена (`adf5e53`, D39.194), смонтировано **18 операций из 21** (три адреса двери: `createExport`/`getExport`/`downloadExport`), долг `П-17` зона пере-сняла. Прежняя проверка «`grep -rn 'createExport\|getExport' platform/internal/` — пусто» БОЛЬШЕ НЕ ПУСТА и как гейт не работает. Не смонтированы по-прежнему `getRun`, `updateBook`, `deleteBook` — `updateBook` несёт продуктовое следствие, строка бэклога 274 (имя книги в выданном файле не чинится ничем). **Слово «отдаёт результат» ложно по букве ТОЛЬКО платформенной половиной** (п.4) · **(2) ~~холодный прогон движка~~ ОТРАБОТАЛ 31.08, закрыт РЕШЕНИЕМ ВЛАДЕЛЬЦА «не доводить»** (отчёт — `archive/reports/COLDRUN_V16_REPORT_2026-08-31.md`, числа относятся к `065d8ac`). Потрачено **$0.436110** из санкции $0.80. ПРЕДЪЯВЛЕНО живьём: объёмный потолок · D39.158 ОБЕИМИ половинами (стоп и не-стоп) · exit 16 · строки 154/198/239/240. ⚠ ~~**Причина большой доли редактора НЕ УСТАНОВЛЕНА**~~ — **УСТАНОВЛЕНА пробой полигона 02.09 по четырём осям: РЕДАКТОР ДУМАЕТ, А НЕ ПИШЕТ.** Размышление — **77.9%** цены редактора без ручки и 45.7% с `low`; у `glm-5` с выключенным thinking — 0.0%. Замер куплен и закончен ($6.17 из $9.50). ⚠ `research/30` §2.4 того же полигона повторяет «причина не установлена» — это про ДРУГОЙ вопрос (доля роли в прогоне против доли размышления в цене роли), и различение обязано попасть в ревью-шапку 30. Доля редактора идёт с ДВУМЯ знаменателями: **49.3%** от всего прогона ($0.214865 из $0.436110) и **81.3%** внутри порции П1 (⚠ ходившее «73%» — фантом, в отчёте его нет ни разу). Следующий шаг — контролируемая проба по ЧЕТЫРЁМ осям (роли осей и три требования, купленные ошибками этого прогона, — §14 отчёта; ось `stale` НЕДОСТИЖИМА — §10 там же). ⚠ **Цена: слово владельца 31.08 — ≈$0.60, потолок $0.80, ДЕСЯТЬ глав ПОЛНОЙ цепью включая редакторскую волну.** ⚠ **КЛЮЧИ: гейт снят словом владельца 30.08, разбор двух путей — D39.176 п.6.** Живое отсюда: конвенционного `.env` НЕТ НИ У ОДНОЙ книги корпуса (заполнены только `projects/textmachine/backend/.env` и `.../eval/.env`), поэтому прогон движка обязан передавать `--keys-file` абсолютным путём и только на `translate`; для п.3 нужен ДРУГОЙ файл — `TM_PLATFORM_ENGINE_KEYS_PATH`, тоже АБСОЛЮТНЫМ путём (`platform/internal/config/config.go`, греп `must be an absolute path`). Лечение операционное и $0 — направить переменную на уже заполненный файл; проверять ПРОГОНОМ, не чтением (гардрейл `.env`) · **(3) живой сквозной прогон через API платформы** — форма манифеста, сайдкара банка и карта причин флагов сочинены платформой САМОСТОЯТЕЛЬНО. ⚠ Уточнено 01.09: гейт СНЯТ холодным прогоном, и настоящие артефакты движка теперь ЕСТЬ (каталог `books/gu-zhenren/coldrun-v16/`), но сверка вышла ТОНКОЙ: на шов реально вышло **одно значение `flag_reason` из пятнадцати** (`sanitizer_stripped`), остальные четырнадцать по-прежнему только чтением кода, а манифест против `platform/internal/ingest/manifest.go` не сверялся вовсе · **(4) дверь выдачи** — строго ПОСЛЕ (3) (⚠ слово владельца 30.08: дверь ВСЕГДА строит файл — отказ юзеру не отдаётся, неполная книга уходит с пометкой, т.е. дверь зовёт `tmctl build --partial`; отказ по умолчанию остаётся операторской ручкой CLI; D29.1(б) «fail-closed» читать как «никогда молча» — честность несут пометки) · **(5) проводка `--max-units`** — снятие гейта, а не стройка: механизм в движке стоит (D39.170), живое основание гейта — `PD-422` · **(6) разморозка фронта** — ПОСЛЕ (4): зеркало отстало на **ВОСЕМЬ** миноров (`frontend/docs/api-contract/openapi.yaml` — `0.2.3` против канона `0.10.0`, и первый из них — ЛОМАЮЩИЙ 0.3.0), **16 операций против 21**; зеркало последний раз двигали 15.08 — ⚠ ходившее «30.08» было взято из текста коммита, а не из `git log` (испр. 05.09), то есть расхождение старше на две недели; отставание ратифицировано D39.142 п.5 на время фриза и две ручки разъехались, размораживать раньше значит перегенерировать типы дважды. ⚠ **ДВА ЗАГЕЙЧЕННЫХ ТРИГГЕРА НАЖАТЫ И НЕ ОТРАБОТАНЫ — строка бэклога 237** (`go.work` · вендор-сверка эффорта `deepseek-v4-pro`; реестр `13-tech-debt-anchors.md` §Б-108). ⚠ **Состояние git — снимать КОМАНДАМИ, не чтением** (ветка `polygon` ушла вперёд, коммит здесь не цитирую): работа полигона лежит на ней и в `main` НЕ влита, а `git status -- eval/` в main-дереве пуст ПО ПОСТРОЕНИЮ и уводит в ложный вывод «работы нет»; ⚠ **«развязка git с origin» — арбитр ПЕРЕ-СНЯТ 04.09 и прежний вердикт БОЛЬШЕ НЕ ВЕРЕН:** `git merge-base --is-ancestor origin/main main` → **rc 1**, `git log main..origin/main` → `afa7700 temp` (коммит владельца 04.09, которым он передавал работу зон). Локальная линия ушла вперёд на семнадцать коммитов и содержит ВСЁ содержимое `afa7700` — оно снято `reset --soft` и заленджено раздельными зонными коммитами, — но формально ветки разошлись. Лечение — force-push рукой владельца; **не пуллить** (его же правило). ⚠ Пункт «развязка git с origin» в листе владельца ниже держится ТОЛЬКО как вопрос про force-push его рукой; утверждением о расхождении дерева он не является — арбитр этой пары именно здесь. diff --git a/docs/README.md b/docs/README.md index 84441161..122bf4ed 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,8 @@ | Роль | Активный промт | Статус | |---|---|---| | Оркестратор | [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) | роль и нормы; счётчик роли — CURRENT-STATE | + | **Бэкенд · ДВЕ ОСТАНОВКИ** | [BACKEND_SOFT_STOP_SESSION_PROMPT.md](BACKEND_SOFT_STOP_SESSION_PROMPT.md) | ⚠ **ВЫДАН 10.09, акт `D39.235`. ПАРНЫЙ — порознь с платформенным НЕ лендится.** Мягкая остановка (первое нажатие) и жёсткая (второе): новых единиц не раздаём, начатое доигрываем, ничего не рвём — деньги за начатые генерации перестают сгорать. Строки **381** · **382** · **377** · **372** · **375** · **379**а. ОДНА сессия, ДВА лендинга с объявленной линией. $0 | + | **Платформа · ДВЕ ОСТАНОВКИ** | [PLATFORM_SOFT_STOP_SESSION_PROMPT.md](PLATFORM_SOFT_STOP_SESSION_PROMPT.md) | ⚠ **ВЫДАН 10.09, акт `D39.235`. ПАРНЫЙ.** Грейс выше законного ожидания движка (600 с против 1240 с), `mode: soft|hard` у `/stop` с мягким дефолтом, путь второго сигнала, чтение оценочных строк. ⛔ Первым делом — `--no-block` у `Stop`: без него мягкий стоп ломает `202` контракта (замерено). $0 | | Бэкенд · ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ 10.09** — акт **D39.232** (`3f05fab`, 28 путей, $0); промт в `archive/prompts/` с баннером-исходом. Движок перестал платить дважды за одну генерацию; закрыты строки 360 · 369 · 78. Восемь кругов ревью; приёмка двумя рубежами нашла пять выживших мутаций на носителях ВИДИМОСТИ денег. Живое из остатков — **374**–**377** | | Бэкенд · ДВИЖОК УЖЕ ЗНАЕТ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДВУМЯ ДОФИКСАМИ 08.09** — акт **D39.228** (`2b6f1f4`, 21 путь); промт в `archive/prompts/` с баннером-исходом. Закрыты строки 355 · 357 · 358; у **253** и **312** закрыта ДВИЖКОВАЯ половина, платформенная жива (испр. 08.09 по кросс-модельному ревью: акт объявил обе закрытыми целиком). Живое из не взятого — **362** (ремонт против ремонта, 77 % денег чанка) и **363** (текстовый сайдкар молчит о неполноте); **360** ждёт слова владельца по денежной половине | | Бэкенд · КОНСИСТЕНТНОСТЬ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 07.09** — акт **D39.222**; промт в `archive/prompts/` с баннером исхода. Строки 267 и 318 закрыты, у 320 исполнена половина (а). Живой остаток зоны ЗАКРЫТ актом **D39.226** (строки 333–338); живое — 353–359 | diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index 5ae716bf..c36b207d 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.234; титул — носитель головы, бампать при каждом аппенде) +# Реестр D-нот — карта актуальности v2 (D1–D39.235; титул — носитель головы, бампать при каждом аппенде) > ⚠ **Колонку «тело» `counts.py --check` НЕ сторожит по устройству:** он сверяет полноту НОМЕРОВ, а не > место тела, поэтому колонка держится дисциплиной лендинга. Не нашёл тело по колонке — иди в слайсы, @@ -293,3 +293,4 @@ | D39.232 | 10.09 | **АКТ: движковый пак «вызов, который оборвали мы» ПРИНЯТ** (`3f05fab`, 28 путей, $0). Движок перестал платить дважды за одну генерацию: граница денег — ДОСТАВКА, дедлайн считается от бюджета, оплаченный обрыв не перепокупается. Закрыты 360 · 369 · 78. ⛔ Приёмка двумя рубежами ДО ратификации нашла 5 блокеров: **пять мутаций выжили на зелёной батарее, все на носителях ВИДИМОСТИ денег** — то есть на УСЛОВИИ слова владельца. ⭐ Куплены четыре нормы об измерении (знаменатель по одному глаголу · глаголы по имени · знаменатель закрывает одну ось) и три класса вырожденных пинов | приёмка · транспорт · деньги · метод | | D39.233 | 10.09 | **ДВА СТОПА получают носитель; две строки закрыты замером; условие слова владельца — без потребителя.** У человека одна кнопка и она ЖЁСТКАЯ (`cmd/tmctl/main.go:207`); мягкий глагол построен, но локален волне и дёргает его только потолок (`waverun.go:353`); второй Ctrl-C НЕ ДЕЛАЕТ НИЧЕГО. ⛔ Контракт (`openapi.yaml:762`) и платформа (`runner.go:56-58`) уже ОБЕЩАЮТ мягкий стоп, которого нет, а `stopGrace` 600 с меньше потолка ожидания 1240 с. ⛔ Пометка «оценка» по шву НЕ едет (0 хитов в кадрах, контроль 6) ⇒ `D39.230` п.1 без потребителя — строка **382**. ✅ Закрыты **374** (вердикт главы остановка уже не портит — пере-снято, пин зелён) и **371** (переплата невозможна: `Billable` требует 2xx). Рекомендована форма «повтор ужесточает», коды выхода не трогать | стоп · деньги · шов · метод | | D39.234 | 10.09 | **СЛОВО ВЛАДЕЛЬЦА по двум остановкам + сверка носителей, которую `D39.232` объявил и не сделал.** Ратифицировано: форма «повтор ужесточает» (первое нажатие — мягкая, второе — жёсткая), потолок ожидания ~20 мин (одна летящая попытка, не цепочка), пак мягкой остановки и строка **377** — одним заходом; коды выхода не трогать. Пак движка ПАРНЫЙ платформенному (грейс 600 с < 1240 с). ⛔ Эррата 10.09-б: три объявленные закрытыми строки сутки стояли в таблице — **78** · **369** · **360** закрыты и убраны, плюс незамеченная **331** (с названным сужением, остаток — **376**). Инвентарь живых доков: `15-money-path.md` чинен в трёх местах и получил новую денежную границу; реестр требований против брифа владельца сходится (V0–V6) | стоп · деньги · слово владельца · сверка | +| D39.235 | 10.09 | **ФОРМА КАДРА ОСТАНОВКИ ЗАФИКСИРОВАНА ДО РАБОТ; два парных промта выданы; сплошной инвентарь живых доков.** Кадр: `Finished.Stop{mode,…}` присутствием (прецедент `Volume`), `estimated{rows,micro_usd}` на ОБОИХ кадрах, `Money` на `stopped` всегда, запасной канал — обязанность платформы; минор потока `1.3→1.4`. ⛔ Эррата к `D39.234` п.1б: «отказ на гейте» ратифицированных ~20 мин НЕ даёт — резервация держит ВСЮ цепочку ретраев, нужен и `retryLoop`. ⛔ Замер исполнением: платформенный `Stop` без `--no-block` блокируется на весь грейс (15,21 с против 0,019 с) ⇒ сломал бы `202` контракта. Инвентарь: **19** мест починено, самый урожайный класс — УСЛОВНОЕ ЗАКРЫТИЕ. Контракт `0.13.0→0.13.1`: `Run.required` требовал отставленный член ⇒ каждый ответ был невалиден. Закрыта строка **166** | стоп · шов · контракт · инвентарь | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 61706d02..e5f91fd1 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.234 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.235 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) > **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Работая с контрактом (греп номера: живой файл → слайсы, целиком НЕ читать — D39.125), держи под рукой, что чем перекрыто: > ⚠ **Эррата 09.08 (D39.125):** D39.111 п.1 предписывал промту S3 «максимум = баланс МИНУС открытые холды» — формула ОШИБОЧНА (вычитание дважды), исправлена D39.115 п.2(а): максимум = Balance КАК ЕСТЬ; тело D39.111 — в слайсе `../archive/architecture/05-decisions-D39-106-123.md` (испр. 05.09: прежнее «живёт ниже в этом файле» протухло подрезкой D39.139) (голова D39.106+). @@ -3291,3 +3291,25 @@ bought NOTHING». ⇒ **`tmctl manifest` есть НИЖНЯЯ граница, **5. Инвентарь живых доков (ответ на вопрос владельца «доки актуализированы?»).** Прогнан по норме ролевого промта — ВСЕ живые доки против текущего состояния, не только тронутые сменой; прибор прочёл **94** живых `.md` вне архива. Найдено и исправлено: `docs/architecture/15-money-path.md` в ТРЁХ местах утверждал закрытый канал как открытый (пункт маршрута, строка таблицы долга, список бэклог-строк) — переписаны, и туда же внесена НОВАЯ денежная граница пака (доставка · класс `attempt_timeout` без ретрая · дедлайн из бюджета · резюм не перепокупает). Проверено и расхождений НЕ найдено: реестр требований против брифа владельца (V0–V6 разобраны все; бриф не менялся с 24.08 — контроль по `ls`), таблица активных промтов, шапка-таблица `09-target-architecture.md` (транспорт в её слои не входит), `docs/research/21` (несёт ⚠-баннер о вердикте). ⚠ Остаток, который чинить НЕ мне: 13 битых `file:line`-якорей линтера — все в чужих зонах (`backend/docs`, `docs/experiments`, `frontend/docs`) либо уехали не от моих правок; адреса пере-сняты и оставлены пингами в зонных журналах. **6. Границы.** Пункты 3 и 4 пере-сняты моим прибором на дереве, каждое закрытие предъявлено `file:line`. Пункт 1 — прямые ответы владельца, не вывод. Пункт 5 — инвентарь по грепу с контрольными величинами; «расхождений не найдено» означает «прибор спросил названные доки и не нашёл», а не «их там нет по построению». + +## D39.235 — ФОРМА КАДРА ОСТАНОВКИ ЗАФИКСИРОВАНА ДО СТАРТА РАБОТ; два парных промта выданы; сплошной инвентарь живых доков (10.09, оркестратор №23) ✅ + +**1. ⛔ ФОРМА КАДРА — РАТИФИЦИРОВАНА ЗДЕСЬ, А НЕ ОТДАНА ДВУМ СЕССИЯМ.** Пак «две остановки» делают ДВЕ зоны параллельно, и кадр событий — их общий контракт. Первая редакция промтов велела движку «объявить форму в отчёте», а платформе «не проектировать в одиночку»: это гарантировало либо простой, либо расхождение на лендинге. Прецедент против такого разделения лежит в самой платформе — её словарь исходов «started as the platform's PROPOSAL, written as code so the engine zone could answer it with a diff» (`platform/internal/ingest/events.go`, греп `PROPOSAL`). ⇒ форма фиксируется ЗДЕСЬ, обоим промтам она «делай РОВНО так», минор словаря потока `1.3 → 1.4` (`backend/internal/runevents/runevents.go`, греп `StreamVersion`): + **(а)** `Finished.Stop{ mode: "soft"|"hard", in_flight_finished, in_flight_cut, cut_estimated_micro_usd }` — ПРИСУТСТВИЕ поля есть признак того, что прогон остановил человек. Форма скопирована с прецедента `Finished.Volume`: нового значения `outcome` не заводится и новый код выхода не берётся, потому что словарь исходов читается из ОБОИХ каналов и значение, живущее только в потоке, заставило бы их сообщать разный исход одного прогона. У чисел словаря нет — факт едет числами. + **(б)** `estimated{ rows, micro_usd }` едет НА ОБОИХ кадрах — и на кумулятивном `spend`, и в `Money` терминального. Довод: платформа отвечает пользователю «не больше Y» ВО ВРЕМЯ прогона, а число, появляющееся только в конце, на этот вопрос не отвечает. + **(в)** `Money` на исходе `stopped` едет ВСЕГДА, а не только когда прежде сработал потолок (сегодня — только тогда: `backend/internal/pipeline/events.go`, греп `ceilingSaid`). Вопрос покупателя «что я купил» от причины остановки не зависит. + **(г)** ЗАПАСНОЙ КАНАЛ — обязанность ПЛАТФОРМЫ, не движка: у прогона, убитого по грейсу, кадра `finished` нет вовсе (его отсутствие само по себе значимо), а `status --json` числа несёт всегда ⇒ платформа читает оба. + +**2. ⛔ ЭРРАТА К СОБСТВЕННОМУ АКТУ: `D39.234` п.1б назвал МЕХАНИЗМ, который ратифицированных ~20 минут НЕ ДАЁТ.** Слово владельца («одна летящая попытка, ~20 минут приемлемо») в силе и не меняется. Неверна была моя скобка «отказ на пер-вызовном гейте резервации». Замер: резервация берётся ОДИН раз на `runAttempt` (`backend/internal/pipeline/stagerun.go:477`, `admit` на `:570`), а `client.Complete` (`:699`) держит под ней ВСЮ цепочку ретраев — это дословно говорит комментарий самого кода: «This timer wraps client.Complete — the WHOLE retry chain» (`stagerun.go:984`); `retryLoop` между попытками смотрит только `ctx.Err()` (`backend/internal/llm/httpllm.go:197`). ⇒ гейт ограничивает ожидание ЦЕПОЧКОЙ (~64 мин), а не попыткой (~20). Чтобы слово владельца стало правдой, транспорт обязан видеть мягкую остановку МЕЖДУ попытками и в бэкоффе — это заказано §4.2 движкового промта. ⭐ Нашёл не я: моя механика прошла в акт непроверенной, и её опроверг чтением кода старший коллега, которого я позвал ревьюить уже написанный промт. + +**3. Два парных промта выданы.** `docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md` (строки **381** · **382** · **377** · **372** · **375** · **379**а) и `docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md`. Порознь НЕ лендятся (`D39.234` п.2). Движковый идёт ОДНОЙ сессией в ДВА лендинга с заранее объявленной линией (резать на две сессии нельзя: §4.2 и §4.8 правят одну функцию `retryLoop`); кончившийся контекст после первого лендинга объявлен штатным исходом. ⚠ Обе редакции промтов правились ДО выдачи по ревью: в движковом восемь мест, в платформенном шесть — включая ДВЕ мои прямые лжи («латч не остановит банковые проходы» — они идут через тот же гейт; «`cancelled` не может затереть настоящую причину» — может, апсерт по ключу стадии ЗАМЕНЯЕТ строку). + +**4. ⛔ ПЛАТФОРМЕННЫЙ `Stop` БЛОКИРУЕТСЯ НА ВЕСЬ ГРЕЙС — замерено ИСПОЛНЕНИЕМ, и без этого пак ломал бы API в первый день.** `platform/internal/runner/runner.go:195` зовёт `systemctl --user stop` без `--no-block`, а `runCommand` своего тайм-аута не имеет. Пробник на транзиентном юните, игнорирующем SIGTERM, с `TimeoutStopSec=15`: `stop` вернулся через **15,21 с** (по SIGKILL), `stop --no-block` — через **0,019 с**. ⇒ при мягком SIGTERM пользователь не получает `202`, который обещает контракт (`WriteTimeout` 30 с), а свип реконсайлера встаёт до двадцати минут на одном прогоне. Заказано §4.7 платформенного промта. Тем же пробником закрыт вопрос второго сигнала: `systemctl kill --signal=…` на юните в `deactivating` до процесса ДОХОДИТ. + +**5. СПЛОШНОЙ ИНВЕНТАРЬ ЖИВЫХ ДОКОВ — по слову владельца «вычитай всю документацию».** Три среза читались панелью опусов (мандат чисто читающий), четвёртый отвечает отдельно. Найдено и починено **девятнадцать** мест. Самые дорогие: шапка-таблица `09-target-architecture.md` объявляла НЕ ПОСТРОЕННОЙ платформенную половину денежного потолка, противореча самой себе двумя предложениями выше (та же команда сегодня даёт 10 файлов при контроле 119), и цитировала ДО-ПАКОВУЮ археологию как действующий ⛔-инвариант («последние ~2 главы любой книги не покупаются») на арифметике из ставки $0.03, которой в коде нет; `01-decisions.md` объявляла «в коде» формулу премиум-бюджета, у которой **0 хитов** по `backend/` и `platform/` (контроль: `budget_usd` — 121); `02-mvp-plan.md` держала открытым вопрос владельца, закрытый `D39.205`, и не знала о построенной выдаче книги файлом; `03-implementation-notes.md` описывала дедлайн как берущийся из профиля; `13-tech-debt-anchors.md` держала §Б-46 без пометки закрытия при обеих опровергнутых половинах и печатный ноль, ставший тройкой. **Класс, который дал больше половины находок: УСЛОВНОЕ ЗАКРЫТИЕ** — «закроется, когда…», у которого условие сработало, а хозяина у обновления нет. + +**6. Контракт: корректирующий минор `0.13.0 → 0.13.1`.** Схема `Run` требовала член `ceiling_chapters`, которого в её же `properties` НЕТ: он отставлен минором `0.11.0` и отбивается на входе `400`. ⇒ каждый ответ `Run` был невалиден для генерируемого по схеме клиента, а `Run` отдают четыре операции. Плюс ПЯТЬ мест живой прозы учили клиента поднимать лимит через тот же отставленный член — а проза контракта компилируется в исходник клиента. Снято и переписано на `chapters`. ⚠ **Открытое отдано платформе, а не решено мной:** четыре свойства, которые провод шлёт всегда (`ordered_chapters` · `ordered_units` · `delivered_chapters` · `term_consistency_funded`), в `required` НЕ внесены — их nullability знает зона провода, и вопрос назван в схеме комментарием. + +**7. Сверка носителей — теперь в том же движении.** Закрыта и убрана строка **166** (обе половины построены, условие «дерево платформы в полёте» отпало: `platform/internal/pricing/pricing.go` тронут последний раз 06.09, дерево чистое). Пере-наведены мёртвые адреса: два в строке 146 бэклога, `render.Messages` в реестре рисков памяти, словарь кодов выхода в README контракта. Строки 212 · 231 · 208 · 210 · 113 · 184, на которые ссылались живые доки как на открытые, помечены закрытыми по месту. + +**8. Границы.** Пункт 4 — единственный замер ИСПОЛНЕНИЕМ, и он не мой: сделан старшим коллегой на его юнитах, платформенной сессии велено пере-снять своей рукой. Пункт 2 пере-снят мной по коду. Пункт 5 — три отчёта панели, каждая находка с `file:line` и контрольной величиной; я пере-снимал выборочно (`09`, `01`, `03`, `13`, свою собственную ошибку в `15`), а не все девятнадцать. ⚠ **Одна из девятнадцати — МОЯ ВЧЕРАШНЯЯ ПРАВКА:** я записал в путь денег, будто оплаченный `2xx` с нечитаемым телом «прежде не попадал в `request_log`», — ветвь писала эту строку с 24.07 (`2f91b04`), пак её не касался (0 хитов в диффе), а процитированный мною носитель обслуживает другой класс ошибки. Исправлено и названо здесь, потому что это ровно тот класс, который я в этом же акте вменяю другим. diff --git a/docs/architecture/09-target-architecture.md b/docs/architecture/09-target-architecture.md index 0ba68e44..b1a8720a 100644 --- a/docs/architecture/09-target-architecture.md +++ b/docs/architecture/09-target-architecture.md @@ -10,7 +10,7 @@ > ⚠ **КАК ЧИТАТЬ ШАПКУ (освежена 05.09 — оба пака 05.09 отражены в строках слоёв: `81a89e9` движковый «денежный стоп», акт D39.206; `7e2226a`+`3e84716` платформенный «пустить внутрь можно», акт D39.201).** Таблица «слой → статус → пак/D» ниже — единственный статус-борд этого файла: она БЬЁТ тело документа, а D-лог бьёт её. **Хроники лендингов паков здесь больше нет** — кто что построил и когда живёт в журнале решений: `05-decisions-log.md` + реестр `05-decisions-index.md` + слайсы `../archive/architecture/` (эры D39.100–D39.190 покрывают всю стройку платформы, шва, писателя книги и числа согласия). > ⚠ **ЗАКОН РАСКРЫТИЯ (D39.181) — норма зоны `backend/`, а не пункт одного пака:** число, которое движок публикует, обязано ехать со СВОИМ БАЗИСОМ, а присутствие поля не может быть булевым признаком, если оно перестало им быть. Из него же — `config_drift_basis` в `status --json` и леджер доставки в кадре `finished` (`StreamVersion` 1.2, D39.182 §4). **D39.182** («деньги и честность выдачи») добавил класс BANK в разложение денег и `noteSourceVintage`. > ⚠ **ПОСТРОЕНО, но в семь слоёв НЕ укладывается** (движковые оси рядом со слоями — читать шапку как полную нельзя без них): объёмный потолок оплаченной работы `--max-units`/`VolumeStop` (`backend/internal/pipeline/volume.go`, D39.170; ратифицированная единица продажи — D39.165 §1) · гейт схемы движка (`backend/internal/store/sqlgate_test.go`, D39.173: каждый SQL-оператор компилируется против схемы, поднятой продовой цепью миграций) · дверь правок банка `POST /books/{bookId}/bank/corrections` смонтирована синхронно (D39.162) · ключи деплоя доезжают до движка аргументом `--keys-file` (строка 211 ЗАКРЫТА, блокером живого прогона больше не является) · полоса прогресса СКВОЗНАЯ, подпись стадии — второе исключение границы (D39.162/163) · пере-проход можно КУПИТЬ (D39.166) · инъекция банка не теряется на провайдере с одним системным слотом (D39.164) · **ЧИСЛО СОГЛАСИЯ НА ПУТИ ВТОРОЙ ПОКУПКИ (03.09, D39.187):** пере-покупка прайсится по модели, которую стадия резолвит СЕЙЧАС, с округлением ВВЕРХ и оговоркой в тексте согласия (D39.150 п.1) — а **обещание точности снято с провода** во всех пяти местах, где стояло: контракт механизма теперь «не точность, а НАПРАВЛЕНИЕ ошибки — никогда не занизить молча»; фигура едет с базисом двумя новыми полями `status --json` (`rebill_historical_rows` · `rebill_model_moved_rows`) · **слот объёмного гранта (вторая половина строки 232):** юнит, начатый прежним прогоном и не отгруженный по оси `finalStageWave`, дописывается ВНЕ гранта и слота повторно не тратит ⚠ **следствие денежное и НЕ ратифицированное:** грант `--max-units N` оплачивает ДО 2N выходных юнитов (замер приёмки: грант 2 → четыре юнита, шесть вызовов провайдера), слово владельца 03.09 — «подумаем на этот счёт», до его решения `--max-units` держит СЛОТЫ, а деньги держит только `--ceiling-usd` (эррата 03.09-а к D39.170). -> ⚠ **Канон контракта — 0.13.0** (⚠ испр. 08.09: стояло 0.12.0; версию брать ФАЙЛОМ — `grep '^ version:' 14-api-contract/openapi.yaml`)** (`D39.211`, лендинг 06.09: заказ по размеру перестал притворяться заказом в главах), не 0.11.0, не 0.10.0, не 0.9.0 и не 0.4.0. Носители — `docs/architecture/14-api-contract/openapi.yaml` и `platform/internal/httpapi/capabilities.go`, они сходятся гейтом. +> ⚠ **Канон контракта — 0.13.1** (⚠ испр. 10.09: корректирующий минор `D39.235` п.6; прежде 0.13.0 (⚠ испр. 08.09: стояло 0.12.0; версию брать ФАЙЛОМ — `grep '^ version:' 14-api-contract/openapi.yaml`)** (`D39.211`, лендинг 06.09: заказ по размеру перестал притворяться заказом в главах), не 0.11.0, не 0.10.0, не 0.9.0 и не 0.4.0. Носители — `docs/architecture/14-api-contract/openapi.yaml` и `platform/internal/httpapi/capabilities.go`, они сходятся гейтом. > > ⚠ **ПЛАТФОРМЕННЫЙ ШОВ — что построено 03.09 (D39.188, пак P13).** Оплаченный прогон продолжается на ТЕ деньги, за которые продан: `reopen` читает бюджет ИЗ ХОЛДА первой попытки, а прогон без холда отвечается ОШИБКОЙ, не цифрой (прежде бюджет пере-выводился по текущей ставке — `PD-168`) · **владение потоком переживает проход свипа:** чтение ОСТАНАВЛИВАЕТСЯ на чужом handshake'е (`ErrForeignStreamAhead`), потому что до этого чужие события ложились на оплаченную попытку — чужой `ceiling` ставил `paused` живому прогону, `unit_done` писал непокупленные главы (`PD-438`, major) · операторские ручки: `run unquarantine` и колонка QUARANTINE · условия хоста батареи ВЫВОДЯТСЯ из тестовых исходников (`make conditions`), а не держатся списком в прозе · два гейта в `platform/internal/gates/`: перечень условий против исходников и класс «тревожный ряд ушёл из счёта незамеченным». ⚠ **Бамп формы манифеста безопасного порядка деплоя НЕ ИМЕЕТ ни в одну сторону** (`PD-436`): гейт интейка — строгое равенство одной константе, окна двух форм нет ⇒ стоп-мир. Пока окно двух паков открыто, форма ЗАМОРОЖЕНА (D39.190 п.5). > diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md index 9f065bf2..e3109645 100644 --- a/docs/architecture/14-api-contract/README.md +++ b/docs/architecture/14-api-contract/README.md @@ -3,7 +3,7 @@ батч 0.3.0 — D39.138, 16.08.2026, оркестратор №17; синк с платформой 0.4.0 — 20.08.2026, контрактная сессия; РАТИФИЦИРОВАН D39.152, 20.08.2026, оркестратор №18) -СТАТУС: РАТИФИЦИРОВАН как контракт API v0 по 0.13.0 включительно (0.11.0 — D39.208 · 0.12.0 — D39.211 · **0.13.0 — D39.221: четыре исхода `POST /v0/books` после синхронного разреза на приёме**; перечень миноров и их +СТАТУС: РАТИФИЦИРОВАН как контракт API v0 по **0.13.1** включительно (0.13.1 — `D39.235` п.6: корректирующий минор, `Run.required` требовал отставленный член) (0.11.0 — D39.208 · 0.12.0 — D39.211 · **0.13.0 — D39.221: четыре исхода `POST /v0/books` после синхронного разреза на приёме**; перечень миноров и их провенанс — ниже по файлу, здесь НЕ дублируется). Нормативная поверхность — openapi.yaml РЯДОМ. ⚠ Зонная копия frontend/docs/api-contract/ ВРЕМЕННО ОТСТАЁТ (0.2.3 при каноне 0.13.0 — ОДИННАДЦАТЬ миноров) — ратифицировано D39.142 п.5 на время фриза фронта; синк байт-в-байт + перегенерация типов = diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml index 3b5be58f..477354fb 100644 --- a/docs/architecture/14-api-contract/openapi.yaml +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: TextMachine API - version: 0.13.0 + version: 0.13.1 summary: Ratified contract between the frontend and the TextMachine platform. description: | **RATIFIED contract.** Canonical copy: `docs/architecture/14-api-contract/`; @@ -618,7 +618,7 @@ paths: operationId: startRun summary: Start a translation run. description: | - `stop_for_signing` and `ceiling_chapters` are parameters of the RUN, not of the book: they + `stop_for_signing` and `chapters` are parameters of the RUN, not of the book: they travel with the start and do not outlive it. With `stop_for_signing` the run pauses once, book-wide, for the user to LOOK at the bank — correct what they care to, then release it with ONE `resume`; without it the run does not stop and uses the bank as it stands. @@ -628,7 +628,7 @@ paths: cause the error carries `blocked`, naming that book. **Raising the limit of a stopped run is done by starting a NEW run** with a larger - `ceiling_chapters`: a paused book is startable, finished work is not bought twice, and the + `chapters`: a paused book is startable, finished work is not bought twice, and the new run continues where the old stopped. `resume` does not do this. ⚠ **"Not bought twice" bounds THIS path, not the whole surface.** A re-pass @@ -794,13 +794,13 @@ paths: | the run stands in | this call answers | the remedy a client offers | |---|---|---| | `awaiting_bank` — with ANY state of decisions | continues it — `202` | this call | - | `paused` — ANY reason, `null` included | `409` `run_not_resumable`, `cause.code: ceiling_reached` | a NEW run with a larger `ceiling_chapters` | + | `paused` — ANY reason, `null` included | `409` `run_not_resumable`, `cause.code: ceiling_reached` | a NEW run with a larger `chapters` | | `stopped` — the user's own stop | continues it — `202` | this call | | `failed`, `ready` | `409` `run_not_resumable`, no `cause` — it is not a stopped run | a NEW run | | `translating` | `409` `run_not_resumable`, no `cause` | nothing; it is already running | ⚠ **The limit of the run is a SECOND axis, and it overrides the two `202` rows above.** A run - that has spent everything its `ceiling_chapters` bought has nothing left to continue WITH — + that has spent everything its `chapters` bought has nothing left to continue WITH — there is no work this call could pay for — and that is true whether it stands in `awaiting_bank` or in `stopped`, and however much credit the account holds. Such a call is `409` `run_not_resumable`, `cause.code: ceiling_reached`, exactly as `paused` is, and the @@ -825,7 +825,7 @@ paths: included.** The limit travels with the START of a run and nothing changes it afterwards, so a run stopped at one is never continued by this call; the reason is a hint about WHY the work stopped, never a gate on what may be started next. The book is startable, - `POST /books/{bookId}/runs` takes a larger `ceiling_chapters`, and finished work is not + `POST /books/{bookId}/runs` takes a larger `chapters`, and finished work is not bought again. A client that waits for a particular reason value before offering that strands the user on the commonest stop there is. @@ -1848,7 +1848,12 @@ components: - status - stop_for_signing - stop_requested - - ceiling_chapters + # ⚠ ceiling_chapters СНЯТ из required 10.09 (0.13.1): член ОТСТАВЛЕН минором + # 0.11.0 и отбивается на входе `400` (см. RunRequest.ceiling_chapters ниже), а свойства с таким + # именем в этой схеме НЕТ — то есть каждый ответ Run был невалиден для генерируемого клиента. + # ⚠ ОТКРЫТО: четыре свойства, которые провод шлёт ВСЕГДА (ordered_chapters · ordered_units · + # delivered_chapters · term_consistency_funded), в required не внесены намеренно — их + # nullability решает платформенная зона, вопрос отдан ей паком «две остановки». - progress - paused_reason - failure_reason @@ -2639,7 +2644,7 @@ components: type: boolean description: | Buy a RE-PASS instead of new chapters: carry the corrections already recorded on the - book's memory bank into text that is already translated. `ceiling_chapters` is then + book's memory bank into text that is already translated. `chapters` is then neither required nor legal — a re-pass buys no chapters, it walks the book the service has already produced and re-does only what the corrections actually touch.