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.
This commit is contained in:
parent
b1bede6311
commit
8fb0336270
10 changed files with 679 additions and 16 deletions
400
docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md
Normal file
400
docs/BACKEND_SOFT_STOP_SESSION_PROMPT.md
Normal file
|
|
@ -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`. Числа — командами. Ничего не коммить.
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
233
docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md
Normal file
233
docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md
Normal file
|
|
@ -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`. Числа — командами. Ничего не коммить.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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** | стоп · шов · контракт · инвентарь |
|
||||
|
|
|
|||
|
|
@ -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 хитов в диффе), а процитированный мною носитель обслуживает другой класс ошибки. Исправлено и названо здесь, потому что это ровно тот класс, который я в этом же акте вменяю другим.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 на время фриза фронта; синк байт-в-байт + перегенерация типов =
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue