# Журнал зоны «Платформа» > **Что это.** Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. > Отработавшие эры вынесены срезами в [`archive/`](archive/) — читать только по конкретной ссылке. ## Состояние зоны на 08.09.2026 | Вопрос | Ответ | |---|---| | последний заленджённый пак | «разрез приёма до готовности и правда о себе» (08.09, `ddcbf0c`, акт **D39.229**), канон контракта `0.13.0`. ⚠ Обе величины берутся ПРИБОРОМ, а не отсюда: `git log --oneline -1 -- platform/` и `grep '^ version:' ../../docs/architecture/14-api-contract/openapi.yaml` — эта строка стареет, они нет. ⭐ И стареет она БЫСТРЕЕ, чем кажется: её уже правил этот пак по §4.5, и лендинг того же пака сделал её неверной снова — читай прибором, а не глазами | | пак в дереве, не закоммиченный | дофикс по паку «разрез приёма» (две позиции, 08.09) — отчёт ниже | | открытые дефекты | `DEFECT_REGISTER.md` (счёт — `python3 docs/scripts/counts.py` от корня) | | нормы и приёмка | `ENGINEERING_STANDARDS.md` · направление — `PLATFORM_DIRECTION.md` · стек и стенд — `STACK_DECISIONS.md` | | незакрытые куски работы | `../BACKLOG.md` (`П-N`) | | как разворачивается | `../deploy/README.md` | ## ДОФИКС ПО ПАКУ «РАЗРЕЗ ПРИЁМА» — ДВЕ ПОЗИЦИИ (08.09, `textmachine-fa`, после лендинга `ddcbf0c`) > Заказ оркестратора №23: он закрывал пробел в СВОЕЙ приёмке (норма требует двух верификаторов по > сданной работе, он их не поставил и поставил задним числом), и слепой верификатор нашёл две вещи в > моей зоне. Обе я пере-проверила прежде, чем чинить. Правило остановки объявлено заказчиком заранее: > дофикса ровно два, дальнейшее — строка бэклога. **1. Рукописная тройка в резерве — класс `D39.216` ВНУТРИ пака, нанятого его убрать.** `cutTailReserve()` возвращал `3 * s.write()`, где тройка — ручной счёт записей, следующих за разрезом, и жила она только в докстринге. Замер подтверждён мной: **0 хитов в тестах** при 3 хитах в не-тестах. Четвёртая пост-разрезная запись — и резерв тихо недокрывает, а последствие называет ⛔-абзац моего же `stepLeaving`: оборванная терминальная запись, книга в `parsing` без задания до свипа. ⇒ Выбрана вторая из двух названных форм — **тройка ПИНИТСЯ**, а не выводится. Довод против первой, чтобы он не пропал: вывести резерв структурно значит дать прогулке список её оставшихся шагов, то есть вернуть тот самый рукописный перечень одним уровнем ниже. Вместо этого — сверка с ДРУГИМ выражением того же факта: `UploadSettle` уже собирает прогулку из разреза и четырёх записей, ровно одна из которых (`StartParsing`) идёт ДО разреза, значит резерв обязан равняться остатку прогулки после выноса разреза и этой одной записи. **Два выражения делят константы, но не маршрут** — потому это сверка, а не тавтология. Предъявлено посадкой: пятая запись, добавленная в `UploadSettle` и НЕ добавленная в счёт, красит `TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt` — и он **ЕДИНСТВЕННЫЙ красный** на трёх пакетах (`books`, `config`, `httpapi`), текстом: «the cut reserves 1m30s … but the walk (4m0s) has 2m0s left once the cut and the write before it are taken out». **2. `PD-464` нёс ровно ту протухшую фразу, которую пак снял с трёх других рядов.** Диспозиция говорила «(08.09, в дереве, статус флипает лендинг)» при колонке статуса `fixed` и состоявшемся лендинге. Замер: рядов с этой фразой в файле был **ровно один — мой собственный**. Приведена к лендингу (`ЗАЛАНДЁН ddcbf0c, акт D39.229`); теперь фразы в файле **0** при 465 рядах, колонок ≠ 7 — **0**, `counts.py --check` — **EXIT=0**. ⭐ **Класс, из-за которого это уцелело, стоит назвать: норма §4.5 сработала на ТРЁХ чужих рядах и не сработала на ОДНОМ моём.** Ряд я писала сама и потому не подпала под собственную правку. Тот же класс оркестратор поймал у себя часом раньше на строке 253. Общее правило: **проход по норме обязан включать строки, которые этот же проход и создал** — иначе он чистит только унаследованное. **Числа дофикса, сняты после последней правки кода:** `make check` со всеми четырьмя гейтами — **MAKE-EXIT=0** · пакетов `ok` **20** · строк FAIL **0** · линтер «**0 issues**» · скипов **5** (условие прежнее и названное). Моих файлов в дереве — **4, все в `platform/`**. ⚠ `git status` показывает **7**: остальные три (`docs/BACKLOG.md`, `docs/ORCHESTRATOR_SESSION_PROMPT.md`, `docs/architecture/05-decisions-log.md`) — живая работа оркестратора в ЕГО зоне, идущая параллельно. Не мои и не тронуты; называю их, чтобы «все в моей зоне» не читалось как «в дереве больше ничего нет». Якоря дофикс НЕ сдвинул: 7 проблемных на `HEAD` и 7 у меня, новых ноль (замер дифференциальный, как в основном отчёте). ⚠ Код выхода взят на этот раз ВЕРНО и подтверждён вторым источником: в прошлый раз я написала `echo "MAKE-EXIT=$?"` после подстановки `$(git rev-parse …)`, и `$?` ловил код `git`, а не `make` — напечатанный ноль не значил ничего. Теперь `st=$?` стоит сразу за `make`, и рядом вердикт самой цели: `make check` СОХРАНЯЕТ `.check.log.` при провале и удаляет при успехе, лога нет. **Попутно — прибрала свой мусор на общем стенде.** Убитые прогоны (в том числе мой, когда я гасила зависший пакет) оставили в общем Postgres **34** скретч-базы ≈9,5 МБ каждая. Удалены все 34, отказов **0**, осталось **0**. ⚠ Инструмент выбран самоохраняющийся: обычный `drop database` БЕЗ `with (force)` — он ОТКАЖЕТ, если между листингом и дропом кто-то подключился, вместо того чтобы выдернуть базу из-под чужого прогона. Верификатор не стал их трогать именно из-за этого риска, и это была верная осторожность. ## ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ОТЧЁТ (08.09, `textmachine-fa`) > Промт `docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md`, вход HEAD `3f4680c`, дерево на входе чисто. > Зона НЕ коммитит — дерево передано оркестратору №23 (`textmachine-a8`). Пак $0, платных вызовов **0**. > **Работа завершена, править не планирую.** Сказано ПОСЛЕ адверсариального круга, а не до него: первая > редакция этого отчёта несла ту же фразу при шести неисправленных дефектах, введённых этим паком. > Дерево — 26 файлов, все в `platform/` (`git status --porcelain -- platform/`), 23 правленых и 3 новых. ### Исход по каждому пункту §4 | Пункт | Исход | |---|---| | §4.1 ограничитель параллелизма | **сделано**; форма — `x/sync/semaphore` на общей точке порождения, ожидание с деградацией в очередь. Предъявлено НАГРУЗКОЙ | | §4.2 бюджет хвоста из кода | **сделано, форма Б (структурная)**; попутно вскрыт и закрыт ЧЕТВЁРТЫЙ промах суммы — квитанция не входила в неё | | §4.3 рантбук | **сделано**; правок рантбука ДВЕ, вторая объявлена ниже с доводом | | §4.4 три места неразличимого сбоя | **сделано все три**, каждое своим лечением; свип вылечен БЕЗ миграции | | §4.5 правда о себе | **сделано**: шапка, два ряда флипнуты, маркер третьего починен, черты заэкранированы | | §4.6 честная причина человеку | **закрыто по построению на моей стороне** — и ПРЕМИСА пака при этом опровергнута замером (ниже) | | §4.7 живой гейт | **рецепт исполнен и РАБОТАЕТ**; довод зоны опровергнут, разрез впервые встретился с настоящим движком | | §4.8 п.2 (комментарий `cutNow`) | **взят вместе с §4.4**, как и предписано | | §4.8 п.4 (мёртвый `enqueue`) | **взят вместе с §4.1**: предикат сведён в одно названное место | | §4.8 п.6 («ВСЕГДА» контракта) | **не беру — пинг оркестратору** с моим выбором из двух (ниже) | | §4.8 остальное | **не делаю**, как объявлено паком | | ⚠ сверх пака | константа контракта `0.12.0` → `0.13.0` — красное на входе, взято по явному указанию оркестратора (ниже) | ### Что стало с деревом — находка → что сделано → чем предъявлено | Находка | Что стало с деревом | Чем предъявлено | |---|---|---| | §4.1 у синхронного входа нет ограничителя параллелизма | `internal/books/limit.go`: потолок на `semaphore.Weighted`, взводится в `s.manifest` — ОДНОЙ строке, через которую к движку идут все три входа. Дефолт — `DefaultMaxCuts = jobs.DefaultWorkers` (4), то есть число, подо что хост уже рассчитан, и носитель у него ОДИН. Конфиг `TM_PLATFORM_MAX_CUTS`; ноль отвергает читатель чисел (`loader.number`), а не отдельная проверка интейка. Наблюдаемость: 3 гейджа + 2 счётчика, публикует существующий телеметрический проход | `TestTheHostRunsNoMoreCutsAtOnceThanItsCapAllows` — 6 загрузок при потолке 2, пик **2**, и это НЕ вакуум: тест сперва дожидается контрольной величины «4 из 6 стоят в очереди» из счётчиков самого потолка · парный `TestWithRoomForEveryCutTheHostRunsThemAllAtOnce` — та же нагрузка при потолке 6 даёт пик **6** (иначе первый тест проходил бы и на фикстуре, где ничего не совпало по времени) · посадка M4 | | упор в потолок не должен стоить пользователю загрузки | ожидание, а не отказ: не дождался ⇒ `errNotConclusive` ⇒ `201 parsing`, дорезает очередь. На очередном пути — claim обратно, НОЛЬ потраченных попыток и `river.JobSnooze`, то есть задание возвращается, не тратя единственную попытку (`giveBack` + `jobs.ErrTryAgainLater`) | `TestAnUploadThatRunsOutOfBudgetWaitingForASlotIsAcceptedRatherThanRefused` · `TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook` (claim ОТДАН и задание ПОСТАВЛЕНО — то, чего первая редакция не утверждала) · `TestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack` (оба исхода: упор в потолок и нехватка бюджета) · `TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt` · посадки M9, N7, N8 | | §4.2 граница хвоста выводилась руками и трижды была неверна | ОДИН отсоединённый дедлайн на весь хвост (`walk`), каждый шаг берёт `min(свой бюджет, остаток)` (`step`). Шаг, добавленный завтра, границу не двигает ПО ПОСТРОЕНИЮ | `TestNoStepOfAnUploadsTailOutlivesTheWalk` — в т.ч. **50** вложенных шагов, каждый просит час · `TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget` (по дедлайну, который движок РЕАЛЬНО получил, а не по секундомеру) · `TestAStepOutsideAWalkKeepsItsOwnBudgetAndSurvivesItsCaller` · посадки M1, M2 | | ⚠ ЧЕТВЁРТЫЙ промах той же суммы, найден моей же посадкой | квитанция идемпотентности (10 с) писалась ПОСЛЕ `Accept` и в сумму не входила ⇒ хвост был длиннее объявленного ровно на неё. Квитанция стала ТЕРМИНОМ: `UploadSettle = CutBudget + 4*writeBudget + ReceiptBudget` = **220 с** (ровно замер 07.09), носитель величины ОДИН — `books.ReceiptBudget`, тратит её `httpapi.settleCtx` | `TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget` · `TestTheReceiptSpendsTheShareTheIntakeSetAsideForIt` (httpapi) · посадки M3, M7'' | | §4.3 рантбук молчал про таймаут ОТВЕТА прокси | `deploy/README.md`: молчание названо числом (220 с), требование к прокси — `TM_PLATFORM_UPLOAD_DEADLINE + 220 с` = 13 мин 40 с на дефолтах, цена ошибки названа (человек получает ошибку на ПРИНЯТОЙ книге и повтором делает вторую) | директивы сверены с вендор-доками ЭТОЙ сессией: nginx `proxy_read_timeout`, дефолт **60 с** (nginx.org, ngx_http_proxy_module) · HAProxy `timeout server` (docs.haproxy.org 3.0, индекс ключевых слов) | | §4.4 `ClaimParse` в `cutNow` молчал о сбое БД | гонка и «хранилище не спросили» разведены: первая — INFO, вторая — ERROR, и текст называет следствие (книга `parsing` без задания до свипа) | `TestTheIntakeTellsALostRaceApartFromAStoreItCouldNotAsk` — обе фикстуры, и каждое сообщение проверено ОТСУТСТВУЮЩИМ в чужой | | §4.4 ветвь `RowsAffected()==0 ⇒ задание НЕ ставить` не запинена | пин на ОБЕ стороны: чужой claim ⇒ задания нет и чужой claim цел; свой ⇒ задание есть и claim снят | `TestOnlyAClaimThatWasReallyGivenBackQueuesTheJobThatFinishesTheBook` · посадка M6' красная ТЕКСТОМ про задание | | §4.4 свип `StuckIntake` считает от `added_at` (штамп ДО тела) | ⭐ вылечено БЕЗ миграции и без колонки: бутовый гейт сверял дедлайн с `min(UploadGrace, ClaimStale)`; добавлено ТРЕТЬЕ окно `books.ClaimGrace` (20 мин — самое узкое). Условие «свип забирает claim у идущей загрузки» стало недостижимо настройкой | посадка M5 красная текстом «does not fit the parse claim's grace» + `TestEveryWindowAnUploadMustFitInsideIsActuallyConsulted`. ⚠ **Исправление к первой редакции этой строки:** я написала, что у каждого из трёх окон свой случай, недостижимый двум другим — это БЫЛО НЕВЕРНО и найдено адверсариальным проходом. `ClaimGrace` сегодня самое узкое, поэтому все пять случаев ловит один терм, и два других можно было удалить из гейта при зелёной батарее. Вылечено выносом выбора окна в `intakeWindow(...)`, который тест кормит значениями, делающими каждое окно самым узким по очереди | | §4.8 п.2 комментарий `cutNow` лгал о том, кто ставит задание | снят тем же движением, что и правка ветви | `grep -rn "the queue job is already enqueued" platform/ --include=*.go` → **0** при 200 осмотренных `.go` (единственный хит по дереву — цитата находки в этом журнале, и она историческая) | | §4.8 п.4 предикат «кто ставит задание» размазан по двум местам | `cutsItsOwnUploads()` — одно названное место, читают оба; doc параметра `StartParsing` больше не выдаёт его за штатный путь | `grep "s.Engine != nil" internal/books/*.go` (без тестов, 4 файла) → **1 хит, и он внутри самого предиката**. ⚠ Рядом остаётся `s.Engine == nil` в `s.manifest` — это НЕ тот предикат, а nil-гард самого вызова, и он не решает, кто ставит задание | | §4.5 шапка журнала лгала о том, где зона | пере-снята прибором: последний пак «деньги и правда» `fda0679`, коммитов в `platform/` после него 0, канон `0.13.0`; в шапку вписаны КОМАНДЫ, которыми числа берутся | `git log --oneline fda0679..HEAD -- platform/ \| wc -l` → **0** · `grep '^ version:' openapi.yaml` → `0.13.0` | | §4.5 три ряда `open` при легшем лечении | `PD-424` и `PD-438` → `fixed`; у `PD-441` починен МАРКЕР, статус оставлен `open` и в ячейке названо, что держит его движковая половина (строка **331**). Фраза «в дереве» снята во всех трёх | `grep -c 'статус флипает лендинг'` → **0** при 465 рядах | | §4.5 незаэкранированные `\|` | заэкранированы в трёх рядах (`PD-375`, `PD-422`, `PD-197`) | эскейп-аware счёт колонок: рядов с числом колонок ≠ 7 — **0** из 465 | | `PD-464` (строка регистра, моя зона) | закрыт ОБЕИМИ половинами и переведён в `fixed` с диспозицией | см. ячейку ряда | ### §6 ось 4: существующее ПРЕЖДЕ велосипеда — что рассмотрено и чем отвергнуто | Кандидат | Исход | Довод | |---|---|---| | **`golang.org/x/sync/semaphore`** | **ВЗЯТ** | `Acquire(ctx, 1)` — ровно нужная семантика: ждёт до слота или до конца контекста, очередь **FIFO** (в отличие от буферизованного канала, где поздний может обогнать раннего и часть загрузок ждала бы весь бюджет). `TryAcquire` у него не «барджит»: `success := s.size-s.cur >= n && s.waiters.Len() == 0` — быстрый путь отказывает, пока список ожидающих непуст, и слот у стоящих в очереди не ворует. ⚠ Прочитано в ИСХОДНИКЕ пинованной версии (`$(go env GOMODCACHE)/golang.org/x/sync@v0.22.0/semaphore/semaphore.go`, `TryAcquire`), а не по памяти: на этом свойстве держится довод про FIFO. Уже был в `go.sum` **косвенной** зависимостью той же версии `v0.22.0`; правка `go.mod` — перевод в прямые, БЕЗ смены версии (`git diff platform/go.mod`: одна строка вверх, одна вниз; `go.sum` −1 строка) | | `errgroup.SetLimit` | отвергнут | ограничивает горутины, которые запускает САМА группа. Здесь группы нет и быть не может: вызывающие независимы и приходят из разных мест (HTTP-обработчик, воркер River, свип). Форма не подходит по существу, а не по вкусу | | `netutil.LimitListener` | отвергнут | ограничивает СОЕДИНЕНИЯ на слушателе — то есть весь API разом, включая чтения, листинги и логин, из-за нагрузки на приём. И не накрывает ни воркера, ни свип: это ровно «потолок в маршруте», который пак запрещает | | `MaxWorkers` очереди (уже стоит) | отвергнут как ЕДИНСТВЕННОЕ средство, но учтён как число | ограничить синхронный вход им нельзя: этот путь намеренно НЕ ставит задание, чтобы воркер не гонялся с разрезом за claim. Зато он назвал дефолт: 4 — то, подо что хост уже рассчитан, и потолок сказан один раз для всех способов запустить движок, а не только для того, что идёт через очередь | | самописный счётчик / буферизованный канал | отвергнут | норма зоны «stdlib или устоявшаяся библиотека прежде своего», и здесь у своего есть конкретная цена — отсутствие FIFO | ### Посадки мутаций — вердикт по ТЕКСТУ падения, а не по цвету Копия дерева с каноном (`cp -a --parents platform docs/architecture/14-api-contract`), базовая линия копии зелёная на всех четырёх пакетах. | Посадка | Вердикт | Текст, по которому он вынесен | |---|---|---| | M1 разрез снова отсоединён от хвоста | **RED** | `the cut was granted 1m29.99s inside a 700ms walk` | | M2 `step` перестаёт капать по хвосту | **RED** | `the walk is not capping it` + `adding a step moves the boundary` | | M3 хвост съедает долю квитанции | **RED** | `want UploadSettle (3m30s) less the receipt's share (10s)` | | M4 потолок не применяется вовсе | **RED** | контрольная величина легла в ноль: `{Limit:2 InFlight:0 Waiting:0 Waited:0 GaveUp:0}` | | M5 гейт забывает окно claim'а | **RED** | `an upload deadline of 21m0s was accepted, though ... does not fit the parse claim's grace` | | M6′ release ставит задание, ничего не вернув | **RED** | `a release that gave back nothing still queued a job (2 in total)` | | M7 значение `ReceiptBudget` изменено | ⚠ **ВЫЖИЛА, и это ВЕРНЫЙ исход** | пере-сайзинг остаётся согласованным по обе стороны шва; ловить надо не значение, а ДРЕЙФ — см. M7'' | | M7'' квитанция возвращается к своему литералу | **RED** (после того, как по находке M7 заведён пин) | `the receipt was given 30.0s, want the share the intake declared for it (10s)` | | M9 занятый хост тратит попытку книги | **RED** | `a queued parse that found no slot answered , want the cap` | | M10 два способа не получить claim свёрнуты обратно в один молчаливый `return` | **RED** | `losing the race said nothing` + `a claim that could not be asked for said nothing, so the book sits `parsing` with no job and nobody knows` + `was not logged at ERROR` | **ТРЕТИЙ круг посадок — по коду, ПЕРЕПИСАННОМУ после адверсариального прохода.** Первые две редакции двух пинов оказались тавтологичны, и посадка это показала, а не рассуждение. | Посадка | Вердикт | Текст | |---|---|---| | N1 у ожидания удалена строка INFO | **RED** | `a cut that queued for a slot and got one said nothing` | | N2 переименована причина `host_at_cut_capacity` | ⚠ ВЫЖИЛА → **N2′ RED** | первая редакция пина сверяла КОНСТАНТУ с самой собой и проходила при любом значении; переписана на литерал: `the reason is "MUTATED", want the stable "host_at_cut_capacity"` | | N4 два гейджа потолка поменяны местами | **RED** | `the exposition is missing "tm_platform_cuts_in_flight 4"` | | N5 из проводки выброшен `MaxCuts` оператора | **RED** | `MaxCuts is 0, want the operator's 7: TM_PLATFORM_MAX_CUTS does nothing` | | N6 `cutsItsOwnUploads` всегда истинен | ⚠ ВЫЖИЛА → **N6′ RED** | счёт заданий РАЗЛИЧИТЬ НЕ МОЖЕТ (сломанный предикат ставит то же одно задание через релиз); переписано на лог с положительным контролем: `a deployment with no engine attempted a cut anyway` | | N7 разрез перестаёт оставлять хвосту резерв | **RED** | `the intake's parse claim was NOT given back` + `the queue was handed 0 jobs` + `has spent 1 attempts` | | N8 `giveBack` перестаёт возвращать задание очереди | **RED** | `the error does not tell the queue to bring the job back, so the single attempt is spent` | | N9 разрез запускается даже когда места на него нет | **RED** | `a cut was started with no room for it (1 calls)` + `the upload is "not_started", want ` + `the pass does not say WHY it did not cut ("no_time_to_cut")` | ⚠ **Единственная выжившая, которую я НЕ чиню и объявляю:** смена значения `DefaultMaxCuts`. Это сайзинг, а не свойство: изменённый потолок остаётся согласованным по всей системе, и «поймать» его можно было бы только пином на литерал, то есть запретом менять число. Что запинено — ПРОВОДКА (оператор получает своё число) и ЕДИНСТВЕННОСТЬ носителя (`DefaultMaxCuts = jobs.DefaultWorkers`). ⚠ **Первая редакция M6 и M7 НЕ КОМПИЛИРОВАЛАСЬ** (`declared and not used: tag`, `imported and not used`). Это не вердикт, а его отсутствие: посадка, которая не собралась, красит батарею по причине, не имеющей отношения к предмету. Обе пере-посажены компилирующимися. ### Классы и знаменатели — «закрыт в N из M», M посчитан командой | Класс | Знаменатель | Как посчитан | |---|---|---| | входы, порождающие процесс движка на разрезе | **3 из 3** (интейк · `parseWorker` · свип `Sweep`) | `s.manifest` имеет РОВНО ОДНОГО вызывающего (`grep -rn 's\.manifest(' --include=*.go internal/ \| grep -v _test` → 1: `parse.go:178`), у `parseClaimed` их два (`Parse`, `cutNow`), у `Parse` — воркер `jobs.go:117` и `Sweep`. Все три сходятся в одну строку | | порождения движка ВНЕ потолка | **1** — `internal/readmodel/readmodel.go:156` | `grep -rn '\.Manifest(' --include=*.go \| grep -v _test` → 2 вызывающих, один из них мой. Оставлен снаружи сознательно, довод — ниже | | шаги хвоста под общим дедлайном | **все** (в `Accept` их 5 + квитанция) | построением, а не перечнем: `writeCtx` идёт через `step`, `step` капает по хвосту. Проверено на 50 шагах, которых в коде нет | | ряды регистра с диспозицией «статус флипает лендинг» | **3 из 3** | `grep -c 'статус флипает лендинг'` → было 3, стало **0** | | ряды с числом колонок ≠ 7 | **3 из 3** | эскейп-аware счёт: было 3, стало **0** при 465 осмотренных | | открытые ряды регистра по МОИМ файлам | 10 путей осмотрено, совпадений — 20 рядов, из них МОЙ предмет **1** (`PD-464`, закрыт); остальные 19 — соседние классы, не тронутые этим паком | `grep` по ПОЛНЫМ путям (норма §3 п.8), контроль: открытых рядов всего **109** | ### Числа и команды — сняты ПОСЛЕ последней правки ``` $ python3 docs/scripts/counts.py --check → EXIT=1, и это ОЖИДАЕМО: ровно два расхождения, ✗ docs/PROGRESS.md: «открытых рядов регистра платформы — 112» против пере-счёта 110 ✗ docs/PROGRESS.md: «... (major 3)» против пере-счёта 1 ⚠ оба литерала — в ЧУЖОЙ зоне (`docs/PROGRESS.md`), туда не лезу. После лендинга и закрытия PD-464 верные числа: **открытых 109, major 1, minor 36, info 72** (`counts.py` по дереву). $ git log --oneline fda0679..HEAD -- platform/ | wc -l → 0 $ grep '^ version:' docs/architecture/14-api-contract/openapi.yaml → 0.13.0 $ go list ./... | wc -l → 20 (носитель скипов чинен первым движением) $ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_ENGINE_BIN=… TM_PLATFORM_TEST_BOOK_TEMPLATE=… \ TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check MAKE-EXIT=0 · пакетов `ok` **20** · строк FAIL **0** · линтер «0 issues» · скипов **5** ⚠ Прогон ПОСЛЕДНИЙ — после адверсариального круга и после последнего добавленного пина. Прежние редакции этих чисел (до круга) в отчёте не оставлены: они были верны и уже не про этот код. ``` ⚠ **Скипы 5, и условие у всех одно, названное** (§10 ниже): нет деплой-артефакта `backend/configs/mining-contrast.zh.txt`. Из четырёх гейтов батареи на этом хосте закрыты все: Postgres · движковый бинарь + шаблон (собран из `git archive HEAD backend`, §4.7) · достижимый пользовательский `systemd` · `MemoryMax` — судится самим `TestARunIsBoundedByItsOwnCgroup` (`STACK_DECISIONS` §«Гейты батареи»: прямой пробы у этого условия нет), и он ОТРАБОТАЛ: в полном перечне скипов, который печатает `check`, его нет, а `FAIL` в прогоне нет вовсе. ⚠ Числа Go-батареи сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются. ### Живой гейт (§4.7) — довод зоны опровергнут ИСПОЛНЕНИЕМ Зона писала, что второй гейт батареи требует `$0`-пайплайна рядом с `backend/prompts/`, то есть записи в чужую зону или полной копии дерева. **Проверено исполнением — неверно.** Рецепт: ``` $ git archive HEAD backend | tar -x -C $W # снапшот чужой зоны, ни байта записи в неё $ cd $W/backend && go build -o $W/tmctl ./cmd/tmctl $ sed -e 's|pipeline: ../configs/...|pipeline: $W/backend/configs/pipeline-c1.yaml|' \ -e 's|models: ../configs/...|models: $W/backend/configs/models.yaml|' \ $W/backend/example/book.yaml > $W/template.yaml # пути абсолютные $ TM_PLATFORM_TEST_ENGINE_BIN=$W/tmctl TM_PLATFORM_TEST_BOOK_TEMPLATE=$W/template.yaml go test ./internal/books/ ``` `TestTheRenderedConfigurationIsOneTheEngineActuallyLoads` — **PASS**. ⭐ **И где именно рассуждение зоны свернуло не туда:** «нужен `$0`-пайплайн» верно для теста `internal/runner`, который гоняет `translate` и падает на `missing API keys`. Оно было ОБОБЩЕНО на гейт целиком — а `manifest` есть `$0`-глагол и ключей не требует по `D20.4`, поэтому боевой `pipeline-c1.yaml` («платный») загружается и режет без единого ключа. То есть довод был верен про один тест и ложен про гейт, и разница видна только исполнением. ### ⚠ Правки, вызванные заказанной сменой поведения (объявляю по `D39.183`) 1. **`TestAnUploadDeadlineIsRefusedUnlessTheWholeUploadFitsTheTighterWindow` → `...TightestWindow`.** Гейт получил третье окно (§4.4, п.7 десятки) — прежний тест утверждал, что дедлайн `26m29s` ПРИНИМАЕТСЯ, и после лечения это неверно. Тест не «починен под зелень»: он пере-написан строже — у каждого из трёх окон свой случай, недостижимый двум другим, и посадка M5 краснит его именем нового окна. 2. **`ClaimGrace` экспортирована** (была `claimGrace`) — переименование затронуло 3 тестовых файла зоны механически; утверждений не тронуто. Основание — то же, по которому экспортирована `UploadGrace`: бут обязан отказывать конфигурации, которая её нарушает. 3. **Вторая правка рантбука сверх §4.3** — абзац про `TM_PLATFORM_MAX_CUTS`. Довод: §4.1 требует, чтобы потолок был «конфигурируемым и наблюдаемым», а ручка, о которой рантбук молчит, оператору не доступна; документировать ручку, которую этот же пак и завёл, — часть §4.1, а не «остальное про выкат». ### ⚠ Константа контракта `0.12.0` → `0.13.0` — что было сломано и кем Батарея была КРАСНОЙ на входе, до единой моей правки: `internal/gates` `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` — «this build announces contract 0.12.0 and the ratified canon is 0.13.0». Улика: файл-носитель (`internal/httpapi/capabilities.go:36`) в моём диффе отсутствовал (`git diff --name-only HEAD | grep -i contract` → пусто). Канон увёл на `0.13.0` коммит оркестратора `3d90943`, константу за собой не потянув; последняя правка константы — `6ceb133`, до него. То есть гейт красен с момента ратификации, **сутки**, и заметила это входная сверка следующей сессии зоны. Взято мной по ЯВНОМУ указанию оркестратора с названным основанием: ратифицированный порядок `D39.208` п.1 — код первым с честно красным гейтом, канон вторым; здесь порядок был обратный, и правка возвращает мир к гейту, а не гейт к миру (`D39.183`, обслуживание). **Авторство ошибки — оркестратор, не прошлый пак:** на `fda0679` канон и константа обе были `0.12.0`, гейт был зелёным, и число батареи в акте `D39.221` честное. ### §4.6 — ответ (а): закрыто по построению НА МОЕЙ СТОРОНЕ, но премиса пака опровергнута Пункт 10 десятки предполагал, что причина отказа не доезжает. **Опровергнуто:** перечислены ВСЕ семь пользовательских отказов приёма — `payload_too_large` и `request_timeout` (корневые коды), `malformed` ×2, `unsupported_pair` ×2, `no_book`, `no_chapter_structure`, `too_long`, `missing_or_late`. Причины, не выразимой перечислимым кодом, я не нашла; расширять `errors[]`/`cause.code` нечем, и минор не нужен. ⚠ **Собственную первую находку снимаю:** я решила, что слишком длинное поле формы уходит с ПУСТЫМ `errors[]` — неверно, оно названо на месте чтения (`v0.go:843`, `ItemTooLong`), а ветвь `Invalid(w, r)` без элемента до него не доходит. ⛔ **А вот премиса пака про клиента ЗАМЕРОМ НЕ ПОДТВЕРЖДАЕТСЯ, и следующая смена не должна её унаследовать.** Пак пишет: «Таблица „код → русская фраза“ у клиента уже есть (`14-api-contract/README.md`, и фронт её рисует)». Замер: `no_chapter_structure` в живых доках — **11 хитов при 1104 осмотренных `.md`**, и НИ ОДИН не таблица фраз; в `14-api-contract/README.md` нет ни `no_book`, ни `no_chapter_structure`. В зоне фронта `no_book`/`unsupported_pair` — **0 хитов при 8114 осмотренных `.ts`/`.tsx`**. Что там есть на самом деле — правило «клиент диспетчеризует по СТАТУСУ и показывает одну нейтральную фразу» (README §вход) и решение владельца 16.08 о машинном коде. ⇒ вывод пака (моя работа тут закончена, клиентская половина — фронт, а он заморожен) остаётся ВЕРНЫМ, но не потому, что таблица есть, а потому, что платформа дала клиенту всё, по чему её можно нарисовать. Разница существенна: с премисой пака работа выглядит сделанной у обеих сторон. ### Пинги оркестратору 1. ⛔ **Литералы в `docs/PROGRESS.md` под гардом `counts.py --check` протухли моим флипом — двигать их тебе.** После лендинга верно: **открытых 109, major 1** (было «112 (major 3)»). Разница в три ряда: `PD-424` и `PD-438` переведены в `fixed` по твоему же маркеру, `PD-464` закрыт этим паком. Гейт красен ОЖИДАЕМО и ровно на этих двух строках — других расхождений он не даёт. 2. **§4.8 п.6, «ВСЕГДА» в дельте контракта — моё мнение, как просил пак: нужна ОГОВОРКА В КАНОНЕ, а не структурная гарантия.** Довод из кода, а не из вкуса. Между `FinishParse` и `ReadBook` свип материализатора может взять долг (он записан ИМЕННО `FinishParse`) и дописать строке `source_chars` и `structure` (`readmodel.refresh` → `SaveStructure`). Но он НЕ МОЖЕТ ни снять вердикт разреза, ни изменить `status`/`chapter_count`: единственный писатель `reject_reason` — `RejectBook` (`pgstore/books.go:394`, один хит по всему коду), а статус двигают только `FinishParse`/`reject`. ⇒ расхождение возможно только В СТОРОНУ БОЛЬШЕГО: ответ либо уже несёт поверхностные поля, либо ещё нет. Структурная гарантия потребовала бы держать что-то поперёк `FinishParse`→`ReadBook` на горячем пути ради полей, которые клиент всё равно перечитывает карточкой. ⚠ И отдельно: **саму фразу «ВСЕГДА» я в каноне не нашла** — `grep 'ВСЕГДА' 14-api-contract/README.md` даёт 0, `grep -i always` в `openapi.yaml` — 12 хитов, все про другое (SSE-кадры, `about:blank`). Назови предложение адресом, и если оно живёт не там, где я искала, мой довод надо перепроверить против него. 3. ⚠ **Премиса пака в §4.6 неверна — см. секцию выше.** «Таблица код → русская фраза у клиента уже есть, и фронт её рисует» замером не подтверждается (0 хитов `no_book`/`unsupported_pair` при 8114 осмотренных `.ts`/`.tsx`; в `14-api-contract/README.md` ни `no_book`, ни `no_chapter_structure`). Вывод пака устоял, основание — нет. Стоит поправить, иначе следующая смена унаследует «у клиента всё готово». 4. **Мелкая неточность адреса в §4.7:** «а 35 строками ниже в том же журнале лежит рецепт снапшота» — реально 261 строкой ниже. Адреса в дереве, которое я сдаю: довод — `platform-PROGRESS.md:452`, рецепт — `:713` (на входном `HEAD` это были `:173` и `:434`; расстояние то же). На существо не влияет: рецепт там и есть, и он работает. 5. **Твой вопрос «есть ли дешёвый способ закрыть сутки красноты между ратификацией и следующей сессией зоны» — есть, и он в ТВОЕЙ зоне.** Пара «канон ↔ объявленная константа» сегодня судится только Go-тестом, который гоняет зона. А `docs/scripts/counts.py --check` уже читает оба дерева, уже висит на зонном pre-commit и уже срабатывает **именно на коммитах с D-логом или PROGRESS** — то есть ровно на ратификационных. Добавить туда одну проверку — `grep '^ version:' openapi.yaml` против `const ContractVersion` в `platform/internal/httpapi/capabilities.go` — стоит десятка строк и ловит ровно тот класс, который стоил суток: он предупреждает того, КТО ДВИГАЕТ КАНОН, в момент движения. Заказом не делаю (файл в `docs/`), рекомендацию записываю. ### Адверсариальный проход по СВОЕЙ готовой работе — восемь находок, и они были настоящие Проход заказан §5.4 и выполнен субагентом (author ≠ reviewer) по готовому диффу, с направлением на классы, которые уже стоили зоне денег. **Круги НЕ сошлись с первого раза: он нашёл восемь, и шесть из них — дефекты, которые ввёл ЭТОТ пак.** Каждую я пере-проверила по коду прежде, чем чинить. | # | Находка | Чем оказалась | Что сделано | |---|---|---|---| | **F1** | комментарий `giveBack` обещал повтор задания с бэкоффом | ⛔ ЛОЖЬ: `ParseArgs.InsertOpts` — `MaxAttempts: 1`, повтора нет вовсе; книга на занятом хосте ждала свип **20 минут** | заведён `jobs.ErrTryAgainLater`; воркер переводит его в `river.JobSnooze(RetryDelay)`, который НЕ тратит единственную попытку. Комментарий приведён к правде. Пин — `TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt` | | **F2** | у выигравшего слот не проверялось, осталось ли время на разбор | ⛔ настоящий: слот, выигранный в конце бюджета, отдавал движку миллисекунды; убитый процесс читается как `parser_unavailable`, а он ТРАТИТ попытку — пять таких удаляют файл пользователя | `takeCutSlot(ctx, reserve)`: `worthStarting` до и ПОСЛЕ ожидания, `waitCtx` обрывает ожидание на резерв раньше. Резерв — `CutBudget` на очередном пути, `0` на интейке (там попытка не тратится) | | **F3** | отказ бута при `MaxCuts < 1` | ⛔ МЁРТВЫЙ КОД: `l.number` уже отвергает всё непозитивное, и мой тест пинил чужой охранник, а не мой | ветвь удалена; в тесте названо, ГДЕ живёт отказ | | **F4** | тест трёх окон | ⛔ ВАКУУМЕН для двух окон из трёх, и его комментарий утверждал обратное — ровно тот класс, который он якобы чинил | выбор окна вынесен в `intakeWindow(...)`; новый тест делает каждое окно самым узким по очереди. Ревьюер пере-мутировал независимо: теперь красный | | **F5** | терминальная запись могла родиться истёкшей | ⛔ настоящий и злой: при спетом хвосте claim НЕ отдавался и задание НЕ ставилось — книга «принята», а доделать её некому 20 минут. Мой тест этого не утверждал | `stepLeaving` + `cutTailReserve`: слабину забирает РАЗРЕЗ, а не записи. Пин — `TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook` | | **F6** | комментарий потолка обещал больше, чем потолок делает | верно: `readmodel` порождает те же процессы мимо него; плюс `DefaultMaxCuts` был вторым литералом числа воркеров | комментарий сужен до правды и называет, что осталось снаружи; `DefaultMaxCuts = jobs.DefaultWorkers` — один носитель, и `config.Runner.Workers` берёт его же | | **F7** | шесть поверхностей пережили мутацию | верно все шесть | закрыты пинами (ниже), кроме значения `DefaultMaxCuts` — это САЙЗИНГ, и его смена не дефект; названо в §10 | | **F8** | баннер `capabilities.go` противоречил себе | верно, и сломала его Я этой же сменой | баннер разводит два порядка: код первым (канон отстаёт) — ратифицированный, канон первым (код отстаёт) — тот, что стоил суток | ### ⛔ НАХОДКА №9 — класс, которого не ловит НИ батарея, НИ мутация Поймана мной при починке F5: **моя починка была дефектной, и её дефект не имел цвета.** `cutTailReserve` был КОНСТАНТОЙ `3 * writeBudget`, а бюджет записи в тестах — полем сервиса (`s.writeBudget`, который фикстуры укорачивают, чтобы достать случаи, недостижимые за 30 секунд). В фикстуре с хвостом 300 мс резерв оставался 90 с — больше всего хвоста ⇒ шаг разреза рождался истёкшим, движок не звался НИКОГДА, и пакет `books` **зависал навсегда** на `<-first`. ⭐ **Почему это отдельный класс.** Батарея его не ловит, потому что зелёного вердикта просто не наступает — но и красного тоже: прогон висит до таймаута `go test`, и в CI это читается как «долго», а не как «сломано». Мутация его не ловит по той же причине: у посадки нет вердикта, есть тайм-аут. Единственное, что его назвало — прогон с УКОРОЧЕННЫМ `-timeout` и чтение стека упавшего по нему процесса (`limit_test.go:162`, `<-first`); по цвету он неотличим от медленной машины. Две вещи из этого: - резерв сделан производным от бюджета В СИЛЕ (`s.cutTailReserve()` = `3 * s.write()`), иначе фикстура молча моделирует не то; - «нет места для разреза» больше не притворяется отказом хранилища: claim берётся на СВОЁМ бюджете записи, разрез — на своём, и пустой разрез отвечает «вердикта нет» (`ReasonNoTimeToCut`), а не падает внутри `ClaimParse`. Это тот же класс, что F1: диагноз, который называет не то, что случилось. Запинено `TestAnUploadWithNoRoomLeftForACutSaysThatAndHandsTheBookOver` + посадка N9. ⚠ **И правило, которое стоит пережить пак:** число, которое фикстура умеет укорачивать, и число, выведенное из него, обязаны быть выведены ОДИНАКОВО. Константа рядом с полем — это две величины, которые совпадают в бою и расходятся в тесте, то есть ровно то, что фикстура сделать не может увидеть. ⚠ **Урок, который стоит пережить этот пак:** шесть из восьми находок — в коде, который я СДАВАЛА как готовый, с зелёной батареей, десятью посадками и отчётом, где написано «круги сошлись». Батарея была зелёной на всех восьми. Ловит их не цвет, а второй читатель, которому названо, ГДЕ у этого пака мягко. ### Якоря, убитые моим переездом — норма §3 п.8 Мои правки сдвинули строки в `internal/config/config.go`, `internal/pgstore/books.go`, `internal/metrics/metrics.go` и `cmd/tmplatformd/runner.go` (везде вставки, сдвиг +6 в первых двух). **Замер дифференциальный, а не «посмотрела»:** линтер на входном `HEAD` даёт **8** проблемных якорей, моё дерево давало **26**. Чтобы отделить своё от унаследованного и от WIP чужой сессии, собрала `git archive HEAD` в /tmp, подменила в копии ТОЛЬКО `platform/` своим и сравнила списки `comm`-ом. ⚠ Кап вывода линтера — 25 строк; на 26 проблемах обрезка читается как отсутствие, поэтому в копии скрипта кап поднят до 500. Появившихся из-за меня — **18**. | Где | Сколько | Что сделано | |---|---|---| | `platform/docs/DEFECT_REGISTER.md` | **14 из 14** | пере-наведены механически: токен найден в цели, адрес заменён; ни одного «руками» | | `docs/PROGRESS.md`, `docs/architecture/05-decisions-log.md` | **4** | ЧУЖАЯ зона — ушли пингом с готовыми адресами и токенами | ⭐ **Находка из этого же хода:** экранирование `\|` в ячейке регистра (§4.5) **ломает якорь**, если черта попала в его токен. `PD-422` держал `internal/runs/runs.go:408`=`resnapshot := book.BankMoved || book.HasPriorRun`; после экранирования токен перестал совпадать с кодом. Вылечено укорочением токена до `resnapshot := book.BankMoved` (единственный хит в файле). Счёт колонок и сверка токена тянут ячейку в разные стороны — следующий, кто пойдёт экранировать черты, наступит на то же. Ушло пингом. Итог: мой лес **12** проблемных якорей против **8** на `HEAD`; остаток — ровно те 4 чужой зоны. ### §10 — что НЕ удалось и что НЕ проверено (это разные исходы) - **Скипов 5 (было 6), и условие у всех ОДНО и названное:** `backend/configs/mining-contrast.zh.txt` нет на этом хосте, и это деплой-артефакт, которого нет в репозитории (снапшот `git archive HEAD backend` его не несёт — `ls backend/configs` даёт `langpacks pairs models.yaml pipeline-*.yaml`, и всё). Скипающиеся: `TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses`, `TestALivePreviewWritesNothingAndALiveApplyWrites`, `TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing`, `TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber`, `TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`. Шестой (`TestARestorePointCanActuallyBeRestored`) закрыт: `pg_dump`/`pg_restore` есть в `~/.local/pgsql/bin`, переменные выставлены. ⚠ Это НЕ «не проверено» про мой предмет: ни один из пяти не касается разреза приёма — они про банк, выдачу и снапшот-гард. Живой гейт МОЕГО предмета закрыт и зелёный. - ⛔ **ЭТО МЕСТО БЫЛО «НЕ ПРОВЕРЕНО» И ОКАЗАЛОСЬ ДЕФЕКТОМ — оставляю как след.** Первая редакция отчёта писала: «полагаюсь на то, что River повторит задание с бэкоффом; сколько попыток он даёт, я не измеряла». Замер (адверсариальный проход, подтверждён мной по коду): `ParseArgs.InsertOpts` — `MaxAttempts: 1`, повтора НЕТ ВООБЩЕ, задание просто списывается, и книга ждала свип 20 минут. То есть моё «рассуждение, а не замер» было не осторожностью, а неверным утверждением в комментарии кода. Вылечено `river.JobSnooze`, который возвращает задание не тратя единственную попытку. ⭐ Урок ровно тот, что записан в каноне зоны: строка «не проверено» — это не смягчение, это место, где ещё не посмотрели, и смотреть надо ДО сдачи. - **НЕ ЗАПИНЕНО ИМЕНЕМ, но покрыто исполнением** (проверено посадками, не грепом по именам): `cutTailReserve` — посадкой N7, `worthStarting` и `waitCtx` — обоими исходами `TestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack` (ожидание обрывается на резерв раньше, поэтому случай «упор в потолок» кончается за ~1,5 с, а не за весь бюджет), `engineNotAsked` — обоими ветвями там же, `jobs.RetryDelay` — новым тестом очереди. Собственного теста по имени у них нет. - **НЕ ПРОВЕРЕНО экспериментально: сколько памяти реально держит один `tmctl manifest`.** Дефолт 4 выбран как число, под которое хост уже был рассчитан (`MaxWorkers` очереди), а не измерен на большой книге. Ручка конфигурируема именно поэтому. - **Опровержение премисы §4.6 сделано ГРЕПОМ ПО КОДАМ**, а не чтением рендера фронта: я искала строки `no_book`/`unsupported_pair` в 8114 `.ts`/`.tsx`. Таблица, ключуемая иначе (например, по корневому `code`), таким грепом не нашлась бы. Утверждаю ровно замеренное. - **Одно новое условное сообщение НЕ запинено:** `«the parse claim could not be given back; the backstop sweep takes the book»` в `giveBack` — ветвь, где отказ `ReleaseParseClaim` накладывается на упор в потолок. Четыре остальных новых сообщения запинены ОБЕИМИ фикстурами (где обязано прозвучать и где обязано молчать), это пятое — нет: чтобы его достать, нужен отказ хранилища ВНУТРИ уже насыщенного потолка, и фикстуру такой конъюнкции я не построила. Называю прямо, а не выдаю шесть из семи за семь. Остальные шесть — включая обе новые («движок не спрошен» и «не осталось места на разрез») — запинены фикстурой, где сообщение обязано прозвучать, И фикстурой, где обязано молчать. - **Ограничитель НЕ накрывает `readmodel`** (`internal/readmodel/readmodel.go:156` — второй и последний вызывающий `Engine.Manifest`) и глаголы `export`/`status`. Это осознанная граница, а не пропуск: материализатор и `status` идут внутри очереди, которая уже ограничена одним `MaxWorkers` на все три типа заданий (`internal/jobs/jobs.go:170`), свипы последовательны, а `readEngine` накрыл бы ещё `Status` на пути СТАРТА платного прогона (`internal/runs/spawn.go:242`) и связал бы запуск прогонов с нагрузкой приёма. Единственным неограниченным источником процессов был синхронный интейк — он и закрыт. ⚠ Если приёмка считает, что хосту нужен потолок на ВСЕ порождения, это отдельная работа со своим дизайном (развязка денежного пути), а не райдер к этому паку. ### Попутно: совместимость с новой секцией `bank.json` (пришло пингом от движковой зоны, проверено моим кодом) Движковый пак добавил в `bank.json` секцию `consolidation` (полнота банка) и поле `never_asked`, версия `tm-bank-v1` НЕ бампнута. **Пере-проверено на моей стороне, не принято на слово:** - `bank.json` разбирает `internal/ingest/bank.go:78` `DecodeBank` — простой `json.Unmarshal`, неизвестный член игнорируется. ⚠ Строгий декодер в зоне ЕСТЬ ровно один (`internal/httpapi/bank.go:137`, `grep -rn DisallowUnknownFields --include=*.go` → **1 хит при 200 `.go`**), но он на ДРУГОМ пути — тело запроса на правки ОТ КЛИЕНТА, где строгость требует сам канон. Пути не пересекаются ⇒ лендинг движка приём банка не ломает. - Читателей секции у зоны нет. ⚠ По подстроке их **2 при 186 `.go` в `internal/`** (`internal/ingest/manifest.go:97`, `internal/pricing/pricing.go:112`) — и оба английская ПРОЗА про «terminology consolidation» в денежных комментариях, а не чтение поля. Счёт по подстроке и счёт по владению здесь расходятся на два: следующему, кто будет снимать этот ноль, читать хиты, а не число. - ⛔ **Закон на будущее:** бит `complete` брать ГОТОВЫМ из артефакта, не выводить у себя (п.6 закона входной двери шва). У движка он считается от среза рендер-паса, а срез классификатора полноты банка не означает — самостоятельный вывод разошёлся бы с движковым молча. Строку под читателя НЕ завожу: это следующий пак зоны, и решение оркестратора — не торопить. ### Вопросы оркестратору - **Нужен ли ряд регистра на остаток §4.1** (порождения вне потолка: `readmodel` + `export`/`status`)? Я его НЕ завела: это не дефект сегодняшнего поведения, а названная граница механизма, и заводить ряд «мы решили иначе» — засорять регистр. Скажи, если хочешь ряд. - **`ReasonHostAtCapacity` — константа, которая НИКОГДА не пишется в БД** (`reject` — единственный писатель причины, а класс потолка возвращает claim до него). Я оставила её строкой рядом с пятью `ReasonX`-константами, потому что читатель приходит за ними туда же, и написала это в комментарии. Если считаешь, что не-хранимой причине там не место — скажу, куда унести. ## ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ЗАПИСКА-ПЛАН (08.09, `textmachine-fa`) > Промт `docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md`, вход HEAD `3f4680c`, дерево на входе чисто > (`git status --porcelain` — пусто). Зона НЕ коммитит. Пак $0. > Baseline снят сам: `python3 docs/scripts/counts.py --check` → «Литералы сходятся с пере-счётом > (8 проверок)», регистр 465 рядов / open 112 / major 3. **Что беру и в каком порядке.** Сначала то, что стоит $0 и является предусловием остального (шапка этого журнала, носитель числа пакетов, ряды регистра), потом код в порядке связности: бюджет хвоста — ограничитель — три неразличимых сбоя, потому что первые два связаны структурно и чинить их по отдельности значит ломать один другим. Живой гейт и рантбук — последними, они судят уже построенное. **Разметка решений, принятых ДО кода — чтобы их можно было опровергнуть по этой записке.** 1. **Бюджет (§4.2) — форма Б, структурная.** Перечень шагов подвёл трижды, и четвёртый перечень был бы той же заплатой (`D39.216`). Беру ОДИН отсоединённый контекст хвоста с дедлайном `UploadSettle`, от которого наследуются все шаги: `context.WithTimeout` на потомке с более ранним дедлайном сам даёт `min(шаг, остаток)`, поэтому добавленный шаг границу не двигает ПО ПОСТРОЕНИЮ, а не по внимательности следующего автора. Пин утверждает САМО свойство (§5.3), а не сумму слагаемых. 2. **Ограничитель (§4.1) — на `books.Service.manifest`.** Замер входов, а не память: `Engine.Manifest` зовут ДВА места (`internal/books/parse.go:440`, `internal/readmodel/readmodel.go:156`), а `s.manifest` — ровно те три, что названы заказом (интейк · `parseWorker` · свип `Sweep`). Шире (`runner.readEngine`, общий на `manifest`/`export`/`status`) НЕ ставлю, и довод замером: очередь у платформы ОДНА и уже ограничена (`internal/jobs/jobs.go:170`, `MaxWorkers` дефолт 4) на все три типа заданий сразу, свипы последовательны — то есть единственный неограниченный источник процессов на хосте это и есть синхронный интейк; а `readEngine` накрыл бы ещё `Status`, который стоит на пути СТАРТА платного прогона (`internal/runs/spawn.go:242`, `bookMeter`), и связал бы запуск прогонов с нагрузкой приёма. Что осталось снаружи — называю в отчёте числом, а не умолчанием. 3. **Форма — ожидание, а не немедленный отказ.** Ожидание внутри уже стоящего `CutBudget` к хвосту ничего не добавляет (приор оркестратора, проверяю кодом), а упор даёт штатную деградацию: не уложился ⇒ `errNotConclusive` ⇒ `201 parsing` ⇒ книгу доделывает очередь. Это строго лучше `503`: пользователь получает книгу. Существующее прежде своего (§6): `x/sync/semaphore`, `errgroup.SetLimit`, `netutil.LimitListener`, `MaxWorkers` — рассматриваю и отвергнутое называю с доводом. 4. **Свип `StuckIntake` (§4.4) — гипотеза лечения БЕЗ миграции.** `StartParsing` не штампует `parse_started_at` (`internal/pgstore/books.go:213`), поэтому `coalesce(parse_started_at, added_at)` в предикате свипа — это `added_at`, поставленный ДО прихода тела; бутовый гейт (`internal/config/config.go:729`) сверяет дедлайн только с `min(UploadGrace, ClaimStale)` = 30 мин и пропускает дедлайн до 26m29s, а `claimGrace` = 20 мин ⇒ условие достижимо настройкой. Кандидат — добавить `claimGrace` третьим окном в тот же `min()`: колонки не нужно, форма гейта уже ровно эта. Не выйдет — пинг, а не полумера молча (§4.8). **Что считаю рискованным.** (а) Форма Б трогает контексты на ВСЁМ пути приёма — класс ошибок здесь «тихо-зелёный»: путь продолжает работать, а гарантия исчезает, поэтому пин обязан быть структурным (дедлайны шагов), а не «уложились по часам». (б) Ограничитель на общей точке касается и очередного входа — нагрузочное предъявление обязано считать ОДНОВРЕМЕННЫЕ процессы, а не суммарные. (в) Фикстуры зоны уже делали два разных числа одним (`D39.208` п.5): везде, где в фикстуре встречаются `writeBudget`, `CutBudget` и `UploadSettle`, беру ТРИ РАЗНЫХ значения. **Чего не делаю:** п.6 десятки (текст контракта) — пинг оркестратору; всё из §4.8. ## ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (06–07.09, `textmachine-bf`) > Промт `docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md`, вход HEAD `e4097cb`, дерево на входе чисто. > Зона НЕ коммитит — дерево передано оркестратору №23 (`textmachine-a8`). ### Что построено | Заказ | Что стало с деревом | Чем предъявлено | |---|---|---| | §4.0 триаж журнала | журнал 5029 → **491** строки, из которых 385 — этот отчёт; две эры вынесены срезами в `archive/` | `carriers.py` по живому файлу — **0** находок при 491 осмотренных строках; вынос разобран построчно (ниже) | | `PD-441` объявленная маржа | каждая строка `settlement` несёт БАЗИС; `runs` печатает `≥` перед суммой и легенду | два независимых пути по деньгам (ниже) · пины `TestEverySettlementSaysThatItsFigureIsAFloor`, `TestOnlyAnEndingTheEngineChoseIsSettledAsComplete`, `TestTheOperatorsTableSaysSpentIsAFloor…` | | `PD-424` окно гонки | арбитр В ХРАНИЛИЩЕ: `AbandonOrder.ProofAttemptID` + `ProofSpawns`, сверка под книжной блокировкой | пины `TestAProofAboutTheAttemptBeforeThisOneIsRefused`, `TestAnAttemptClaimedWhileSystemdWasBeingAskedIsRefused` | | `PD-438` видимость парковки | колонка `run_attempts.parked_at`, гейдж `tm_platform_parked_attempts`, ячейка PARKED в `runs` | пины `TestAParkedAttemptIsVisibleInTheRowTheGaugeAndTheListing`, `TestTheOperatorsTableSaysSpentIsAFloor…` | | строки 285 + 325 | ⛔ **ПОСТРОЕНО, НО НЕ ГОТОВО — сработало правило остановки, см. секцию ниже.** Синхронный $0 `tmctl manifest` на приёме: fail fast и вход сужен по ЧИСЛУ ГЛАВ | 4 пина `failfast_test.go` + `TestOnlyTheHolderOfTheParseClaimCanDeleteARefusedIntake` + пин медленного разреза (круг 3) и три пина круга 4 (деплойные ветви · материализация · очередь) | | строка 305 | `make check` при таймауте называет пакет, тест и причину | воспроизведено и предъявлено сквозным `make check` на копии | | §4.4б | комментарий `THE FIX IS NOT IN THIS PACKAGE` приведён к поведению | греп по отозванной формулировке — 0 при 197 осмотренных `.go` | ### Заказ по пунктам — исход каждого | Пункт промта | Исход | |---|---| | §4.0 триаж журнала | **сделано**; независимо пере-проверено оркестратором №23 мультимножеством и `carriers.py` по самим срезам | | §4.1 `PD-441` | **сделано в достижимой половине**; вторая половина — движковая, названа поимённо и ушла строкой бэклога | | §4.1 `PD-424` | **сделано**, и предмет оказался шире ряда: окон два | | §4.1 `PD-438` | **сделано** | | §4.2 строка 282 | **сознательно не делаю** — снято до выдачи, платформенная половина уже построена | | §4.3 строки 285 и 325 | ⛔ **построено и ОСТАНОВЛЕНО**: пять кругов самопроверки, девять major из одиннадцати — в этом одном механизме, третий подряд промах бюджета хвоста. Предмет уезжает отдельным паком по правилу остановки оркестратора №23; семь незакрытых пунктов перечислены поимённо | | §4.4 два малых дока | **не трогаю** — зона оркестратора; расхождения ушли пингами (ниже) | | §4.4а строка 305 | **сделано**, предъявлено сквозным `make check` на копии | | §4.4б комментарий | **сделано** | | §4.5 запреты | **соблюдены**, проверено исполнением: контракт, `ENGINEERING_STANDARDS`, `PLATFORM_DIRECTION`, `backend/` — 0 изменённых файлов; коммитов 0; индекс пуст | | пинги оркестратора: `PD-458`, `П-10` | **пере-сняты по дереву, оба подтвердились**; `PD-458` → `fixed`, `П-10` закрыта актом зоны | ### Два круга самопроверки — что нашёл каждый ⚠ **Круги сошлись на ТРЕТЬЕМ, а не на втором.** Второй я вела сама и объявила сходимость преждевременно: адверсариальный субагент нашёл четыре major, три из них — дефекты, которые ввёл ЭТОТ пак и которых не видел ни один мой пин. | круг | находка | что сделано | чем предъявлено | |---|---|---|---| | 1 (мой, по готовой работе) | посадка «снят сравнитель попытки» ВЫЖИЛА — случай ловился чужим охранником | новой попытке дан тот же счёт заявок + сверка ТЕКСТА отказа | пере-посадка красная, текст называет именно сравнитель попытки | | 1 | посадка «снят охранник `is null`» ВЫЖИЛА — два охранника прикрывали друг друга | добавлено обращение к записи напрямую | пере-посадка красная: `a second mark moved the stamp` | | 1 | синхронный разрез добавил хвост, о котором бутовый гейт не знал | `UploadSettle = CutBudget + writeBudget + 30s`, `CutBudget` стал константой | пин + мутация, текст называет оба числа | | 2 (по заказу и по первому кругу) | «≥» и PARKED были предъявлены ЧТЕНИЕМ КОДА, а не исполнением | заведён пин, читающий настоящий вывод команды | он же поймал мою ошибку в ожидании: `money.USD()` не печатает `$` | | 2 | охранники `DeleteRefusedIntake` (стадия, claim, отсутствие прогона) не запинены | заведён пин | мутация «охранник claim'а всегда истинен» → `gave , want ErrNoBook` | | 2 | дельта контракта врала про `character_count_exact` и `structure` | ⚠ **починка второго круга оказалась НЕВЕРНОЙ и пере-сделана четвёртым:** материализация с интейка снята, поэтому в `201` оба поля отсутствуют ВСЕГДА, а не «когда материализация удалась» | дельта переписана; пин `TestTheIntakeNeitherMaterializesNorEnqueuesWhenItsCutSucceeds` | | 2 | числа отчёта устаревали четырежды | сняты заново после последней правки | команды и выводы — в разделах ниже | | **3 (опровергатель, `fable`)** | ⛔ **пере-чтение строки для `201` шло на контексте, открытом ДО тела** (бюджет 30 с), а разрез длится до 90 с ⇒ после медленного разреза `201` нёс `parsing`/0 глав над строкой `not_started`/500 | свежий `writeCtx` для пере-чтения | пин `TestASlowCutStillAnswersWithTheRowAsItStandsAfterIt`; бюджет укорочен сеемым полем, а не ожиданием | | 3 | ⛔ **три ветви деплойного класса НЕ отдавали claim и тратили попытку**, а одна писала `rejected`, который `201` выносил пользователю | все три при `atIntake` возвращают `errNotConclusive`, не трогая `defer_`/`reject` | пин `TestADeploymentFaultLeavesNoVerdictAboutTheFile`: `parse_attempts = 0`, `parse_started_at is null` | | 3 | ⛔ **синхронная материализация читательской поверхности** (до 5 мин) держала запрос и не входила в `UploadSettle` | на интейке не материализуем — долг записан, забирает свип; `UploadSettle = CutBudget + 3×writeBudget` с перечнем шагов | гейт `internal/config` читает саму константу | | 3 | ⛔ **маппинг обоих отказов на HTTP не был запинен** — обмен кодов местами оставлял пакет зелёным | пин по КОДУ, а не по факту 400 | `TestTheIntakesOwnRefusalsReachTheWireWithTheirOwnCodes` | | 3 | ⚠ **сужение входа действует только на синхронной ветви** | закрыть сегодня нечем: словарь `RejectReason` закрыт, ближайшее значение УДАЛЯЕТ исходник | заведён `PD-463`, пинг ниже | | 3 | `PD-458` помечен `fixed(в дереве пака…)` — ложная атрибуция | `fixed(0632a30)` | `git merge-base --is-ancestor 0632a30 e4097cb` → да | | 3 | ряды `PD-441`/`PD-424`/`PD-438` не несли диспозиции пака | дописаны, форма ряда сохранена | `awk -F'|' '{print NF-2}'` по трём рядам → 7, 7, 7 при шапке 7 | | 3 | `TestTheUploadSettleBudgetCoversTheSynchronousCut` — ТАВТОЛОГИЯ | тест удалён, вместо него перечень шагов в комментарии константы | `CutBudget = 10h` оставлял его зелёным, а `internal/config` давал 12 красных | | 3 | замер денег не воспроизводился: скратч-БД удалена, команда не названа | стал ПИНОМ, печатающим обе стороны | `TestTheRawLedgerAndTheReadModelAgreeOnWhatWasSpent` | | 3 | живой `STACK_DECISIONS` отсылал в архив, чей баннер запрещает исполнять инструкции | сказано, что правило целиком стоит на месте, а архив — археология | — | | 3 | док-комментарий `cutNow` склеен с `cutResult` | разделены | `gofmt` и `go vet` чисты | | 3 | числа «потеряна 1 строка» и «195 осмотренных `.go`» | пере-сняты: **10** строк (все названы) и **197** | раздел «Команды и их вывод» | | **4 (опровергатель, `fable`)** | ⛔ **задание очереди ставилось в транзакции `StartParsing` и гонялось с синхронным разрезом за один claim** — выигрывает очередь ⇒ fail-fast молча нет; выигрывает разрез ⇒ job съеден впустую, и после возврата claim'а книгу подбирает только свип через 20 мин | job ставится ТОЛЬКО там, где разрез не идёт; на пути «вердикта нет» — вместе с возвратом claim'а, одной транзакцией (`ReleaseParseClaim`) | пины `TestTheIntakeNeitherMaterializes…` и `TestACutWithNoVerdictGivesTheClaimBackAndEnqueuesTheJob`, обе мутации красные | | 4 | ⛔ **`UploadSettle` снова не покрывал хвост:** `StartParsing` (30 с) выпал из перечня, а квитанция идемпотентности 10 с, не 30 ⇒ худший путь 190 с при константе 180 | `CutBudget + 4*writeBudget`, перечень шагов пере-написан ПО КОДУ и по бюджету каждого | гейт `internal/config` читает саму константу | | 4 | ⛔ **из «трёх ветвей деплойного класса» запинена была одна**; мутации на `ErrStorageGone` и `ErrDirectoryGone` выживали | обе ветви решаются ДО вызова движка, поэтому запинены на своём уровне | `TestTheBranchesDecidedBeforeTheEngineAlsoLeaveNoVerdict` | | 4 | ⛔ **починка «не материализуем на интейке» не имела пина** | заведён | мутация «снят `!atIntake`» → `the intake materialized the reading surface (1 claims, 1 refreshes)` | | 4 | ⛔ **дельта контракта противоречила починке третьего круга** — обещала `character_count_exact: true` там, где его теперь не бывает | дельта переписана: оба поля отсутствуют в `201` ВСЕГДА | пин выше | | 4 | `Settle` потерял док-комментарий: мой тип встал между ним и функцией | комментарий возвращён | `go doc ./internal/pgstore Store.Settle` печатает его | | 4 | `PD-461` сам нёс дефект, который описывает (9 полей), и занижал счёт | ряд переписан: полей 7, строк ЧЕТЫРЕ, а не две | `awk` по разделителю: `PD-461` → 7 | | 4 | в таблице «Что построено» стоял носитель-призрак — удалённый тест | заменён на настоящие | `grep` по имени → 0 | | 4 | ⚠ **опровергатель ошибся** в одном числе: `character_count_exact` «в 3 файлах» | пере-снято: **4** (`v0.go` 2 · `project.go` 1 · `v0_test.go` 2 · `books.go` 2); он грепал только строковый вид имени | команда в разделе ниже | | 4 | ⚠ мутация «снят охранник стадии у `DeleteRefusedIntake`» выживает | **по построению, а не из-за дыры:** каждый терминальный переход (`FinishParse`, `reject`) обнуляет `parse_started_at`, поэтому непустой claim влечёт `status = 'parsing'` — охранник избыточен | `grep -n parse_started_at internal/pgstore/books.go` — все четыре писателя | ### `PD-424`: окон оказалось ДВА, а не одно Ряд называл одно — заявку на спавн между пробой systemd и коммитом. По коду их два, и второе опаснее: `AbandonRun` под книжной блокировкой читает **любую живую попытку** (`a.ended_at is null`), поэтому разблокировавшаяся расплата + `restart` подставляют под доказательство о попытке N **попытку N+1** с живым процессом. Лечение — доказательство приходит парой (`ProofAttemptID`, `ProofSpawns`) и сверяется в той же транзакции; расхождение — `ErrProofOvertaken`, отказ, а не применение. `unit_name` в свидетели не годится: `ReleaseSpawnClaim` возвращает его в NULL. Отсюда монотонный счётчик `spawns`, инкремент — внутри самого CAS `RecordSpawn`. ### Миграция `00034` (санкционирована оркестратором №23) `run_attempts`: `spawns integer not null default 0` · `parked_at timestamptz`. Down-путь ГОНЯЕТСЯ (`pgstore.TestMigrationsRollBackAndReapply`, зелёный на живом Postgres). Существующие строки поведения не меняют: сверка идёт с ПРОЧИТАННЫМ числом, не с абсолютным. План гейджа снят исполнением: обе подвыборки (`parked_at`/`quarantine_reason`) идут `Index Scan using run_attempts_live_idx` — новый индекс не нужен. Запись парковки — только на ПЕРЕХОДЕ, в установившемся состоянии ноль операторов. ### `PD-441`: чем ограничена платформенная половина Маржу платформа **измерить не может** — биллинга провайдера у зоны нет, а движковый леджер её не несёт. Что сделано: цифра перестала читаться как цена. Что НЕ сделано и почему: показывать маржу конечному пользователю нечего — недо-счёт бьёт по ДЕПЛОЮ, баланс пользователя завышен в его же пользу (решение оркестратора №23, 06.09; формулировка «показать пользователю» из промта снята). **Движковая половина — заказ следующему паку, поимённо:** 1. `backend/internal/pipeline/stagerun.go`, ветвь `No 2xx ever arrived: nothing was billed` — сеттлить ОЦЕНКУ резервации, а не ноль, когда отменённый вызов уже ушёл; различитель — факт ухода, порог обязан быть ЗАМЕРЕН, а не назначен (ряд называет латентность кандидатом: 20 мс против 156 000 мс). 2. `tmctl status --json` — публиковать рядом с `committed_usd` число и сумму строк, чья цена ОЦЕНОЧНАЯ или неизвестна (движок уже печатает `estimated-cost rows: N`, строка бэклога **78**). Без этого платформа умеет говорить «≥ X», но никогда «≥ X, до Y». ### Строки 285 и 325: одна правка, предикат — по данным `tmctl manifest` зовётся синхронно после последнего байта. Четыре исхода: `глав ≥2` → `FinishParse` инлайн, `201` несёт `not_started` и число глав · `глав 0` → `400` `invalid_request`, `errors[{file, no_book}]`, не принято ничего · `глав 1` → `400`, `errors[{file, no_chapter_structure}]` · **деплойный класс или таймаут** → `201 parsing`, очередь доделывает, claim ВОЗВРАЩАЕТСЯ (иначе задание очереди нашло бы книгу занятой и ничего не сделало). ⭐ Отказ **по числу глав, а не по расширению**: выдача кладёт по одному XHTML на главу ДВИЖКА, значит «книга одним полотном» ⟺ движок нарезал <2 глав. `.epub` движок режет по nav/NCX и он проходит — блокировка по расширению отказала бы тому, что мы умеем доставлять. Go по паре и формату не ветвится. #### Дельта контракта, которую ратифицирует оркестратор (минор `0.12.0` → `0.13.0`) ⚠ **Канон я НЕ трогаю** — это зона оркестратора. Здесь лежит ТЕКСТ дельты, чтобы её не выводили заново. **Что становится ложным:** `docs/architecture/14-api-contract/openapi.yaml`, описание `201` у `createBook` — фраза «**The `201` carries `parsing`, not `uploading`**». После синхронного разбора `201` несёт `parsing` только на одном из четырёх исходов. ⛔ **ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОЙ ДЕЛЬТЫ ОТОЗВАНА (строка 285), и вот что она говорила неверно.** Она обещала, что `character_count_exact` и `structure` зависят от того, «удалась ли материализация читательской поверхности», и что при удаче приезжают в том же `201`. Это было верно ровно до починки четвёртого круга: интейк БОЛЬШЕ НЕ МАТЕРИАЛИЗУЕТ читательскую поверхность (она стоит два движковых прогона и держала бы запрос загрузившего), поэтому оба поля в `201` отсутствуют **ВСЕГДА** и приезжают следующей ревизией библиотеки. ⚠ Сказано вслух, а не заменено молча: при конфликте редакций действует эта. **Что несёт ответ `POST /v0/books` в каждом исходе:** | исход | ответ | тело | |---|---|---| | движок нарезал **≥ 2 глав** | `201`, заголовок `Location` | `Book.status = "not_started"` · `chapter_count` = число глав движка · `character_count` = счёт интейка с **`character_count_exact: false`** и **`structure: null`** — ВСЕГДА. Оба поля пишет `SaveStructure` вместе с читательской поверхностью, а интейк её не материализует (уплотняет запрос на два движковых прогона): они приезжают позже, проходом материализатора, и клиент видит их следующей ревизией библиотеки. Правило `null` не меняется | | движок прочёл и нарезал **0 глав** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_book" }]`; книга НЕ принята — ни строки в библиотеке, ни каталога на диске | | движок прочёл и нарезал **1 главу** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_chapter_structure" }]`; тоже не принято ничего | | **деплойный класс или таймаут** | `201`, заголовок `Location` | `Book.status = "parsing"` — прежнее поведение маршрута целиком; очередь и страховочный свип доделывают | **Деплойные классы перечнем** (ни один не отказывает пользователю): `not_configured` — у книги нет конфигурации движка, или шаблон деплоя не читается · `storage_unavailable` — корень хранилища книг не смонтирован · `schema_mismatch` — проектная БД книги не той схемы, что бинарь · `parser_unavailable` — движок не удалось ЗАПУСТИТЬ, либо он ответил классом, которого эта сборка не знает, либо обычным выходом 1 · плюс превышение бюджета синхронного разбора (`books.CutBudget`). **Новых значений `ErrorCode` НЕ заводится.** Оба отказа — существующий `invalid_request`; `no_book` и `no_chapter_structure` живут в `errors[].code`, который сама спека объявляет НЕ закрытым («Not closed, like `cause.code`»). ⚠ `no_chapter_structure` — ВРЕМЕННЫЙ: он снимается, когда построена структура глав для выдачи (строка бэклога **283**), и в коде ветки стоит этот номер, чтобы её нашли и убрали. ### Батарея, мутации и деньги - **`make check` MAKE-EXIT=0**, 20 пакетов `ok`, красных 0, скипов **8**. Невыполненное условие хоста названо: `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (рецепт требует $0-пайплайн РЯДОМ с `backend/prompts/`, то есть записи в чужую зону или полной копии дерева). - **Тесты: 853 → 875 (+22).** Тестов, существовавших ДО пака, не удалено ни одного (`git diff -- '*_test.go' | grep -c '^-func Test'` → 0). ⚠ Один тест, добавленный ЭТОЙ ЖЕ сменой, удалён третьим кругом как тавтологичный (`TestTheUploadSettleBudgetCoversTheSynchronousCut`: `UploadSettle` определён через `CutBudget`, поэтому утверждение выполнялось всегда) — в диффе против `943617a` это не видно, и потому названо здесь. ⚠ Число снималось ПЯТЬ раз и первые четыре были неверны: «866 → 863» (глоб захватил не-тестовые файлы), «853 → 863», «853 → 864», «853 → 866» и «853 → 869» — каждое снято до пинов, добавленных следующим кругом самопроверки. В отчёте последнее, после последней правки. ⚠ Само по себе это и есть измеренная цена преждевременного объявления сходимости: шесть замеров одного числа, потому что кругов оказалось не два, а пять. - **Мутации: посажено 18, поймано 14 сразу, выжило 3, одна поимка отозвана** (её тест удалён третьим кругом, см. таблицу). Две выживших — дыры в МОИХ пинах, обе починены и пере-посажены (после починки красные); третья выживает СОЗНАТЕЛЬНО и названа. ⚠ Шестнадцатая посадка ОТБРОШЕНА мной как негодная: первая версия мутации охранника `claim`'а краснила тест ошибкой ТИПИЗАЦИИ Postgres (`could not determine data type of parameter $2`), то есть давала правый вердикт по неправой причине; пере-посажена корректно типизированной, и в таблице стоит вторая. | посадка | текст падения (или почему выжила) | хеш восстановлен | |---|---|---| | `report-failures` без строки сводки пакетов | `does not carry "textmachine/platform/internal/money"` | да | | `report-failures` возвращён к грепу `--- FAIL` (исходный дефект 305) | то же, на всех трёх фикстурах | да | | `report-failures` без счёта улик | `does not carry "Evidence, 1 lines"` | да | | `AbandonRun` без сравнителя ПОПЫТКИ | ⚠ **ВЫЖИЛА**: случай ловился сравнителем ЗАЯВОК — правый вердикт по неправой причине. Пин починен (новой попытке даётся тот же счёт заявок + сверка ТЕКСТА отказа), пере-посажена → `answered , want ErrProofOvertaken` | да | | `AbandonRun` без сравнителя ЗАЯВОК | `answered , want ErrProofOvertaken: the unit name reads exactly as the proof saw it` | да | | `RecordSpawn` без инкремента свидетеля | `the claim counter went 0 → 0: a witness that does not move cannot catch the race` | да | | свип не пишет парковку | `the parked attempt carries no ParkedAt` | да | | свип пишет парковку каждый проход (снят порог) | ⚠ **ВЫЖИВАЕТ ПО ПОСТРОЕНИЮ**: охранник записи всё равно не двигает метку. Порог покупает СТОИМОСТЬ, а не свойство; названо в комментарии теста | да | | `MarkParked` без охранника `is null` | ⚠ **ВЫЖИЛА**: два охранника прикрывали друг друга. Добавлено обращение к записи НАПРЯМУЮ, пере-посажена → `a second mark moved the stamp from … to …` | да | | снятие метки сделано неисполнимым | `the attempt is materializing again and the row still says parked since …` | да | | парковка посчитана карантинным гейджем | `parked=0 quarantined=1, want the park counted once and in its own series` | да | | разрез выпал из `UploadSettle` (состояние ДО находки) | ⚠ **поимка НЕВОСПРОИЗВОДИМА:** ловивший её тест удалён третьим кругом как тавтологичный, и в «поймано» она больше не считается | да | | знак `≥` снят с колонки SPENT | `SPENT prints an exact amount; the engine's meter is a lower bound` | да | | ячейка PARKED слита с QUARANTINE | `a parked attempt shows no elapsed time in PARKED, so «how long has it been quiet» has no answer` | да | | охранник claim'а у `DeleteRefusedIntake` всегда истинен | `a delete under somebody else's claim gave , want ErrNoBook` | да | | пере-чтение снова на контексте, открытом ДО тела | `after a cut that outlived the write budget the response says "parsing" with 0 chapters, want the parsed row` | да | | ветвь «манифест не читается» снова тратит попытку | `the intake's pass spent 1 attempts of a budget that bounds how often a broken HOST is asked` + `the claim is still held (…)` | да | | коды двух отказов на HTTP поменяны местами | `item code no_chapter_structure, want "no_book": the two refusals are different answers to the user` | да | - **Деньги двумя независимыми путями** на настоящих строках: сырой SQL по `credit_ledger` (7 строк, сумма 4919187 micro) и чтение платформы `ReadAccount` (Balance=LedgerSum=$4.919187) — сходятся. Расхождение до/после починки на тех же строках: было `note=""`, стало `at least: …` и, для оборванной попытки, `…cut off mid-work… (PD-441)`. ### ⛔ СРАБОТАЛО ПРАВИЛО ОСТАНОВКИ — синхронный разрез НЕ ГОТОВ и должен уехать отдельным паком Пятый круг дал major **внутри бюджета хвоста загрузки**, а оркестратор №23 отнёс этот бюджет к пути синхронного разреза именно на этот случай. Правило исполнено: путь больше не чинится, находки ниже записаны для следующего пака и НЕ закрыты. **Чем правило сработало (замер, а не оценка).** `UploadSettle` не покрывает хвост ТРЕТИЙ раз подряд, и каждый раз по новой причине. Сегодняшняя: перечень шагов в комментарии константы называет `FinishParse` и `ReleaseParseClaim` АЛЬТЕРНАТИВАМИ («whichever end the cut reaches»), а код их СКЛЕИВАЕТ — ошибка `FinishParse` уходит наверх (`internal/books/parse.go`, греп `owed, err := s.Store.FinishParse`), и `cutNow` на любой не-`ErrBadIntake` ошибке зовёт `ReleaseParseClaim` на СВЕЖЕМ `writeCtx`. Худший путь: `StartParsing 30 + разрез 90 + FinishParse 30 + Release 30 + ReadBook 30 + квитанция 10 = 220 с` при `UploadSettle = 210 с`. **Измерено, а не оценено (двоичный поиск по бутовому гейту через `Load()`):** гейт принимает дедлайн до **26m29s**, ложь начинается с **26m21s** — окно шириной восемь секунд, достижимое только ручной настройкой почти вплотную к потолку самого гейта; на дефолте **10m0s** запас **16m20s**. Наблюдаемое следствие — **дубль книги, а не потеря**: претензия на ключ идемпотентности, пережившая `ClaimStale`, перехватывается повтором с новым токеном, и человек, повторивший «висящую» загрузку, получает вторую книгу вместо реплея первой. ⚠ Достижимость худшего пути без искусственного замедления **не измерена**: она требует конъюнкции «большая книга» и «три полных `writeBudget` подряд», а гейт живого движка на этом хосте не закрыт — подставное замедление на вопрос «достижимо ли БЕЗ него» не отвечает. Ряд — `PD-464`. ⚠ Батарея этого не видит по построению: пин на сумму был тавтологичным и снят третьим кругом, а мутация `4*writeBudget → 3*` оставляет и `internal/config`, и `internal/books` зелёными. **Почему это остановка, а не ещё одна починка.** Из одиннадцати major, найденных кругами 3–5, девять лежат в одном месте — синхронном разрезе на приёме. Это новый механизм в конкурентном платном пути (claim · очередь · транзакция · бюджет), и он ведёт себя как такой механизм: каждая починка открывает новую площадь. Три круга подряд одна и та же константа оказывалась короче хвоста, и каждый раз по другой причине — это свойство предмета, а не невнимательности. **Что остаётся НЕ ЗАКРЫТЫМ в этом пути** (для пака, который его заберёт): 1. `UploadSettle` короче хвоста на 10 с (`PD-464`). ⛔ Лечение — **не поднять константу**: она выводилась руками трижды и трижды была неверной, каждый раз по новой причине, поэтому четвёртый вывод руками — подпорка (`D39.216`). Границу надо выводить **ИЗ кода пути**. 2. Комментарий в `cutNow` («the queue job is already enqueued») противоречит починке, ради которой функцию правили: на этом пути задание ставит сам `ReleaseParseClaim`. 3. Отказ `ClaimParse` в `cutNow` не различает рутинную гонку (`ErrParseClaimed`) и сбой БД, и во втором случае молчит: книга остаётся `parsing` без задания и без строки лога до свипа. 4. Параметр `enqueue` у `StartParsing` в бою мёртв (движок настроен всегда), а его doc описывает его как штатный путь; решение «кто ставит задание» размазано по двум местам на одном предикате. 5. Охранник `RowsAffected() == 0 → задание не ставить` в `ReleaseParseClaim` не запинен ничем. 6. «ВСЕГДА» в дельте контракта — гарантия по ТАЙМИНГУ, не по построению: свип материализатора теоретически успевает между `FinishParse` и `ReadBook`. Практически вероятность близка к нулю, но тексту контракта полагается либо структурная гарантия, либо оговорка. 7. Свип `StuckIntake` — третий претендент на claim, в разборе гонки не назван: при `TM_PLATFORM_UPLOAD_DEADLINE > claimGrace` он может взять claim раньше разреза. 8. Разрез потерял единственный ограничитель параллелизма: задание на приёме больше не ставится, а `MaxWorkers: 4` был у очереди — одновременных разрезов теперь не ограничивает ничто. 9. После последнего байта тела маршрут МОЛЧИТ до 210–220 с; промежуточный прокси об этом не спрашивали. 10. Отказ уходит наружу только машинным кодом (`Detail` не заполняется) ⇒ «честная причина человеку» из §4.3 сегодня не доезжает. Фронт заморожен, значит либо причина едет в `Detail`, либо пункт признаётся неисполненным. ### Замер, который должен пережить пак: очередь выигрывала гонку у разреза 5 раз из 6 Синхронный разрез берёт `ClaimParse` ПОСЛЕ коммита `StartParsing`, а `StartParsing` ставил задание очереди ВНУТРИ той же транзакции — то есть воркер становился видимым тем же коммитом и гонялся с разрезом за один и тот же claim. Проба (энкьюер зовёт `Parse` сразу после коммита, как это делает River на видимости строки), шесть прогонов: **очередь выиграла 5 раз, разрез 1 раз**. Оба исхода стоят книге, и это делает гонку дефектом, а не шероховатостью: - **выигрывает очередь** — синхронного вердикта нет вовсе, пустой файл принимается `parsing`, попытка бюджета потрачена, отказ приезжает асинхронно и оставляет книгу в библиотеке; - **выигрывает разрез** — задание съедено впустую (`ErrParseClaimed` → `nil`), и после возврата claim'а книгу подбирает только страховочный свип, то есть через `claimGrace` = 20 минут. ⇒ задание ставится ТОЛЬКО там, где синхронного разреза не будет, а на пути «вердикта нет» — вместе с возвратом claim'а одной транзакцией. Цена названа: процесс, умерший между строкой и разрезом, оставляет книгу без задания, и её берёт свип через `claimGrace`, а не сразу. ### Команды и их вывод — числа этого отчёта Сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются. ``` $ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check 20 строк `ok`, ни одной `FAIL`, MAKE-EXIT=0 --- did NOT run: 8 skipped … UNSET: TM_PLATFORM_TEST_BOOK_TEMPLATE, TM_PLATFORM_TEST_ENGINE_BIN $ git grep -h '^func Test' 943617a -- 'platform/**/*_test.go' | wc -l → 853 $ find platform -name '*_test.go' -exec grep -h '^func Test' {} + | wc -l → 875 $ git diff -- '*_test.go' | grep -c '^-func Test' → 0 $ git diff -- '*_test.go' | grep -c '^[-+]func Fuzz' → 0 $ python3 docs/scripts/carriers.py platform/docs/platform-PROGRESS.md carriers: 0 помеченных утверждений без носителя · осмотрено строк: 491 $ grep -c '^| PD-' platform/docs/DEFECT_REGISTER.md → 465 $ python3 docs/scripts/counts.py --check ✗ docs/PROGRESS.md: «открытых 106» против пере-счёта 112 · «всего 458» против 465 (зона docs/) $ grep -rc 'THE FIX IS NOT IN THIS PACKAGE' --include=*.go platform/ | grep -v ':0' | wc -l → 0 контроль: .go файлов осмотрено 197 · 'character_count_exact' найдено в 4 (v0.go, project.go, v0_test.go, books.go) $ git status --short -- docs/architecture/14-api-contract/ platform/docs/ENGINEERING_STANDARDS.md \ platform/docs/PLATFORM_DIRECTION.md → пусто $ git status --short -- backend/ | wc -l → 17 ⚠ и это НЕ мой след: 17 файлов правит параллельная бэкенд-сессия. Что этот пак в `backend/` не писал, из дерева НЕ измеримо — измеримо лишь то, что один файл я туда положила по инерции и убрала (`backend/configs/` чист). Прежняя редакция подавала это как замер; это не замер. $ git diff --cached --name-only | wc -l → 0 ``` ### Вынос журнала — что именно потеряно, пере-снято после всех правок `comm` по непустым строкам: было **4156**, стало **4420** (три файла плюс баннеры срезов), не сошлось **10** строк — и все десять названы, потому что «одна» в прежней редакции этого абзаца была верна лишь на момент замера и устарела от моей же последующей правки: - заголовок «Пинги оркестратора (живые ссылки для следующих сессий)» → «…ИСПОЛНЕНЫ» (все пять пингов под ним отработаны); - девять строк трёх ХВОСТОВЫХ секций-указателей («Эра пака P7 — в архиве», «Закрытые эры P0–P3 — в архиве» и дублирующий их буллет) — они дублировали блок «Архив эр» и слиты в него; обе ссылки (`-P7.md`, `-P0-P3.md`) в живом файле сохранены и резолвятся. ⚠ То есть потеряны ФОРМУЛИРОВКИ дублей, не содержание, и это утверждение проверяемо: все шесть ссылок на срезы в живом файле разрешаются в существующие файлы. Ссылки ИЗ других файлов в вынесенные диапазоны пере-нацелены: `sqlc.yaml`, `STACK_DECISIONS.md` (условия стенда — там же сказано, что архив читается как археология, а не как инструкция), `README.md` (шапка и таблица). ### Находка собственного круга: хвост загрузки, о котором бутовый гейт не знал Синхронный разрез добавил до полутора минут к тому, что загрузка делает ПОСЛЕ тела, а бутовый гейт (`internal/config`: `UploadDeadline + books.UploadSettle < min(UploadGrace, ClaimStale)`) про этот участок не знал — то есть оператор мог настроить дедлайн, при котором загрузка переживает окно идемпотентного ключа. Вылечено переносом бюджета в саму константу: `UploadSettle = CutBudget + writeBudget + 30s`. Существующий бутовый тест читает `UploadSettle`, а не литерал, поэтому подъём `CutBudget` теперь автоматически ужимает допустимый дедлайн. **Цена того, что `CutBudget` стал КОНСТАНТОЙ, а не настройкой — явно.** Деградация мягкая: бюджет работает ПОРОГОМ, а не потолком — на хосте, где разрез книги законно дольше полутора минут, загрузка не падает, она возвращается на прежний асинхронный путь (`201 parsing`, очередь доделывает), и единственная потеря — менее информативный `201`. Ручка убрана не ради чистоты: за ней стоял способ выстрелить себе в ногу — настройка, которой можно вытолкнуть хвост загрузки за окно, в котором её ключ ещё можно переиграть, а окно это ни один экран не показывает. ### Дофикс по приёмке оркестратора №23 — шесть пунктов, все закрыты | пункт | что было | что сделано | чем предъявлено | |---|---|---|---| | **Д1** | ⛔ **базис расчёта ВЫВЕРНУТ на самом частом окончании:** `finish()` считает исход в локальную переменную, а `settle()` получает ТОТ ЖЕ до-финишный снапшот ⇒ у прогона, кончившегося чисто, в леджер уезжал `halted` | снапшот несёт исход, который этот же вызов только что решил (`l.Status, l.PausedReason = status, pausedReason`) | **ИСПОЛНЕНИЕМ, не по коду:** прогон доведён до `ready`, строка леджера прочитана SQL-ом. До починки: `status="ready"`, а `note` — «cut off mid-work». Пин `TestACleanEndingIsSettledOnTheBasisOfTheEndingItReached`, мутация возвращает дефект и красит его текстом | | **Д2** | гейдж `PARKED` не запинен на уровне ЭКСПОЗИЦИИ, и проводка `Observe → metrics.Runner` в демоне не запинена вовсе | ассерт на серию в экспозиции + гейт, читающий композитный литерал демона и требующий, чтобы каждое поле бралось из поля СВОЕГО имени | две мутации: подмена серии → `the exposition is missing "tm_platform_parked_attempts 7"`; обмен полей в демоне → `one state's number is published under another's name` | | **Д3** | гейт 305 пинил ТАРГЕТ, а не его использование: откат ветви отказа к голому грепу оставлял батарею зелёной | гейт держит ветвь отказа рецепта `check`: она обязана звать `report-failures` и НЕ грепать `--- FAIL` сама | мутация — буквальный откат строки — красит обе проверки | | **Д4** | рантбук утверждал, что у парковки нет ни колонки, ни гейджа, ни строки; после пака это ложь | рантбук переписан: у парковки СВОИ три сигнала, карантинных нет и не будет, потому что снимать её не надо. Плюс `≥` у SPENT и пятый отказ `run abandon` | `deploy/README.md`, греп `tm_platform_parked_attempts` и `ПЯТЫЙ ОТКАЗ` | | **Д5** | комментарий `PD-424` объявлял исчерпывающие «two ways», а способов ТРИ | комментарий перестал объявлять полноту и называет третий способ с его радиусом; сам способ — ряд `PD-465` | заявка коммитится в `RecordSpawn`, юнит поднимается строкой ниже (`spawn.go`, `Runner.Start`) | | **Д6** | 18 протухших якорей в регистре, сдвинутых этим паком | пере-нацелены ПО ТОКЕНУ, а не по памяти; девятнадцатый — мой собственный, я записала токен с многоточием, которого в коде нет | `counts.py --lint`: было 25 проблемных якорей в 120 доках, стало **5**, в регистре **0** | | **Д7** | не заказан приёмкой, найден её же батареей: новый ряд `PD-465` несёт два маркера тревоги и не был объявлен в `alarmBaseline` | объявлен, с доводом почему он НИЖЕ major (окно в миллисекунды, расход ограничен потолком того же прогона, аккаунтом не эксплуатируем) | гейт `TestOpenRowsBelowMajorThatCarryAlarmMarkers…` красил батарею именно на нём; ⭐ и назвал его прибор **305** — «`check` упал» отличилось от «тест упал» на настоящем падении | ⚠ **Один якорь остался и он НЕ мой по зоне:** `docs/architecture/05-decisions-log.md:2678` целит в `platform/internal/pgstore/books.go:314` по токену `FinishParse records a parsed book`; мой пак сдвинул цель на **343**. Правка в зоне оркестратора — число передано пингом. ⚠ **Что приёмка нашла и что чинить НЕ надо** (правило остановки в силе, дописано к семи пунктам остановленного разреза): разрез потерял единственный ограничитель параллелизма — задание на приёме больше не ставится, а `MaxWorkers: 4` был у очереди · маршрут молчит до 210–220 с после последнего байта, промежуточный прокси об этом не спрашивали · отказ уходит наружу только МАШИННЫМ кодом, `Detail` не заполняется, то есть «честная причина человеку» из §4.3 сегодня не доезжает. ### Пять кругов самопроверки — что дал каждый | круг | кто | major | итог | |---|---|---|---| | 1 | я, по готовой работе | — | две дыры в МОИХ пинах (посадки выживали) + хвост загрузки, о котором бутовый гейт не знал | | 2 | я, против заказа | — | «≥» и парковка были предъявлены чтением кода, а не исполнением; охранники `DeleteRefusedIntake` не запинены; дельта контракта неверна. ⚠ **Сходимость объявлена здесь ПРЕЖДЕВРЕМЕННО** | | 3 | опровергатель `fable` | **4** | три — дефекты, введённые этим паком: мёртвый контекст пере-чтения · три ветви без возврата claim'а · синхронная материализация; плюс незапиненный маппинг на проводе | | 4 | опровергатель `fable` | **5** | гонка задания очереди с разрезом за один claim (**очередь выигрывала 5 из 6**) · бюджет снова короче хвоста · две ветви из трёх без пина · `!atIntake` без пина · ложная дельта контракта | | 5 | опровергатель `fable` | **1** (+6 minor) | бюджет короче хвоста ТРЕТИЙ раз, по третьей причине ⇒ **правило остановки** | ⚠ **Цена преждевременной сходимости, измеренная:** одно число (счёт тестов) снималось ШЕСТЬ раз, потому что кругов оказалось не два, а пять; и дельта контракта, которую оркестратор собирался ратифицировать, дважды была ложной — первый раз по существу, второй раз после моей же починки. ⭐ **Что из этого стоит унести дальше:** восемь major из одиннадцати нашёл ЧУЖОЙ прибор, а не я, и все восемь — в коде, который я только что написала и считала проверенным. Оба круга, которые я вела сама, дали настоящие находки, но НИ ОДНОГО major в новом механизме: свой код я проверяла по тому, что он должен делать, а опровергатель — по тому, что он делает. ### Что НЕ удалось и что считаю слабым местом - **Не измерено:** маржа `PD-441` в деньгах — приборa нет на этой стороне (см. движковую половину). - **Не предъявлено живым прогоном:** синхронный разбор гонялся на фикстурах и на живом Postgres, но не на настоящем `tmctl` — гейт `TM_PLATFORM_TEST_ENGINE_BIN` требует записи в `backend/`. - **Слабое место, которое называю сам:** `settlementBasis` судит по СТАТУСУ попытки, а не по факту наличия вызовов в полёте. Классы огрублены сознательно (пере-пометка стоит менее точного сигнала, недо-пометка прячет деньги), но `awaiting_bank` отнесён к «чистым» по чтению кода движка, а не по замеру: если окажется, что банк-стоп тоже рвёт волну, класс придётся сузить. - **Третье, названное опровергателем и оставленное сознательно:** если удаление отказанной книги само упрётся в БД (`discard` → `removeIntake`), пользователь получит `400`, а строка останется `parsing` под claim'ом; свип через `claimGrace` перечитает манифест, потратит бюджет попыток и оставит книгу `rejected` в библиотеке — то есть ровно то состояние, которого отказ и избегает. Не лечу: путь требует отказа БД РОВНО между двумя её же операциями в одном запросе, а лечение — компенсирующая транзакция, то есть механизм заметно крупнее устраняемого класса. Названо, чтобы следующий пак не открывал его заново. - **Второе слабое место:** отказ книге без глав — продуктовое сужение. Оно временное и помечено строкой 283 в коде ветки, но пока владелец его не подтвердил, это решение сессии. **Работа завершена, править больше не планирую.** Дерево передано оркестратору №23 незакоммиченным: 37 файлов, все внутри `platform/`, индекс пуст. Синхронный разрез построен и ОСТАНОВЛЕН правилом остановки — его семь незакрытых пунктов перечислены выше поимённо и уезжают отдельным паком; всё остальное закрыто и предъявлено. ## Живые нормы зоны, у которых нет другого носителя Каждая прошла триаж 06.09: она не выводится из кода и не стоит ни в `ENGINEERING_STANDARDS.md`, ни в `STACK_DECISIONS.md`. ⚠ Оркестратору предложено абсорбировать их в нормы зоны — до тех пор живут здесь. - **Ничего не отдавать на лендинг без прогона ПАКЕТА, которого правка касалась.** «Код написан и пин заведён» — не то же, что «прогнано»; числа снимаются после ПОСЛЕДНЕЙ правки, включая комментарные (генезис — `D39.211` п.6, где это зафиксировано как ошибка оркестратора, а не как норма зоны). - **`go test` без `-v` строк `--- SKIP` не печатает вовсе**, поэтому греп по ним в не-verbose логе даёт ЛОЖНЫЙ НОЛЬ скипов (`D39.213` п.3). - **Зелёная батарея — это полный список ПАКЕТОВ плюс отсутствие `FAIL`**, а не отсутствие красных строк в хвосте вывода (`D39.169`). - **Гейты доков перегоняются ПОСЛЕ последней правки доков, а не кода**, и число из растущего файла — не число (`D39.188`). - **Шаблон книги второго гейта батареи обязан указывать на СНАПШОТ движковых конфигов того же коммита, что и бинарь** (`git archive backend/configs backend/prompts`), а не на рабочее дерево: иначе живой тест читает файлы параллельной бэкенд-сессии и краснеет без дефекта (`PD-432` покрывает БИНАРЬ, не шаблон). - **Механизм, который агрегирует чужой каталог и отдаёт результат наружу, обязан иметь СПИСОК ТОГО, ЧТО НЕ БЕРЁТ, и список начинается с секретов.** Исключать по расширению — ловушка в обе стороны (`D39.201`). - **Красный тест, мерящий ВРЕМЯ, на загруженной машине — не результат:** прежде чем звать его дефектом, гонять изолированно и смотреть `uptime` (`D39.194`). ## Вопросы и пинги оркестратору — ОТКРЫТЫ - **Счёт рядов регистра сдвинут этим паком** и живёт в чужой зоне: `docs/PROGRESS.md` объявляет «открытых 106 / всего 458», пере-счёт даёт **112 / 465** (`python3 docs/scripts/counts.py --check`). Заведены `PD-459`…`PD-465`, `PD-458` переведён в `fixed`. - ⛔ **Нужен пятый `RejectReason` в контракте (`PD-463`), либо решение, что асинхронная ветвь приёма остаётся проницаемой.** Сужение входа до того, что умеет выдача, действует только на синхронной ветви; на ветви, куда книга уходит при деплойном классе, «книга одним полотном» попадает в библиотеку молча. Закрыть нечем: словарь закрыт четырьмя значениями, и ближайшее по словам `source_unreadable` ТЕРМИНАЛЬНО и УДАЛЯЕТ исходник — то есть уничтожает файл пользователя из-за НАШЕГО ограничения. - **Предложение движка провести `--ceiling-usd` в `status`** (пинг №15, 09.08) не принято и не отклонено с тех пор; `backend/internal/pipeline/status.go` отвечает «a read path never carries a run-scoped override». Нужна диспозиция или строка бэклога. - **`counts.py` держит регистр ОДНИМ путём и слайсы не глобит.** Это предусловие плана нарезки `DEFECT_REGISTER.md` (`docs/DOC_CLEANUP_PLAN.md`, Б14в): вынос без него молча уронит счёт. - **§2.12 компаньона контракта** (`docs/architecture/14-api-contract/README.md`) утверждает, что пять денежных полей не могут доехать, тогда как аллоулист шва несёт `committed_usd`/`reserved_usd` (`platform/internal/ingest/resync.go`); якоря на `pipeline/status.go` там же мертвы. Зона `docs/`. - **Зеркало контракта у фронта — `0.2.3` против канона `0.12.0`** и несёт отозванное правило «денег в интерфейсе MVP нет вообще» (`D39.196` п.2 его снял). Зона фронта заморожена; пинг на разморозку. - **Восстановление из бэкапа не предъявлено на книге в сотни мегабайт, при заполненном диске и при чужих соединениях** (`PD-462` покрывает соседний класс, этот — нет). Нужна строка или ряд. - **Правило записи адресов в доках, живущее только в архиве и относящееся к прибору из `docs/`:** адрес пишется БЕЗ якорной нотации там, где текст РАССКАЗЫВАЕТ о форме (линтер `counts.py` разбирает форму где угодно, включая объяснение этой формы), а мёртвый адрес цитируется ПОРОЗНЬ — имя файла в кавычках, номер словами, — иначе цитата сама становится якорем. В `counts.py` этого нет; носитель нужен в зоне `docs/`. - **Рантбук деплоя ни разу не прогонялся end-to-end с настоящим `migrate`** — названо предусловием первого выката ещё пингом №17 и носителя не получило. ## Архив эр - **Паки 04–06.09** («закрыть цикл» · «пустить внутрь можно» · «форма заказа перевода» · наблюдение за живым платным потоком) — [archive/platform-PROGRESS-2026-09-04-06.md](archive/platform-PROGRESS-2026-09-04-06.md). Ратификации **D39.194**, **D39.201**, **D39.208**, **D39.211**–**D39.214**. - **Эры P9–P13 (27.08–03.09)** плюс приёмка P8-REVIEW, раздел «Состояние эры P8» и исполненные пинги оркестраторов №15–№22 — [archive/platform-PROGRESS-P9-P13.md](archive/platform-PROGRESS-P9-P13.md). Ратификации **D39.159**, **D39.162**, **D39.166**, **D39.169**, **D39.172**, **D39.180**, **D39.188**. - **Эра пака P8-FIX (21–22.08) — В АРХИВЕ.** Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle — [archive/platform-PROGRESS-P8.md](archive/platform-PROGRESS-P8.md). Ратификация — D39.154, лендинг `31f1f82`. Живое из этой эры: четыре открытые строки регистра (`PD-370` мажор — контрактная половина · `PD-371` · `PD-372` · `PD-373`/`PD-374`), инвентарь каналов шва в `STACK_DECISIONS.md`, граница sqlc в `BACKLOG.md` П-19. - **Записи акта 5 пака P7, остававшиеся в живом журнале, — ДОСЛАНЫ в [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md)** 22.08: заголовок «эра P7 в архиве» стоял, а тела лежали здесь. - **Паки P4, P5, P6 и их дофиксы (08–15.08) приняты и залендены.** Ратификации: **D39.123** (P4 «раннер», `d29e30c`; формула аргумента потолка = `committed + прирост`, PD-158) · **D39.130** (P5, `69d485a`; Go-floor 1.26.6 — `toolchain`-директива в `go.mod` плюс сравнивающий гейт `make version-check`; интейк пишет `book.yaml` формой Б) · **D39.131** (эмиттер шва движка; словарь кадров — `internal/ingest/events.go`) · **D39.132** (P6 + дофикс; `PD-113` закрыт, у батареи появился второй гейт окружения). Записи паков с дофиксами, живыми пробами и посадками — [archive/platform-PROGRESS-P4-P6.md](archive/platform-PROGRESS-P4-P6.md); промт P5 — `archive/PLATFORM_P5_SESSION_PROMPT_2026-08-10.md`. ⚠ Счёт регистра и тестов тех дней устарел на порядок: живые числа берутся `python3 docs/scripts/counts.py --check` и `DEFECT_REGISTER.md`, а не отсюда. - **Эра пака P7 (16–20.08)** — пять актов, приёмка и фикс-раунды: [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md). - **Эры P0–P3** (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены (D39.107/109/112/114): [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md) (D39.124). Решения оттуда живут в D-логе и `DEFECT_REGISTER.md`.