# Журнал зоны «Платформа» > **Что это.** Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. > Отработавшие эры вынесены срезами в [`archive/`](archive/) — читать только по конкретной ссылке. ## ОТЧЁТ 17.09 — ПРОГОН НЕ ВРЁТ О СЕБЕ (ряды 398 · 394 · 396 · 399 · 400 · 416 · 486, контрактный минор 0.17.0) Пак `docs/PLATFORM_RUN_TELLS_THE_TRUTH_SESSION_PROMPT.md`, сессия `textmachine-main-78`, выдача №15 очереди №23. НЕ коммичу, дерево передаю оркестратору. Записка-план — ниже этим же блоком, писана ДО первой правки кода и задним числом не переписана: два её решения переменились, и это видно. **Предмет пака — не семь функций, а ОДИН инвариант в трёх адресатах:** о прогоне нельзя узнать неправду ни из кадра (оператор), ни из экрана (платящий), ни из суммы согласия (счёт). Отчёт построен по этой оси, а не по рядам. ### 1. Исход КАЖДОГО пункта заказа | Пункт | Исход | Чем предъявлено | |---|---|---| | **§4.1 кап попыток (398)** | СДЕЛАНО | предел на НОМЕРЕ попытки · `TM_PLATFORM_RUN_MAX_ATTEMPTS`, дефолт 20 из зоны · 6 пинов · `PD-215` закрыт с пере-взвешиванием | | **§4.2 пометка нашего грейса (394)** | ПЕРЕ-РЕШЕНО доводом, ратифицировано | четвёртого значения на проводе НЕ заводим (ось словаря) · пометка уже есть в `exit_result` · построен ГЕЙТ словаря на три носителя · слово `abandoned` для вердикта оператора (находка вне заказа) | | **§4.3 развилка «стоп без намерения» (396)** | ПЕРЕ-РЕШЕНО ЯВНО: оставить, с капом как условием | пин обеих сторон · альтернатива с уликой перезагрузки названа и НЕ построена, с ценой | | **§4.4 слово покупателю (399)** | СДЕЛАНО (форма ратифицирована ДО стройки) | `Run.progress_lagging` в каноне 0.17.0 + провод + чтение · 4 пина, среди них «чужак — ЭТОТ ЖЕ прогон» | | **§4.5 смерть между правкой банка и записью (400)** | СДЕЛАНО | миграция `00037` · намерение вперёд · 6 пинов, среди них ПОРЯДКОВЫЙ · цена ряда исправлена (см. §3) | | **§4.6 согласие (416)** | СДЕЛАНО в сужении (провенанс и видимость) + пояс | `Run.rebill_consent_micro_usd` в каноне + провод · пояс на съеденное меньшее согласие · движковая половина — заказом следующему паку | | **§4.7 харнесс мутаций (486)** | СДЕЛАНО шире ряда | 4 условия засчёта · `-race` · фикстурный каталог В ДЕРЕВЕ · пин на сам харнесс · решение зоны №37 | ### 2. Граница класса, как я её провела - **Наша политика** — конец, который выбрала платформа или человек через нашу дверь: наш грейс (`timeout`), исчерпание капа (`attempts-exhausted`), вердикт оператора (`abandoned`), стоп пользователя (`stop-requested`). - **Авария** — конец, который навязал деплой или хост: `oom-kill` · `core-dump` · `watchdog` · `resources` · `protocol` · `exec-condition` · нечитаемый маркер. - **Провод этой оси не несёт и не должен.** `RunFailureReason` отвечает на ОДИН вопрос — «стоит ли предлагать повтор», — и канон это говорит дословно; комментарий констрейнта добавляет, что четвёртое значение с тем же ответом было бы лишь четвёртой фразой. Ось «кто закончил» живёт в `run_attempts.exit_result`: колонка объявлена без констрейнта (`00009_runner.sql:57`), канону неизвестна, словарь в ней — ПЛАТФОРМЕННЫЙ, и новое слово там не требует ни минора, ни миграции. ⇒ пак добавил в неё два слова и не добавил ни одного на провод. ### 3. Находки О САМОМ ЗАКАЗЕ — то, что в промте и рядах оказалось неверным 1. **⭐ Цена ряда 400 ПРОТУХЛА, а предмет — нет.** Ряд (и `PD-425`) говорят: «следующий прогон допускается без пере-снапшота ⇒ умирает на снапшот-гарде уже ПОСЛЕ взятого холда». На СТАРТЕ это больше не так: `--resnapshot` едет на каждом продолжении книги, у которой был прогон (`internal/runs/runs.go`, `resnapshot := book.BankMoved || book.HasPriorRun`, PD-422), а книга у двери правок банка по построению имеет прошлый прогон — банк берётся из майнинга. Харм ЖИВ на **РЕЗЮМЕ**: `reopen` читает факт двери ОДИН, без расширения. ⇒ пин поставлен на резюме; на старте фикстура удовлетворялась бы ЧУЖИМ фактом (`HasPriorRun`) и стояла бы зелёной над сломанным механизмом. Это класс «пин удовлетворяется чужой уликой», и здесь его поймал не прогон, а чтение условия. 2. **Контроль промта в §4.2 не воспроизвёлся.** «Хитов `RunFailureReason` в `internal/gates/` ноль при контроле 12 у соседнего словаря»: ноль подтверждён, контроль **12** не воспроизводится ни на одном соседе (`PausedReason` · `BookStatus` · `ContractHaltReason` — по нулю). Оркестратор пере-снял сам и назвал причину: он считал вхождения СЛОВА в файле гейта, а не гейты на словари. Вывод устоял и стал сильнее: машинка чтения канона в зоне уже есть (`enumOfSchema`), и словарь можно было запинить без всякой ратификации — что и сделано. 3. **Ряд 486 просил два пункта, а половина второго уже была построена.** «Ненулевой выход без строки падения = НЕ ИЗМЕРЕНО» инструмент нёс с прошлого пака; строить надо было не это, а зелёную базу и засчёт по НАЗВАННОМУ пину. Плюс два условия, которых в ряду нет: каталога посадок в репозитории не было ни одного (новому полю `pin` не на чем пере-проверяться), и инструмент гонял БЕЗ гонки, тогда как батарея с ней. 4. **Носитель зоны врал числом гейтов.** `STACK_DECISIONS` объявляет «гейтов ЧЕТЫРЕ» и объявлен ЕДИНСТВЕННЫМ носителем этого числа; прибор `make conditions` печатает **шесть** переменных (`PGDUMP`, `PGRESTORE`, `BANK_READOUT` прибавились). Сессия, честно сверившаяся с числом, не поднимет два гейта и спишет скипы на известные ей условия. Исправлено. 5. **И предупреждение «0 FAIL СНЯТО» на сегодняшнем `HEAD` неверно.** Носитель обещает следующей смене красное; мой базовый прогон на чистой зоне дал 0 FAIL. Записана третья точка — с оговоркой, что полноту всё равно сверяют списком на своём дереве, а не числом из дока. 6. **Находка ВНЕ заказа, того же класса, что ряд 394:** `AbandonRun` пишет прогону `failure_reason = 'service_error'` (`internal/pgstore/runs.go:807`), то есть **терминальный вердикт ОПЕРАТОРА предъявляется пользователю как поломка деплоя**, и в `exit_result` при этом не писалось ничего. Провод не тронут (на его оси `service_error` — правильный ответ: повтор сам не поможет), а колонка теперь несёт `abandoned`. ### 4. Круги и что нашёл каждый **Круг 1 — чтение.** Нашёл §3 п.1 (цена ряда 400), §3 п.2 (контроль промта), §3 п.6 (abandon), ось словаря для §4.2, и то, что улика перезагрузки на хосте есть, а зона её не читает. **Круг 2 — исполнение (кампания посадок).** Нашёл **три дефекта в МОИХ ЖЕ пинах** и один в предмете: | Находка | Что сделано | Чем предъявлено | |---|---|---| | Правка общей формы чтения прогона (`runRow`) сломала латераль `lastRun` с ЯВНЫМ списком колонок — `column r.accept_rebill_micro does not exist`. Пины, зелёные час назад, этого не показывали: они были прогнаны ДО правки | колонка добавлена в латераль | условие «зелёный базовый прогон» инструмента: пять записей вернулись «НЕ ИЗМЕРЕНА», а не ложным RED | | Пин `TestAVerbThatMayHaveDiedMidWriteKeepsItsMark` НЕ ДОХОДИЛ до ветки, которую утверждал: фикстура ломала РАННЕР, а на этом пути дверь возвращается раньше | фикстура пере-построена на exit 15 («глагол ответил: запись не завершена») + в доккомментарии названа граница, которую фикстура пересекает | посадка `B5` выжила на первой редакции, краснеет на второй | | Пин `TestAParkAnEarlierAttemptEndedWith…` измерял ПУСТОЙ сценарий: тот же проход свипа сам стирал парковку (`MarkParked(false)`), потому что журнала в фикстуре нет | улика ставится ПОСЛЕ прохода, плюс утверждена ПОСЫЛКА (попытка закончена И несёт парковку) | посадка `L2` выжила на первой редакции, краснеет на второй | | Пин на слово оператора сравнивал колонку с ТОЙ ЖЕ КОНСТАНТОЙ, которую код в неё пишет — класс `PD-1`: проходит при любом значении, включая пустое | утверждение на ЛИТЕРАЛЕ, плюс отдельная сверка «константа = литерал» | посадка `D3` выжила на первой редакции, краснеет на второй | **Круг 3 — пере-съём на свежей копии.** Новых находок нет: 22 поимки, 1 объявленная выжившая, 0 неизмеренных. Круги сошлись. ### 5. Мутации — полный каталог, поимённо Каталог живёт В ДЕРЕВЕ: `platform/tools/mutations/run-tells-the-truth.json` (урок ряда 486 — число без прибора, которым оно снято, пере-снять нечем). **Команда и прибор** (инструмент зоны ПОСЛЕ правки по ряду 486 — четыре условия засчёта, `-race`, восстановление копии после каждой записи): ``` ~/tm-78-stand/campaign.sh > ~/tm-78-stand/campaign-final.log = python3 tools/mutate.py tools/mutations/run-tells-the-truth.json \ ~/tm-78-stand/mutroot/platform /home/ubuntu/projects/textmachine-main/platform итог: записей 24 · поймано 23 · ВЫЖИЛО 1 · НЕ ИЗМЕРЕНО 0 после кампании: diff -rq platform ~/tm-78-stand/mutroot/platform → копия байт-идентична источнику ``` ⚠ **Кругов было ТРИ, и числа разных кругов я не смешиваю** — иначе «поймано 23» читалось бы как свойство пинов, а не как результат их починки: | круг | записей | поймано | выжило | НЕ ИЗМЕРЕНО | что это был за круг | |---|---|---|---|---|---| | 1 | 23 | 14 | 4 | **5** | первый прогон; нашёл сломанную латераль и три дефекта моих пинов | | 2 | 23 | 22 | 1 | 0 | после починки, на СВЕЖЕЙ копии (записи круга 1 недействительны — сняты до правок) | | 3 | 24 | 23 | 1 | 0 | плюс запись `V1` на объявление конца | | 4 (финальный) | 24 | **23** | **1** | **0** | пере-снят ПОСЛЕ последней правки кода (`CommandContext` в моём гейте), свежая копия | **Каталог по предметам** (все записи — в дереве, `platform/tools/mutations/run-tells-the-truth.json`): - **кап (§4.1), 8 записей:** `C1` предел снят · `C2` off-by-one · `C3` предел на счётчике, который рестарт обнуляет · `C4` упёрлись и не закончили прогон · `C5` наше слово подменено на «юнит исчез» · `C6` исчерпание выпало из списка причин · `C7` строка перезапуска не несёт потолка · `C8` дефолт за полосой своего довода. **Все восемь краснеют ИМЕНОВАННЫМ пином.** - **банк (§4.5), 5 записей:** `B1` намерение не ставится · `B2` читатель игнорирует намерение · `B3` ретайр факта не снимает намерение · `B4` штамп не снимает намерение · `B5` намерение снимается на любом исходе. - **отставание проекции (§4.4), 3 записи:** `L1` молчит на карантине · `L2` история предъявлена как сегодняшнее отставание · `L3` провод роняет член. - **согласие (§4.6), 3 записи:** `K1` сумма не уходит на провод · `K2` согласие читается нулём · `S1` пояс (ВЫЖИЛА, объявлено заранее). - **словарь и слово оператора (§4.2), 4 записи:** `D1` словарь разъезжается вверх · `D2` вниз · `D3` слово оператора пустое · `D4` вердикт оператора затирает машину. - **объявление конца, 1 запись:** `V1` конец прогона не называет слово машины. ⚠ **Числа ИНСТРУМЕНТА — отдельно и в пакетный счёт НЕ входят.** У самого харнесса свой фикстурный каталог (6 записей, `internal/gates/testdata/mutantfixture/catalogue.json`) и свой пин `gates.TestTheMutationHarnessCountsOnlyWhatItCanMeasure`, который требует от него ровно четыре исхода (`1 RED · 1 ВЫЖИЛА · 4 НЕ ИЗМЕРЕНА`) и байт-идентичного восстановления копии. Это измерение ПРИБОРА, а не предмета, и складывать его с 24 записями пака нельзя. **Выжившие — поимённо, с причиной:** - `S1-belt-on-a-shrinking-consent` — **ОЖИДАЕМАЯ, объявлена ДО прогона.** Пояс §4.6(а) стережёт переход, у которого сегодня нет населения: единственное ненулевое согласие — бюджет прогона, а он у прогона не меняется, поэтому «меньшее ненулевое» недостижимо. Форма «недостижимо сегодня и удержано намеренно» в этом файле уже живёт; условие, при котором вывод перестаёт держаться, написано рядом с поясом. Оркестратор велел пояс оставить. ### 6. Числа — каждое с командой, снято ПОСЛЕ последней правки **Гейт зоны — одна команда, вердикт строкой из лога:** ``` ~/tm-78-stand/battery.sh > <лог> # = make check из platform/, PATH с sqlc, шесть гейтов подняты ~/tm-78-stand/verdict.sh <лог> # MAKE_EXIT + ПОЛНОТА списком (comm -23) + скипы ``` | круг | дерево | `MAKE_EXIT` | пакетов `go list` | вердиктов в логе | `comm -23` | упавших тестов | скипов | |---|---|---|---|---|---|---|---| | рубеж входа (до моих правок) | зона на `HEAD` | **0** | 20 | 20 | пуст | 0 | 6 | | финал, круг 1 | моё | **2** | 20 | 20 | пуст | **1** | — (до печати скипов не дошло) | | финал, круг 2 | моё | **0** | 20 | 20 | пуст | **0** | 6 | | контроль, круг 1 | чистый `HEAD` (`git archive`) | **0** | 20 | 20 | — | 0 | 6 | | контроль, круг 2 | чистый `HEAD` | **0** | 20 | 20 | — | 0 | 6 | ⚠ Плюс ДВА прогона, которые я сама объявила финальными и которые НИЧЕГО не измерили: `MAKE_EXIT=2` на цели `fmt` и `MAKE_EXIT=2` на `lint` — разбор в блоке 7. Вердиктов у пакетов в них 0 из 20, и `comm -23` это показал честно. **Шесть скипов, поимённо** (те же, что на рубеже входа, и ни один не мой): **пять** живых движковых проб `internal/runner` (`backup_live` · `bankapply_live` · две в `build_live` · `translate_resnapshot_live`) без артефакта контраста банкового контура (`backend/configs/mining-contrast.zh.txt` на хосте нет — строка бэклога 251) плюс оракул `TestARealReadOutIsReadTheWayThisBuildClaims` без `TM_PLATFORM_TEST_BANK_READOUT` (артефакт прошлого пака, купленного прогона у меня нет). **Единственное падение — что о нём известно и чего я НЕ доказала.** `internal/books` → `TestAnUploadThatRunsOutOfBudgetWaitingForASlotIsAcceptedRatherThanRefused`: `limit_test.go:176: an upload that met a busy host was refused: pgstore: commit: timeout: context deadline exceeded`. - **Цепь звеньев, названная по коду, а не выведенная:** фикстура САМА ставит `writeBudget = 20 ms` и `uploadSettle = 300 ms`; оба поля неэкспортированы и по умолчанию НУЛЕВЫЕ — их собственный доккомментарий говорит, что «дойти до конца трёхминутного бюджета — это тестовая вещь» (`internal/books/books.go:91-97`). Под нагрузкой полной батареи (20 пакетов с гонкой против одного Postgres) коммит выходит за 20 мс, `Accept` отвечает ошибкой, и `t.Fatalf` срабатывает. - **Изоляцией: 0 падений из 5** (`go test ./internal/books/ -race -count=1 -run <имя>` пять раз). - **Моё ли:** мой дифф не касается пути записи интейка; единственная правка ТАБЛИЦЫ `books` — nullable колонка без индекса (миграция `00037`), которая вставке ничего не добавляет. - ⚠ **Чего я НЕ доказала: равенства частот.** Моё дерево — 1 красный на 3 полных прогона, `HEAD` — 0 на 2. Выборка мала, и искусственной нагрузки я не создавала СОЗНАТЕЛЬНО: на этой машине работают другие сессии, и загрузить хост значило бы испортить их замеры. ⇒ утверждаю механизм и изоляцию, а не статистику; приёмке, если нужно, довольно ещё двух кругов в обоих деревьях. **Тулчейн против пинов** (`golangci-lint 2.12.2` · `sqlc v1.31.1` · Go ≥ 1.26.6): ``` golangci-lint --version → 2.12.2 (built with go1.26.2, 2026-05-06) sqlc version → v1.31.1 ⚠ лежит в ~/go/bin и НЕ на PATH — ловушка подтвердилась go version → go1.26.7 ≥ 1.26.6 ``` **Стенд Postgres** — живость спрошена ТЕМ ЖЕ DSN, которым читают тесты, а не TCP-пробой: ``` ~/.local/pgsql/bin/psql 'postgres://postgres@/postgres?host=/tmp&port=55433&sslmode=disable' \ -tAc 'select 1, current_setting($$server_version$$)' → 1|18.4 ``` ### 7. Что не удалось · что не проверено · ГДЕ МОЙ ПРИБОР СЛЕП **Не строила сознательно, с доводом:** - **Улика перезагрузки хоста** как способ различить ребут от ручного `systemctl --user stop` фактом, а не асимметрией. Она есть и зоной не читается: `/proc/stat btime` = 2026-09-15 10:57:41 UTC, `boot_id` на месте; хитов `btime` · `boot_id` · `BootID` · `uptime` · `InvocationID` в `platform/` — по нулю (контроль прибора: тем же грепом `restart` в `internal/runs/reconcile.go` — 47 хитов, то есть искал в населённом дереве). Не строила: ошибка в сторону «ребута не было» возвращает дефект строки 138 на ВЕСЬ хост (терминирование пользовательского менеджера мимо ребута читается как ручной стоп), а выигрыш — та же ограниченность, которую уже даёт кап. Оркестратор завёл строкой трекера. - **Движковая половина ряда 416** (проекция пере-покупки) — не моя зона по построению: считать её умеет только движок. Заказ следующему паку. - **Ветка exit 12** (`PD-216`): её блокер снят капом, но правка — смена продуктового поведения на терминальном исходе, то есть заказ. Запись в реестре обновлена. **⛔ Что я сделала неправильно в ПРОЦЕССЕ, и это стоило двух ложных «финальных» замеров.** Первый прогон, который я объявила финальным, дал `MAKE_EXIT=2` на цели `fmt` (`internal/httpapi/v0.go`), второй — на `lint` (`noctx`: три `exec.Command` в моём же гейте вместо `exec.CommandContext`). Тестов в обоих не было ВООБЩЕ: цель, падающая первой, отменяет все последующие, и `comm -23` честно показал двадцать пакетов без вердикта. Причина одна и простая: `gofmt -l` я гоняла по КАТАЛОГАМ, которые правила последними (`internal/runs`, `internal/gates`), а не по зоне, и линтер не гоняла вовсе до батареи. ⇒ «все мои пины зелёные» было правдой, которая не значила ничего: до батареи мой код не доходил до `test`. **⛔ Где прибор слеп, и я это знаю:** 1. **Отказ двери при невозможности записать намерение (§4.5) проверен ЧТЕНИЕМ и ПОСТРОЕНИЕМ, но не исполнением.** `Service.Store` — конкретный `*pgstore.Store`, не интерфейс, поэтому стор, который отказывает на ОДНОМ методе, не подставить; рвать пул убивает и остальные чтения того же вызова. Заведено `П-25`. Чем закрывается: узкий интерфейс на пишущие методы двери — решение зоны, не моё. 2. **Кап НЕ проверен на боевом пути «маркер не пишется двадцать раз подряд»** — только на фикстуре, где юнит исчезает без маркера. Живой прогон с намеренно сломанным `MarkerArgv` я не ставила: деплой-мутация на стенде, а пак $0. Что это значит: кап проверен на СОСТОЯНИИ, которое даёт тот же вход реконсилятору, но не на причине, которая его в проде производит. 3. **`progress_lagging` не проверен на КАРАНТИНЕ end-to-end через живой тейлер** — карантинная половина пина ставит причину в колонку SQL-ом. Парковочная половина прогнана настоящим журналом с чужим хендшейком. Чем закрывается: живая проба карантина требует нечитаемого потока, и у зоны для этого есть отдельные тесты `internal/ingest` — соединять их с чтением прогона я не стала. 4. **Число «~290 скрытых тестов» из носителя я не пере-снимала.** Мой замер — 6 скипов при поднятых гейтах, а не «сколько спрятано без DSN»; для этого нужен прогон без DSN, который я не делала. 5. **Гонка на моих собственных пинах прогнана, но не в ПОЛНОЙ батарее с `-count>1`.** `make check` гоняет `-race -count=1`; чувствительность моих пинов к состоянию процесса не мерена. ### 8. Что передаётся оркестратору - **Лендинг одним коммитом**: `platform/` + `docs/architecture/14-api-contract/` (канон 0.17.0 и компаньон правлены мной, как правила предыдущая платформенная сессия; красного окна между константой и каноном нет — они в одном дереве). - **Ратификация нотой** минора 0.17.0 и пере-решений §4.2/§4.3 — твоя; в компаньоне номер ноты стоит как «при лендинге», без выдуманного числа. - **Трекер**: ряды 398 · 399 · 400 · 486 закрываются работой; **416 закрыт платформенной половиной, движковая ОТКРЫТА**; 394 и 396 закрываются ПЕРЕ-РЕШЕНИЕМ (провод не тронут) — формулировку исхода выбираешь ты. Зонные: `PD-215` → `fixed`, `PD-216` остаётся открытой без блокера, заведены `П-24` и `П-25`. ### 9. Завершённость - у **каждого** из семи пунктов заказа есть исход — таблица §1; два пункта закрыты ПЕРЕ-РЕШЕНИЕМ с доводом, и оба пере-решения ратифицированы оркестратором очереди №23 до стройки; - **круги сошлись:** последний круг новых находок не дал, прежние — в таблице §4 «находка → что сделано → чем предъявлено»; - **каталог мутаций полный и в дереве** (24 записи): 23 поймано ИМЕНОВАННЫМ пином, 1 выжившая названа поимённо с причиной и объявлена ДО прогона, НЕИЗМЕРЕННЫХ — ноль; - **числа сняты после последней правки** — батарея круга 2 и кампания круга 4; у каждого числа стоит команда, рядом с каждым нулём — контрольная величина; - **всё живое — в дереве:** каталог посадок, фикстурное дерево харнесса, миграция, пины, каталог условий; ничего существенного не осталось в письме или в скретчпаде; - **гейт зоны зелёный целиком** на последнем круге: `MAKE_EXIT=0`, 20 вердиктов на 20 пакетов, `comm -23` пуст, 6 скипов — все чужих условий; - ⚠ и честно: ОДИН из трёх полных прогонов моего дерева был красным на чужом load-sensitive тесте — разбор и чего я не доказала — в §6. **Работа завершена, править не планирую.** (⚠ Эта фраза верна на момент СДАЧИ; круг приёмки вернул пять находок, и правки по ним — в §10 ниже. Фразу не снимаю и не пере-писываю: видно, где кончилась сдача.) Дерево передаю оркестратору: `platform/` целиком плюс `docs/architecture/14-api-contract/` (канон 0.17.0 и компаньон). 22 файла правлено, 10 путей новых. ### 10. КРУГ ПРИЁМКИ — что нашли линзы оркестратора и что я по этому сделала ⚠ Раздел дописан ПОСЛЕ слов «работа завершена» в §9: приёмка вернула пять находок, две из них требовали правки в моей половине, и я их внесла. Сами §1–§9 не пере-писаны — видно, где кончилась сдача и начался круг приёмки. | Находка | Чья ошибка | Что сделано | Чем предъявлено | |---|---|---|---| | **F1.** Канон описывал происхождение согласия У́ЖЕ, чем оно есть: «даёт `resume` над поправленным банком» — а `runs.go:470`=`resnapshot := book.BankMoved \|\| book.HasPriorRun` плюс `:579`=`func rebillConsent` дают non-null согласие и на ОБЫЧНОЙ второй покупке книги | моя, в тексте канона | описание расширено до истинной популяции: согласие несёт всякий прогон, идущий с пере-снапшотом, и назван довод, почему вторая покупка — защита, а не аппетит (проекция, выросшая за виденное покупателем, обязана ОТКАЗАТЬ, а не быть купленной молча). Фраза «never by the platform's own restart» оставлена: свип передаёт нули | пере-снято мной по коду до правки; канон разобран YAML-парсером | | **F2.** `progress_lagging` объявлен «Always sent», а в `Run.required` его не было ⇒ клиент, сгенерированный по канону, сделал бы член опциональным — канон противоречил бы себе на одном свойстве | моя | член внесён в `required` (12 вместо 11), рядом — довод и ссылка на зеркальный корректирующий минор `0.13.1` | `requiredOf` парсит канон: `progress_lagging` в required = да, `rebill_consent_micro_usd` = нет (nullable по замыслу) | | **F2⭐ класс, а не случай** | предложение оркестратора, аргумент мой | построен гейт `httpapi.TestEveryAlwaysSentMemberOfARunIsPromisedByTheCanon`: он ходит по ЖСОНу, который провод отдаёт для пустого прогона, и требует, чтобы каждый всегда-посылаемый член был либо в `required`, либо в ИМЕННОМ реестре исключений с причиной. Реестр честен в обе стороны: исключённый член, который канон уже обещает, и исключённый член, которого провод не шлёт, — обе строки краснеют | две посадки каталога: канон перестаёт обещать член · провод переименовывает член | | **F4.** `failureReasonsInDDL` читает миграцию целиком, не различая `+goose Up` и `+goose Down` | моя | условие, при котором вывод перестаёт держаться, дописано в хелпере, и названо лечение (резать файл по маркеру `+goose Down`, а не нацеливать гейт на файл по имени) | по нашему же правилу про выводы в комментариях | | **F4-бис.** ⛔ И я внесла F4 в ОДИН носитель из двух: условие легло в хелпер, а комментарий на СAЙТЕ ВЫЗОВА продолжал утверждать «последняя миграция и есть действующая» без всякой оговорки — приёмка читала именно его и справедливо сказала «не внесена» | моя, и **третий раз за смену один класс**: поправить один носитель и оставить второй утверждать прежнее (первые два — заголовок «гейтов ЧЕТЫРЕ» и снятое предупреждение про «0 FAIL») | на сайте вызова условие названо УКАЗАТЕЛЕМ на хелпер, а не второй копией: копия условия — это второе, чему стареть, и именно так этот комментарий и разошёлся со своим механизмом | `gates` зелёный с гонкой, линтер `0 issues.` | | **§2.28 без токенизированных якорей** | моя | четыре прозаические ссылки стали якорями с токенами (`sink.go:463` · `sink.go:473` · `reconcile.go:818` · `reconcile.go:1681`), каждый токен проверен на уникальность в своём файле | грепом: 4 якоря с токенами в разделе | | **Вес `PD-215` не пере-взвешен:** статус я поставила `fixed`, а ячейка веса осталась `info`, хотя проза строки доказывает `minor` — ряд 394 просил именно пере-взвешивания | моя | вес `minor`, с пометкой, что было `info` и где довод | ячейка в реестре | **Две находки оркестратор взял себе строками трекера, и обе — честные ограничения моей работы:** - **Харнесс засчитывает ВЫЖИВШЕЙ мутацию, чей шаблон `-run` не выбрал ни одного теста** (`verdict()`: `code == 0 → SURVIVED` безусловно). Направление безопасное — лишняя «дыра», не лишняя «поимка», — и числа этого пака не испорчены (все якоря на месте, записей с несуществующим пином нет). Седьмой формы «шаблон пуст» в моём фикстурном каталоге нет, и это дыра в пине прибора, а не в предмете. - **Фикстура капа держит трату НУЛЁМ** (`Spend: usd(0)` во всех четырёх местах) ⇒ утверждение «холд вернулся целиком» измерено на нулевой трате, а отчитывается за класс. «Кап на прогоне, который УЖЕ потратил» идёт другой ветвью расчёта и не измерен ничем. Это ровно норма «фикстура держит что-то постоянным — пин отчитывается за класс, а меряет срез», и назвать её честнее, чем чинить в последний час. **Одна находка СНЯТА разбором, и снята не в мою пользу, а в пользу точности заказа:** линза увидела «противоречие пака о `service_error`» — один мой пин называет это слово ложью о деплое, другой сам его выставляет. Противоречия нет: это два РАЗНЫХ субъекта на одной оси «стоит ли повторять». Убитый нашим грейсом обязан читаться `interrupted` (там `service_error` солгал бы); исчерпавший кап обязан читаться `service_error` (там `interrupted` обещал бы ретрай сразу после двадцати ретраев). Неточной была ФОРМУЛИРОВКА ЗАКАЗА — «исчерпание не должно выглядеть как авария» смешивает «выглядит поломкой» с «отвечает на вопрос о ретрае»; оркестратор записал это исправлением посылки промта, а не находкой к работе. **Числа после круга приёмки, пере-снятые на свежей копии** (каталог вырос на две записи под новый классовый гейт): ``` итог: записей 26 · поймано 25 · ВЫЖИЛО 1 · НЕ ИЗМЕРЕНО 0 ``` Выжившая — та же и единственная (`S1`, пояс без населения). Обе новые записи краснеют названным пином: `G1` — канон перестаёт обещать всегда-посылаемый член; `G2` — провод шлёт член под другим именем. **Границы приёмки, которые идут в акт границами, а не зеленью** (его замер, привожу, потому что они ограничивают и мои утверждения): слепая линза `make check` не гоняла и прогнала 6 записей каталога из 24 — её «поймано 5» есть утверждение о её выборке; полную кампанию снял оркестратор; линза вне карты не исполняла ничего вовсе; **рантайм живым демоном не проверял никто, поэтому всё про «что увидит оператор» остаётся PLAUSIBLE, а не CONFIRMED.** ## ЗАПИСКА-ПЛАН 17.09 — ПРОГОН НЕ ВРЁТ О СЕБЕ (ряды 398 · 394 · 396 · 399 · 400 · 416 · 486) Пак `docs/PLATFORM_RUN_TELLS_THE_TRUTH_SESSION_PROMPT.md`, сессия `textmachine-main-78`, выдача №15 очереди №23. НЕ коммичу, дерево передаю оркестратору. Это записка ДО первой правки кода (блок 7 промта); задним числом её не переписываю — если решение переменится, это будет видно ниже в отчёте. ### Рубеж входа — снят ДО правок, на чистой зоне `platform/` в дереве был чист (`git status --porcelain -- platform` → 0 путей), поэтому базовый прогон и есть рубеж самого `HEAD` — развёртка `git archive` не понадобилась. Команда и вердикт: ``` ~/tm-78-stand/battery.sh > ~/tm-78-stand/battery-baseline.log # make check из platform/, гейты поднятыы MAKE_EXIT=0 · пакетов в go list 20 · вердиктов в логе 20 · comm -23 пуст · 0 FAIL · 6 скипов ``` Гейты подняты четыре из четырёх, а переменных условий прибор печатает ШЕСТЬ: живой стенд Postgres (18.4, сокет `/tmp`, порт 55433 — живость спрошена ТЕМ ЖЕ DSN, `select 1` → `1|18.4`, не TCP-пробой), движковый бинарь, собранный из ТЕКУЩЕГО `backend/` (`BUILD_EXIT=0`, несмотря на чужую незакоммиченную работу на 28 путях), шаблон книги с АБСОЛЮТНЫМИ `pipeline:`/`models:`, плюс `pg_dump`/`pg_restore` из `~/.local/pgsql/bin`. Шесть скипов: пять живых движковых проб (артефакта контраста `backend/configs/mining-contrast.zh.txt` на хосте нет — строка 251, контроль: файлов в `backend/configs` 7) и один оракул `TM_PLATFORM_TEST_BANK_READOUT` (артефакт прошлого пака, у меня его нет). ⚠ **Две поправки к носителю зоны, снятые этим прогоном** (правлю `STACK_DECISIONS.md` в конце пака): - «Гейтов батареи ЧЕТЫРЕ» — прибор `make conditions` печатает **шесть** переменных: сверх четырёх прибавились `TM_PLATFORM_TEST_PGDUMP`, `TM_PLATFORM_TEST_PGRESTORE` и `TM_PLATFORM_TEST_BANK_READOUT`. - «Число `0 FAIL` СНЯТО, замер 11.09 его опроверг» — **на сегодняшнем `HEAD` оно воспроизводится**: 0 FAIL при поднятых гейтах. Предупреждение носителя («получишь красное и решишь, что сломала сама») на 17.09 неверно, и следующая смена не должна искать у себя поломку, которой нет. - Тулчейн пину удовлетворяет: `golangci-lint 2.12.2` · `sqlc v1.31.1` (лежит в `~/go/bin`, НЕ на PATH — ловушка подтвердилась, `export PATH=$PATH:$(go env GOPATH)/bin` в скрипте батареи) · Go 1.26.7 ≥ 1.26.6. ### Граница класса: «наша политика» против «авария» — как я её провожу Это главный вопрос пака, и он не про слово на проводе, а про ОСЬ. - **Наша политика** — конец, который выбрала сама платформа или человек через нашу дверь: наш грейс (`TimeoutStopSec` → маркер `timeout/killed/KILL`), исчерпание капа попыток (§4.1), терминальный вердикт оператора (`run abandon`), стоп пользователя. - **Авария** — конец, который деплой или хост НАВЯЗАЛ и никто не выбирал: `oom-kill`, `core-dump`, `watchdog`, `resources`, `protocol`, `exec-condition`, нечитаемый маркер. - **Провод этой оси не несёт и, я утверждаю, не должен.** `RunFailureReason` — три значения, и канон прямо называет их ось: «Why a run ended in `failed` … the one a client decides a retry from», а комментарий самого констрейнта (`00016_read_surface.sql:58`) добавляет: «a fourth value that answered that question the same way would only be a fourth phrase to write». - **Ось «кто закончил» живёт НИЖЕ провода — в `run_attempts.exit_result`**, и это словарь ПЛАТФОРМЫ: колонка объявлена `add column exit_result text` (`00009_runner.sql:57`) БЕЗ констрейнта, канон её не знает, значит новое слово в ней не требует ни бампа минора, ни миграции. Оператору она и адресована. ### Решения по семи предметам **398 — кап попыток. ДЕЛАЮ.** Кап на НОМЕРЕ попытки прогона (`run_attempts.attempt_no`, инкремент `pgstore/runs.go:1568`), а не на счётчике внутри попытки: счётчик отсрочек живёт на попытке (`:404`, очистка `:443`), рестарт открывает новую строку, и кап на нём повторил бы дефект, выглядя вылеченным. Пере-снял довод ряда сам: `MaxAttempts` в не-тестовом коде платформы — **7 хитов, все в `internal/jobs/jobs.go`, все про очередь** (контроль: слово `restart` в `internal/runs/reconcile.go` — 47 хитов), про попытки ПРОГОНА нет ничего. Форма: настройка деплоя с дефолтом, печатаемая на буте, как все прочие; проверка — в `restart`, ПОСЛЕ расчёта старой попытки и ВМЕСТО открытия новой (иначе холд останется открытым, и лечение петли само станет денежной утечкой); конец — через тот же `finish`, которым закрываются потолок и банк-стоп, со СВОИМ словом в `exit_result`. Видимость ДО упора — операторский листинг (`tmplatformctl runs` уже печатает колонку ATTEMPT). ⚠ `Resume` капом НЕ гейчу: человек, нажимающий кнопку, — не петля, а бюджет его собственный. **394 — своя пометка убийства по грейсу. ПЕРЕ-РЕШАЮ, доводом, и предъявлю формой оркестратору.** Половина ряда вылечена 11.09 (в аварийную причину уходят ровно шесть значений, `reconcile.go:1180`; нашего `timeout` среди них нет, `:1183` отдаёт `interrupted`; стерегут два пина `gracekill_test.go:21` и `:62`). Остаток ряда — «у класса нет СВОЕЙ пометки». Замер: **пометка ЕСТЬ и лежит в `run_attempts.exit_result`** (писатель `internal/pgstore/sink.go:580`, значение — `$SERVICE_RESULT`, для нашего грейса это `timeout`), плюс строка лога на финише несёт `result`. ⇒ Четвёртого значения на проводе НЕ предлагаю: оно отвечало бы на вопрос «стоит ли повтор» РОВНО так же, как `interrupted`, и нарушило бы ось словаря. Строю операторскую видимость того, что уже записано. ⭐ **И называю инстанс класса, которого в промте нет:** `AbandonRun` (`internal/pgstore/runs.go`) пишет прогону `failure_reason = 'service_error'` — то есть терминальный вердикт ОПЕРАТОРА уже сегодня предъявляется как поломка деплоя. Это ровно предмет ряда 394, одним уровнем выше. **396 — развилка «код остановки без намерения». ПЕРЕ-РЕШАЮ ЯВНО, исход — оставить, с новым условием.** Довод ветки («чтение ручной остановки как прерывания стоит ОДИН рестарт», `reconcile.go:1195-1221`) становится правдой только с капом §4.1 — до него «один рестарт» был неограниченной петлёй. ⇒ Оставляю перезапуск и пиню выбор с ДВУХ сторон: конец формы ребута перезапускается, и перезапуск ограничен капом. ⚠ И называю альтернативу, которую НЕ строю, с ценой: улика перезагрузки на хосте есть и зоной не читается вовсе (`/proc/stat btime` = 2026-09-15 10:57:41 UTC, `boot_id` на месте; хитов `btime` · `boot_id` · `BootID` · `uptime` · `InvocationID` в `platform/` — по нулю, контроль прибора ниже в отчёте) — она различила бы ребут от ручного `systemctl --user stop` ФАКТОМ, а не асимметрией. Не строю потому, что ошибка в сторону «ребута не было» возвращает дефект строки 138 на ВЕСЬ хост (перезапуск пользовательского менеджера мимо ребута читается как ручной стоп), а выигрыш — та же ограниченность, которую уже даёт кап. Кандидат на отдельную строку, а не хвост этого пака. **399 — слово покупателю. ФОРМА → ПИНГ, кода до ратификации не будет.** Операторские носители парковки (четыре) не трогаю. ⭐ Уточняю сам дефект замером: экран покупателя НЕ «замерший» — он ЛЖЁТ. У запаркованной попытки ремонтный канал продолжает обновлять `eta_seconds` и метку свежести (`internal/pgstore/sink.go`, `ApplyStatus`), а полоса выведена из `chapters`, которых этот канал не пишет ⇒ **ETA идёт, полоса стоит**, и `eta_seconds` — обязательное поле канона (`required: [done, total, stage, eta_seconds]`). Движущаяся оценка над стоящей полосой читается как «работа идёт» сильнее, чем честно замерший экран. Формулировка для покупателя обязана уважать оговорку кода (`reconcile.go:791`): «чужак» может быть ЭТИМ ЖЕ прогоном — значит текст не вправе утверждать чужое вмешательство. Фикстуру на этот случай сделаю специально. **400 — смерть между правкой банка и записью о ней. ДЕЛАЮ.** Ветка ошибки стора обработана (`internal/runs/bank.go:221-229`), а смерть ПРОЦЕССА между возвратом глагола и `RecordBankMove` канала не имеет: у факта один писатель и ни свипа, ни второго читателя. Форма: намерение ВПЕРЁД — отметка «на этой книге идёт правка банка», поставленная ДО спавна глагола и снимаемая вместе с записью факта; читатель факта (`book.BankMoved`) считает неснятое намерение движением банка. Направление консервативное, и цена уже ратифицирована в дереве: «the stale fact costs one harmless `--resnapshot` on the next run, never a dead one» (`reconcile.go:995-1002`). ⚠ Мина, которую при этом обязан не пропустить: `ClearBankMove` должен снимать ОБЕ отметки, иначе флаг станет вечным. **416 — согласие. БЕРУ В СУЖЕНИИ (провенанс и видимость), величину не строю.** Величины для ограничения у платформы в точке решения нет по построению, и код это уже записал (`reconcile.go:1600`); стор расширяет согласие только вверх (`pgstore/runs.go:1605`, довод `:1483`). ⇒ моя половина: (а) молчаливое расширение перестаёт быть молчаливым — просьба о МЕНЬШЕМ согласии, чем стоящее, становится названной, а не съеденной `greatest()`; (б) провенанс суммы — контракт-видимая часть, идёт формой оркестратору. Движковую половину (проекция пере-покупки) называю строкой-заказом следующему паку. **486 — харнесс мутаций. ДЕЛАЮ, и беру шире ряда.** Ряд просит два пункта; половина второго в инструменте УЖЕ есть — ненулевой выход без строки `--- FAIL` считается НЕ ИЗМЕРЕННЫМ (`tools/mutate.py:78-84`), и это надо не строить, а не сломать. Остаётся: зелёный БАЗОВЫЙ прогон пакета до первой посадки; засчёт только по падению НАЗВАННОГО пина (новое поле каталога). Сверх ряда беру два, которых в нём нет: **фикстурный каталог В ДЕРЕВЕ** (иначе новое поле не на чем пере-проверить — посадок в репозитории нет ни одной) и **`-race`**, потому что батарея гоняет с гонкой, а инструмент без неё (`tools/mutate.py:51`) ⇒ мутация, которую ловит только гонка, прочитается как выжившая. И пин на сам харнесс: посадка, ломающая критерий засчёта, обязана краснеть. ### Порядок работы и чем буду судить Порядок по цене ошибки: 398 → 400 → 486 (он стережёт мои же числа) → операторская видимость 394 → пин 396 → 416 → формы 399/416 пингом. Оси ревью беру промтовы: деньги (ни один путь не берёт холд, который некому вернуть) · правда о состоянии · возобновление без мусора. Числа пере-снимаю ПОСЛЕ последней правки; у каждого — команда; рядом с каждым нулём — контрольная величина. ## ОТЧЁТ 17.09 — ЧЕЛОВЕКУ ПЕРЕСТАЛИ ПОКАЗЫВАТЬ ПУСТОЙ ЭКРАН (ряды 224 · 253, контрактный минор 0.16.0) Пак `docs/PLATFORM_BANK_READOUT_SESSION_PROMPT.md` (180c41e), сессия `textmachine-main-94`. НЕ коммичу, дерево передаю оркестратору. Записка-план — ниже этим же блоком, она писалась ДО первой правки кода и намеренно не переписана задним числом: два её решения я потом ПЕРЕМЕНИЛА, и это видно. ### 1. Числа ДО и ПОСЛЕ — командой, на обоих купленных файлах Собственный контроль ряда **224** (он в теле ряда: «`json:"proposed` в `platform/` — 0 хитов»): ``` git grep -l 'json:"proposed' HEAD -- 'platform/**/*.go' → 0 файлов grep -rn 'json:"proposed' platform/ --include=*.go → 1 (internal/ingest/bank.go:184) ``` Собственный контроль ряда **253** («`grep -rn 'consolidat\|unanswered\|batches_dropped' platform/internal/ --include=*.go` → 0»). ⚠ **Пере-снят и на HEAD — там НЕ 0, а 3**, и это первая мелкая находка отчёта: ``` git grep -l '…' HEAD -- 'platform/internal/**/*.go' → 3 файла (manifest.go · pricing.go · sweep_test.go) grep -rln '…' platform/internal/ --include=*.go → 10 файлов, из них 7 мои контроль, что прибор читал населённое дерево: 194 файла при HEAD, 198 сейчас ``` Три хита на HEAD — прозаические упоминания в комментариях, читателей среди них нет, так что СУТЬ ряда верна. Неверно ЧИСЛО, напечатанное рядом с нулём: команда ряда даёт 3, а не 0. Класс знакомый — контроль у нуля обязан быть воспроизводимым той командой, которая рядом написана. Исполнением, на самих купленных проекциях (оракул `TestARealReadOutIsReadTheWayThisBuildClaims`, копии на чтение, `sha256` совпал, `mtime` оригиналов не тронут): ``` прогон A: boundary=signature_requested offered=69 terms=0 consolidation=true прогон B: boundary=signature_requested offered=66 terms=0 consolidation=true ``` **Было 0, стало 69 и 66.** `terms=0` осталось нулём — и это правда движка на стопе, а не дефект. ### 2. Что построено | Слой | Что | Файл | |---|---|---| | Ридер | две секции + якорь свежести (`as_of`, `run_id`), которого ридер не читал вовсе | `internal/ingest/bank.go` | | Хранилище | миграция `00036`: `bank_read_out` (граница · идентичность прогона · ДВА флага наличия) + `bank_offered_terms` | `internal/pgstore/migrations/00036_bank_read_out.sql` | | Шов | `SaveBank` берёт ВЕСЬ развёрток, а не его строки; одна транзакция | `internal/pgstore/readmodel.go`, `internal/readmodel/readmodel.go` | | Провод | ресурс `GET /books/{bookId}/bank/signing-stop` + агрегат `BankPage.consolidation` | `internal/httpapi/{v0,reading,project,capabilities}.go` | | Контракт | минор **0.16.0** + компаньон: §2.27 (провенанс минора) и §2.26-бис (две идиомы «может быть null», по наблюдению сплошного аудита оркестратора, ряд 480). ⚠ испр. 17.09: здесь стояла ОДНА секция вместо двух | `docs/architecture/14-api-contract/{openapi.yaml,README.md}` | ### 3. Исход КАЖДОГО пункта заказа ⚠ Сверено механически, а не глазами: заголовки пака (`^## N.` и `^### N.N.`) против строк этой таблицы. Заказов с §4 и дальше — **14**, строк здесь **15** (у §4.1 и §4.3 по две: у каждого две разные обязанности). ⚠ Первую редакцию таблицы поймала приёмка: в ней не было строки под §9, и тем же прибором нашлись ещё две — §11 и §12. Довод «пункт исполнен, не хватает лишь исхода в таблице» верен, но таблица, объявляющая себя полной по КАЖДОМУ пункту, и есть носитель этой полноты: три пропуска в ней — это три места, где читатель не смог бы отличить «сделано» от «не рассматривалось». | Пункт пака | Исход | |---|---| | §4.1 прочитать `proposed` и `consolidation`, поля снять ЗАМЕРОМ | **сделано**, и замер дал находку — см. §4 | | §4.1 не потерять `invented` | **сделано**; пин `TestTheClassAPersonReadsFirstSurvivesTheReadOut`, и поле доезжает до провода | | §4.2 перенести КЛАСС гарантий ридера | **сделано**: «секции нет» ≠ «секция пуста» (указатели на всех трёх ярусах), «поля нет» ≠ «ноль» (четыре числа — указатели), граница читается ВМЕСТЕ с секцией и является УСЛОВИЕМ выдачи | | §4.3 контрактный минор, форму решаю сама | **сделано**, форма ПЕРЕМЕНЕНА относительно записки — довод в §5 | | §4.3 развилка идентичности предложения | **решено без пинга**: контракт уже отвечает — см. §5 | | §4.4 хранилище назвать явно | **в БД**, миграция часть пака; довод и доказательство, что дельта и ревизия не ломаются — §6 | | §4.5 не строить UI, не трогать движок, не подпирать заплаткой | **соблюдено**; рефакторинг — §8 | | §5 самопроверка исполнением + адверсариальный проход | **сделано**: гейт зоны целиком, мутационная кампания, один старший советчик, чьи посылки я проверила и одну опровергла | | §6 три оси ревью | **§9** | | §7 купленное сырьё только на чтение | **соблюдено**: работала с копиями, база рядом с B не открывалась вовсе | | §8 записка-план до кода | **сделано**, лежит ниже | | §9 эхо-протокол старта: десять строк своими словами ДО работы | **сделано первым действием** — блок в `/tmp/textmachine-channel` и письмо оркестратору до первой правки: скоуп, инварианты, чего не делаю | | §10 что не удалось и где прибор слеп | **§10** | | §11 канал вопросов и право отказа | **использован**: восемь писем оркестратору по ходу пака, два из них — улики, менявшие работу (состав полей, смена формы). Правом отказа не пользовалась: предмета для «этого делать не надо» не нашлось | | §12 критерий завершённости | **§12**, по пунктам | ### 4. ⚠ ГЛАВНАЯ НАХОДКА ЗАМЕРА: состав полей в промте был неверен, и это не мелочь Промт давал контроль «восемь обязательных (`src dst kind channel freq spread conf variants`) и четыре опциональных (`conventions invented contradicts bank_holds`)». Снято с писателя (`backend/internal/pipeline/bankexport.go:125-160`) и с обоих файлов: **счёт 8+4 верен, состав — нет.** Обязательность определяется наличием `omitempty` у писателя, и по нему обязательны `src dst kind channel freq spread` **`conventions`** `conf`, а опциональны `invented contradicts bank_holds` **`variants`**. **И объяснение «в A поля нет ни у одной строки» через опциональность — ЛОЖНОЕ.** У `conventions` нет `omitempty`, значит сериализатор опустить его не мог ⇒ **прогон A написан сборкой движка до `0997e41` (11.09), добавившего поле.** Доказательство построением, а не совпадением двух снимков. Следствие несущее и оно в коде: **у обязательного числового поля «поля нет» ≠ «ноль»**, потому что ноль у каждого из четырёх — собственный ответ движка. `freq: 0` — «поверхность видела только черновая сторона» (`terminology.Candidate.Freq`); `conf: 0` — «роль сказала 0 %», по её же комментарию самая важная строка листа; отрицательный `conf` — «роль не сказала ничего». Сборка, читающая отсутствие нулём, показала бы человеку 69 терминов, чьи варианты «единогласны», на листе, весь смысл которого — разногласие. ### 5. Две развилки, где я ПЕРЕДУМАЛА после записки — с доводами **(а) Форма. Записка обещала обе секции на `BankPage`. Довод записки оказался ложным по коду.** Я писала «обе описывают ОДИН момент стопа»; это опроверг мой советчик, и я проверила деревом: `r.lastTerminology` ставится в `backend/internal/pipeline/mining.go:171` — ДО развилки «стоп/не стоп» — и живёт в Runner до конца процесса ⇒ секция полноты едет на ТРЁХ границах из пяти, а секция вопросов — ровно на одной («Empty at every boundary that is not a stop»). Оси свежести разные уже у движка. ⇒ **полнота — агрегат `BankPage`** (буква ряда 253: «поле на `BankPage`», и по сути это свойство банка), **вопросы стопа — свой ресурс**, потому что они появляются и исчезают вместе с ПРОГОНОМ, пока ревизия банка не двигается. ⚠ Проверка советчика поймала его перегиб: он утверждал, что третий член «только первой страницы» требует правки шапки спеки про «Two exceptions». Шапка называет КЛАСС («the aggregates of `BankPage`»), а не перечень ⇒ новый агрегат входит в существующее исключение, шапка не тронута. **(б) Имя. Записка обещала `suggestions`. Отвергла по существу.** «Suggestion» — то, что можно проигнорировать, а здесь **игнор есть ПРИНЯТИЕ**. ⚠ Адрес довода испр. 17.09 приёмкой, и дважды: я писала `mining.go:281`, настоящая строка — `:280` (внутри многострочного `WarnContext`), а **сильнейший носитель фразы вообще не там** — `backend/cmd/tmctl/render.go:225-227`: «whatever you leave undecided still goes to the model, as part of the same law block as the terms you signed (D39.104: the bank on the wire is law for every row)». Это операторский ВЫВОД — текст, который человек читает в момент подписи, — и он ссылается на ратифицированное `D39.104`, то есть довод от поправки стал крепче, а не слабее. ⚠ И поправка к поправке: приёмка заявила «`leave undecided` в `mining.go` — 0 хитов»; пере-снято мной — **1 хит** (`:280`; контроль: `bank` в том же файле — 114). Носителей фразы в движке три. Взято слово, которым канон УЖЕ называет этот предмет: `BankSignatureCount` описан как «the count against the surfaces the LAST signing stop **offered**» ⇒ `offered[]` / `OfferedTerm` / `readBankSigningStop`. Слово `proposed` наружу не идёт ВОВСЕ, третьего смысла нет, существующий `TermStatus.proposed` не тронут. **⭐ И там же нашлась ратифицированная ФОРМА моей гарантии — я её не изобретала.** У `BankSignatureCount` есть `unreadable` с ровно моим обоснованием: «`true` — the two numbers mean NOTHING… Without this flag, „could not count“ would be byte-identical to „nothing left undecided“ — the one thing this schema must never say by accident». Взяла буквой. `unreadable: true` истинен по трём причинам сразу (материализации ещё не было · документ не несёт секции · документ снят на границе, которая её нести не может) — они не разделяются, потому что лекарство одно. **Вот где граница у меня НЕСУЩАЯ, а не прочитанная и выброшенная:** секция, стоящая на границе, которая её не несёт, — документ, который эта сборка не понимает, и отдать его строки значило бы нарисовать стоп, которого не держит ни один прогон. Это условие выдачи, и его стережёт посадка. ### 6. Идентичность предложения — закрыто контрактом, пинга не потребовалось `id` нет ни в одном из **135** объектов; выдумывать запрещено. Развилка разбивается надвое, и обе половины закрываются без выдумки: - **чтение:** дельта над списком не выразима ПО ПОСТРОЕНИЮ (нечего называть), ресурс отдаётся целиком, без курсора. Написано прозой операции, а не подразумевается; - **действие:** контракт УЖЕ говорит, чем адресуется поверхность, которой в банке нет — кортежная форма `BankCorrection`: «for a surface the bank does not list, send `sense: ""` and `null` windows». **Дельта и ревизия не ломаются, и вот чем это предъявлено.** Строки стопа лежат ОТДЕЛЬНОЙ таблицей, заменяемой целиком, и в машинерию `bank_reset_revision` не входят. Втянуть их туда было бы дефектом: без идентичности КАЖДАЯ материализация читалась бы как «строки исчезли» и держала бы книгу в вечном сбросе ревизии. Пин `TestTheStopsRankingIsTheOrderThatComesBackAndTheListIsReplacedWhole`; вся прежняя батарея `internal/pgstore` (включая тесты ревизии и дельта-чтения) зелёная. ### 7. Гейт зоны — и КРАСНЫЙ ПАКЕТ, который оказался не моим `make check` = `build vet fmt lint sqlc-check` + батарея. Прогонов было пять, и стоит назвать все, потому что три из них учат разному. **Прогон 1 — `MAKE_EXIT=2`, а уведомление харнесса сказало «exit code 0».** Гейт умер на `tools-check`: `sqlc` лежал в `~/go/bin` вне `PATH`. Первая цель, упавшая первой, отменила все последующие; ни одного теста прогнано не было. Код обёртки — не код работы, и вердикт я читаю строкой `MAKE_EXIT` из лога. **Прогон 2 — ЗЕЛЁНЫЙ ЦЕЛИКОМ, и полнота сверена СПИСКОМ, а не отсутствием слова FAIL:** ``` MAKE_EXIT=0 · линтер 0 issues · sqlc diff чист · FAIL 0 go list ./... → 20 · вердиктов → 20 · comm -23 список вердикты → 0 строк контроль, что сравнение умеет говорить: убрать один вердикт → 1 строка ``` **Прогоны 3 и 4 — красные, и оба единственным красным дали `internal/books`, пакет, которого мой диф не касается.** Прогон 3: `pgstore: commit: timeout: context deadline exceeded` за 145 с. Прогон 4: паника таймаута 600 с, тест висел 7 м 26 с. Разные тесты, один пакет. ⛔ **Флейком по цвету я это не назвала — проверила цепь ЗВЕНЬЯМИ.** (1) Текст падения сам называет звено: коммит перерос свой срез. (2) Фикстуры `internal/books` НАМЕРЕННО сжимают 220-секундный продуктовый бюджет до 700 мс и 300 мс и делают внутри настоящий коммит в Postgres — иначе разрез не удержал бы ни одного среза. (3) Звено, через которое могла бы войти МОЯ правка, — длительность установки схемы; пере-снято: лишняя миграция стоит ≈0.12 с на прогон, и она лежит ВНЕ измеряемого окна. (4) В том же логе все пакеты шли примерно в 2.4 раза дольше обычного — на машине работала чужая мутационная кампания. ⭐ **РЕШАЮЩИЙ ЗАМЕР снят правильным прибором — ДВУМЯ полными батареями ОДНОВРЕМЕННО, а не по очереди.** Развёрнут `git archive HEAD` (35 миграций, ни строки пака) и запущен в одну секунду с батареей пака, под одной и той же чужой нагрузкой: | дерево | вердикт | что упало | |---|---|---| | чистый `HEAD` | FAIL `internal/books` 201 с | `TestAnIntakeStoppedByTheCap…` (4.48 с) · `TestTheCutOfAnUploadIsBoundedByTheWalk…` (2.81 с) | | дерево пака | FAIL `internal/books` 600 с | таймаут на `TestAnUploadThatRunsOutOfBudgetWaitingForASlot…` | **Красны ОБА, в одном пакете, на РАЗНЫХ тестах** ⇒ краснота принадлежит ПАКЕТУ, а не правке, и разные тесты при каждом падении — подпись голодания планировщика, а не логического дефекта. ⚠ Последовательным прогоном этот вывод не снимался бы: условия между двумя прогонами не совпадают, и сравнение вышло бы о разной нагрузке. Изолированно пакет целиком зелен в ОБОИХ деревьях даже при load average 11 — губит его не нагрузка сама по себе, а нагрузка ПЛЮС собственная батарея зоны под `-race`. Заведено **PD-469**, родня — **PD-420** того же класса. Не чиню: не мой предмет и не мой заказ. **Прогон 5 — ФИНАЛЬНЫЙ, на дереве, которое я передаю, с поднятой переменной оракула: ЗЕЛЁНЫЙ ЦЕЛИКОМ.** Гейт, снятый ДО двух последних правок (пина на владение и строк регистра), я за финальный не выдавала — именно за это канон и бьёт, — и дождалась окна, когда чужая кампания отпустила процессор (load 3.18). ``` MAKE_EXIT=0 · линтер 0 issues · sqlc diff чист · FAIL 0 · скипов 9 go list ./... → 20 · вердиктов → 20 · comm -23 список вердикты → 0 строк контроль перевёрнутого сравнения: убрать один вердикт → 1 строка ``` ⚠ **Что оракул РЕАЛЬНО бежал внутри батареи, доказано разностью, а не словом:** скипов было 10 в прогоне 2 и 9 в прогоне 5, и единственная разница — `TestARealReadOutIsReadTheWayThisBuildClaims`; ничего нового скипаться не начало (`comm` в обратную сторону — 0 строк). **Условия гейта на этом хосте, названные вместе с числом скипов (стандарт зоны §3.1).** Скипов **10** в зелёном прогоне 2. `TM_PLATFORM_TEST_DSN` был UNSET — **подняла** по рецепту `STACK_DECISIONS.md` (Postgres без root, порт 55433); контроль, что гейт РЕАЛЬНО открыт, а не объявлен открытым: тот же селектор `-run Bank` в `internal/pgstore` даёт **4 SKIP** без переменной и **4 PASS** с ней. НЕ подняты `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (пара): шаблон на машине есть, но его `pipeline:`/`models:` указывают на пути ДРУГОГО хоста (`/home/ubuntu-26/…`). Они открывают 9 из 10 скипов — живые движковые пробы, ни одна не про читатель проекции; `artifacts_test.go`, где живёт мой предмет, этой парой НЕ гейтится. Десятый скип — МОЙ оракул, он гейтится новой переменной `TM_PLATFORM_TEST_BANK_READOUT`, которая выводится в `make conditions` автоматически (перечень условий там ПАРСИТСЯ из исходников, руками его никто не ведёт). ### 8. Что отрефакторено и почему 1. **`SaveBank` принимает `ingest.Bank`, а не `[]ingest.BankTerm`.** Не косметика: развёрток — одна проекция одного момента, и два сохранения дали бы экран, где вопросы этого стопа стоят рядом с банком другого момента. Одна транзакция, одна книжная блокировка. ⚠ **Правка тестов ОБЪЯВЛЯЕТСЯ** (D39.183): **8** вызовов в `internal/pgstore/{readmodel,events}_test.go` (⚠ испр. 17.09 приёмкой: стояло «9» — я посчитала изменённые СТРОКИ диффа, а один вызов занимает две) и фейк в `internal/readmodel/readmodel_test.go` переписаны под новую сигнатуру. Поведение не ослаблено — каждый передаёт тот же список термов внутри `ingest.Bank{Terms: …}`; гарантия никуда не уехала. 2. **`ingest.BoundaryOrUnknown` / `ingest.ChannelOrNone` — гарды закрытых словарей у ШВА записи.** Заведены не для красоты: приёмочный прогон показал, что нулевое значение `ingest.Bank`, которое любой вызывающий вправе построить, нарушает CHECK новой таблицы. Лечение по существу — значение, «заявляющее о себе меньше всего», а не отказ: падать всем сохранением банка из-за ярлыка границы было бы худшей сделкой. 3. **Больше ничего.** Существующий ридер, `ReadBank`, ListBank-курсор и ревизионная машинерия не тронуты: заказ их не менял, а трогать работающее ради стройности — то, что канон запрещает. ### 9. Три оси ревью **Контрактная.** Минор законен (мажор `0`, только добавление: новый путь, четыре схемы, один необязательный агрегат — ни одно существующее поле не сужено и не переименовано). Полнота предъявлена ГЕЙТОМ, а не глазами: `TestEveryMemberTheCanonRequiresIsOnTheWire` читает `required` ИЗ канона и сверяет с реальными байтами ответа в ОБЕ стороны — объявленное и не отданное красит так же, как отданное и не объявленное. Константу версии подняла до `0.16.0`, и её стережёт чужой гейт против самого канона. Компаньон: §2.27 плюс ОБА носителя номера в шапке (список ратификаций и строка про отставание зеркала) — именно на втором предыдущие три бампа спотыкались три раза подряд. **Дисциплина ридера.** Гарантии перенесены, а не процитированы, и каждую стережёт посадка (§11). Одну вещь я перенесла ШИРЕ образца и называю это явно: образец различает «нет» и «пусто» на уровне ДОКУМЕНТА, а у меня то же различение стоит и на уровне ПОЛЯ — потому что замер показал, что именно там оно и ломается. **Человеческий узел.** Чего человеку для решения НЕ хватает, хотя я этого не строю: 1. **`variants` — склеенные ярлыки** (`"третий оборот ×1"`, с алиасом `"… (proposed for X)"`). Части не опубликованы, разбирать нельзя (парсер живёт рядом с писателем не случайно) ⇒ клиент не может отрисовать это на языке читателя. Движковый долг, оркестратор завёл рядом **479**. 2. **Пустой `dst` неотличим по причине.** На печатном листе движка тот же символ несёт четыре факта, и один из них — `SettledByBank`, «решать нечего», — движок намеренно в проекцию не кладёт (ряд **353**). Контроль, который у меня это ловит: пустых `dst` — **0 и 0**, `never_asked` — **0 и 0**, сходится; класс реален, но на купленном сырье не проявился. ⚠ **Гейт ряда 353 сработал: читатель у секции появился**, и там же висит долг «у `SettledByBank` нет свидетеля в списке полей, которых сайдкар намеренно НЕ несёт». Это движковая зона — пинг, не правка у себя. 3. **Ни одной подсказки, СКОЛЬКО осталось.** Счёт живёт только на квитанции правок, и канон сам называет это «named narrowness». Мой ресурс его не дублирует намеренно: популяции совпали на обоих прогонах (69/69 и 66/66, разность множеств 0 в обе стороны), но механизм расхождения реален — реверс-секция капается (`mining.go:143-147`), а при выключенной роли её нет вовсе ⇒ равенство в контракте не обещано и «N из M» между ними запрещено прозой. ### 10. Что не удалось — и где прибор слеп - ⛔ **`contradicts` и `bank_holds` — 0 из 135 объектов.** Поля объявлены и доезжают, но проверены только синтетикой ЭТОЙ ЖЕ сборки, то есть пин спрашивает «декодер согласен сам с собой», а не «согласен с движком». Не чинила: лечение — не код, а УЛИКА (прогон, где конфликт случился). Строка **PD-468**. - ⛔ **`dst: ""` и отрицательный `conf` не встретились ни разу** (0 из 135). Ветки построены и запинены синтетикой; на живых данных не наблюдались. Закрывается той же уликой. - ⛔ **Якорь свежести хранится целиком и не сверяется.** Ресурс обещает ПОСЛЕДНИЙ стоп, а не текущий, и прозой это сказано — но клиент, не прочитавший `Run.status`, покажет вопросы ушедшего прогона. Сравнение построить можно и дёшево, но материализация идёт по долгу КНИГИ и прогона в руках не держит. Не строила намеренно: расширять обещание без механизма — взять форму якоря без гарантии. Строка **PD-467**. - ⛔ **Два гейта зоны на этом хосте не подняты** (движковый бинарь + шаблон книги): 9 скипов из 10. Ни один не про мой предмет, но сказать «батарея без скипов» нельзя, и я этого не говорю. - ⛔ **`unreadable` схлопывает три причины.** Оператор по нему не отличит «старая сборка движка» от «ещё не материализовали». Сделано сознательно (лекарство одно, и контракт служит клиенту), но диагностика беднее, чем могла бы быть. - ⛔ **Пятая граница, `redrive/re-seeded`, живого носителя в моих уликах не имеет** — обе проекции сняты на стопе. Перевод всех пяти проверен синтетикой против ГРЕПА по вызовам движка, а не против пяти живых документов. ### 11. Мутационная кампания — посадки и их вердикты Копия дерева ВМЕСТЕ с каноном контракта (`cp -a --parents platform docs/architecture/14-api-contract`, норма `D39.113`/`PD-395`), один мутатор на копию, копия защищена ПОСТРОЕНИЕМ: харнесс отказывается работать, если по корню нет `platform/go.mod` и канона, или если корень внутри рабочего дерева. Базовые прогоны всех четырёх пакетов ЗЕЛЁНЫЕ до первой посадки — без этого вердикт был бы о них. Засчитывается по ТЕКСТУ падения: харнесс требует, чтобы упал ИМЕННО тот пин, который обязан назвать посадку, и выдаёт отдельный исход «не измерена» на посадке, которая не собралась. | Посадка | Что ломает | Вердикт | |---|---|---| | A | отсутствующая секция читается как пустая | RED · `…SectionTheDocumentDoesNotCarryIsNotASectionThatIsEmpty` | | B | отсутствующее `conventions` читается нулём | RED · `…MandatoryNumberTheReadOutDoesNotCarryIsNotZero` | | C | неназываемая граница читается как СТОП | RED · `…BoundaryIsTranslatedAndAnUnnameableOneIsNeverTheStop` | | D | неназываемый детектор читается как «подтверждено обоими» | RED · `…DetectorVocabularyDoesNotReachAClient` | | E | отсутствующая полнота читается как обнулённая | RED · `…SectionTheDocumentDoesNotCarryIsNotASectionThatIsEmpty` | | F | `invented` теряется | RED · `…ClassAPersonReadsFirstSurvivesTheReadOut` | | G | граница перестаёт быть УСЛОВИЕМ выдачи | RED · `…TermsStandingAtABoundaryThatCannotCarryThemAreNotServed` | | H | «развёртка нет» читается как читаемый стоп | RED · `…ThreeWaysAStopCanHaveNoQuestionsAreNotOneAnswer` | | I | наличие секции выводится из числа строк | RED · `…ThreeWaysAStopCanHaveNoQuestionsAreNotOneAnswer` | | J | неизмеренная полнота отдаётся | RED · `…CompletenessIsAbsentUntilSomethingMeasuresIt…` | | K | ранжирование заменено сортировкой | RED · `…StopsRankingIsTheOrderThatComesBackAndTheListIsReplacedWhole` | | L | прежний список не очищается | RED · тот же пин (+1) | | M | `null`-число исчезает с провода (`omitempty`) | RED · `…EveryMemberTheCanonRequiresIsOnTheWire` | | N | неизмеренная полнота проецируется обнулённой | RED · `…CompletenessRidesTheFirstPageAndItsAbsenceIsNotWholeness` | | O | через шов едут только строки | RED · `…WholeReadOutCrossesTheSeamAndNotOnlyItsRows` | | P | пустой список уходит как `null` | RED · `…MeasurementNobodyTookReachesTheClientAsNullAndNotAsZero` | | Q | проверка владения снята | RED · `TestAnotherUsersStopIsNotReadable` | **17 посадок — 17 RED, выживших 0, неизмеренных 0.** ⭐ **И ОТДЕЛЬНО — про сам ОРАКУЛ, потому что «скип превратился в PASS» доказывает, что тест ИДЁТ, а не что он ДЕРЖИТ.** В кампании оракул скипался (переменной у харнесса не было), то есть все посадки поймала СИНТЕТИКА. Спросила его посадкой отдельно: посадка **B** (отсутствующее `conventions` читается нулём) при поднятой переменной даёт `--- FAIL: TestARealReadOutIsReadTheWayThisBuildClaims` с топичным текстом «row 0: the document does not state `conventions` and the reader reported it stated», а при снятой — `--- SKIP`. ⇒ **эта посадка поймана ДВУМЯ независимыми приборами** — синтетическим пином и оракулом холодного прогона на настоящем купленном документе, — значит сломать чтение отсутствующего поля и получить зелень нельзя даже мимо синтетики. ⚠ Посадка Q прогнана ОТДЕЛЬНО и на пере-снятой копии: пин владения написан после первой кампании, и копия его не несла. Контроль чистоты после кампании — не `diff -rq` (он говорит о дереве, а не о числах), а вопрос «на месте ли ЦЕЛЬ каждой правки»: **16 целей проверено, пропавших 0.** ### 10-бис. КРУГ ДОФИКСА ПО ПРИЁМКЕ 17.09 — находка → что сделано → чем предъявлено | Находка приёмки | Что сделано | Чем предъявлено | |---|---|---| | **F1 (мажор, денежный экран).** Канон объявляет `confidence` как `0..100`, движок шлёт `-1` для «роль не назвала», и вся цепь несла минус на провод — при том что компаньон обещал `null`. Класс: вместо ложного НУЛЯ — ложное ЧИСЛО | **⛔ И это оказался КЛАСС, а не поле — нашёл мой же новый гейт.** Свернула у ШВА (`ingest`), где файл велит переводить все пересечения, и потом гейт показал, что канон бьёт диапазоном ЧЕТЫРЕ числа, а проходили насквозь все четыре | `statedCount`/`statedConfidence`/`inRange` в `ingest/bank.go`; `TestTheEnginesNoConfidenceSentinelNeverReachesAClient` | | **Почему гейт этого не поймал** — `TestEveryMemberTheCanonRequiresIsOnTheWire` читает из канона только `required` и по построению не видит `minimum`/`maximum`: «взять форму образца и не взять гарантию», но у ГЕЙТА | Заведён гейт, читающий из канона ДИАПАЗОНЫ и падающий на любом ранжированном члене, которого ридер не ограничивает — то есть закрыт класс, а не случай | `TestEveryRangeTheCanonDeclaresIsOneThisReaderEnforces`: «ranges read from the canon and exercised: 4»; на момент заведения краснел по ЧЕТЫРЁМ полям | | **F2.** `omitempty` на nil-указателе ОПУСКАЕТ ключ, поэтому обещанный каноном `null` («никто не мерил») недостижим, а комментарий строкой выше утверждал обратное | У члена ДВЕ разных пустоты, и тег умеет одну: на поздней странице — отсутствие, на первой — `null`. Перешла на `json.RawMessage` | `TestCompletenessRidesTheFirstPageAndItsAbsenceIsNotWholeness` | | **F3.** Носителей номера версии в шапке компаньона **четыре**, подняты два — и предупреждение рядом само утверждает, что их два | Поднят четвёртый (счёт отставания зеркала: тринадцать → четырнадцать). `README.md:6` не тронут намеренно: он про РАТИФИКАЦИЮ, её делает акт оркестратора | греп `ЧЕТЫРНАДЦАТЬ`; в тексте правки названо, что носителей четыре, а не два | | **F4.** «Роутер монтирует 19 операций из 21» — минор двинул оба числа | Пере-снято: операций в каноне **22**, маршрутов **20**, без маршрута те же две, что названы в строке | `yaml`-разбор канона против таблицы `contractSurface` | | **F5.** Нового ресурса нет в таблице зависимостей компаньона, которая объявляет себя единственным местом, где это ведётся | Заведены ДВЕ строки — ресурс стопа и агрегат полноты — с носителями 224 и 253 | греп `bank/signing-stop` в README | | **A.** Удалён доккомментарий `DecodeBank`, несший довод гарда («пустой банк и документ, который эта сборка не умеет читать, декодируются одинаково») | Восстановлен дословно. §8 говорил «больше ничего» — удаление под «ничего» не подходило | греп `replace a book's whole bank with nothing` | | **B/C.** Гард закрытого словаря стоит на канале и не стоит на `kind`; и сам гард якобы дублирует `nullif` | ⛔ **Замер опроверг вторую половину и усилил первую:** `nullif` делает ДРУГУЮ работу, и без гарда движковое слово не «ложится как неназванное», а **роняет CHECK и всю запись банка**. Гард добавлен на `kind`, оба запинены | `TestAnEngineWordThatLeakedThisFarIsStoredAsNotNamedRatherThanTakingTheBankDown` — до правки красный именно на `kind` | | **D.** §8 говорит «9 вызовов», их 8 | Испр.: я посчитала изменённые СТРОКИ диффа, а один вызов занимает две | `grep -c 'SaveBank('` → 7 + 1 | | **Семь чисел консолидации** не получили второй половины вывода — условия, при котором он перестанет держаться | Условие названо: семь полей вошли в писателя ОДНИМ коммитом, поэтому «present» сегодня всегда значит «все семь»; в день восьмого числа `unanswered: 0` станет ложным нулём, и они станут указателями | комментарий у `decodeConsolidation` | | **Адрес довода §5(б)** неверен | Испр. трижды: `:281` → `:280` → настоящий носитель `cmd/tmctl/render.go:225-227`. ⚠ И поправка к поправке приёмки: «0 хитов в `mining.go`» — неверно, там **1** | §5(б), контроль напечатан рядом | | **§2 отчёта** называл одну новую секцию компаньона вместо двух | Испр., §2.26-бис назван | таблица §2 | ### 11-бис. Опись дерева — снята ПОСЛЕ последней правки **Двадцать один путь, и все внутри разрешённых паком зон** (`platform/` + `docs/architecture/14-api-contract/`): ``` docs/architecture/14-api-contract/README.md platform/internal/ingest/bank.go docs/architecture/14-api-contract/openapi.yaml platform/internal/pgstore/readmodel.go platform/docs/DEFECT_REGISTER.md platform/internal/pgstore/readmodel_test.go platform/docs/platform-PROGRESS.md platform/internal/pgstore/events_test.go platform/internal/httpapi/capabilities.go platform/internal/pgstore/migrations.sha256 platform/internal/httpapi/project.go platform/internal/readmodel/readmodel.go platform/internal/httpapi/reading.go platform/internal/readmodel/readmodel_test.go platform/internal/httpapi/v0.go platform/internal/httpapi/v0_test.go новые: platform/internal/pgstore/migrations/00036_bank_read_out.sql platform/internal/ingest/bankreadout_test.go platform/internal/pgstore/bankreadout_test.go platform/internal/httpapi/signingstop_test.go ``` ⚠ **В дереве лежит ЧУЖОЕ, и оно не моё — коммитить его нельзя:** 16 путей под `backend/` (пак лестницы попытки, сессия `textmachine-main-12`) плюс `docs/BACKLOG.md` и `docs/experiments/00-provider-quirks.md`. Я их не трогала ни разу. Коммит — только pathspec-формой по списку выше. ⚠ **Двадцать первый путь появился ПОСЛЕ зелёного гейта и назван отдельно** — `platform/docs/STACK_DECISIONS.md`, одна грабля в раздел «Грабли стенда, каждая стоила времени»: TCP-проба стенда отвечает `Connection refused` на ЖИВОМ сервере, потому что рецепт поднимает его с `listen_addresses=''` и слушает он только Unix-сокет. Правка инертна к гейту, и это ЗАМЕРЕНО, а не объявлено: файл цитируется в прозе комментариев 12 раз и не открывается кодом НИ РАЗУ (`grep` по `platform/**/*.go` вне комментариев — 0); исполняемых путей (`.go`/`.sql`) новее лога зелёного прогона — **0**, при контроле «новее предыдущего лога — 1», то есть сравнение умеет говорить. Записано потому, что цена этой грабли уже уплачена приёмкой, а знание, оставшееся в переписке, следующей смене не достаётся. ⚠ **Один побочный эффект, который надо знать при лендинге:** две мои строки в `DEFECT_REGISTER.md` (а теперь три — `PD-467`, `PD-468`, `PD-469`) двигают числа, которые стережёт `docs/scripts/counts.py`, а живут они в `docs/PROGRESS.md` — зона оркестратора, куда платформа не пишет. Оркестратор знает и правит тем же коммитом. **Стенды, оставленные для приёмки** (вне репозитория, git не видит): `/home/ubuntu/tm-mut-94-1709` — копия под мутации вместе с каноном; `/home/ubuntu/tm-head-94-1709` — развёрнутый чистый `HEAD` для сравнения двух батарей. Postgres стенда поднят мной на порту 55433 и оставлен работать. ### 12. Критерий завершённости — по пунктам пака - у каждого пункта заказа исход — **§3**; - круги сошлись, находки закрыты таблицей — **§13**; - числа ДО и ПОСЛЕ на обоих купленных файлах предъявлены командой — **§1**; - ряды 224 и 253 — **§14**; - список отрефакторенного — **§8**; - гейт зоны зелёный целиком — **§7**; - **работа завершена, править не планирую.** ### 13. Находки круга самопроверки: находка → что сделано → чем предъявлено | Находка | Кто нашёл | Что сделано | Чем предъявлено | |---|---|---|---| | Состав «8 обязательных / 4 опциональных» в промте неверен; `conventions` обязательно, `variants` опционально | я, замером писателя | замер принят, промт не подгонялся | §4; оркестратор подтвердил и правит промт | | «В A нет `conventions`» — не опциональность, а ДРУГАЯ СБОРКА движка | я | четыре числа стали указателями | посадка **B**, пин `…MandatoryNumber…` | | «Обе секции — один момент» ЛОЖНО: полнота едет на трёх границах, вопросы на одной | советчик, проверено мной по `mining.go:171` | форма переиграна: агрегат + отдельный ресурс | §5(а), эррата над запиской | | `suggestions` врёт читателю: игнор здесь есть принятие | советчик; адрес трижды уточнён (`:281`→`:280`→`render.go:225`) | имя `offered` из слов самого канона | §5(б) | | Гарантию «не читается» изобретать не надо — она ратифицирована у `BankSignatureCount` | я, при проверке имени | `unreadable` взят буквой вместе с обоснованием | посадка **H**, контракт §`BankSigningStop` | | Советчик перегнул: третий член первой страницы якобы правит шапку спеки | я, чтением шапки | шапка НЕ тронута — там КЛАСС, а не перечень | §5(а) | | Советчик: популяции списка и счётчика подписи расходятся | я, замером карты подписи | **опровергнуто на сырье** (69/69, 66/66, разность 0), но механизм реален ⇒ равенство не обещано | §9, проза операции | | Нулевое значение `ingest.Bank` нарушает CHECK новой таблицы | приёмочный прогон | гарды `BoundaryOrUnknown`/`ChannelOrNone` у шва записи | §8 п.2; вся батарея `pgstore` зелёная | | Уведомление харнесса сказало «exit 0» на упавшем гейте | я, чтением лога | вердикт читается строкой `MAKE_EXIT` из лога, не уведомлением | §7 | | Контроль ряда 253 даёт 3, а не заявленный 0 | я, пере-снятием на HEAD | суть ряда верна, число — нет; названо оркестратору | §1 | | `contradicts`/`bank_holds` — 0 из 135, проекция исполнением не проверена | я | не закрыто: нужна улика, а не код | **PD-468** | | Якорь свежести хранится и не сверяется | советчик, принято | не строила; обещание ресурса сужено прозой до «ПОСЛЕДНИЙ стоп» | **PD-467** | | `variants` — склеенные ярлыки с английским фрагментом | я, по пункту 4 аддендума | пропущено как непрозрачный текст, разбирать нельзя | ряд **479** (завёл оркестратор) | | Гейт ряда 353 сработал: у секции появился читатель | я | пинг движковой зоне через оркестратора | §9 п.2 | ### 14. Ряды 224 и 253 — что закрыто и что осталось **224** — платформенная половина **ЗАКРЫТА**: у секции есть читатель, хранилище и поверхность; её собственный контроль ушёл с 0 на 1. Осталось за пределами ряда — ряд **479** (части ярлыка `variants`). **253** — платформенная половина **ЗАКРЫТА**: поле на `BankPage` есть, бит `complete` берётся у движка ГОТОВЫМ и не выводится у меня (ратифицированное требование ряда соблюдено буквой). ⚠ Осталась ЧУЖАЯ работа, которую этот пак только разбудил: ряд **353** обещал вернуться к исчерпывающему пину контракта секции «когда у неё появится ЧИТАТЕЛЬ», и читатель появился — вместе с висящим там долгом про отсутствие свидетеля у `SettledByBank`. Это движковая зона. ### 15. Аддендум владельца 17.09 — отдельным пунктом, как просил оркестратор 1. **«Тщательно проектируй решение».** Три развилки предъявлены запиской ДО кода; две из трёх я потом переиграла, и обе — не по вкусу, а по опровержению довода кодом (§5). Эррата над запиской оставляет видимым и прежнее решение, и причину отмены. 2. **«Комментарии — только нужные, без странных гарантий».** Аддендум пришёл, когда ридер уже был написан, и **изменил написанное**: я прошла по своим комментариям и сократила их, оставив «почему» и выбросив пересказ строки. Где комментарий несёт ВЫВОД, названо условие его конца — прямо у `Invented`: «плоский bool, потому что движок опускает поле при `false`; в день, когда `omitempty` уйдёт, отсутствие перестанет значить `false` и поле обязано стать указателем — и НИЧТО здесь в этот день не покраснеет, гарда — эта фраза». Гарантий, которых не стережёт пин, я не писала: каждое утверждение §4.2 имеет посадку в §11. 3. **«Прозу в код не писать»** — и плоскости не спутаны: в Go минимум, в спецификации описание, потому что там проза НОРМАТИВНА и компилируется в исходник генерируемого клиента. 4. **«Решение общее для книг и языков».** Ревью-вопрос «заработает ли пара, которой в репо нет, без правки Go?» — **да**: ни одно поле секций и ни одно значение их словарей не несёт пара-специфики, ветвлений по паре/книге в добавленном Go нет, а закрытые словари (`kind`, `channel`, граница) — про устройство пайплайна, не про язык. ⚠ И ровно этот вопрос дал находку: **`variants` несут английский фрагмент фразы внутри данных** («proposed for X»), то есть клиент не отрисует их на языке читателя. Не зашила и не пере-вывела — назвала; ряд **479**. 5. **«Советчиков до двух»** — использован ОДИН, с постоянным контекстом, на проектировании (развилка формы и имени). Его посылки проверены деревом: одна опровергнута (шапка спеки), одна опровергнута замером (популяции), остальные приняты и изменили работу. 6. **«Право грепать доки и полигон»** — воспользовалась при проверке доводов советчика. ## ЗАПИСКА-ПЛАН (17.09) — ЧИТАТЕЛЬ СЕКЦИЙ ПРЕДЛОЖЕНИЙ И КОНСОЛИДАЦИИ (ряды 224 · 253) Пак `docs/PLATFORM_BANK_READOUT_SESSION_PROMPT.md` (180c41e), сессия `textmachine-main-94`. Записка написана ДО первой правки кода, как требует §8 пака: по ней приёмка судит работу против замысла, а не против домысла. Решения ниже названы явно; передумаю с доводом — напишу это здесь же. > ⚠⚠ **ЭРРАТА 17.09 — ДВА РЕШЕНИЯ ЭТОЙ ЗАПИСКИ ОТМЕНЕНЫ, ЧИТАТЬ ВМЕСТЕ С §5 ОТЧЁТА ВЫШЕ.** > Тело записки НЕ переписано задним числом намеренно: смена замысла и её довод — сама по себе улика, > и затирать её значило бы предъявить приёмке замысел, которого у меня в тот момент не было. > **(1) Имя.** Записка обещала `suggestions` — **отменено**, действует **`offered`** (ресурс > `.../bank/signing-stop`, схема `OfferedTerm`). Довод записки был «ноль хитов в спеке» — он > доказывает, что имя СВОБОДНО, а не что оно ВЕРНО. Отменяющий довод: игнор здесь есть ПРИНЯТИЕ > (сильнейший носитель — `backend/cmd/tmctl/render.go:225-227`, операторский вывод, который человек > читает в момент подписи; ⚠ адрес испр. 17.09 — в первой редакции стоял `mining.go:281`), > а «suggestion» обещает необязательность; и `offered` контракт уже употребляет > об этом же предмете («the surfaces the LAST signing stop offered»). > **(2) Форма.** Записка обещала обе секции на `BankPage` — **отменено**: полнота осталась агрегатом > `BankPage`, а вопросы стопа уехали в свой ресурс. Довод записки («обе описывают ОДИН момент») > опровергнут кодом движка: `r.lastTerminology` ставится ДО развилки «стоп/не стоп», поэтому полнота > едет на трёх границах из пяти, а вопросы — на одной. > Остальное записки — состав полей, три состояния уверенности, перевод словаря границ, отказ выдумывать > идентичность, хранилище в БД — **действует без изменений**. ### Что меряю и чем (замер, не пересказ) Обе купленные проекции прочитаны копиями в скретчпаде (`sha256` копий совпал с оригиналами, `mtime` оригиналов не тронут). Числа сошлись с промтом по КОЛИЧЕСТВУ и **разошлись по СОСТАВУ** — это находка, и она ушла пингом оркестратору, а не выровнялась молча: | | A | B | |---|---|---| | `terms` | 0 | 0 | | `proposed` | 69 | 66 | | `consolidation` | объект из 7 полей | объект из 7 полей | | `as_of` | `bank-mining/signature-stop` | `bank-mining/signature-stop` | | различных `src` | 69 из 69 | 66 из 66 | Восемь полей, которые движок пишет ВСЕГДА (у них нет `omitempty` в `backend/internal/pipeline/bankexport.go`): `src dst kind channel freq spread` **`conventions`** `conf`. Четыре с `omitempty`: `invented contradicts bank_holds` **`variants`**. ⚠ Промт называл `variants` обязательным, а `conventions` опциональным — наоборот; счёт 8+4 верен, состав нет. ### Находка, ради которой состав и переснимался `conventions` отсутствует у **всех 69** строк прогона A и стоит у **всех 66** строк прогона B. У поля нет `omitempty`, значит `json.Marshal` его не мог опустить ⇒ **A написан сборкой движка ДО `0997e41` (11.09), добавившего поле**. Это доказательство построением, а не догадка. Следствие несущее: у ОБЯЗАТЕЛЬНОГО поля «поля нет» значит «другая сборка», и прочесть его нулём — напечатать измерение, которого никто не делал. Ровно тот ложный ноль, ради которого пак заказан, только на ярус ниже — не у секции, а у поля. `contradicts` и `bank_holds` не встретились НИ РАЗУ (0 из 135 объектов). Читатель их несёт, но живым сырьём они не проверены — назову это в секции «где прибор слеп». ### Что строю **1. Ридер (`platform/internal/ingest/bank.go`).** Две новые секции плюс якорь свежести (`as_of`, `run_id`), которого ридер сегодня не читает вовсе. Наследую КЛАСС гарантий существующего ридера, а не цитирую его: - «секции нет» ≠ «секция есть и пуста» — обе секции указателями; `nil` ⇔ член документа отсутствовал; - у ОБЯЗАТЕЛЬНЫХ числовых полей «поля нет» ≠ «ноль»: `conventions` и `conf` — указатели. У `conf` три состояния, а не два: нет поля · отрицательное («роль не назвала уверенности», закон движка) · `≥0` (названная уверенность, включая осмысленный ноль); - у полей с `omitempty` отсутствие есть нулевое значение ПО ЗАКОНУ ПИСАТЕЛЯ, и комментарий назовёт условие, при котором вывод перестанет держаться (движок снимет `omitempty` — и тогда `invented` обязан стать указателем); - **граница читается вместе с секцией.** «Граница не та» обязано отличаться от «предложений нет»: документ с `as_of: run-finished` и без секции — это не «нечего подписывать», это «read-out снят не в том месте». **2. Словарь границ ПЕРЕВОДИТСЯ, а не проходит насквозь.** Шапка `bank.go` ратифицирует: каждое пересечение словаря переводится, движковые слова наружу не идут. `bank-mining/signature-stop` — имя СТАДИИ конвейера, тот самый класс. Пять значений сняты грепом `exportBank(` по `backend/` (пять живых вызовов, не из комментария), неизвестное шестое читается как то, что заявляет о себе меньше всего, и НИКОГДА как стоп. **3. Наружу — контрактный минор `0.16.0`** (канон `0.15.0`, полоса мажора `0`, добавление = минор), вместе с ратифицированным компаньоном `README.md`. ⚠ Номер живёт в ДВУХ местах шапки компаньона, и предыдущие три бампа правили только `openapi.yaml` — правлю оба. **Коллизия имени — решение.** `proposed` в контракте уже значит СТАТУС строки (`TermStatus`), в проекции — ИМЯ СЕКЦИИ. Третьего смысла не завожу, существующий не переименовываю, наружу слово `proposed` не беру вовсе. Внешнее имя — **`suggestions`** (контроль: `suggestion`/`Suggestion` в `openapi.yaml` — 0 хитов, в компаньоне — 0). Не `candidate`: это движковый тип (`terminology.Candidate`), то есть словарь, который шапка `bank.go` велит переводить, а не заимствовать. **Идентичность предложения — решение, а не пинг.** `id` у предложения нет ни в одном из 135 объектов, и выдумывать его у себя запрещено. Развилка разбивается надвое, и обе половины закрываются БЕЗ выдумки: - **чтение:** предложения в дельта-чтении не участвуют ВОВСЕ и едут всегда целиком. Дельта над ними не выразима по построению (нет идентичности), а порядок — ранжирование стопа, которое между прогонами меняется. Это пишется в контракт прозой, а не подразумевается; - **действие:** контракт УЖЕ говорит, чем адресуется поверхность, которой в банке нет — `BankCorrection`, кортежная форма: «for a surface the bank does not list, send `sense: ""` and `null` windows». То есть дверь для решения по предложению построена и ратифицирована; своего ключа ей не надо. **4. Хранилище — в БД, миграцией `00036`, не на лету.** Довод — исполнением, а не вкусом: реплика API может не иметь движка вовсе (`platform/internal/readmodel/readmodel.go`: «A replica with no engine serves what was materialized before»), а путь артефакта берётся из манифеста, чтение которого стоит вызова движка с ре-чанком исходника (`PD-248`). Чтение на лету в HTTP-пути перенесло бы пустой экран с одной причины на другую — и именно на том развёртывании, где его труднее всего увидеть. Дельта-чтение и ревизию это не ломает, и вот почему: предложения ложатся ОТДЕЛЬНОЙ таблицей, заменяемой целиком, и в машинерию `bank_reset_revision` не входят. Втянуть их туда было бы дефектом: у них нет идентичности, значит КАЖДАЯ материализация читалась бы как «строки исчезли» и держала бы книгу в вечном сбросе ревизии. **5. Чего НЕ отдаю наружу.** `run_id` проекции хранится, но на провод не идёт: это движковая идентичность (`tm-stream--`, `pgstore.EngineStreamID`), а шов движковых идентичностей не экспортирует. Хранится потому, что у движка якорь свежести из ДВУХ полей, и взять форму без её гарантии — известный класс ошибки. ### Гейт и его условия на этом хосте (называю ВМЕСТЕ с числом, как требует §3.1 стандарта зоны) `TM_PLATFORM_TEST_DSN` был UNSET — **поднят мной** по рецепту `STACK_DECISIONS.md` («Postgres на стенде без root», порт 55433). Контроль, что гейт действительно открыт, а не объявлен открытым: тот же селектор `-run Bank` в `internal/pgstore` даёт **4 SKIP** без переменной и **4 PASS** с ней. Не подняты: `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (пара). Они открывают живые движковые пробы `internal/runner` (`backup_live` · `bankapply_live` · `build_live` · `priceprojection_live` · `translate_resnapshot_live`) и `internal/books` — ни одна из них не про читатель проекции; `artifacts_test.go`, где живёт мой предмет, этой парой НЕ гейтится (контроль: греп `TM_PLATFORM_TEST_ENGINE_BIN` по `platform/internal/runner/*_test.go` даёт 5 файлов, и `artifacts_test.go` среди них нет). ## ⚠ ПИНГ ОРКЕСТРАТОРА №23 (10.09) — ЧЕТЫРЕ АДРЕСА В ВАШЕЙ ЗОНЕ ПОСЛЕ ДВИЖКОВОГО ПАКА И РАТИФИКАЦИИ ДВУХ ОСТАНОВОК Зона ваша, рукой не трогаю. Всё ниже пере-снято моим прибором сегодня; каждый адрес — от корня репозитория. **(1) Два комментария утверждают о движке неверное — ПРОЗА протухла, КОД у вас верен.** `platform/internal/runs/reconcile.go:1534` и `platform/internal/pgstore/runs.go:320` пишут «the engine catches SIGTERM and exits 1». Движок на пойманном сигнале отдаёт **5** (`backend/cmd/tmctl/main.go:100` = «5 graceful stop»), и ваш собственный код это знает: `platform/internal/ingest/exit.go:34` = `ExitStopped = 5`, `reconcile.go:1134` сверяется именно с ним. То есть врут только комментарии — но их цитируют доки, и ложь уезжает дальше. Тот же текст, вероятно, в `platform/internal/runner/control_test.go` (у меня адрес из чужого замера, сам не открывал). **(2) `platform/internal/runner/runner.go:56-58` и `:163-164` — НЕ ошибка, а ЦЕЛЬ; удалять НЕЛЬЗЯ.** Там сказано, что движок «finishes the in-flight chunk before exiting» и что SIGTERM даётся ему, «so it can finish the chunk it is paying for». Сегодня это ЛОЖНО: у движка одна кнопка и она жёсткая (`backend/cmd/tmctl/main.go:207` отменяет контекст прогона, летящие вызовы рвутся). Но **владелец 10.09 ратифицировал ровно то, что ваш комментарий описывает** — акт `D39.234` п.1: первое нажатие мягкая остановка (новых единиц не раздаём, начатое доигрываем), второе — сегодняшняя жёсткая. ⇒ это случай «док впереди кода»: комментарий станет истинным, когда сядет пак. Пометьте его как цель, а не правьте под сегодняшнее дерево. **(3) ⛔ ЧИСЛОВОЙ КОНФЛИКТ, который включится в ту же секунду, как SIGTERM станет мягким.** `platform/internal/runner/runner.go:59` = `stopGrace = 10 * time.Minute` (600 с), и он же уезжает в `TimeoutStopSec`. Законное ожидание ОДНОГО вызова у движка — до `attempt_max_s` **1240 с** (`backend/configs/models.yaml`, deepseek), и владелец это ожидание ратифицировал (`D39.230` п.3, потолок подтверждён `D39.234` п.1б). ⇒ мягкий SIGTERM при нынешнем грейсе означает SIGKILL по ЗАКОННОМУ ожиданию, а SIGKILL не оставляет ни пометки `cancelled`, ни сеттла оценки — деньги списаны, следа нет. Грейс обязан вырасти выше потолка ожидания ПЛЮС время жёсткой фазы, либо второй сигнал шлёте вы сами по своему продуктовому дедлайну. **(4) То, что вы просили, движок публикует — но не туда, где вы это возьмёте.** `platform/internal/pgstore/credits.go:235` просит «publish the count and sum of estimated-price rows beside committed_usd, which is what would let this side say "at most Y"». Движок их считает и печатает — `backend/internal/pipeline/status.go:321` (`estimated_rows` / `estimated_usd` в `status --json`) — но по ШВУ они не едут: в кадрах `backend/internal/runevents/runevents.go` слова `estimated` **0 хитов** (контроль: `committed` в том же файле — **6**; денежный кадр несёт один `committed_micro_usd`, `runevents.go:244`). С вашей стороны читателя тоже нет: в `platform/internal` вне тестов `estimated` — **2 хита, оба комментарии** (контроль: `committed_micro` — 1 хит; go-файлов вне тестов прибор прочёл **81**). Носитель — строка бэклога **382**; чинится ПАРНО с движком. **Что из этого следует для очереди.** Пак движка «мягкая остановка» и платформенная половина лендятся ПАРОЙ и порознь не лендятся (`D39.234` п.2). Платформенная половина, как я её вижу: `mode` у `/stop`, грейс выше потолка ожидания, путь второго сигнала (`systemctl kill --signal=SIGTERM`, юнит не трогая), чтение оценочных строк из потока. Промт ещё не написан — если у вас есть довод против любой из четырёх позиций, скажите ДО того, как я его напишу. ## ОТВЕТ ДВЕРИ НЕ ЗАВИСИТ НИ ОТ ПОРЯДКА, НИ ОТ ВЕЗЕНИЯ — ОТЧЁТ (11.09, `textmachine-37`) > Пак `docs/PLATFORM_ANSWER_THAT_DOES_NOT_DEPEND_ON_LUCK_SESSION_PROMPT.md`. Дерево передано > оркестратору №23, **сессия не коммитит**. Платных вызовов ноль. Зона записи — `platform/` плюс канон > `docs/architecture/14-api-contract/` ровно в объёме минора **0.15.0** и только вместе с кодом. ### 0-бис. ВТОРОЙ ДОФИКС ПО ВТОРОМУ КРУГУ ПРИЁМКИ (11.09) — семь находок, и ОДНО ОБЩЕЕ ПРАВИЛО ⛔⛔ **ПРАВИЛО, КОТОРОЕ СВЯЗЫВАЕТ ОБЕ ДЕНЕЖНЫЕ НАХОДКИ И ДОРОЖЕ ИХ ОБЕИХ: оба моих денежных предохранителя проверены НЕ ТЕМ ПРИБОРОМ, и одним и тем же движением — предмет судили ПО ДОКУМЕНТУ там, где исполнение было доступно и бесплатно.** Гард сверял ШАБЛОН, хотя движок грузит РЕНДЕР; рецепт $0-шаблона сверялся текстовым сканером, хотя рядом на диске лежал загрузчик. Оба раза прибор отвечал на соседний вопрос, и оба раза я записала ответ как доказательство. ⭐ **И ответ лежал В ДЕРЕВЕ — его написал комментарий того самого хелпера, строкой выше того места, где остановилось моё чтение.** `writeProbeBook` не отдаёт движку пайплайн шаблона: он БЕЗУСЛОВНО выводит из него $0-пайплайн (`zeroCostPipeline`), и его собственный комментарий говорит это прямо, включая «a paid model, on every stand built by the recipe in STACK_DECISIONS». Механизм строился под задачу, которую дерево уже решило и задокументировало. Норма, которую это подтверждает третий раз за смену: **комментарий рядом с предметом — носитель, и его читают ПЕРЕД тем, как строить механизм.** | # | что было неверно | что сделано | чем предъявлено | |---|---|---|---| | **1** | ⛔ **Гард судил ШАБЛОН, а движок грузит РЕНДЕР.** ⇒ он отказывал стендам, чья настоящая конфигурация бесплатна, и **не мог увидеть единственный случай, в котором деньги тратятся** — вендора, ПЕРЕЖИВШЕГО дериват. А дериват — рерайтер ПО КЛЮЧАМ, ровно класс F1 | вопрос перенесён на рендер — файл, который проба кладёт рядом с книгой. Плюс: платный рендер стал **дефектом** (`Fatalf`), а не условием хоста, потому что рендер — НАШ артефакт | `runner.TestTheGuardSeesAVendorThatSurvivedTheZeroCostDerivation` (вендор под ключом, которого дериват не знает, → гард называет его) и `runner.TestTheRenderOfAnOrdinaryPaidTemplateComesOutFree` (обычный платный шаблон → рендер свободен) | | **2** | **Мой новый рецепт $0-шаблона производил пайплайн, который движок отказывается грузить** — и это F1 второй раз, слоем ниже: рецепт снова проверен гардом-сканером вместо загрузчика | ⭐ **рецепт снесён целиком, а не починен** (санкция оркестратора): $0-шаблон никогда не берёг денег, проба выводит $0-пайплайн сама. Развилка П-22 схлопнута, размен П-23 снят — мина стережётся по умолчанию | мой прогон настоящего `tmctl`: `stage "draft": escalate_to must differ from the primary model "local-qwen3-8b"`, `EXIT=10`; контроль — нетронутый `pipeline-c1` там же, `EXIT=0` и выданный манифест | | **3** | ⛔ **M26 — НЕ эквивалентный мутант. Моё «окно недостижимо» — худший методологический промах за оба круга:** зонд считал, доходят ли МОИ фикстуры, а я записала ответ как утверждение о достижимости | фикстура приёмки вставлена: окно берётся не гонкой, а аранжировкой под замком книги — держать строку книги, дождаться по `pg_stat_activity`, что резюм ДЕЙСТВИТЕЛЬНО встал, вписать в зазор попытку победителя и перевести прогон в `paused`, отпустить | `runs.TestTheLoserOfTheIndexRaceIsAnsweredFromTheRowAndNotFromItsSnapshot`: на дереве PASS с `ceiling_reached`, на мутанте **RED** — `the loser was handed a "paused" run with no error` | | **5** | `continuing` на ветви `translating` — голый `ReadRun` выживал; приёмка фикстуру построить не смогла и сказала это прямо | **фикстура построена:** ветвь достижима через ВНУТРИПРОЦЕССНЫЙ замок книги, который стоит между снимком и веткой. Стальность снимка АСЕРТИТСЯ по формулировке: «ended while this call was deciding» против «it is failed» | `runs.TestTheGoingRunsAnswerIsMadeOfTheRowAndNotOfTheSnapshot`: 3 из 3 PASS, на мутанте **RED** — `a caller whose snapshot said «translating» was handed a "failed" run with no error` | | **4** | **Вычерк на самой громкой площадке (`resync failed`) не запинен:** страж `sourceThere` перехватывал мою фикстуру «каталог удалён», и до строки с вычерком пин не доходил | фикстура того же вида, что для settlement: каталог НА МЕСТЕ, юнит ЖИВ, движок падает на файле внутри | `runs.TestTheResyncOfAPresentDirectoryCarriesTheEnginesTextWithoutTheBook`; мутант «вычерк снят» → **RED** с текстом про WARN над присутствующим каталогом | | **6** | **Пятый носитель класса, которого не было в моей переписи:** `exports.go` кладёт `"book", row.BookID` на INFO | снят; ручкой остаётся id экспорта — собственный опаковый идентификатор сервиса | `exports.TestTheBuiltExportIsLoggedByItsOwnIdAndNotByItsBook`; мутант «book_id вернулся» → **RED** | | **7** | Две ветви гарда без пинов | закрыты фикстурами: шаблон/каталог, которых нет или которые не разбираются; каталог без моделей; и отдельный пин на адреса — петлевой по имени, IPv6, приватный-но-чужой, **пустой** (у движка `base_url` не обязателен для `kind: anthropic`) и неразбираемый | `runner.TestAnAddressIsThisHostsOnlyWhenItSaysSo` (8 подтестов) и шесть подтестов `TestTheLiveGuardRefusesWhatItCannotResolve` | ⛔ **И ЕЩЁ ОДИН ДЕФЕКТ ПРИБОРА, найденный мной при исполнении этого же дофикса: мой мутационный харнесс читал ненулевой выход `go test` как «поймано».** Два пакета склеивались в ОДИН аргумент, команда не разрешалась, выход был ненулевым — и три записи получили ложное RED. Нашла пере-проверкой одной из них руками. **Харнесс положен в дерево** (`platform/tools/mutate.py`) с обоими правилами внутри кода, а не в дисциплине: пакеты идут списком, а ненулевой выход без единой строки `--- FAIL` считается НЕ ИЗМЕРЕННЫМ, а не пойманным. Числа этого пака сняты уже исправленным. ⚠ **ЧТО ОСТАЛОСЬ НЕ ЗАПИНЕННЫМ И ПОЧЕМУ — третий случай одного класса за смену, называю как класс.** Посадка «гард спрашивает шаблон вместо рендера» **ВЫЖИЛА**, и причина не в гарде: на ЭТОМ хосте живая проба скипается ВНУТРИ `writeProbeBook` (нет артефакта контраста банкового контура) ещё до того, как гард спросят. То же было с посадкой «согласие снято у живого теста» в первом дофиксе. **Класс: АДРЕС вызова — то, О ЧЁМ спрашивают и спрашивают ли вообще — не пинится изнутри того же набора, когда сам вызывающий тест не бежит.** Поймает это хост, у которого артефакт контраста есть: там проба доходит до гарда, и мис-аим краснеет `Fatalf`-ом, который для этого и поставлен. Артефакт подделывать не стала: он операторский вход чужой зоны, и фальшивый список частотности — это отладка не своего предмета. ### 0. ДОФИКС ПО ПРИЁМКЕ (11.09) — шесть находок, все взяты, две были ДЕНЕЖНЫМИ Приёмка судила готовую работу независимым проходом: ядро §4.1 выдержало все посадки (ветка снята → RED ×3, перенесена ниже стража → RED, оператор инвертирован → RED ×3, вырез снят → RED, «ровно один холд» — 8 RED из 8 одним текстом), а вокруг него нашлось шесть дефектов. Все мои. | # | что было неверно | что сделано | чем предъявлено | |---|---|---|---| | **F1** | ⛔ **ГАРД СОГЛАСИЯ ОЧИЩАЛ КАК БЕСПЛАТНЫЙ ТОТ САМЫЙ $0-ШАБЛОН, РАДИ КОТОРОГО СТРОИЛСЯ.** Он собирал модели под ключом `model`, а эскалация живёт под `escalate_to:`, `escalation.chains.default: [...]`, `adult: [...]` — и рецепт зоны переписывал только `model:`-строки | **предикат перестал зависеть от ключей вообще**: каталог говорит, какие модели не на этом хосте, и гард ищет их ИМЕНА в байтах пайплайна. Новый ключ не может его ослепить по построению; имя вендора в комментарии тоже останавливает прогон — сторона ошибки безопасная | замер на файле, собранном ровно прежним рецептом: строк `model:` с вендором **0**, вхождений вендорских имён **17** (`deepseek-v4-pro` 9, `gemini-3.1-pro-preview` 3, `deepseek-v4-flash` 2, `glm-5.1` 2, `grok-4.3` 1). Новый гард на нём отвечает `names "deepseek-v4-flash"` и не пускает | | **F1-рецепт** | рецепт $0-шаблона производил файл, который гард (верно) отвергает ⇒ выход «возьми $0-шаблон» был мнимым | рецепт пере-писан: замена по ИМЕНАМ моделей, выведенным из `models.yaml`, **без списка ключей** — список ключей и был тем, что промахнулось. Плюс рецепт теперь ПРОВЕРЯЕТСЯ ГАРДОМ, а не глазом | на правильно собранном файле гард пропускает и тест доходит до следующего условия; на платном — отказывает, назвав модель | | **F2** | **слепота гарда гасилась СКИПОМ**: хелпер был и ассертом, и скип-условием, поэтому мутация «перестань видеть модели» выходила зелёной | хелпер стал ЧИСТОЙ функцией `(paid, decided, why)`; скипает вызывающий живой тест, а пин утверждает `decided=false` на фикстуре слепоты | мутант «слепота = бесплатно» → RED: `the guard answered (paid "", decided true), want ("", false)` | | **F5** | **`continuing` — функция, на которой стоят ОБЕ гарантии, — не была запинена ничем**; и её свежая проверка отдавала 409 без причины там, где канон велит `ceiling_reached` | запинена ПРЯМО, четырьмя состояниями, плюс причина приведена к канону | мутанты: «свежая проверка снята» → RED ×3, «причина снята» → RED ×2, оба с текстом про предмет | | **F3/F4** | **носителей утечки пути было ЧЕТЫРЕ, а не два**: классификация спрашивала `os.Stat` КОРНЯ, и движок, упавший на файле ВНУТРИ существующего каталога, нёс путь мимо неё; четвёртый носитель — **мой собственный WARN в `backup.go`**. ⚠ И моя же «вторая половина» пина брала единственную движковую ошибку БЕЗ пути — выбери она любую другую, соседний пин упал бы | **правило перестало быть перечнем случаев**: `books.WithoutItsPath` ВЫЧЁРКИВАЕТ каталог книги и её id из любого текста перед логированием, на всех четырёх площадках. Слепая половина пина заменена зрячей — теперь фикстура берёт ошибку, которая путь НЕСЁТ | мутанты: «вычерк ничего не вычёркивает» → RED, «вычерк съедает всё» → RED (диагноз пропал), «id сам по себе не вычёркивается» → RED, «вычерк снят у `backup.go`» → RED | | **F6** | вакуумный блок в проводном пине: после `Fatalf` сравнение с соседними причинами недостижимо | снят; соседние причины пинятся там, где ОТВЕЧАЮТСЯ (`runs.TestResumeOverAnEmptyAccountSaysTheCreditIsUnavailableAndNotTheCeiling`) | посадкой приёмки | | **F7** | размен П-23 был хуже объявленного — обе ветки развилки непроверенное состояние | закрыт вместе с F1-рецептом; ряд пере-написан с замером | см. F1-рецепт | ⛔ **ДВЕ ВЕЩИ, КОТОРЫЕ НАШЛА НЕ ПРИЁМКА, А МОЙ СОБСТВЕННЫЙ ПЕРЕ-ЗАМЕР, и обе — дефекты приборов.** 1. **Мой мутационный харнесс склеивал два пакета в ОДИН аргумент `go test`**, тот не мог его разрешить, выходил ненулевым — и харнесс читал это как «поймано». Так три записи получили ложное «RED». Поймано на M30: пере-проверка руками показала, что мутант ЖИВ. Харнесс исправлен (пакеты списком), все затронутые записи пере-сняты; M30 после этого действительно выжила, и под неё написан пин. 2. **Мой же новый пин гарда флейковал**: ответ зависел от порядка обхода карты, и фикстура с ДВУМЯ вендорами называла то одного, то другого. Ответ отсортирован, и на это заведён отдельный пин из двадцати повторов. ⚠ **И одно, о чём я говорю прямо: M26 («проигравшему гонку возвращён голый `ReadRun`») ВЫЖИЛА, и я считаю её эквивалентным мутантом — с замером, а не с доводом.** Ветка проигравшего достижима только после того, как победитель вставил попытку N+1, то есть прогон в этот момент `translating`, а там `continuing` и `ReadRun` неразличимы. Зонд на ветке: **достигнута 11 раз, статус, отличный от `translating`, — 0 раз**. Развести их могла бы только отмена прогона между двумя соседними операторами, а `Service.Store` — конкретный `*pgstore.Store` без шва, куда можно вклиниться. ⇒ окно реально (в бою реконсилятор может завершить прогон в нём), фикстурой недостижимо, и `continuing` — безопасная его сторона. Называю слепым пятном, а не поимкой. ### 1. Что закрыто: резюм стал идемпотентным по существу (§4.1, `PD-448`) **Ответ двери больше не зависит от того, кто успел первым.** Ветка `case "translating"` отвечает ПРОГОНОМ — тем же оператором `continuing`, которым отвечает проигравший уникальному индексу, так что два пути не могут разойтись правкой одного. Вырез: прогон с УЖЕ запрошенным стопом остаётся `409` с причиной `stop_requested` (`D39.240` — остановка жёсткая, и «продолжается» там ложь). Ветка стоит ВЫШЕ проверки re-pass; сама проверка уехала ПОД свитч, к прочим стражам ПЕРЕ-ОТКРЫТИЯ, и это превращает свитч в единственное место, где решает статус. ⛔ **ПРЕДМЕТ РЯДА ОКАЗАЛСЯ ШИРЕ, ЧЕМ РЯД, и это находка замера, а не уточнение прозы.** Ряд описывал гонку в миллисекунды под `load average 42–48`. Замерено счётчиками путей в копии дерева: | вопрос | команда | ответ | |---|---|---| | держится ли гарантия сегодня | `go test ./internal/runs/ -race -count=20 -run '^TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer$'` | **20 PASS из 20** — пин зелен | | ЧЕМ он удовлетворён | счётчики путей в `Resume`, `-count=5` | **5 из 5: `winner=1 staleLoser=1 freshRefusal=0`** — держит только ветка устаревшего снимка, дефектная НЕДОСТИЖИМА | | второй порядок | два `Resume` подряд, та же фикстура, `-count=5` | **5 из 5 отказ**: `first: status="translating" err=` · `second: err=runs: the run cannot be continued: it is translating` | ⇒ отказ получал **любой** резюм живого прогона; гонка — узкий частный случай. Человеку довольно секунды между кликами. Старый пин был зелёным именно потому, что его фикстура измеряла половину сценария. Тело ряда `PD-448` пере-написано под это, ряд закрыт, вес оставлен `minor` с доводом: рубрика регистра взвешивает ПОТЕРЮ, а денег на отказном пути не двигалось — `attempts=2 reserved=3.319828 balance=6.180172 ledger=6.180172`. **Остаток окна, названный, а не закрытый.** `stop_requested` читается в снимке, взятом ДО замка книги, а `Stop` замка книги не берёт вовсе (его первый оператор — запись намерения). Стоп, нажатый между снимком и ответом, даёт один `202` на сворачивающийся прогон: один кадр, денег не двигает, и тело ответа несёт `stop_requested` из СВЕЖЕГО чтения строки — клиент узнаёт про стоп тем же ответом. Закрыть окно значило бы сериализовать `Stop` за замком книги, то есть поставить ожидание на путь, чей смысл в том, что он не ждёт. Цена бездействия названа: один кадр, ноль денег. **Канон — тем же деревом, минор `0.15.0`.** Правлены строка `translating` таблицы §`resumeRun` (теперь две строки: идущий прогон и прогон под стопом) и фраза «a `202` therefore means work was actually reopened» — она была неверна и ДО этого пака, потому что дерево отдавало `202` проигравшему гонку и это было запинено шапкой собственного теста зоны. Второй уровень причин расширен `stop_requested`; словарь `ErrorCause` объявлен каноном ОТКРЫТЫМ, поэтому номер версии двигает строка таблицы, а не причина. Провенанс — §2.26 компаньона. Константа `httpapi.ContractVersion` поднята тем же деревом. ### 2. Что НЕ строится, с доводом и ценой **§4.2 `PD-162` — автоматического терминального состояния НЕ будет.** Довод не в цене работы, и он опирается на замер, а не на чтение. | носитель клина | прогон доходит до конца? | холд возвращается? | чем | |---|---|---|---| | попытка НЕ дошла до движка (нет юнита, нет базовой линии) | да — собственным стопом пользователя | **да, целиком и автоматически** | уже построенный `ReleaseUnspawned` | | попытка ДОШЛА, счётчик движка нечитаем | да — тем же стопом | **нет** — до `run abandon` оператора | эскроу `П-18` | Замер (8 проходов свипа над прогоном с удалённым каталогом): `status=translating failures=1…8 openHolds=1 held=0.444828`, строка в списке оператора с пятого прохода; затем стоп пользователя — `status=stopped finished=true`, холд остаётся. Для второго носителя (отказ спавна без всякого удаления каталога): `AFTER 6 PASSES: status=translating openHolds=1` → `AFTER STOP: status=stopped openHolds=0 reserved=0.000000 balance=20.000000`. ⇒ **Утверждение ряда «ни один путь не приходит к терминальному состоянию» неверно**: путь есть и он в руках у того, чьи это деньги. Не возвращается только ХОЛД, и только в одном из двух носителей. А чтобы его вернуть, надо ответить «сколько списать, когда мерить нечем» — это и есть эскроу: отдать целиком значит не взять денег за работу, уже оплаченную провайдеру; взять целиком — наказать пользователя за аварию оператора. Обе половины цены названы в самом CLI (`run abandon`: «would return the WHOLE hold and charge nothing for what the run actually spent»). Плюс два довода, которые стоят сами по себе: счётчик неудач такого вопроса не решает («I could not ask» никогда не «the run is gone» — записано в дереве, комментарий `backoff`), и снятие холда обязано ехать с КОРРЕКТНОЙ остановкой процесса, иначе вечный холд меняется на потерянные деньги плюс живой движок, тратящий против прогона, который никто не считает. Продуктовый пробел назван и НЕ закрыт: человек видит «идёт» и не знает, что стоит нажать стоп. **§4.3 `PD-466` — факт в БД НЕ строится сейчас.** Названное дешёвым лечение дешёвым не является: писателя у этого факта в дереве НЕТ (пропажу видит только проход бэкапа и разбор интейка — для книги, уже прошедшей приём, никто её в базу не пишет). Завести факт значит завести проход, который статит каталог КАЖДОЙ книги, а такт реконсилятора ПОСЛЕДОВАТЕЛЕН и его собственный файл уже замерил цену блокирующего прохода: он задерживает расчёт денег, ретраи интейка, GC экспортов и все гейджи. То есть цена зависшего монтирования, которую ряд увёл с опрашиваемого пути, вернулась бы на денежный. И второе: кэшированное «каталога нет» — это класс `PD-192` с более длинным фитилём; предикат двери верен именно тем, что спрашивает в момент решения. Ряд остаётся открытым честным остатком: обещание формы ложно, денег это не двигает, дверь остаётся авторитетом. **§4.4 `PD-139` — одна диспозиция, и она выведена замером.** ⚠ Формулировка заказа («в дереве две диспозиции на один класс») предполагала то, что надо было сначала померить, и оркестратор принял поправку релеем. Мерил первым: путь каталога и идентификатор книги — ОДНО И ТО ЖЕ. Интейк кладёт книгу в `/`, и на живом стенде холодного прогона это так у **5 книг из 5** (`psql … -c "select id, workdir from books"` по базе `tm_coldrun_door`: `bk_EQWUDWUXXALA7BFP` → `…/books/bk_EQWUDWUXXALA7BFP`). ⇒ класс ОДИН, и двух диспозиций быть не может. **Правило:** платформа никогда не КЛАДЁТ идентичность книги в лог сама. Где предложение сочиняет она — называются операция и errno без пути. Где текст чужой программы — он несётся как написан, потому что переписывать чужой диагноз хуже, чем нести его; работа платформы тогда — сделать так, чтобы обычный случай до чужого текста не доходил. **Что из этого построено.** Замер показал утечку НИЖЕ уровня ERROR, там, где интейк свою половину уже запинил: на клине пропавшего каталога **10 строк из 20 несли путь книги, все на WARN**, одним сообщением — расчёт цитировал текст движка. Теперь заблокированный расчёт классифицируется своим же предикатом (`s.sourceThere`) и говорит своими словами, различая пропавший каталог книги и не смонтированный том; `journalSize` приведён к той же форме, что `sourceThere`. Текст движка остаётся только там, где каталог НА МЕСТЕ и назвать причину платформе нечем. Остаток назван: на ERROR путь по-прежнему законен, и закрыть исключение можно, дав `pgstore.StalledRun` колонку каталога — сегодня её там нет. ### 3. Правка теста по ЗАКАЗАННОЙ смене поведения — объявляется (`D39.121`) - **Что утверждала фикстура раньше:** два одновременных резюма отвечают прогоном и берут один холд. Утверждение верное, фикстура — половинная: два горутины, обе читают состояние сразу, и на тихой машине ветка дефекта недостижима (замер выше, 5 из 5). - **Что она утверждает теперь:** то же самое, но КАЖДЫЙ из двух порядков имеет свою фикстуру, в которой он неизбежен. Контендированный порядок держится замком строки, который берёт сам тест, и ВРЕМЯ ОЖИДАНИЯ асертится — вызов, который не ждал, контенции не встретил и ничего не доказал. Плюс утверждение о границе: пока оба вызова внутри, закоммиченный статус обязан быть `stopped`. - **Куда уехала гарантия:** имя теста и его смысл сохранены, файл — `internal/runs/resumeorder_test.go`; прежнее место в `control_test.go` несёт указатель. Фикстура гоняется на ДВУХ сервисах, потому что замок книги внутрипроцессный и шапка теста всегда описывала деплой из двух экземпляров. - ⚠ **Вторая смена поведения БЫЛА и СНЯТА по находке адверсариального прохода.** Первая форма правки уводила проверку re-pass под свитч, и это меняло ответ re-pass-прогону в статусах `paused` (`ceiling_reached`), `failed`/`ready` («it is ») — а лекарство `ceiling_reached` канон пишет «a NEW run with a larger `chapters`» над покупкой, у которой глав нет вовсе. Форма исправлена: ветка `translating` — отдельный `if` ВЫШЕ стража, страж вернулся на своё место. **В сданном дереве ответ статусам, которые re-pass никогда не продолжает, не изменён ничем.** - ⚠ **ТРЕТЬЯ правка тестов, не заказанная паком и объявляемая отдельно: два пина re-pass укреплены.** Что утверждали раньше: `errors.Is(err, ErrNotResumable)` — и это удовлетворялось ДВУМЯ чужими отказами, так что со снятым стражем оба пина проходили 8 из 8. Что утверждают теперь: фикстуры закрывают оба чужих отказа И проверяются СЛОВА стража. Куда уехала гарантия: никуда — она впервые появилась. Это не правка ради зелени, а обратное: пин, который ничего не измерял, начал измерять (`ENGINEERING_STANDARDS` §3 п.4 — свойство без ловящего мутацию теста считается НЕ закрытым). ### 4. Мутации ПЕРВОГО круга: 16 посадок · поймано 14 · ВЫЖИЛО 2 · одна запись недействительна ⚠ **Заголовок исправлен по второму кругу приёмки: здесь стояло «выживших 0», и это противоречило телу собственной таблицы** (строка M17-до помечена «ВЫЖИЛ 8 из 8») **и секции 0** (M26 выжила, и я объявила её эквивалентным мутантом — ошибочно, см. 0-бис). Правда: на первом круге выжили ДВЕ — M17-до (страж re-pass не был запинен) и M26 (ветвь проигравшего). Обе закрыты, первая в первом дофиксе, вторая во втором. ⛔ **И ТРИ ЗАПИСИ ЭТОЙ ТАБЛИЦЫ БЫЛИ СНЯТЫ СЛОМАННЫМ ПРИБОРОМ — называю поимённо, как и требует приёмка.** Харнесс склеивал два пакета в один аргумент `go test`, и ненулевой выход неразрешимой команды читался как «поймано». Пострадали записи, у которых в каталоге стояло ДВА пакета: **M27** («вычерк ничего не вычёркивает»), **M28** («вычерк съедает всё») и **M30** («вычерк снят у `backup.go`»). Все три ПЕРЕ-СНЯТЫ исправленным харнессом: M27 и M28 действительно RED, **M30 действительно ВЫЖИЛА** — и под неё написан пин (`backup.TestTheSweepsOwnWarningDoesNotNameTheBook`), после которого она краснеет. Прибор положен в дерево (`platform/tools/mutate.py`), так что пере-снять любую запись теперь есть чем. Копия с каноном (`cp -a --parents platform docs/architecture/14-api-contract`), базовая линия копии зелёная, вердикт судился по ТЕКСТУ падения. | # | что сломано | итог | чем поймано (текст) | |---|---|---|---| | M1 | ветка `translating` снята | RED | `resume 1: runs: the run cannot be continued: it is translating — the run is continuing, and a refusal makes the answer depend on which call reached the database first` | | M2 | вырез `stop_requested` снят | RED | `resuming a run under a stop answered , want ErrStopRequested` | | M3 | guard re-pass поднят над свитчем | RED | `resuming a re-pass that is RUNNING answered … a re-pass is bought again rather than resumed` | | M4 | `continuing` отдаёт пустой прогон | RED | `resume 1 answered run "" in "", want "run_…" translating` | | M5 | классификация в `settle` снята | RED | `a WARN line carries the book's own directory, which is its identifier: …` | | M6 | путь возвращён в `stat journal` | RED | `the error carries the book's own directory: runs: stat journal: stat …/bk_THISISTHEBOOKSOWNID/events.jsonl: permission denied` | | M7 | `ContractVersion` откачена | RED | `this build announces contract 0.14.0 and the ratified canon is 0.15.0` | | M8 | причина снята с провода | RED | `cause = , want {code: stop_requested}` | | M9 | замок снят из МОЕЙ фикстуры гонки | RED | `the run is committed as "translating" while both callers are still inside: … this fixture is measuring the other order` | | M10′ | ветка `translating` ПЕРЕ-ОТКРЫВАЕТ | RED | пойман утверждением о ПОРЯДКЕ, а не о холде — см. ниже | | M13 | класс назван верно, путь приписан полем | RED | та же строка про WARN | | M14 | вырез читает не то поле (`StartedAt` вместо `StopRequestedAt`) | RED | `resuming a run under a stop answered , want ErrStopRequested` | | M15 | ветка снята **И** снят страж открытой резервации | RED | `2 open holds of 6.639656 after two resumes, want exactly one of 3.319828` | | M16 | классификация `resync` снята (второй носитель утечки) | RED | `a WARN line of a LIVE run carries the book's own directory: … "msg":"resync failed" …` 3 из 3 | | M17 | **страж re-pass снят ЦЕЛИКОМ** — посажен ПОСЛЕ укрепления пинов, по находке прохода | RED | **8 FAIL из 8 каждому** из двух пинов: `resuming a re-pass answered (run {… Stage:re_pass …}), want ErrNotResumable (bought again)` | | M17-до | тот же страж, снятый ДО укрепления | ⛔ **ВЫЖИЛ 8 из 8** — это и есть находка №1 прохода: оба пина были удовлетворены чужими отказами | — | ⛔ **M10 — НЕДЕЙСТВИТЕЛЬНАЯ ЗАПИСЬ, и это мой методологический промах, а не дыра.** Я ослабила собственное утверждение (`holds != 1` → `holds > 2`) и записала «выжила». Ослабленное утверждение по построению не может покраснеть на верном коде; это не мутация. Заменено на M10′ и M15. ⭐ **И из M10′/M15 вышла настоящая находка о МОЁМ пине: «ровно один холд» держит не моя ветка, а база.** Снятие ответной ветки ко второму холду НЕ приводит — его держит страж открытой резервации в `reopen`, и счёт холдов краснеет только при ДВУХСАЙТОВОЙ посадке. То есть пин счёта — регрессионный сторож над гарантией, которую держит СУБД, и говорить, что его удовлетворяет только моя правка, было бы неверно. ### 5. Числа после последней правки **`make check` со всеми четырьмя гейтами, пере-снят ПОСЛЕ ВТОРОГО дофикса: `MAKE-EXIT=0` · 20 пакетов · **20 `ok` · 0 `FAIL`** · линтер «0 issues» · скипов 5 при ОДНОМ условии — артефакт контраста банкового контура (строка бэклога 251).** ⚠ Второе условие («гард отказал живому переводу») исчезло вместе с развилкой шаблонов: гард спрашивает о рендере, рендер свободен, и живая проба больше не скипается по согласию. Шаблон второго гейта — настоящий `pipeline-c1.yaml` (дефолт, выбранный оркестратором №23 по развилке П-22). - **Скипы, 5, по условиям — пере-снято командой, а не по памяти** (`go test ./internal/runner/ -v`, группировка причин): **4** — артефакт контраста банкового контура (`mining-contrast.zh.txt` не на этом хосте, строка бэклога 251); **1** — гард согласия отказал живому переводу под платным шаблоном (`this deployment's book template names "deepseek-v4-flash"`). Второе условие — принятый размен, носитель П-23. - ⚠ **Это НЕ то число, с которым пак шёл в середине смены.** На $0-шаблоне батарея давала `MAKE-EXIT=2 · 19 ok · 1 FAIL` (два теста проекции цены в `internal/runner`), и оба падения были пре-существующими — доказано контролем: дерево, возвращённое к `HEAD` пофайлово (15 файлов через `git show HEAD:`, два новых удалены), давало те же два. Зелень пришла не от правки тестов, а от смены дефолтного шаблона, и её довод — в `STACK_DECISIONS` и П-22. - **Пакеты, которых касается пак:** `internal/runs`, `internal/httpapi`, `internal/gates`, `internal/pgstore`, `internal/runner` — все `ok`. - Оба порядка §4.1 гонялись по десять раз: `go test ./internal/runs/ -race -count=10 -run 'TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer|TestAResumeOfARunAlreadyGoingAnswersWithThatRun'` → **10 PASS из 10 каждому**. Флейка нет; адверсариальный проход пере-ранил и получил те же 10 из 10. - Линтер якорей `python3 docs/scripts/counts.py --lint`: в зоне `platform/` **0 проблемных**; всего по дереву 15, из них **4 сломаны моим переездом и лежат в `docs/` — не моя зона, отдаю пингом** (§7), остальные 11 — чужих зон и были до меня. ⚠ Свои якоря пере-снимала ТРИЖДЫ: каждая следующая правка кода двигала строки под уже починенными ссылками. - **Дифф `^func Test`, снятый командой ПОСЛЕ ВТОРОГО дофикса:** снят один (`TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer` из `control_test.go` — пере-построен под тем же именем в `resumeorder_test.go`), добавлено **двадцать четыре**: 7 в `runs/resumeorder_test.go`, 7 в `runner/liveconsent_test.go`, 5 в `runs/logprivacy_test.go`, 2 в `books/logprivacy_test.go`, по одному в `httpapi/control_test.go`, `backup/backup_test.go` и `exports/exports_test.go`. ### 5-бис. Адверсариальный проход (субагент `fable`) — восемь находок, СЕМЬ приняты, две ломали мои утверждения Мандат был «найди отсутствующее и противоречащее». Нашёл, и две находки — дефекты в том, что я объявила сделанным. Дерево проход не трогал (мутации в своей копии), базовая линия рабочего дерева у него сошлась с моей. | # | находка | что сделано | чем предъявлено | |---|---|---|---| | 1 | ⛔ **Страж «re-pass покупается заново» НЕ ЗАПИНЕН: мутант со снятым стражем выживал 8 из 8.** Моя контрольная половина и старый K4-пин проверяли только `errors.Is(err, ErrNotResumable)`, а его удовлетворяют ДВА чужих отказа по бокам стража — «a newer run of this book exists» (часы фикстуры стоят, у обоих прогонов книги один `started_at`, и `newestRun` решает по `id`) и «its previous attempt is still being settled» (SQL-фикстура ставила `settled_at`, но резервацию не закрывала) | **ОБА пина укреплены**: фикстуры закрывают оба чужих отказа (`started_at` сдвинут, резервация закрыта) И утверждают СЛОВА стража | после правки мутант «страж снят целиком» даёт **8 FAIL из 8 каждому** из двух пинов, с текстом про предмет | | 2 | ⛔ **Второй носитель утечки: `resync failed` на WARN несёт путь книги, пока юнит ЖИВ,** и повторяется каждый `ResyncEvery`. Мой пин держал юнит МЁРТВЫМ и до этой ветки не доходил — то есть моё «текст движка остаётся только там, где каталог на месте» было ЛОЖНЫМ | **классификация добавлена и на этот путь**, плюс НОВЫЙ пин с живым юнитом | пин печатает `4 строки, 4 несут id прогона, 3 называют каталог`; мутант «классификация снята» → **3 FAIL из 3** с текстом `a WARN line of a LIVE run carries the book's own directory` | | 3 | **Paused re-pass стал отвечать `ceiling_reached`**, чьё лекарство канон пишет «a NEW run with a larger `chapters`» — над покупкой, у которой глав нет вовсе | **форма исправлена: ветка `translating` стала отдельным `if` ВЫШЕ стража re-pass, а сам страж вернулся на своё место над свитчем.** Ответ статусам, которые re-pass никогда не продолжает, больше не меняется — незаказанная смена поведения снята | сборка + прежние пины зелёные; заказ §4.1 («ветка ВЫШЕ проверки re-pass») исполнен буквально | | 4 | **Окно «прогон кончился между снимком и ответом» названо только для стопа**: над прогоном, успевшим уйти в `paused`, ответ был `202`, а канон велит читать `202` как «идёт» | **закрыто построением**: `continuing` теперь судит по той же строке, из которой делает ответ, — не `translating` в свежем чтении ⇒ отказ, а не `202`. Окно сжалось до «состояние изменилось ПОСЛЕ ответа», то есть до течения времени | сборка + пины порядка зелёные | | 5 | **Довод под `os.Stat` в `settle` неверен в той форме, в какой я его написала**: «вызов движка уже ходил в тот же каталог» ложно для самой частой формы (`PD-384` — снесённый бинарь: exec падает, тома не касаясь) | **комментарий переписан честно**: стат назван ПЕРВЫМ касанием монтирования, сказано, что контекст такта он не чтит и такт последователен, экспозиция помечена «названа, не замерена» | — (правка прозы; замерить нечем — зависшее монтирование на этой машине не воспроизводится) | | 6 | **Сентинел `ErrStopRequested` на пути `reopen` не запинен** — гарантия «оба стража отвечают одним словом» живёт в комментарии | **не чиню, называю**: путь гоночный (стоп между снимком и замком строки внутри `RestartRun`), фикстуры под него у меня нет, а выдуманная фикстура пинила бы не его | — | | 7 | **Граница пина утечки**: исключение «на ERROR путь можно» пропускает и ПОВТОРЯЮЩИЙСЯ идентификатор | принято дословно в §6 «где прибор слеп» | — | | 8 | **Проза и якоря**: докстринг `Resume` устарел · комментарий `bank_test.go` ложен для живого re-pass · голый номер `reconcile.go:1721` в `PD-162` уехал (гейт якорей голых номеров не видит) · компаньон канона говорил «канон 0.13.0, ОДИННАДЦАТЬ миноров» при 0.15.0 · **моё число «10 строк из 20» из дерева не воспроизводится** | всё исправлено; число заменено на то, что печатают сами пины (14/6 на мёртвом юните, 4/3 на живом), с пометкой, откуда взялось прежнее | якорь `:1808` пере-снят командой; счёт миноров исправлен с пометкой, что промах третий подряд и число живёт в ДВУХ местах шапки | ⭐ **Чего проход НЕ нашёл, и это тоже результат:** деньги — ни ветка, ни вырез, ни `continuing` не двигают резерв и баланс; таблица канона против исполнения сходится по всем шести строкам; порядок сентинелов в `v0.go` верен; и он подтвердил независимо мою же находку о том, что «ровно один холд» держит база, а не моя ветка. Плюс пере-считал мои числа из сырья: `reserved=3.319828` и `balance=6.180172` сошлись арифметически, «14 строк / 6 называют» сошлось, `-race -count=10` по обоим порядкам дал те же 10 из 10. ⚠ **И честная оговорка о самом проходе:** он гонял мутанты в СВОЕЙ копии и по СВОЕМУ каталогу, а не по моему; его «M1 … M15» — его нумерация, не моя, и совпадение имён случайно. Пере-проверять его выводы я могла только тем, что воспроизводила у себя, — что и сделано по находкам 1 и 2. ### 6. Что НЕ удалось · ГДЕ ПРИБОР СЛЕП И Я ЭТО ЗНАЮ **Не измерено.** - **Многоэкземплярный деплой не проверялся вживую.** Контендированная фикстура моделирует два экземпляра ДВУМЯ `Service` над одной базой; настоящих двух демонов я не поднимала. Утверждение «в двух экземплярах путь устаревшего снимка — обычный» выведено из устройства замка книги, а не замерено. - **Цена `os.Stat` в `settle` на зависшем монтировании не замерена.** Я назвала её ограниченной бюджетом фазы и тем, что вызов движка к тому же каталогу повис бы первым; это рассуждение, не замер. Зависшее монтирование на этой машине воспроизвести нечем. - **Продакшн-форма пути** (`/`) подтверждена на 5 книгах живого стенда и чтением кода интейка. Книги, заведённые оператором через `book add --workdir`, кладутся куда он скажет — для них равенство «путь = идентификатор» не доказано, и мой пин их не покрывает. **Где прибор слеп, и я это знаю.** - **Пин утечки смотрит на ОДНУ строку-подстроку — сам идентификатор книги.** Утечка через производную форму (хеш, срез, кодировка) им не ловится. Проверять надо бы предикатом «в строке нет ничего, что резолвится в книгу», а такого прибора нет. - **Уровень ERROR пин не смотрит вовсе** — это названное исключение, а не пробел прибора; но следующий читатель обязан знать, что зелёный пин про ERROR не говорит ничего. - **Гейт версии сверяет НОМЕР, а не ФОРМЫ.** Минор `0.15.0` он подтверждает; что таблица §`resumeRun` описывает именно то, что делает `Resume`, не проверяет ничто, кроме человека. Это известный класс (строка 309 общего бэклога), и мой минор его не уменьшает. - **Причина `stop_requested` не гейчена каноном** — словарь `ErrorCause` объявлен открытым, гейта формы для него нет и быть не может; пин держит её ЛИТЕРАЛОМ на проводе. - **Сентинел `ErrStopRequested` на пути `reopen` НЕ ЗАПИНЕН** (находка прохода): гарантия «оба стража отвечают одним словом» живёт в комментарии. Путь гоночный — стоп между снимком и замком строки внутри `RestartRun`, — фикстуры под него у меня нет, а выдуманная пинила бы не его. - **Исключение «на ERROR путь можно» пропускает ПОВТОРЯЮЩИЙСЯ идентификатор** (находка прохода): строка ERROR, повторяющаяся каждый проход, законна по принятой диспозиции и несёт книгу столько же раз, сколько запрещённая WARN. Правило про род текста это не ловит; ловило бы правило про частоту, которого нет. - ⛔ **Двух носителей утечки я нашла ОДИН, и второй нашёл не я.** Пин держал юнит мёртвым, и живая половина класса — `resync`, повторяющийся каждый `ResyncEvery`, — была объявлена мною закрытой, пока была открыта. Урок дороже находки: **пин, у которого фиксированное значение фонового условия (`alive = false`), измеряет ОДНУ ветвь класса, а отчитывается за класс.** Оба носителя теперь имеют свой пин, но искать третий я не умею — прибора «перечисли все места, где текст движка попадает в лог» у меня нет. - **`os.Stat` в `settle` и в `resync` — первое касание монтирования на последовательном такте**, и контекст такта он не чтит. Экспозиция названа в комментарии, НЕ замерена: зависшее монтирование на этой машине воспроизвести нечем. ### 6-бис. Внеплановая находка о СТЕНДЕ — вне моего диффа, зафиксирована тремя носителями Рецепт «Гейты батареи» внутренне противоречив: $0-шаблон нужен `TestTheSnapshotGuard…` и он же обнуляет проекцию цены, которую читают два соседних теста, — зелёного `internal/runner` рецепт не даёт ни при какой конфигурации. Плюс денежная половина, помеченная ВЫВОДОМ: на хосте, где выполнены все три условия скипа, ПЛАТНЫЙ шаблон запустит настоящие вызовы при исполнении гейта. Платную ветку я не гоняла — санкции нет, и оркестратор запрет подтвердил. Носители: `docs/STACK_DECISIONS.md` «Гейты батареи» (развилка, команды, снятие числа «0 FAIL» с атрибуцией приёмке смены №23 — по её же просьбе) и строка **П-22** зонного бэклога. ⚠ И поправка к собственному первому сообщению: я написала оркестратору, что гейт скипается по свободному порту; замер батареи показал, что на этом хосте он скипнулся ТРЕТЬИМ условием — артефактом контраста, — то есть условий не два, а три, и моё утверждение было у́же истины. ### 7. Пинги оркестратору ⚠ **Четыре якоря, сломанные моим переездом, лежат в `docs/` — чужая зона, не трогаю. Числа посчитаны, правка в одно движение** (все — по дереву, которое я передаю; после лендинга пере-снять): - `docs/BACKLOG.md`: `docs/architecture/14-api-contract/openapi.yaml:2191` → `:2200` («is reserved and is the server») - `docs/BACKLOG.md`: `…/openapi.yaml:2367` → `:2376` («declining a surface removes EVERY window») - `docs/BACKLOG.md`: `…/openapi.yaml:3159` → `:3168` («unless its own status») - `docs/architecture/05-decisions-log.md:2666`: `platform/internal/runs/runs.go:479` → `:490` («if quote.Chapters == 0») ⚠ **`PD-448` помечен `fixed(пак «ответ двери», дерево сессии)` и остался в своей секции регистра** — по прецеденту `PD-455`; переезд между секциями делает лендинг, если зона так решит. Его id снят из `alarmBaseline` тем же деревом, как гейт и требует. ⚠ **`PD-139` и `PD-466` ВОШЛИ в класс alarm моей ПРОЗОЙ, а не свойством,** и внесены в `alarmBaseline` с поимённым разбором слов (`заблокированный`/`замолчал` у первого, `блокируется` у второго). Я не стала переписывать текст рядов ради цвета гейта: слова в рядах верны, и подгонка под регексп была бы ровно тем, что `D39.121` запрещает. Цена внесения названа там же: класс подрос двумя рядами, чья беда не деньги и не тишина. ### 8. Комплектность против §4 — сверка таблицей, а не перечтением | пункт | что заказано | исход | |---|---|---| | §4.1 | резюм идемпотентен, критерий — инвариантность к порядку | **сделано**, оба порядка предъявлены исполнением, остаток окна назван абзацем, канон `0.15.0` тем же деревом | | §4.2 | `PD-162`: граница с эскроу, решить и аргументировать | **не строю, с доводом и замером**; вопрос «а что с процессом» отвечен: снятие холда без остановки юнита меняет дефект на худший | | §4.3 | `PD-466`: надо ли строить факт в БД | **не строю, с доводом**: у факта нет писателя, а завести его значит вернуть цену зависшего монтирования на денежный такт | | §4.4 | `PD-139`: одна диспозиция на класс | **принята одна**, выведена замером (5 книг из 5), и подкреплена двумя починками плюс двумя пинами | | §4.5 | чего не брать | эскроу не строила, квот не заводила, `backend/` и `docs/` вне канона не трогала, новых ручек нет | | §5 | самопроверка исполнением | 16 посадок, таблица выше; адверсариальный проход отдельным субагентом (`fable`, один) — восемь находок, семь приняты, две ломали мои утверждения и обе починены с новыми пинами | | §6 | деньги · порядок · провод и канон | каждая новая ветвь отвечает числом; оба порядка исполнением; версия и гейт согласованы | ⛔ **РАБОТА ЗАВЕРШЕНА, править не планирую.** Дерево передаю оркестратору №23: **27 путей — 25 в `platform/`** (пять новых: четыре тест-файла и `tools/mutate.py`), **2 в каноне** `docs/architecture/14-api-contract/` (минор 0.15.0, едет ТЕМ ЖЕ лендингом), и ни одного файла вне этих двух мест. `docs/PROGRESS.md` не трогала ни разу — платформа туда не пишет, и в дереве он лежит с чужой незакоммиченной правкой. Платных вызовов ноль. Не коммичу. ## ОТВЕТ ДВЕРИ НЕ ЗАВИСИТ НИ ОТ ПОРЯДКА, НИ ОТ ВЕЗЕНИЯ — ЗАПИСКА-ПЛАН (11.09, `textmachine-37`) > Пак `docs/PLATFORM_ANSWER_THAT_DOES_NOT_DEPEND_ON_LUCK_SESSION_PROMPT.md`. Зона записи `platform/` > плюс канон `docs/architecture/14-api-contract/` ровно в объёме минора **0.15.0** и только вместе с > кодом. Сессия не коммитит. Платных вызовов ноль. HEAD на входе `218ee4e`, дерево `platform/` чистое. ### Замер, который решал, остаётся ли §4.1 в прежнем виде — снят ПЕРВЫМ, до единой правки Опасность назвал я сам в эхо-протоколе: пин `TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer` (`internal/runs/control_test.go:881`) носит имя ровно той гарантии, которую пак собирается строить. Развилка была полная: либо гарантия уже есть и `PD-448` описывает мимо, либо фикстура не доводит до гонки. **Ответ: фикстура меряет ОДИН из двух порядков, второй в ней недостижим.** | Замер | Команда | Результат | |---|---|---| | Пин зелёный на тихой машине | `go test ./internal/runs/ -race -count=20 -run '^TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer$'` | **20 PASS из 20**, `ok` 16.760s | | ЧЕМ он удовлетворён | счётчики путей в копии дерева (`/home/ubuntu-26/tmwork-37`), `-count=5` | **5 из 5: `winner=1 staleLoser=1 freshRefusal=0`** — утверждение держит ТОЛЬКО путь «проигравший прочитал устаревший `stopped`, дошёл до `reopen`, упал на уникальном индексе попытки и получил ПРОГОН» | | Второй порядок | два `Resume` подряд, та же фикстура, `-count=5` | **5 из 5 отказ**: `first: status="translating" err=` · `second: err=runs: the run cannot be continued: it is translating`, счётчики `winner=1 staleLoser=0 freshRefusal=1` | **Три следствия, каждое меняет чтение ряда.** 1. Гарантия НЕ цела: пак остаётся в прежнем виде, пере-целивать нечего. 2. ⛔ **Предмет `PD-448` ШИРЕ, чем гонка.** Ряд описывает окно в миллисекунды под `load average 42–48`; на простаивающей машине тот же отказ **детерминирован**: любой резюм ЖИВОГО прогона отвечает 409, и двум последовательным кликам с паузой в секунду гонка не нужна вовсе. Гонка — частный случай, а не предмет. 3. Деньги на отказном пути целы и это пере-снято, а не процитировано: `attempts=2 reserved=3.319828 balance=6.180172 ledger=6.180172` — второй холд не берётся, кэш и леджер сходятся. ### Что беру и в каком порядке 1. **§4.1, ядро.** Ветка `translating` в `Resume` отвечает ПРОГОНОМ, вырез — `l.StopRequestedAt != nil` (409 `run_not_resumable` + `cause.code: stop_requested`), ветка стоит ВЫШЕ проверки re-pass. Форма: свитч по статусу становится единственным местом, где решает СТАТУС, а проверки, стерегущие ПЕРЕ-ОТКРЫТИЕ (re-pass, `sourceThere`), уезжают под него. Канон §`resumeRun` правится тем же деревом — минор **0.15.0**: меняется строка `translating` таблицы И фраза «a `202` therefore means work was actually reopened», которая уже сегодня неверна (дерево отдаёт 202 проигравшему, `D39.246`). 2. **§4.2 `PD-162`.** Сначала отвечаю на вопрос «а что с процессом» (находка оркестратора из моего эхо-протокола), и только потом решаю, строится ли терминальное состояние. Снятие холда без корректной остановки живого юнита меняет вечный холд на потерянные деньги плюс процесс-сироту. 3. **§4.3 `PD-466` и §4.4 `PD-139`** — решения с доводом; по `PD-139` сначала ЗАМЕР (выводится ли `book_id` из пути), потом диспозиция. Релей оркестратора 11.09: если классов окажется два, это полноценный исход пункта. 4. Адверсариальный проход по своей готовой работе, мутации в копии с каноном. ### Где жду сопротивления - **Пин гонки нельзя оставить в нынешней фикстуре.** Она недостижима для второго порядка, то есть измеряет половину сценария. Нужна фикстура, где ОБА порядка неизбежны, а не вероятны, и оба гоняются много раз. Это правка теста по ЗАКАЗАННОЙ смене поведения (`D39.121`) — объявляю её отдельно. - **Остаток окна §4.1.** `stop_requested` читается в снимке, взятом ДО замка книги, а `Stop` замка книги не берёт вовсе. Назову интервал абзацем; строить под него не буду, пока не покажу, что цена не нулевая. - **Форма (а) меняет ответ re-pass-прогону в статусах `paused`/`failed`/`ready`** — с «a re-pass is bought again» на ответ его собственного статуса. Это КАНОН-конформнее, но смена поведения, и она объявляется, а не проскакивает. ## ПРАВДА У ДВЕРИ — ОТЧЁТ (11.09, `textmachine-8e`) > Предмет: платформа говорила человеку не то, что произойдёт. Три ряда — **`PD-455`** (форма обещает > старт, в котором дверь откажет) · **`PD-162`** (прогон над пропавшим каталогом берёт холд и висит > вечно) · **`PD-448`** (двойной клик по «продолжить»; строить было ЗАПРЕЩЕНО, заказан разбор). > Вход: HEAD `05f690e`, дерево зоны чистое, батарея на входе зелёная и снятая ДО первой правки — > `MAKE-EXIT=0 · 20 пакетов ok · 0 FAIL · линтер 0 issues · 5 скипов`. Сессия не коммитит: дерево > передано оркестратору №23. ### Комплектность против заказа — по пунктам §4 промта | Пункт | Исход | Чем предъявлено | |---|---|---| | §4.1 `PD-455` — форма обязана знать про живой прогон | **сделано** | предикат с двумя привязками + `blocked.code: run_in_flight`; пины; живая проба на стенде | | §4.2 `PD-448` — исследуй и предложи, НЕ строй | **разбор, не строил** | замер воспроизводимости, три пути с ценой, рекомендация, расхождение канона отдельным пунктом | | §4.3 `PD-162` — отказ ДО денег | **сделано у ОБЕИХ денежных дверей**, ряд остаётся открытым остатком | пины с контрольным прогоном; живая проба; ряд пере-написан | | §4.4 интейк-гигиена | **не делал** (подписанный пропуск) | одна строка про следующий предмет `PD-175` — ниже | | §4.5 якоря регистра | **сделано**, и их оказалось не четыре, а пятнадцать | `counts.py --lint` до и после | | §5 самопроверка исполнением | **сделано** | батарея · `make vuln` · мутации на изолированной копии · живая проба · адверсариальный проход | ### `PD-455` — одно определение, две привязки Правило допуска по фактам КНИГИ стояло инлайном в `Start` тремя проверками (статус · материализованное дерево глав · живой прогон), и форма заказа о них не знала: вердикт считался из остатка книги и баланса, а `ErrRunInFlight` жил только у двери. Человек видел `covers_all`, жал и получал 409. Сделано: три проверки вынесены в **один предикат** `platform/internal/runs/runs.go:666`=`func startable`, у него две привязки РАЗНОЙ авторитетности — `Start` зовёт его под замком книги (решает), `Order` зовёт на опрашиваемом пути (`runs.go:304`, совещательно и заведомо устаревает). Мерило пака — «сколько ОПРЕДЕЛЕНИЙ придётся тронуть, если правило изменится» — выполнено: одно. Три решения, которые я принял внутри этой свободы, и довод каждого: 1. **`PricedBook` НЕ расширен.** Рядом с ним стоит письменное возражение (`platform/internal/pgstore/books.go:1476`: «It is a second read beside ReadBookForRun rather than more columns on it, because the two answer different questions of different shapes»). Форма делает ВТОРОЕ чтение — `ReadBookForRun`, ту самую запись, которой судит дверь. Возражение этим исполнено, а не переступлено: две разной формы записи остаются двумя, и определение по-прежнему одно. Цена, которую я принял: +1 запрос на КАЖДЫЙ опрос формы (рядом уже три чтения, одно из них с пер-главным агрегатом по `units`). Это моё суждение, а не замер: бенчмарка я не снимал. 2. **`verdict` не тронут.** Он отвечает про ДЕНЬГИ, и баланс книгу действительно покрывает; четвёртое значение закрытого enum сломало бы генерённых клиентов. Рост пошёл через `Blocked.code`, который канон предавторизует. 3. **Порядок двух причин в `blocked`.** Член один, кандидатов два, и они не равны: свой живой прогон — это «старта нет вовсе», чужой холд — «шкала короче, чем мог бы позволить баланс». Свой прогон выигрывает: сказать второе, когда верно первое, значит отправить человека останавливать ЧУЖОЙ прогон, после чего клик всё равно откажет. Пин: `httpapi.TestARunOfThisBooksOwnOutranksAnotherBooksHold`. Контракт: минор **0.14.0** ратифицирован оркестратором (`D39.244`) по моему пингу — `Blocked.code` получил значение `run_in_flight`, смысл схемы расширен с «что держит шкалу короче» на «почему старт не предлагается так, как обещал бы один баланс», `book_id` при этой причине — ЭТА ЖЕ книга. Константа `httpapi.ContractVersion` переведена на `0.14.0` тем же деревом; гейт `gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` зелёный. ### `PD-162` — отказ до денег, и вторая дверь, которой ряд не называл Проверка каталога книги стоит на допуске ДО холда (`runs.go:707`=`func (s *Service) sourceThere`, зовётся из `Start` после предиката книги и перед чтением счёта). ⚠ **Трудности, которую промт объявлял главной, действительно нет** (§4.3 промта): каталог создаётся на интейке ДО строки в БД, а исключение первого прогона принадлежит ЖУРНАЛУ внутри каталога (`journalSize` мапит ENOENT в нулевой офсет) — так что `os.Stat(workdir)` на допуске не отвергает ничего законного, и никакой ветви-исключения я не строил. **Что я нашёл сверх заказа: резюм — вторая денежная дверь, и клин там тот же.** Измерено исполнением ДО правки: тест `runs.TestAResumeOverAMissingDirectoryIsRefusedBeforeTheHold` на дереве без гарда дал `a resume over a missing directory answered , want ErrSourceGone` — то есть резюм над пропавшим каталогом ВОЗВРАЩАЛ прогон и брал холд остатка бюджета. Оркестратор подтвердил, что это в границах пака («предмет `PD-162` — прогон, который никогда не поедет, а не конкретная ручка»), и гард стоит теперь и там (`platform/internal/runs/reconcile.go:1721`, перед `reopen`). **Вина книги и вина хоста разведены и отвечают РАЗНЫМИ словами** — это урок `PD-192`, который зона уже оплатила один раз. Итоговое правило (третья и четвёртая строки приехали дофиксом по ревью): | что с каталогом | ответ | почему так | |---|---|---| | его нет, книга ПОД корнем интейка, сентинел на месте | `ErrSourceGone` → 409 `book_not_ready` + `cause: source_gone` (на резюме — 409 `run_not_resumable` + та же причина) | это и вправду конец этой книги, и ожидание не поможет — так причина и говорит | | его нет вместе с СЕНТИНЕЛОМ корня (форма размонтирования) | `ErrStorageUnavailable` → 503 | вина ХОСТА; сказать здесь «ваша книга непригодна» — ровно тот дефект, который в интейке отклонил все книги тома | | его нет, а книга лежит ВНЕ корня интейка (`tmplatformctl book add --workdir`, инстанс без `BooksDir`) | `ErrStorageUnavailable` → 503 | сентинела на том томе нет вовсе: чужой здоровый маркер уликой про этот том не является, и платформа не объявляет конец книги на основании каталога, которого не создавала | | путь ЕСТЬ, но это не каталог (файл, симлинк на файл) | `ErrStorageUnavailable` → 503 | `os.Stat` на файле успешен, движок открывает каталог; писала этот файл не платформа | | путь есть и НЕ ЧИТАЕТСЯ (права, I/O) | `ErrStorageUnavailable` → 503, в сообщении операция и errno БЕЗ пути | ошибка уходит в ERROR-строку, а норма зоны §2 запрещает id книги в логах | Предикат корня не скопирован, а **вызван**: `books.storageIsThere` стал экспортируемым `platform/internal/books/books.go:606`=`func StorageIsThere(booksDir string) bool`, а «книга под нашим корнем?» — `books.go:594`=`func Owns(booksDir, dir string) bool`; методы остались обёртками в одну строку. Копия была бы вторым экземпляром правила, у которого первая редакция стоила зоны всех книг хоста — и мутация, ломающая `StorageIsThere`, красит не только мой тест, но и ДВА чужих пина `PD-192` в `internal/books`, что и есть доказательство, что предикат один. **Ряд остаётся ОТКРЫТЫМ, и остаток пере-написан честно:** класс шире удаления каталога — прогон, УЖЕ живущий, который не поедет никогда (запинен к старой сборке движка, без `reserved_usd`, беспризорный деплой), по-прежнему висит с холдом до прихода человека. Это половина эскроу (`П-18`), автоматического терминального вердикта у реконсилятора нет и он не строился. ### `PD-448` — разбор, а не стройка ⚠ Ниже — ТОЛЬКО разбор; кода по этому ряду я не написал ни строки. **Механика, снятая чтением (адреса пере-сняты сегодня).** `Resume` читает состояние прогона ДО замка книги: `platform/internal/runs/reconcile.go:1702`=`s.Store.ReadRunForResume`, затем `reconcile.go:1663`=`unlock, err := s.lockBook(ctx, l.BookID)`, и только потом судит по `l.Status` (`reconcile.go:1696`) — по значению, прочитанному ДО замка. Из этого следуют два разных проигравших: * **проигравший ВНУТРИ окна** (его чтение случилось до коммита победителя) держит устаревший `stopped`, доходит до `reopen`, его вставка `run_attempts` падает на уникальном индексе `run_attempts_run_id_attempt_no_key` → `ErrNoRun` → `Resume` отвечает **прогоном** (`reconcile.go:1736`=`return s.Store.ReadRun(ctx, userID, runID)`), и холд не берётся: транзакция откатывается целиком; * **проигравший СНАРУЖИ окна** (его чтение случилось уже после коммита победителя) видит `translating`, падает в `default:` и получает `ErrNotResumable` — «the run cannot be continued: it is translating». **Вот это и есть дефект ряда.** ⇒ **Окно дефекта — НЕ гонка за индекс, а интервал между чтением состояния и взятием замка.** И отсюда же объяснение, почему изолированно тест зелёный, а в полном пакете под нагрузкой красный: замок книги (`platform/internal/runs/bank.go:400`=`func (s *Service) lockBook`) — ВНУТРИПРОЦЕССНЫЙ мьютекс, так что два резюма одного демона идут друг за другом, и под голоданием по процессору горутины стартуют дальше друг от друга — проигравший чаще успевает прочитать уже переведённое состояние. Формулировка в шапке теста («both calls pass the state check together and race for attempt N+1») описывает состояние ДВУХ экземпляров демона; в одном процессе это последовательность, а не гонка. **Воспроизводимость — замер сегодня, субагентом, инструментом и с числом прогонов** (вывод в `scratchpad/pd448/`, 67 файлов): | что запускал (всё — `go test ./internal/runs/ -race`) | раз | красных | контроль | |---|---|---|---| | изолированно `-count=10 -run '^TestTwoResumes…$'`, load 0.17 | 10 | **0** | в выводе 10 строк `=== RUN` и 10 `--- PASS` с этим именем, других `---` нет | | полный пакет на простаивающей машине, `-count=1` | 3 | **0** | в каждом прогоне 183 верхнеуровневых теста, целевой `--- PASS` = 1, `--- FAIL` = 0 | | `GOMAXPROCS=1` (`-cpu 1`) | 5 | **0** | в каждом `=== RUN` = 1, `--- PASS` = 1 | | **под нагрузкой** (32 CPU-жруна на 8 ядрах, load 29–50), подмножество `TestTwoResumes\|TestResumeIsRefused` | 20 | **4** | `--- PASS` 16, `--- FAIL` 4 | | полный пакет под нагрузкой | 4 | **не измерено** | все четыре уперлись в `-timeout 9m` на ДРУГИХ тестах; целевой в каждом успел пройти зелёным до обрыва | Текст всех четырёх падений ОДИН и тот же, и он читается, а не считается цветом: `control_test.go:898: resume 1: runs: the run cannot be continued: it is translating, want the run back` — то есть падает ровно утверждение «проигравший обязан ответить прогоном», и источник ошибки — ветка `default` из разбора выше. ⚠ **Запись ряда «полный пакет упал 2 из 2» сегодня НЕ пере-проверена:** полный пакет под нагрузкой ни разу не дошёл до конца, и это названо «не измерено», а не «не воспроизвелось». ⚠ Замер шёл на МОЁМ дереве (гард каталога в `Resume` уже стоял). Путь гонки он не трогает, а если и влияет, то в сторону РЕЖЕ: лишний `os.Stat` внутри замка отодвигает коммит победителя, то есть чаще оставляет проигравшего в «хорошей» ветке. Числа выше поэтому — нижняя граница, а не верхняя. **Чего сегодня нет, чтобы различить два случая.** Ни `LiveRun`, ни `run_attempts` не несут ни заявителя, ни ключа идемпотентности: колонки таблицы — `id · run_id · attempt_no · engine_run_id · started_at · ended_at · exit_code · last_seq` (`platform/internal/pgstore/migrations/00002_readmodel.sql:72-87`, и ни один из четырёх поздних альтеров этого не добавил). То есть у проигравшего состояние побайтово то же, что у законного резюма часового прогона. **Три пути промта, с ценой каждого:** | путь | что делает | цена | закрывает ли предмет | |---|---|---|---| | **(а)** `Idempotency-Key` на `resumeRun` | второй клик под ТЕМ ЖЕ ключом реплеит ответ первого | контрактный минор (параметр операции) + клиент обязан прислать один ключ на оба клика | **не полностью:** при ОДНОВРЕМЕННЫХ кликах второй получает `409 key_in_flight` + `Retry-After` (`platform/internal/httpapi/idempotency.go:110`), то есть снова ошибку; прогоном он ответит лишь на ПОВТОРЕ | | **(б)** claim-токен или колонка в транзакции рестарта | записать, КТО и чем открыл попытку, и дать проигравшему это увидеть | **миграция** + суждение по временному окну («открыто секунду назад резюмом») | да, но окно — это лотерея под другим именем | | **(в)** перенести чтение состояния ПОД замок | убрать интервал | одна строка | ⛔ **делает хуже ДЕТЕРМИНИРОВАННО:** проигравший тогда видит `translating` ВСЕГДА | **Рекомендация — четвёртый путь, которого в промте нет: сделать резюм идемпотентным ПО СУЩЕСТВУ.** Ветка `case "translating"` в том же switch (`reconcile.go:1696`) отвечает **прогоном**, а не отказом — с единственным исключением «стоп уже запрошен» (`l.StopRequestedAt != nil`; поле есть, `platform/internal/pgstore/runs.go:322`), потому что такой прогон сворачивается, а не продолжается, и 202 там был бы ложью. Тогда оба проигравших — и внутри окна, и снаружи — отвечают прогоном, и мерить больше нечего: гонки не остаётся, потому что исходы совпали. Цена: **одна строка кода и одна строка канонной таблицы.** Ни миграции, ни ключа, ни нового состояния. Семантика честная: `POST /resume` означает «пусть этот прогон идёт»; если он уже идёт — требование выполнено, это ординарная идемпотентность записи. **Довод ПРОТИВ, который я вижу сам:** клиент теряет различие «я продолжил» и «оно и так шло», а канон этим свойством дорожит строкой выше — `paused` отвечает 409 именно потому, что 202 был бы «тихим no-op». Разница, на которой я стою: у `paused` НИЧЕГО не переоткрывается и лекарство другое (новый прогон), а у `translating` состояние ровно то, которого просил пользователь. **Чего я НЕ знаю:** как это увидит фронт — у него сегодняшний `409 run_not_resumable` описан таблицей канона, а его кода я не читал (чужая зона). И не знаю, есть ли деплой с ДВУМЯ экземплярами демона: при одном экземпляре внутрипроцессный замок делает «оба прошли проверку вместе» невозможным, при двух — возможным, и тогда путь (г) закрывает и эту форму тоже, а (а) и (б) — нет. **ДИСПОЗИЦИЯ ОРКЕСТРАТОРА №23, пришла в тот же день.** Четвёртый путь ПРИНЯТ как направление, строит его СЛЕДУЮЩИЙ платформенный пак — не этот. Довод принятия оказался не тем, с которого я начинал: сомнение было в том, что идемпотентный `202` ломает канонную гарантию «202 = работа реально пере-открыта», а проверка показала, что зона её уже НЕ держит — дерево отдаёт `202` проигравшему, у которого пере-открыл не он, и это закреплено шапкой самого теста. Значит выбор был не между починкой и обходом, а между двумя смыслами глагола. Критерий, по которому это лечение: обход оставляет ответ зависимым от интерливинга и молчит об этом, лечение делает его ИНВАРИАНТНЫМ К ПОРЯДКУ. **Три вещи поехали в ряд `PD-448`, чтобы следующий пак их не потерял** — и каждую я пере-проверил своим прибором, а не принял на слово: вырез «стоп уже запрошен» обязателен (`l.StopRequestedAt`, `pgstore/runs.go:322`; слово `stop_requested` уже на проводе, `httpapi/v0.go:242`); проверка re-pass стоит ДО свитча (`reconcile.go:1687` против `:1696`) и после новой ветки должна пропускаться, иначе двойной клик по re-pass снова получит отказ; и остаток окна, который правка НЕ закрывает — `stop_requested` читается в том же незамкнутом снимке, а `Stop` замка книги не берёт вовсе (первый же его оператор — `RequestStop`), так что стоп между снимком и ответом даст один `202` на сворачивающийся прогон: один кадр, денег не трогает. Канон при этом НЕ двинут осознанно: строка таблицы — минор `0.15.0`, и бамп сегодня покрасил бы гейт версии на сданном дереве, где константа несёт `0.14.0`. ### ⚠ Расхождение канона и дерева — отдельным пунктом (правит оркестратор) **1. `translating` у `resumeRun`.** Канон обещает `409 run_not_resumable` («nothing; it is already running» — таблица §`resumeRun` в `docs/architecture/14-api-contract/openapi.yaml`), а отгруженный код проигравшему гонки за уникальный индекс возвращает ПРОГОН, 202 (`reconcile.go:1736`). То есть одна ручка отвечает на одно состояние двумя разными способами в зависимости от того, где её застали. Это расхождение СТАРШЕ моего пака и им не создано; рекомендация (г) как раз и сводит обе стороны к одному ответу. **2. Отказ резюма над пропавшим каталогом — ввёл Я, называю прямо.** `Resume` теперь может ответить `409 book_not_ready` + `cause: source_gone` (`reconcile.go:1721`), а таблица §`resumeRun` перечисляет ответы по СТАТУСУ прогона и такого отказа не знает. Генерённого клиента это не ломает: операция объявляет общий `409 Conflict`, а `book_not_ready` — законное значение `ErrorCode`, которое эта же поверхность уже отдаёт на соседней ручке. Но текст канона неполон, и строка таблицы нужна. ### Одна строка про `PD-175` (§4.4) Следующим предметом ряда я считаю **ВЫХОД ИЗ `rejected`**, а не свип сирот и не потолок аплоадов. Довод: у отклонённой книги сегодня выхода нет ВООБЩЕ — единственный переход в `not_started` стоит в `platform/internal/pgstore/books.go:360` и требует `status = 'parsing'`, то есть перепарса не существует, а ручки удаления нет в контракте вовсе (`PD-122`). Такая строка живёт в библиотеке человека навсегда, и цена этого — доверие на КАЖДОЙ неудачной загрузке, тогда как сироты и потолок диска стоят операторского диска, который в закрытой бете ограничен её же размером. Свип и ретеншен при этом остаются за гейтом открытой регистрации (`D39.176` п.1), а счётный потолок аплоадов — это квота под другим именем, и он запрещён. ### Якоря регистра (§4.5): их было не четыре, а пятнадцать Промт называл три уехавших якоря в зоне плюс четвёртый, линтером не ловимый (без токена, внутри `PD-162`). Все четыре починены. **Остальные одиннадцать убили МОИ СОБСТВЕННЫЕ переезды строк** — восемь на первом круге правок и ещё три после дофикса по ревью, — и это ровно то правило зоны, по которому «якорь, убитый твоим переездом, чинит тот, чей переезд его убил». Прибор до и после: `python3 docs/scripts/counts.py --lint` из корня репозитория — **23 проблемных якоря → 12**, и группа «по КОРНЮ ЦЕЛИ … internal 8» из выдачи исчезла целиком. ⚠ Двенадцатый живой ✗ — **не мой к починке**: он лежит в `docs/architecture/05-decisions-log.md:2661` (зона оркестратора) и указывает В мою: `platform/internal/runs/runs.go:451` → `:479`. Убил его мой переезд, поэтому адрес назван ему пингом, а файл я не трогаю. ⚠ И один урок про сам прибор: якорь БЕЗ токена (`path:line` без `=`) линтер не проверяет вовсе, так что мои собственные такие ссылки (`v0.go:610` в ряду `PD-455`) уехали МОЛЧА и были пере-сняты рукой. Ставя новый якорь, ставь токен. ### Находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО | находка | что сделано | чем предъявлено | |---|---|---| | форма заказа не спрашивала правило допуска (`PD-455`) | один предикат `startable`, две привязки; на проводе `blocked.code: run_in_flight` | пины (ниже) + живая проба на стенде: `blocked":{"code":"run_in_flight","book_id":"bk_7JDPZD3S7J22XS3I"}` и 409 на втором клике | | **резюм — ВТОРАЯ денежная дверь с тем же клином** (ряд её не называл) | гард `sourceThere` и там, перед `reopen` | до правки тест дал `a resume over a missing directory answered , want ErrSourceGone`; после — зелёный 8/8 | | **мой собственный `fmt.Errorf` унёс бы путь книги в ERROR-лог** (класс `PD-139`) | путь выброшен, оставлены операция и errno | пин `runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath`; в логе стенда `tmstand-truth` встречается 2 раза, обе — эхо конфига на буте | | мои переезды строк убили 8 чужих якорей (3 были сломаны до меня) | пере-снял все 11 в своей зоне + 12-й без токена внутри `PD-162` | `counts.py --lint`: 23 → 12 проблемных, группа «internal 8» исчезла | | 12-й якорь моего переезда живёт в ЧУЖОЙ зоне | НЕ правлю, сообщаю адрес оркестратору | `docs/architecture/05-decisions-log.md:2661`: `platform/internal/runs/runs.go:451` → `:479` | | `PD-455` покинул класс тревог гейта регистра | id убран из `alarmBaseline` с причиной, ТЕМ ЖЕ деревом (инструкция самого гейта) | `ALARM PD-count: 14 … (baseline 14)`, строка `ALARM PD-455 LEFT` исчезла, тест зелёный | | форма молчит про ПРОПАВШИЙ каталог | НЕ чинил, завёл ряд `PD-466` с доводом, почему опрос — не место для `os.Stat` | ряд в регистре | | носитель числа скипов батареи (`STACK_DECISIONS` §«Гейты батареи») называет не все условия | дописал два измеренных условия | два прогона одной смены назвали РАЗНЫЕ условия для одного и того же теста — оба вывода в отчёте ниже | | **рецепт стенда называл сторону НАОБОРОТ**: «относительные `pipeline:`/`models:` — отличие ШАБЛОНА», хотя относительные они в ОРИГИНАЛЕ, а в шаблоне обязаны быть абсолютными | сторона названа явно, добавлена проверка на месте и цена ошибки | замерено: рабочий шаблон стенда несёт АБСОЛЮТНЫЕ пути (строки 20–21), и с ним `books.TestTheRenderedConfigurationIsOneTheEngineActuallyLoads` зелёный против свежесобранного `tmctl`; по этой строке приёмка получила восемь красных живых проб, которые были дефектом шаблона, а не дерева | ### Какой тест что пинит (норма зоны §3 п.4) | свойство несущего пути | пин | |---|---| | форма несёт отказ двери про живой прогон, и дверь отвечает то же | `runs.TestTheFormSaysWhatTheDoorWillRefuseOverABookThatIsAlreadyRunning` | | на проводе это `blocked.code: run_in_flight` с `book_id` ЭТОЙ книги | `httpapi.TestTheFormCarriesTheRunInFlightThatWillRefuseTheClick` | | свой живой прогон важнее чужого холда в одном члене `blocked` | `httpapi.TestARunOfThisBooksOwnOutranksAnotherBooksHold` | | старт над пропавшим каталогом отказывает ДО холда | `runs.TestABookWhoseDirectoryIsGoneIsRefusedBeforeTheHold` | | резюм над пропавшим каталогом отказывает ДО холда | `runs.TestAResumeOverAMissingDirectoryIsRefusedBeforeTheHold` | | размонтированный том — вина деплоя, и книгу она не винит | `runs.TestAVanishedBooksVolumeIsTheDeploymentsFaultAndNotTheBooks` | | нечитаемый каталог — тоже деплой, и путь книги в ошибку не попадает | `runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath` | | два отказа выходят на провод по-разному (409+`source_gone` против 503) | `httpapi.TestAVanishedSourceAndAVanishedVolumeAnswerDifferently` | | объявленная версия контракта = ратифицированный канон | `gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` (существующий) | ### Числа, снятые ПОСЛЕ последней правки * **Батарея с ЧЕТЫРЬМЯ гейтами** (`TM_PLATFORM_TEST_DSN` на стендовый Postgres · `ENGINE_BIN` собран из ТЕКУЩЕГО `backend/` по `PD-432` · `BOOK_TEMPLATE` стенда · достижимый пользовательский systemd): `MAKE-EXIT=0` · **20 пакетов `ok` · 0 `FAIL`** · линтер `0 issues` · `sqlc diff` чисто · 5 скипов. (Перечислены все двадцать: `cmd/tmplatformctl` `cmd/tmplatformd` `auth` `backup` `books` `config` `exports` `gates` `httpapi` `ingest` `jobs` `login` `metrics` `money` `pgstore` `pricing` `readmodel` `reqid` `runner` `runs` — «зелёная батарея» без полного списка ничего не значит.) * **`make vuln`** (отдельная цель, в `check` не входит, блокер лендинга по §2 стандарта): `No vulnerabilities found.`, `VULN-EXIT=0`. * ⚠ **Оба числа ПЕРЕ-СНЯТЫ ПОСЛЕ ДОФИКСА по верификатору** (дофикс тронул `internal/gates` и `cmd/tmplatformd`, то есть код, а не только доки): батарея снова `MAKE-EXIT=0 · 20 ok · 0 FAIL · линтер 0 issues · 5 скипов`, `make vuln` снова `No vulnerabilities found.`. Числа совпали с до-дофиксными — но это ИЗМЕРЕНО, а не унаследовано: правка кода после снятых чисел обнуляет их независимо от того, насколько она мала. * **Скипы: 5, и условий у них ДВА, а не одно** — это отдельная находка сегодняшнего дня. Четыре пробы `internal/runner` скипаются словами «this deployment's pipeline enables the bank contour and names `<репо>/backend/configs/mining-contrast.zh.txt`, which is not on this host» (артефакт чужой зоны, его отсутствие — не поломка платформы). Пятая, `runner.TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`, умеет скипнуться ВТОРЫМ условием — `listen tcp 127.0.0.1:11434: bind: address already in use`, — и два честных прогона одной смены назвали для неё РАЗНЫЕ условия. Носитель числа (`STACK_DECISIONS` §«Гейты батареи») знал ноль из двух; оба дописаны туда же вместе с предупреждением, что по одному прогону условия не считают. * **Денежные пины не флейковые:** четыре теста × 8 прогонов под `-race` = **32 строки `--- PASS`, 0 `--- FAIL`** (`go test ./internal/runs/ -race -count=8 -run '<четыре имени>'`). * **Живая проба на стенде** — свой демон (порт 18080, свой `BOOKS_DIR` под `$HOME`, своя база `tmstand_truth`), книга заведена ЖИВЫМ интейком через `tmplatformctl seed`, и порт сверен с PID своего демона перед сидом (шаг 3а рецепта). ⛔ `TM_PLATFORM_BOOKS_DIR` ни секунды не смотрел на `<репо>/books`; проверка стоит в самом скрипте стенда, а не в намерении. - форма над книгой в покое: `"blocked":null`, `verdict":"covers_all"`; - первый клик: `202`, баланс 25.000000 → 22.862733 (холд взят); - форма над той же книгой С ЖИВЫМ ПРОГОНОМ: `"blocked":{"code":"run_in_flight","book_id":"bk_7JDPZD3S7J22XS3I"}` — то есть **ровно то, что дверь и ответила** вторым кликом: `409 run_in_flight`; - каталог книги удалён (руками, внутри своего стенда) → `409 book_not_ready` + `cause: source_gone`, **баланс 25000000 до и 25000000 после** — холд не взят; - сентинел `.tmplatform-books` убран (форма размонтирования) → `503 service_unavailable`, баланс тот же, и в логе ERROR-строка `run refused: this deployment cannot start runs`; - ⚠ **Живая движковая половина на этом хосте ДОСТИЖИМА, и «5 скипов» читать как «сюда не добраться» нельзя:** приёмка №23 прогнала пробы `internal/runner` с шаблоном, чьи `pipeline:`/`models:` абсолютные, и получила **42 PASS · 0 FAIL** при тех же пяти скипах; условие одно — артефакт контраста, строка бэклога **251**, чужая зона. Числа приёмки, не мои. - ⚠ $0: движок на стенде провайдерских ключей не имеет (`.env` рядом с `book.yaml` нет, ключей в окружении 0), прогон честно умер `exit-code`, и холд вернулся сам. ### Мутационные посадки — 18 штук, выживших НЕТ Изоляция по зонному стандарту §3 п.3: копия взята ВМЕСТЕ с каноном (`cp -a --parents platform docs/architecture/14-api-contract <куда>/`), перед каждой правкой скрипт утверждает `test -f go.mod` и что путь — копия, а не дерево. **База копии зелёная** (`internal/runs` ok 57 с · `httpapi` ok 2.8 с · `gates` ok 0.46 с · `books` ok 25 с, exit 0), и вердикты судятся по ДЕЛЬТЕ против неё. ⚠ Первый прогон базы упёрся в `-timeout 20m` на `internal/books` — под ЧУЖОЙ нагрузкой (load 46, соседний замер), — и скрипт ОТКАЗАЛСЯ судить мутации против красной базы. Числа ниже со второго прогона. Та же ловушка поймала и адверсариальный проход: его первая батарея была красной по таймауту трёх пакетов из-за параллельной батареи, а не из-за дерева. | # | что сломано | падает | ТЕКСТ падения (по нему и засчитано) | |---|---|---|---| | 1 | дверь перестаёт спрашивать про живой прогон | форма | `the form says over a book that is being translated, want the door's run_in_flight` | | 2 | форма перестаёт НЕСТИ ответ двери | форма | то же сообщение с другой стороны | | 3 | провод предпочитает чужой холд своему прогону | `httpapi` ×1 | `blocked = map[book_id:bk_other code:credit_held], want this book's own run` | | 4 | `blocked` называет чужую книгу | `httpapi` ×2 | `blocked.book_id = , want the book being asked about` | | 5 | допуск перестаёт смотреть на каталог | `runs` ×5 | `a start over a missing directory answered , want ErrSourceGone` | | **6** | **каталог смотрится ПОСЛЕ холда, а не до** | `runs` ×5 | **`a refused start moved money: {Balance:10000000 Reserved:0} -> {Balance:6180172 Reserved:3819828}`** — денежный пин не вакуумен: на кону были настоящие $3.819828 | | 7 | резюм перестаёт смотреть на каталог | `runs` ×1 | `a resume over a missing directory answered , want ErrSourceGone` | | 8 | пропавший ТОМ свалить на книгу | `runs` ×2 | `answered … its source directory is not on this deployment, want ErrStorageUnavailable` | | 9 | пропавший КАТАЛОГ свалить на деплой | `runs` ×2 | `answered … the book storage is not mounted on this deployment, want ErrSourceGone` | | **10** | **маркер СВОЕГО корня читается как улика про ЛЮБОЙ том** | `runs` ×1 | `a start over a book outside the root answered … not on this deployment, want the deployment's` — пин находки ревью | | 11 | сентинел хранилища перестаёт спрашиваться | `runs` ×1 **плюс два ЧУЖИХ пина `PD-192`** в `internal/books` | `the host is at fault, not the book` | | 12b | путь, который не каталог, проходит дверь | `runs` ×1 | `a start over a FILE in the book's place answered runs: stat journal: …` | | 13 | пропавший источник перестаёт быть видом `book_not_ready` | `runs` ×1 | `the refusal is not a book_not_ready: runs: its source directory is not on this deployment` | | 14 | причина `source_gone` замолкает у СТАРТА | `httpapi` ×1 | `cause = , want "source_gone" — without it the client offers waiting` | | 15 | РЕЗЮМ отвечает словами старта | `httpapi` ×1 | `code = book_not_ready, want "run_not_resumable"` | | **16** | **оба новых значения провода переименованы** | `httpapi` ×4 **и гейт** | `this build serves blocked.code: runInFlight and the canon's enum does not carry it: [credit_held run_in_flight]` — до дофикса эта посадка ВЫЖИВАЛА | | 17 | объявленная версия контракта уезжает от канона | `gates` ×1 | `this build announces contract 0.13.1 and the ratified canon is 0.14.0` | **Итог: 18 посадок (17 + одна пере-посаженная), 17 пойманы, 0 выживших**, и каждая засчитана по ТЕКСТУ падения, а не по цвету. ⚠ **Четыре посадки в ПЕРВОЙ редакции не собрались** — снятие блока оставляло неиспользованными `facts`, импорт `books`, переменную `st`, — и «ни один тест не упал» там означало «пакет не собрался», то есть НЕ ИЗМЕРЕНО, а вовсе не «дыра». Пере-посажены так, чтобы менялось РЕШЕНИЕ, а идентификаторы оставались в деле, и только тогда засчитаны. Это тот самый случай, когда вердикт по неправой причине отличается от поимки лишь тем, прочёл ли кто-нибудь текст. ⚠ **Посадка 11 — улика «предикат ВЫЗВАН, а не скопирован»:** ломая `books.StorageIsThere`, она красит и мой тест, и ДВА чужих пина `PD-192`. Копия так бы не покраснела. ⚠ **Посадка 1 показала границу:** снятие `HasLiveRun` из предиката красит только ФОРМУ — дверь по-прежнему отвергает вторую покупку, потому что за ней стоит частичный уникальный индекс `runs_one_live_per_book` (`pgstore.StartRun`, ветка `isUnique`). У двери есть второй рубеж, у формы его не было — это и есть предмет `PD-455` одной фразой. ### Греп ОТКРЫТЫХ рядов регистра по СВОИМ ПОЛНЫМ путям (норма зоны §3 п.8) Прибор: греп по полным путям семи тронутых файлов с разбором таблицы регистра — **34 открытых ряда** цитируют мои файлы. Диспозиции: * **тронуты механизмом — четыре:** `PD-455` закрыт · `PD-162` сужен, остаток пере-написан · `PD-448` разобран и остаётся открытым · `PD-466` заведён этим паком. * **назван, но не закрыт — один:** `PD-139` (путь каталога книги утекает в ERROR-логи внутри обёрнутых ошибок). Моя новая ветка МОГЛА стать его третьим носителем и не стала — путь выброшен из ошибки, пин `runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath`. Сам ряд я не лечил: два его существующих носителя — тейлер и обёртки спавна — мой дифф не трогает. * **`PD-175`** — §4.4, одна строка выше. * **остальные 28 — совпадение по ИМЕНИ ФАЙЛА, а не по предмету** (реконсилятор, свип, телеметрия, интейк, читающая модель). Проверяемо: в `reconcile.go` мой дифф — ОДИН хунк на 8 строк (`git diff -- platform/internal/runs/reconcile.go | grep -E '^@@'` даёт ровно одну строку, `@@ -1713,6 +1713,14 @@`), в `books.go` — экспорт одной функции, в `runner.go` — одно поле конфигурации. Оставлены открытыми с этой причиной. ### Что сказал адверсариальный проход (author≠reviewer, исполнением) Направленный второй читатель по пяти осям §5 промта, со своей копией дерева и своими посадками. **Девять находок, четыре существенные, и три из них я сам не видел.** Что сделано с каждой: | находка прохода | вердикт | что сделано | |---|---|---| | **F3.** `sourceThere` спрашивал сентинел КОРНЯ ИНТЕЙКА про любую книгу — а книга от `tmplatformctl book add --workdir` живёт на томе, которого платформа не писала. Проход показал пробой: том такой книги пропал, корень интейка здоров ⇒ ответ был «конец КНИГИ». **Это `PD-192` заново, моими руками** | принята целиком | сентинел спрашивается только под своим корнем (`books.Owns`); где спрашивать нечего — ответ деплоя. Пин `runs.TestABookOutsideTheIntakesRootIsNotDeclaredDeadByAnotherVolumesMarker` | | **F2.** Оба новых значения провода (`run_in_flight`, `source_gone`) не были запинены НИЧЕМ: все мои ассерты сравнивали провод с той же Go-константой. Проход переименовал обе — **вся батарея осталась зелёной** (класс `PD-1`) | принята целиком | ассерты переписаны на ЛИТЕРАЛЫ + гейт со ВТОРЫМ независимым источником: `gates.TestTheBlockedVocabularyServedIsTheOneTheCanonEnumerates` читает enum `Blocked.code` из канона и сверяет в обе стороны | | **F1.** Отказ резюма выходил словом СТАРТА (`book_not_ready` на `runId`), которого таблица §`resumeRun` не знает, и не был покрыт ни одним тестом | принята | резюм отвечает своим словарём — `409 run_not_resumable` + `cause: source_gone`; это форма, какой канон уже пользуется для двух осей, перекрывающих таблицу. Пин `httpapi.TestAResumeOverAGoneSourceIsRefusedInTheResumeHandlesOwnWords`. Строка канонной таблицы всё равно нужна — пункт оркестратору | | **F5.** «Одно определение» верно для предиката, но отображение «ответ предиката → член провода» — вторая рукописная вещь с МОЛЧАЛИВЫМ хвостом: новый отказ завтра оставит форму с `covers_all` | принята | хвост стал громким: незнакомый отказ пишет ERROR-строку оператору. Компайл-тайм это не ловит (предикат отвечает `error`), и я это говорю, а не прячу | | **F8.** `os.Stat` успешен на ФАЙЛЕ: путь, который есть, но не каталог, проходил дверь | принята | `IsDir()`, ответ деплоя; пин `runs.TestAFileWhereTheBooksDirectoryShouldBeIsRefusedToo` | | **F9.** Фраза «the wire this build serves is the 0.13.1 shape» осталась в настоящем времени над константой `0.14.0` | принята | время исправлено | | **F4.** Свойство «путь книги не течёт в ERROR» запинено у `sourceThere` и сломано строкой позже на том же вызове: `journalSize` (`internal/runs/spawn.go:301`) отдаёт `*fs.PathError` целиком, и он уходит в `default:` → 500 | **не лечил, назвал** | это носитель ряда `PD-139`, у которого СВОЯ диспозиция («решение, а не побочный эффект»); я записал в ряд и своё решение по своей ветке, и второй носитель адресом. Класс закрыт на ОДИН носитель из двух, и в отчёте это сказано так | | **F6.** `bank.go` несёт посимвольную копию первой ветки предиката | **не лечил, назвал** | копируется не ПРАВИЛО, а предложение: сам предикат `readyToTranslate` там и вызывается, а живой-прогон половина у двери банка законно ДРУГАЯ (исключение `awaiting_bank`, ратифицированное находкой P9). Сводить их значило бы либо сломать исключение, либо втащить внутрь флаг «я дверь банка» | | **F7.** Реконсилятор при своём респавне каталог не спрашивает, и окно между проверкой и спавном остаётся | **граница, а не дефект** | ровно то, что абзац «ОТКРЫТЫМ остаётся» ряда `PD-162` и говорит; половина живого прогона — эскроу (`П-18`) | ⚠ **Проход также подтвердил три моих утверждения исполнением, а не чтением:** автоматического терминального вердикта живому прогону в дельте нет (грепы по `+367` добавленным строкам `reconcile.go`); `PricedBook` не тронут (`pgstore/books.go` вообще отсутствует в `git status`); контракт-первичность соблюдена — канон уехал на 0.14.0 РАНЬШЕ кода, коммитом `ba8542a`. ⚠ И назвал ловушку прибора, которую я знал по чужому опыту, а он встретил сам: его ПЕРВАЯ батарея была красной по таймауту трёх пакетов — из-за параллельной батареи на той же машине, а не из-за дерева; на чистом перепрогоне те же пакеты дали 48 с / 148 с / 123 с. ### Чего НЕ удалось / не измерено (§10) * **Полный пакет `internal/runs` под нагрузкой ни разу не дошёл до конца** — четыре попытки уперлись в `-timeout 9m` на ДРУГИХ тестах (`TestALiftedQuarantineMaterializesTheJournalAgainFromTheCursor`, `TestARestartHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince`, `TestADeadlockDoesNotStopTheProjection`). Значит запись ряда `PD-448` «упал 2 из 2 полным пакетом» сегодня **не пере-проверена**; гонка воспроизведена подмножеством. Это «не измерено», а не «не воспроизвелось». * **Цена второго чтения на пути формы не замерена.** Я утверждаю, что +1 индексный запрос на опрос приемлем рядом с уже стоящим там пер-главным агрегатом, но бенчмарка не снимал — это суждение. * **Ветка «стат каталога упал не-ENOENT» проверена только правами** (`chmod 000` на корне): EIO, залипшее сетевое монтирование и симлинк-в-никуда я не воспроизводил. * **Гонка «каталог удалён МЕЖДУ проверкой и спавном» остаётся** — проверка сужает окно, а не закрывает его; это уже половина живого прогона, то есть эскроу (`П-18`), и её пак не брал. * **Фронт я не читал** (чужая зона), поэтому не знаю, как он сегодня рисует `blocked` и что сделает с новым значением кода; канон обязывает клиента терпеть незнакомое значение, но проверить это я не мог. * **Число скипов батареи снято дважды и дало РАЗНЫЕ условия** для одного и того же теста (см. ниже) — считать условия по одному прогону нельзя, и я не знаю, сколько их всего. ### Что я сам считаю слабым местом сделанного 1. **Форма по-прежнему может обещать старт над пропавшим каталогом** (`PD-466`). Я разрезал правило на «факты БД спрашивают обе площадки» и «диск спрашивает только дверь», и довод у разреза есть (опрос — не место для `os.Stat` зависшего монтирования), но это именно РАЗРЕЗ: половина обещания формы осталась неправдой, и я назвал её, а не закрыл. 2. **`Options.Refusal` — поле типа `error` в структуре-значении.** Оно даёт слою провода решать словарь (что правильно: коды живут в `httpapi`), но это необычная форма, и следующий читатель может принять её за «ошибку чтения формы», а не за «ответ двери». Комментарий это говорит; форма всё равно на любителя. 3. **Обе главные находки прохода — мои СЛЕПЫЕ ПЯТНА, и они одного рода.** Я построил фикстуру `onTheVolume` под свою же картину мира (книга лежит под корнем интейка) и посадил двенадцать мутаций — но ни фикстура, ни каталог мутаций не могли найти случай, которого в моей картине не было: книгу ВНЕ этого корня. И мутировал я ЛОГИКУ, ни разу не тронув СЛОВАРЬ, поэтому «переименуй оба новых значения провода» в мой каталог не попало — а именно эта посадка и выживала. Вывод, к которому я пришёл не сам: свои мутации проверяют то, что автор считает важным, и ровно поэтому второй читатель не роскошь. 4. **Мой предикат не покрывает третью площадку, которая судит те же факты** — дверь правок банка (`internal/runs/bank.go:124-142`) читает `readyToTranslate` и `HasLiveRun` своим кодом, потому что у неё СВОЁ правило (исключение для `awaiting_bank`, ратифицированное находкой P9). Я сознательно её не тронул: свести их в один предикат значило бы либо сломать исключение, либо втащить в предикат флаг «я — дверь банка», после которого «одно определение» становится лозунгом. Но факт остаётся: слово «одно определение» верно для ДВУХ площадок из трёх, и я предпочитаю сказать это прямо. ### Дофикс по верификатору приёмки (11.09, тот же день) — один ВЫЖИВШИЙ мутант и одно тихо-зелёное Проход верификатора по замороженной копии: девять посадок, семь чистых поимок, **один выживший и одно тихо-зелёное**. Оба — вне моей карты находок, оба однострочные, и оба ломали ровно тот механизм, что пак объявил главным. Обе находки я пере-проверил исполнением, прежде чем чинить. 1. **ВЫЖИВШИЙ: единственная проводка `BooksDir` в дверь не была утверждена ничем.** `cmd/tmplatformd/runner.go:39` — единственное производственное присваивание, а соседний тест-свидетель несёт комментарий «Mutation caught: dropping any assignment in runsConfig» и **`BooksDir` не утверждал**. Посадка «снять строку» пережила полную батарею. Цена: с непроведённым корнем `books.Owns("", …)` ложен для КАЖДОЙ книги, и книга, чей собственный каталог пропал, отвечает `503` и операторским «this deployment cannot start runs» вместо `409 book_not_ready`+`source_gone` — то есть путаница `PD-192` возвращается одной выпавшей строкой, молча, при целых деньгах. Запинено своим случаем в `TestTheOperatorsRunnerKnobsReachTheReconciler`; посадка теперь краснеет текстом «BooksDir is "", want the intake's …: the run door cannot tell whose fault a missing directory is». ⚠ И класс тут тот же, что мы записали нормой сегодня: **комментарий теста утверждал ШИРЕ, чем тест делал.** Теперь утверждение и комментарий сошлись — покрыты все одиннадцать полей. 2. **ТИХО-ЗЕЛЁНОЕ В МОЁМ ЖЕ НОВОМ ГЕЙТЕ.** Регулярка `(?s)\n Blocked:\n.*?\n +enum: \[…\]` телом схемы не ограничена: с удалённым из канона enum она лениво дотягивалась до СЛЕДУЮЩЕГО `enum:` и зачитывала `Usage.state` семьюдесятью строками ниже — красное по неправой причине; а если те же два слова положить под соседнюю схему, гейт отвечал **`--- PASS` над каноном, который их больше не ратифицирует**. Чтение теперь ограничено телом схемы (`enumOfSchema`), и у самого читателя есть пин на трёх синтетических документах — `TestTheSchemaEnumReaderStopsAtTheSchemasOwnBody`. Проверено посадкой верификатора на копии: удаление enum из канона даёт теперь МОЙ текст «schema "Blocked" carries no enum», а не чужие значения. ⚠ Горькая деталь: этот гейт я построил ровно как противоядие от своего слепого пятна по сигналу прохода — и построил его с собственным слепым пятном того же рода. Прибор, проверяющий словарь, сам обязан иметь пин; теперь имеет. 3. **Носитель числа скипов нёс ДВА разных числа.** Мой новый блок говорил «5 скипов», а нетронутая строка того же раздела — «ожидание при всех четырёх: … **скипов 0**» (замер 29.08). Читатель, попавший на вторую, объявил бы приёмку при пяти скипах или счёл бы пять поломкой. Разведено по УСЛОВИЯМ: ноль относится к дереву 29.08, где не было ни артефакта контраста (строка бэклога 251), ни занятого локального адреса провайдера; счёт скипов сверяется с блоком, который эти условия называет. 4. **Якорь `PD-448` на сам тест уехал** (`control_test.go:846` → `:881`) — уехал ДО пака и промтом не назывался, починен заодно. Плюс мой собственный `contract_test.go:16` → `:17`, который сдвинул импорт, добавленный этим же дофиксом. ⚠ **Чего в этом списке НЕТ и почему.** Верификатор назвал пятым пунктом отсутствие фразы «работа завершена, править не планирую» в зонном журнале — **фраза там была и есть**, в конце моей секции (на момент проверки — `platform-PROGRESS.md:481`, после этой вставки она уехала ниже; греп по файлу даёт ТРИ хита: сама фраза, её упоминание в этом абзаце и пак 08.09). Проверил прибором, прежде чем «чинить»: приёмку своей работы тоже надо проверять, иначе в журнал уедет вторая копия той же фразы и следующая смена будет гадать, какая из них настоящая. **Работа завершена, править не планирую.** Дерево — 14 путей, все в `platform/` (13 изменённых плюс новый `internal/runs/admission_test.go`), вне зоны не тронуто ничего; `docs/PROGRESS.md`, `docs/experiments/`, `backend/docs/` в дереве — работа соседних смен. Сессия не коммитит: передано оркестратору №23 вместе с тремя пунктами, которые правит он (якорь в журнале решений · строка канонной таблицы §`resumeRun` · расхождение по `translating`, старше этого пака). ## ДОРАБОТКА ПО ИНВАРИАНТУ `D39.240` — ОТЧЁТ (11.09, `textmachine-5c`) > Три предмета: строки **401** (вторая половина), **394**, **392**. Вход HEAD `d61469f`, дерево зоны > на входе чистое. Записка-план ниже по странице; порядок исполнен как объявлен. ### 401 — отметка о расчёте больше не остаётся пустой навсегда Расчёт — ДВА действия: `Settle` закрывает резервацию своей транзакцией, `MarkSettled` ставит `settled_at` вторым оператором. Смерть процесса между ними оставляла леджер верным, а столбец пустым **навсегда**: рабочий список ключуется на ОТКРЫТОЙ резервации, а её уже нет — прогон выпадает из всех списков, которые могли бы к нему вернуться. **Сделано:** отметка не запоминается, а ВЫВОДИТСЯ. `Store.StampSettledRuns` одним оператором доштамповывает прогоны, чьи жизнь и деньги кончились («прогон завершён И у него нет открытых резерваций»), и фаза расчёта зовёт его последним действием прохода. Ни новой колонки, ни нового состояния; «навсегда» стало «до следующего прохода». Число починенного идёт WARN-ом — это факт о деплое (процесс умер посреди расчёта), а не о прогонах. ⚠ **Названная цена:** отметка ставится временем ПОЧИНКИ, а не моментом закрытия денег; точное время живёт в `closed_at` самой резервации. Прогон, починенный здесь, опаздывает штампом не больше чем на один проход. ⛔ **Своя находка адверсариального прохода — своя же правка чуть не стала дороже дефекта.** Запрос фильтрует `finished_at is not null and settled_at is null`, и ни один существующий индекс `runs` ему не помогает: один частичный по `finished_at is null` (противоположная половина), второй по книге. ⇒ починка была бы **последовательным сканом `runs` на КАЖДОМ тике**, а `runs` только растёт. Заведён частичный индекс миграцией `00035`, предикат в предикат: в здоровом деплое множество ПУСТО, потому что обычный путь ставит отметку в том же проходе, — индекс держит ровно то, что оставил сбой. ### 394 — «убили своим грейсом» больше не читается как поломка деплоя **Замерено исполнением, с контрольной посадкой.** Юнит нашей формы (`TimeoutStopSec=3`, процесс ловит SIGTERM и не выходит) даёт маркер `{"result":"timeout","code":"killed","status":"KILL"}`; тот же юнит с процессом, который по SIGTERM выходит, даёт `{"result":"exit-code","code":"exited","status":"5"}`. Тройка `timeout/killed/KILL` и есть «сработал НАШ `TimeoutStopSec`», и прибор эти случаи различает. **Сделано:** `timeout` выведен из группы «поломка деплоя» в `interrupted`. Довод — не вкус, а невыполнение довода самой группы: там написано «the next spawn dies the same way», что верно для OOM (следующий процесс упрётся в тот же лимит) и ложно для убийства по грейсу (следующий процесс никто не останавливает, работа предшественника в чекпоинтах). Контракт при этом НЕ трогается: `interrupted` — существующее значение, и его определение дословно описывает этот случай («retrying IS the remedy, and finished work is not bought again»). ⛔ **Проверено прибором по требованию оркестратора: в эту развилку `timeout` приходит ДВУМЯ ветвями, а не одной.** Замер трёх состояний намерения: намерение СТАРШЕ маркера → `outcome` отдаёт `stopped` и до причины отказа не доходит вовсе; намерения НЕТ → `failed`; намерение МЛАДШЕ маркера → `failed`. Последнее — не угол: часы разные (маркер несёт хостовые, намерение платформенные), и «нажали стоп после убийства» — обычное состояние. Обе достигающие ветви хотят одного ответа, поэтому правка одна, но запинены обе. ⚠ **Чего НЕ сделано и почему** — не переведён в `stopped`. Грейс срабатывает только после того, как остановку у systemd попросили, но при перезагрузке хоста менеджер останавливает ВСЕ юниты, и прогон, не успевший свернуться, получил бы `stopped` и остался мёртвым после штатного ребута. Это ровно асимметрия, ради которой написан `interruptedBySomeoneElse`. ### 392 — гонка имени юнита: ВОСПРОИЗВЕДЕНА, потом закрыта Строка описывала механизм грубее, чем он есть. Бóльшая часть окна уже была закрыта: `reopen` отказывает рестарту при записанном намерении, а подзапрос читает «последнюю не кончившуюся попытку», то есть при любом порядке КОММИТОВ ответ верен. **Настоящая дыра — в изоляции READ COMMITTED:** `RequestStop` был одним UPDATE; когда он ждёт замок строки `runs` (его держит рестарт, пока кончает одну попытку и открывает следующую), Postgres после разблокировки пере-проверяет `WHERE` против НОВОЙ версии строки, **а подзапрос в `RETURNING` считается по снимку начала оператора**. **Предъявлено исполнением на живой БД, в обе стороны:** ``` до правки: рестарт закоммитил "unit-NEW"; RequestStop ответил "unit-OLD"; живой юнит "unit-NEW" после правки: рестарт закоммитил "unit-NEW"; RequestStop ответил "unit-NEW" ``` **Сделано:** `RequestStop` стал транзакцией — `select … for update of r` по строке прогона, затем чтение имени и запись намерения. В READ COMMITTED блокирующее чтение после взятия замка пере-читает последнюю зафиксированную версию, поэтому каждый следующий оператор транзакции видит рестарт, который был в полёте. Имя юнита и намерение принадлежат одному моменту. ⚠ Ожидание замка правкой НЕ добавлено: одиночный UPDATE ждал того же замка. Изменилось не то, ждёт ли вызов, а что он видит, проснувшись. ### Мутация — пять посадок на копии, засчитано по ТЕКСТУ Копия жила в `~/.cache/tm-5c/mut2/platform` (не в общем скретчпаде), перед каждой правкой утверждались `test -f go.mod` и точный `pwd`, после — `diff -rq` с деревом «идентично» и копия удалена. | посадка | что сломано | вердикт и **текст** | |---|---|---| | A (392) | снято блокирующее чтение, назад к одному оператору | КРАСНО: «the stop answered "unit-OLD" while the live unit is "unit-NEW": the caller would signal a unit that is already dead…» | | B (394) | `timeout` возвращён в группу поломок деплоя | КРАСНО ×2: «a "timeout" kill is reported to the client as "service_error", want "interrupted"» и то же во второй ветви | | C (401) | из фазы расчёта убрана починка | КРАСНО: «a run whose money had closed is still carrying no settled mark after a full sweep…» | | D (401) | починка штампует и ЖИВЫЕ прогоны | ⛔ **ВЫЖИЛА** на первом заходе | | D′ (401) | то же, против пере-строенной фикстуры | КРАСНО: «a run that is still LIVE was stamped settled … because it happened to hold nothing» | ⛔ **Выжившая посадка — дефект В МОЁМ СОБСТВЕННОМ ПИНЕ, и он важнее трёх пойманных.** Починка исключает живой прогон ДВАЖДЫ — потому что у него открыт холд, и потому что его жизнь не кончилась. Первое исключение ПРЯЧЕТ второе: моя зеркальная фикстура держала прогон с открытым холдом, поэтому снятие условия «прогон завершён» не меняло ничего. Фикстура пере-строена на состояние, где условия расходятся, и это состояние ОБЫЧНОЕ, а не угловое: **между попытками** — свип рассчитал кончившуюся попытку (резервация закрыта) и ещё не открыл следующую; прогон жив и не держит ничего, и только «кончился ли прогон» удерживает починку от него. На пере-строенной фикстуре посадка краснеет, и краснеет ИМЕННО во второй подветви — то есть первая действительно её прятала. ### Числа, сняты ПОСЛЕ последней правки ``` $ TM_PLATFORM_TEST_DSN=…55433 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 gofmt / go vet / sqlc diff чисты · ALARM PD-count: 15 (baseline 15) — не сдвинут скипы поимённо, условие у всех ОДНО и названное — нет `configs/mining-contrast.zh.txt`: TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses · TestALivePreviewWritesNothingAndALiveApplyWrites · TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing · TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber · TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem ``` ### ⚠ Правка ЧУЖОГО теста, вызванная заказанной сменой поведения — объявляю по `D39.183` `TestWhyARunFailedDecidesWhetherARetryIsWorthOffering` (`internal/runs/reconcile_test.go`) держал ряд «killed by the stop timeout → `service_error`». После правки 394 он честно покраснел в батарее — не подогнан, а **описывал поведение, которое заказано сменить** (строка 394, ратифицировано письмом оркестратора «делай, обе половины»). Ряд приведён к новому поведению, и вместе с ним исправлен КОММЕНТАРИЙ теста, который нёс тот самый довод, что правка и опровергает. **Куда уехала гарантия:** в `TestAKillByOurOwnGraceIsNotReportedAsABrokenDeployment`, где живёт и довод, и замер маркера, и контроль `oom-kill`, который обязан ОСТАТЬСЯ `service_error`. Остальные шесть рядов старого теста не тронуты — довод группы для них выполняется. ### Что НЕ удалось **1. Цепочка «свип → `Stop` → настоящий systemd» по-прежнему не пройдена ни одним тестом.** Объявлено заранее в записке-плане и остаётся верным: такого стенда в зоне нет, и я его не строила. Предмет 392 живёт в изоляции транзакции, и ЕГО стенд есть — доказательство полное для гонки и неполное для цепочки. **2. Ущерб по 392 предъявлен на уровне СТОРА, а не продукта.** Тест показывает, что `RequestStop` возвращал мёртвое имя; что обработчик после этого шлёт сигнал в пустоту и пользователь держит `202` — следствие, выведенное чтением, а не поставленный сценарий. **3. Гонка воспроизведена НА ФИКСТУРЕ, а не на настоящем рестарте.** Транзакция-соперник пишет те же три оператора, что и `reopen` (замок строки прогона, конец попытки, вставка следующей), но это моя запись, а не вызов `ReopenRun`. Если `reopen` однажды перестанет брать замок строки `runs` первым, фикстура продолжит воспроизводить старую форму гонки, а не новую. **4. По 394 не проверено исполнением, что `timeout` не приходит от НАЧАЛЬНОГО тайм-аута.** У наших юнитов `Type=simple` (дефолт `systemd-run`), для которого стартовая фаза завершается сразу, поэтому `timeout` может прийти только от остановочной, — но это РАССУЖДЕНИЕ о systemd, а не мой замер. Сконструировать стартовый тайм-аут на `Type=simple` я не пробовала. **5. Взяты три предмета из десяти.** 398, 399, 400 и три носителя мусора на диске не тронуты по границам захода; 400 и мусор — ещё и потому, что я их не читала. ## ДОРАБОТКА ПО ИНВАРИАНТУ `D39.240` — ЗАПИСКА-ПЛАН (11.09, `textmachine-5c`) > Три предмета из улова отменённого пака, открытые владельцем словом «если остались баги или > рефакторинг про останов — доделать просто»: строки **392**, **394** и **401** (вторая половина). > Инвариант: остановка ЖЁСТКАЯ, деньги терять допустимо, но **без гонок, без половинчатых состояний, > с верным возобновлением**. Вход HEAD `d61469f`, дерево зоны на входе чистое. > ⛔ Границы заданы оркестратором и приняты: **398** и **399** (логика служебного обхода), **400** и > носители мусора на диске (**401**а/б/в) НЕ берутся — последние два потому, что я их не читала, и > давать себе предмет, которого не смотрела, под видом «доделать» нельзя. ### 392 — гонка имени юнита: механизм назван точно, и он тоньше, чем строка Строка говорит «между коммитом намерения и вызовом systemd свип успевает рестартовать». Разбор кода показывает, что бóльшая часть этого окна УЖЕ закрыта, и закрыта хорошо: `reopen` отказывается рестартовать прогон с записанным намерением (`ErrStopRequested`), а `RequestStop` читает имя юнита подзапросом «последняя не кончившаяся попытка» — то есть при любом порядке КОММИТОВ ответ верен. ⇒ **Настоящая дыра — не в порядке коммитов, а в изоляции READ COMMITTED.** `RequestStop` сегодня один UPDATE. Когда он блокируется на замке строки `runs`, который держит идущий `reopen`, Postgres после разблокировки пере-проверяет `WHERE` против НОВОЙ версии строки (EvalPlanQual) — но **подзапрос в `RETURNING` считается по снимку, взятому в начале оператора**. Значит намерение ложится на новую попытку, а имя юнита возвращается от СТАРОЙ. Обработчик шлёт сигнал мёртвому юниту, живой работает, пользователь держит `202`. **Чинить так:** `RequestStop` становится транзакцией — `select … for update` по строке прогона (в READ COMMITTED он после взятия замка пере-читает последнюю зафиксированную версию), затем чтение имени живой попытки, затем UPDATE. Тогда имя юнита и запись намерения принадлежат одному моменту. **Предъявлять исполнением**, а не рассуждением: тест на настоящем Postgres, где рестарт и стоп идут параллельно, и утверждение — какое имя юнита вернулось. ⚠ **Про стенд, честно и заранее.** Цепочку «свип → `Stop` → настоящий systemd» по-прежнему не гоняет ни один тест, и я такого стенда не строю: он не влезает в этот заход. Но предмет 392 живёт НЕ там — он живёт в изоляции транзакции, и её стенд в зоне есть (живой Postgres). ⇒ доказательство будет полным для гонки и неполным для цепочки; второе пойдёт в «что не удалось», как и предупредил оркестратор. ### 394 — «убили своим грейсом» читается как поломка деплоя **Замерено исполнением** (юнит нашей формы, `TimeoutStopSec=3`, процесс ловит SIGTERM и не выходит): маркер, который пишет systemd, — `{"result":"timeout","code":"killed","status":"KILL"}`. **Контрольная посадка на том же стенде** (тот же юнит, процесс выходит по SIGTERM) — `{"result":"exit-code","code":"exited","status":"5"}`. То есть тройка `timeout/killed/KILL` и есть «сработал НАШ `TimeoutStopSec`», и прибор эти два случая различает. Сегодня она попадает в общий список с `oom-kill` и даёт `failed` + `service_error` (`reconcile.go`, `failureReason`). ⛔ **И довод, записанный над этим списком, ДЛЯ `timeout` не работает**: там сказано «an out-of-memory kill and a stop-timeout SIGKILL both told the client that retrying would help, which for those two is exactly false: the next spawn dies the same way». Для OOM это верно — следующий процесс упрётся в тот же лимит. Для убийства по грейсу неверно: следующий процесс НИКТО не останавливает, и работа его предшественника лежит в чекпоинтах, то есть повтор не покупается заново. ⇒ **Делаю: `timeout` уходит из группы `service_error` в `interrupted`** — «ретрай и есть лекарство» — и довод пишется на месте, вместе с тем, почему общий довод не покрывает этот случай. ⛔ **ОБЪЯВЛЯЮ ГРОМКО: это правка ПРОДУКТОВО ВИДИМОГО поведения поверх решения с записанным доводом.** Она в моей зоне и по предмету строки, но если оркестратор считает, что вопрос «предлагать ли пользователю повтор» — не мой, пусть скажет, и я откачу этот пункт, оставив вторую половину. ⚠ **Чего я НЕ делаю и почему:** не перевожу такой прогон в `stopped`. Соблазн есть — грейс срабатывает только после того, как остановку у systemd попросили, — но при перезагрузке хоста менеджер останавливает все юниты, и прогон, не успевший свернуться, получил бы `stopped` и остался бы мёртвым после штатного ребута. Это ровно та асимметрия, ради которой написан `interruptedBySomeoneElse`. ### 401 (вторая половина) — отметка о расчёте может остаться пустой навсегда Расчёт и отметка — ДВА действия: `Settle` закрывает резервацию своей транзакцией, `MarkSettled` ставит `settled_at` отдельным оператором (`reconcile.go`, четыре места). Смерть процесса между ними оставляет леджер верным, а столбец пустым — **навсегда**, потому что рабочий список `UnsettledRuns` ключуется на ОТКРЫТОЙ резервации, а она уже закрыта. Читателей у столбца вне тестов ноль (пере-снято: три писателя, одна очистка, ноль чтений; контроль — 15 упоминаний в тестах). **Чинить самолечением, а не новым состоянием.** Отметка ВЫВОДИМА: «прогон кончился И у него нет открытых резерваций». Значит фаза расчёта одним оператором доштамповывает такие прогоны, и «навсегда» превращается в «до следующего прохода». Ни новой колонки, ни нового смысла. ⚠ Альтернатива — удалить столбец, у которого ноль читателей, — отвергается: он несёт СМЫСЛ, который охраняет ветка `--release-hold` («a field that lies is a field the next reader believes»), и удаление было бы решением про будущее, которого я не знаю. ### Порядок и почему такой **401 → 394 → 392.** От дешёвого и однозначного к тому, что меняет продуктовое поведение, и дальше к тому, что требует конкурентного стенда: если заход придётся сдавать неполным, неотданным останется предмет с самым узким радиусом, а не самый дешёвый. ### Что предъявляю исполнением 1. **392** — конкурентный тест на живом Postgres: рестарт и стоп в параллель, утверждение про ВОЗВРАЩЁННОЕ имя юнита; мутация — вернуть одиночный UPDATE, ждать красноты по тексту. 2. **394** — маркер грейс-убийства уже замерен (выше, с контрольной посадкой); в батарее — пин на разбор этого маркера, с контролем на `oom-kill`, который обязан ОСТАТЬСЯ `service_error`. 3. **401** — тест: расчёт прошёл, отметка не поставлена (симулируем смерть между двумя действиями), следующий проход её ставит; контроль — живой прогон отметку НЕ получает. ### Мандат самопроверки Мутация обязательна и засчитывается по ТЕКСТУ падения, а не по цвету. Отдельным заходом — адверсариальный проход по своей готовой работе. Секция «что не удалось» непустая по построению: цепочка «свип → systemd» в ней уже есть. ## ДОФИКС ПО ОТМЕНЁННОМУ ПАКУ: `--no-block` С ПИНОМ (11.09, `textmachine-5c`) > Взят владельцем ОТДЕЛЬНО от отменённого пака и взят ровно в той форме, которую зона просила: правка > вместе с пином, а не правка с довеском. Довод, который дошёл дословно: без пина в коде остаётся > утверждение «asking again is free and idempotent», которое держится на недокументированном > поведении systemd и которое никто не проверяет. > ⛔ **Границы захода:** только это. Ни режима остановки, ни второго сигнала, ни кадра событий — пак > отменён и остаётся отменённым. ### Что сделано, тремя правками **1. `Runner.Stop` больше не ждёт конца прогона** (`platform/internal/runner/runner.go`). Замер, ради которого правка и берётся, уже в журнале решений: блокирующая форма вернулась через **15,058 с** (по SIGKILL, юнит с `TimeoutStopSec=15`), `stop --no-block` — через **0,014 с**. Комментарий над функцией несёт оба следствия и НЕ несёт третьего, которого нет (см. правку 3). **2. Пин на то, что раньше держалось на удаче** — `runner.TestARepeatedStopNeitherSignalsNorExtendsTheGrace`, живой, на настоящем systemd. Он спрашивает у systemd ровно те два свойства, которые УТВЕРЖДАЕТ комментарий свипа: повторный `stop` на юните в `deactivating` (а) не доставляет второго сигнала и (б) не перезапускает `TimeoutStopSec`. Числа прогона: три стопа в t+0 / t+2 / t+4 против грейса 6 с → процесс получил **1** сигнал, юнит умер на **t+6,06 с**. ⚠ **Контроль встроен в сами числа, отдельного прогона не требуется, и это сказано в теле теста:** «один сигнал» не читается как отсутствие, потому что тот же лог доказывает, что фикстура сигналы СЛЫШИТ (первый стоп доставил); а юнит не «просто не убит» — он УБИТ, и утверждение о том, по чьим часам: по первому стопу (t+6), тогда как перезапущенный таймер дал бы t+10. **3. Комментарий свипа перестал утверждать без основания** (`platform/internal/runs/reconcile.go`, ветка пере-выдачи стопа): «free and idempotent» теперь названо ЗАМЕРЕННЫМ свойством systemd, с обеими половинами и с ценой потери каждой, и со ссылкой на пин. ### Мутационная проверка — выполнена, на КОПИИ, три посадки ⚠ Это то, чего смена не сделала в отменённом паке и записала в «что не удалось». Здесь сделано. Копия дерева зоны жила в `~/.cache/tm-5c/mut/platform` (не в общем скретчпаде — его чистит не только свой процесс), перед каждой правкой утверждались `test -f go.mod` и точный `pwd`; после прогона копия сверена с деревом (`diff -rq` по `internal/runner` — идентично) и удалена. | посадка | что сломано | вердикт и **текст** падения | |---|---|---| | M1 | снят `--no-block` у `Stop` | КРАСНО, `TestTheSoftStopDoesNotWaitForTheRunToEnd`: «the stop waits for the unit to be gone: [systemctl --user stop tm-run-X-1.service]» | | M3 | пере-выдача ДОСТАВЛЯЕТ (`systemctl kill --signal=SIGTERM` вместо `Stop`) | КРАСНО: «the process received **3** signals from three stops, want exactly 1» | | M4 | часы грейса идут от ПОЗДНЕГО стопа (первый стоп сдвинут на t+4) | КРАСНО: «the unit died at **t+10,09s** against a 6s grace: a repeated stop restarted the stop timeout» | ⭐ **Каждая посадка засчитана по ТЕКСТУ, а не по цвету:** в каждом случае сообщение называет ровно то, что сломано. И две половины утверждения оказались независимо различающими — M3 краснит только счёт сигналов, M4 только часы. ⚠ **M1 живой пин НЕ ловит, и это названо, а не обойдено:** без `--no-block` первый стоп блокируется на весь грейс, остальные два приходят уже на мёртвый юнит, и числа сходятся прежние. Флаг ловит argv-пин, который для того и написан отдельно — живой тест скипается на хосте без пользовательского менеджера systemd, и флаг на таком хосте остался бы непроверенным вовсе. ⚠ **Флейковость проверена, а не предположена:** живой пин прогнан **5 раз подряд** — 5 зелёных, разброс смерти 6,04–6,12 с при пороге 8 с, счёт сигналов 1 во всех пяти. Пин не маргинален ни по одной из двух осей. ### Числа ``` $ TM_PLATFORM_TEST_DSN=…55433 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 gofmt / go vet / sqlc diff чисты · ALARM PD-count: 15 (baseline 15) — не сдвинут скипы поимённо, условие у всех ОДНО и названное — нет деплой-артефакта `configs/mining-contrast.zh.txt`: TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses · TestALivePreviewWritesNothingAndALiveApplyWrites · TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing · TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber · TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem $ go test ./internal/runner/ -run TestARepeatedStopNeitherSignalsNorExtendsTheGrace -count=1 -v (×5) 5 зелёных · сигналов 1 во всех пяти · смерть юнита 6,04 / 6,09 / 6,09 / 6,10 / 6,12 с при пороге 8 с ``` ⚠ **Новый пин ВНУТРИ батареи отработал, а не проскочил:** цель `check` гоняет `-v` и печатает КАЖДЫЙ `--- SKIP`; скипов ровно пять, и пина среди них нет, при нуле падений. Числа выше — из отдельных прогонов той же командой, потому что `t.Logf` в сводку `make check` не попадает. ⚠ Гейты стенда закрыты все четыре намеренно: без `TM_PLATFORM_TEST_DSN` та же батарея печатает те же `ok` и прячет ~370 тестов (эррата `08.09-д`). ### Что НЕ удалось **1. Живой пин не покрывает саму правку, и это названо выше, а не обойдено** — снятие `--no-block` ловит только argv-пин. Разделение осознанное (живой тест скипается без пользовательского менеджера systemd), но означает: на хосте без systemd свойство «стоп не ждёт» проверено, а «systemd инертен к повтору» — нет, и это не чинится в зоне. **2. Пин говорит о ЮНИТЕ, а утверждение живёт в СВИПЕ.** Тест проверяет `Runner.Stop` напрямую; никакой тест не гоняет пере-выдачу ЧЕРЕЗ реконсайлер против настоящего systemd — такого стенда в зоне нет. То есть цепочка «свип → `Stop` → systemd» на живом юните не пройдена ни разу, и если однажды свип начнёт звать не `Stop`, а что-то другое, пин этого не заметит. **3. Ущерб от блокировки не предъявлен исполнением — ни одна половина.** Что запрос повиснет, а свип запишет ложный клин, взято ЧТЕНИЕМ кода (`reconcileOne`, комментарий про израсходованный бюджет) плюс замером самой блокировки. Сценария «блокирующий стоп внутри прохода довёл прогон до отсрочки» я не ставила; на предмет правки это не влияет, но утверждение о механизме ущерба остаётся выведенным. **4. Прочие девять адресов улова не тронуты** — они вне границ захода и живут строками бэклога **392**–**397**. ## ПАК «ДВЕ ОСТАНОВКИ» — ОТМЕНЁН ВЛАДЕЛЬЦЕМ, РАБОТА ОТКАЧЕНА (10.09, `textmachine-5c`) > Промт `docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md` (редакция `ac9a24d`), вход HEAD `ac9a24d`. > **Отменён владельцем по размаху**, а не по качеству: две остановки того не стоят. Новый инвариант > объявлен тем же словом: **стоп остаётся ЖЁСТКИМ, терять деньги ДОПУСТИМО, но остановка обязана быть > КОРРЕКТНОЙ** — без гонок и половинчатых состояний, с правильным возобновлением. Предмет переехал с > «не сжечь деньги» на «не оставить мусор». > > ⚠ **Эта секция — не отчёт о построенном, а НАЙДЕННОЕ.** Код зоны возвращён к HEAD целиком (кроме > константы версии контракта, см. ниже). Здесь остаётся то, что зона УЗНАЛА о собственном дереве: > оно пережило пак и стоит дороже, чем стоил бы код. ### Что откачено и что оставлено Возвращены к HEAD **26** путей в `platform/`. Удалены девять моих новых файлов: `internal/gates/stopgrace_test.go` · `internal/httpapi/stopmode_test.go` · `internal/pgstore/migrations/00035_stop_mode.sql` · `internal/pgstore/migrations/00036_estimated_and_stop_ledger.sql` · `internal/pgstore/stopmode_test.go` · `internal/runner/stop_systemd_test.go` · `internal/runner/stop_test.go` · `internal/runs/estimated_test.go` · `internal/runs/stopmode_test.go`. **Оставлено ОДНО и по прямому указанию оркестратора:** `internal/httpapi/capabilities.go`, константа `ContractVersion`. Это чужой долг, взятый попутно: канон уехал на `0.13.1` актом `D39.235` п.6, а константа осталась на `0.13.0`, и гейт `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` краснел с момента ратификации — базовая линия зоны была КРАСНОЙ на входе, и не по вине пака. ⚠ В ходе пака константа стояла на `0.14.0` (свой минор: `mode` у `/stop` плюс `Run.stop_mode`, по ратифицированному порядку «код первым»); с отменой пака этого минора нет, поэтому при откате она приведена к **`0.13.1`** — к тому, чему билд действительно служит. ### ⛔ Что зона узнала о СЕБЕ — это и есть результат смены Ниже разделено на «замерено исполнением», «прочитано в коде» и «латентно» намеренно: смешивать их дороже обычного, потому что по ним заводятся строки. Адреса — по HEAD `ac9a24d`. **1. `Runner.Stop` блокируется на весь грейс — ЗАМЕРЕНО.** `internal/runner/runner.go:195` зовёт `systemctl --user stop` без `--no-block`, `runCommand` (`:106-112`) своего тайм-аута не имеет. Пробник на транзиентном юните с `TimeoutStopSec=15`, процесс ловит SIGTERM и не выходит (systemd 259): блокирующая форма вернулась через **15,058 с** (по SIGKILL), `stop --no-block` — через **0,014 с**, юнит в `deactivating`. Сегодня движок на жёстком сигнале умирает за секунды, поэтому это невидимо. ⚠ **Испр. 11.09 по эррате `10.09-з`:** носитель последствия НЕ `WriteTimeout` — тридцать секунд стоят у слушателя МЕТРИК (`cmd/tmplatformd/main.go:204-208`, комментарий там прямо говорит «for once a WriteTimeout too: nothing here streams»), а у API-слушателя его НЕТ намеренно (`internal/httpapi/serve.go:36`, «No WriteTimeout» — срезал бы SSE). Пере-снято моей рукой: во всей зоне вне тестов `WriteTimeout` УСТАНАВЛИВАЕТСЯ **в одном месте** — `main.go:208`. Контроль, что прибор спрашивал существующее: `grep -rn WriteTimeout platform/ --include=*.go | grep -v _test.go` на `1c5bd2a` даёт четыре строки, из которых три — комментарии, и ДВЕ из них объясняют, почему у API-слушателя его нет. ⭐ И механизм, на который поправка меняет ущерб, в зоне УЖЕ ЗАРЕГИСТРИРОВАН: `PD-103` — «зависший Postgres паркует хендлеры и ждущих в пуле, пока клиент сам не уйдёт», ровно потому что `WriteTimeout` у сервера отсутствует по проекту (SSE) и `TimeoutHandler` в цепочке нет. Блокирующий `Stop` — тот же класс, другой источник блокировки; ⇒ поправка садится на существующий носитель, а не заводит новое утверждение. ⇒ ущерб не «оборванный `202`», а **зависший запрос с удержанной горутиной обработчика** (контракт описывает этот вызов АСИНХРОННЫМ) плюс ложный клин свипа (п.2). **2. Израсходованный бюджет прогона свип считает ПРОВАЛОМ реконсиляции — прочитано в коде.** `reconcileOne` (`internal/runs/reconcile.go:89-90`) заворачивает каждый прогон в `s.runBudget()` (дефолт 60 с), и комментарий на `:92-115` говорит буквально «SPENDING the budget counts as a failure even when nothing reported one». ⇒ блокирующий `Stop` внутри свипа (`:580`) даёт `ReconcileFailures++`, отсрочку и через `StalledAfter` ЛОЖНУЮ тревогу оператору, плюс съеденную долю прохода для всех остальных аккаунтов. ⚠ Прежняя формулировка оркестратора («свип встанет до двадцати минут») неверна по механизму и исправлена актом `D39.236` п.5 по возражению этой смены. **3. Повторный `stop` на юните в `deactivating` ИНЕРТЕН — ЗАМЕРЕНО, и это несущее свойство.** Три стопа в t+0 / t+2 / t+4 против грейса 6 с: процесс получил **один** сигнал, юнит умер на **t+6,04 с** (не t+10). То есть пере-выдача не шлёт второго сигнала и не перезапускает `TimeoutStopSec`. ⭐ Ценность не в хорошей половине, а в том, что комментарий свипа (`reconcile.go:576-579`) УЖЕ утверждает «asking again is free and idempotent», и это утверждение держится на недокументированном поведении systemd, которое до этой смены никто не проверял. ⚠ И вторая половина того же комментария — «it is the only thing that closes the first case» — **не выполняется**, если юнит уже начал останавливаться: пропущенный первый SIGTERM свип не чинит, юнит стоит в `deactivating` до SIGKILL по грейсу (сегодня — до десяти минут прогона, который ничего не делает, с зарезервированным холдом). **4. Два SIGTERM подряд Go-процесс видит как ОДИН — ЗАМЕРЕНО, ратифицировано `D39.238`.** gap 0 мс → оба сигнала увидены в **0 из 20** прогонов (в длинной серии слиплись 40 из 40); gap 1 мс → **20 из 20**; буфер принимающего канала **8** — коалесценция живёт НИЖЕ канала, в рантаймовом БИТЕ на сигнал. ⚠ Первый мой прибор давал «2 из 2» и подтверждал удобное: `kill` шёл из шелла, и запуск двух процессов сам создавал зазор. Прибор чинен, число перевернулось. Норма, выведенная из этого оркестратором: **замер, чей результат подтверждает удобное, проверяется на ПРИБОР прежде, чем на предмет.** **5. ⛔ `finish` рассчитывает деньги из ДО-ДРЕНАЖНОЙ выписки — ЛАТЕНТНО, но класс настоящий.** `reconcile.go:955-958`: расчёт внутри `finish` объявлен оппортунистическим и получает тот самый `l`, который проход прочитал ДО дренажа журнала. `finish` вручную освежает `l.Status`/`l.PausedReason` перед вызовом (`:~918`) — то есть автор знал про класс и закрыл ровно два поля. **Любое число, которое дренаж ЭТОГО ЖЕ прохода записал в строку попытки, для этого расчёта невидимо.** ⚠ Граница честности: на HEAD ничего не сломано — `settle` не читает из выписки ни одного поля, которое пишет дренаж. Класс предъявлен исполнением только потому, что пак добавил такое поле: строка попытки читала 9 оценочных строк, выписка — 0, и расчёт взял число, которое случайно знал запасной канал. Поле ушло с паком, класс остался. Родственник в этом же файле уже стоил зоне дефекта: `freshRunState` (`reconcile.go:~660`) написан ровно потому, что выписка не может знать, что записал её собственный дренаж. **6. Обработчик `/stop` шлёт сигнал по имени юнита из СВОЕЙ ЖЕ выписки — прочитано в коде.** `Service.Stop` (`reconcile.go:1541`) берёт `unit` из `RequestStop` и зовёт `Runner.Stop(ctx, unit)`. Успел ли свип между этими двумя рестартовать попытку — сигнал уйдёт СТАРОМУ юниту. Самолечение есть (следующий проход увидит `alive && StopRequestedAt != nil` и пере-выдаст стоп текущему, `:575-583`), но приходит оно от СВИПА, а не от обработчика: окно равно одному тику, и всё это время пользователь держит `202`, а прогон работает. **7. Двойной клик по `/stop` — гонка по построению, сегодня безвредная.** `RequestStop` (`internal/pgstore/runs.go:1298-1309`) — один UPDATE; под READ COMMITTED два одновременных нажатия не различимы самим оператором, потому что `RETURNING` отвечает НОВОЙ строкой и не может отличить собственную запись от чужой. Схлопывание в первый таймстамп делает оба нажатия одинаковыми, поэтому вреда нет — ровно до той секунды, когда ответ или действие начнут зависеть от того, кто нажал первым. Форма лечения, проверенная в отменённом паке: `select … for update` в одной транзакции с UPDATE. **8. `stoppedOnRequest` сравнивает ДВОЕ РАЗНЫХ ЧАСОВ, и код это признаёт.** `reconcile.go:1137-1153`, довод на `:~1000`: «the two clocks being compared are not even the same one — the marker carries the host's, the intent the platform's». Сегодня цена ошибки — ЯРЛЫК. В любой конструкции, где от этого ярлыка зависит ДЕЙСТВИЕ (перезапуск, возврат холда, возобновление), это гонка по построению. **9. Каталог движка: потолка ожидания нет у пяти провайдеров из восьми — ЗАМЕРЕНО прибором.** Программа на `config.LoadModels` + `Timeouts.Profile().DeadlineFor(B)` над снапшотом `git archive HEAD backend` (ни байта записи в чужую зону): `attempt_max_s` задан у **3** (`deepseek` 1240 · `kimi` 1200 · `gemini` 1200), инертен у **5** (`zai` · `xai` · `openai` · `mistral` · `local`), и у них `DeadlineFor` растёт с бюджетом линейно и сверху не ограничена. Максимум по каталогу при наибольшем бюджете сегодняшней конфигурации (**35 200** ток. = редакторские `max(8000×2.2, 16000) = 17 600` с одним удвоением по `regenerate_before_escalate: 1`) — **1240 с**, deepseek. Беcключевой `zai` перерастает его при **43 400** ток., то есть при `edit_ceiling_out` ≈ 9 900 против сегодняшних 8 000 — одна строка yaml, которая читается как ручка качества. Носитель — строка бэклога **383**. **10. Dev и прод расходятся по грейсу в 20 раз, и это не объявлено нигде.** `internal/ingest/supervisor.go:18` — `stopGrace = 30 * time.Second` (уезжает в `cmd.WaitDelay`, `:75`) против `600 с` в проде (`internal/runner/runner.go:59`). Один и тот же движок против тех же платных провайдеров получает на свёртку полминуты на dev-пути и десять минут на боевом. **11. Пять открытых рядов реестра — про холды и остановку, и это один предмет, а не пять.** Их печатает сам гейт зоны (`make check`, `internal/gates`, строка `ALARM PD-count: 15 open rows … (baseline 15)`): **PD-418** (у settling-строки живого прогона нет ручки, а рантбук обещает обратное) · **PD-244** (единственный выход `settle`, оставляющий холд открытым МОЛЧА) · **PD-162** (книга с пропавшим каталогом заклинивает прогон навсегда с открытым холдом) · **PD-465** (заявка на спавн коммитится ДО подъёма юнита) · **PD-217** (незакрытый холд блокирует апгрейд движка бессрочно). Под новым инвариантом это готовый предметный список, уже приоритизированный реестром. **12. Что в дереве оказалось ЛУЧШЕ ожидаемого — и это тоже находка.** Гонка «кто пишет исход мёртвого прогона» ЗАКРЫТА, и закрыта правильной формой: `FinishRun` возвращает `closed bool`, и `finish` (`reconcile.go:903`, ветка `!closed` ~`:937`) разбирает случай «прогон уехал под этим проходом» явно; второй расчёт получает `ErrNoReservation`, потому что резервация закрывается под `state = 'open'`. Столкновение реконсайлера с `/stop` названо в трёх местах и в каждом закрыто: `finishStopped` (`:678-685`), отказ рестарта по `ErrStopRequested` (`:1324`), пере-проверка заявки под замком в `FinishUnspawnedStop` (`:~710`). ⇒ **модель «записать условно и прочитать вердикт» уже живёт в зоне**, и новому инварианту стоит брать её образцом, а не изобретать другую. **13. И одна развилка, которую новый инвариант обязан пересмотреть ЯВНО.** `interruptedBySomeoneElse` (`reconcile.go:539`, функция `:1129`) ПЕРЕЗАПУСКАЕТ прогон по коду 5 без записанного намерения. Это осознанное решение с записанным доводом (ребут против ручного `systemctl stop`, асимметрия цен), а не дефект — но это главная развилка «правильного возобновления», и наследовать её молча нельзя. ### Числа смены ``` $ make check (все четыре гейта стенда закрыты: Postgres 55433 · движковый бинарь и шаблон из `git archive HEAD backend` · достижимый пользовательский systemd · MemoryMax) вход (до первой правки): MAKE-EXIT=2 · 19 `ok` · 1 FAIL · линтер «0 issues» · скипов 5 единственный красный — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` («this build announces contract 0.13.0 and the ratified canon is 0.13.1»), НЕ по вине смены скипы 5, условие у всех одно и названное: нет деплой-артефакта `configs/mining-contrast.zh.txt` ПОСЛЕ ОТКАТА (последний прогон, снят после последней правки): MAKE-EXIT=0 · 20 `ok` · 0 FAIL · линтер «0 issues» · скипов 5 `gofmt`/`go vet`/`sqlc diff` чисты · `ALARM PD-count: 15 open rows … (baseline 15)` — не сдвинут красный входа ПОГАШЕН правкой константы: канон `0.13.1`, билд `0.13.1` ``` ⚠ Гейты стенда закрыты все четыре намеренно: без `TM_PLATFORM_TEST_DSN` та же батарея печатает те же `ok` и прячет ~370 тестов (эррата `08.09-д`). Счёт скипов печатается рядом со счётом `ok`. ### Что НЕ удалось **Ничего из заказанного не сдано — пак отменён на середине, и это не оценка работы, а факт.** Построенное и зелёное на момент отмены (грейс суммой с названными слагаемыми, `--no-block`, путь второго сигнала через наблюдение `SubState`, режим в записи намерения с атомарной заявкой на второй сигнал, чтение минора `1.4` из обоих каналов, пометка оценки на строке расчёта, живой systemd-стенд) откачено целиком. Из адверсариального прохода §5.5 успели отработать три направления из шести — (а) гонка двух нажатий, (б) остановка мёртвого юнита, (д) пере-выдача не должна эскалировать; (в) чтение мягко остановленного прогона реконсиляцией, (г) идемпотентность `/stop` и (е) удлинённое окно `deactivating` до отмены проверены не были. Мутационная проверка СВОИХ новых пинов — та, которую норма зоны требует отдельным заходом, — не выполнена ни для одного из них: до неё смена не дошла. Поэтому ни один пин выше в дереве не остаётся, и ни на один нельзя ссылаться как на проверенный. ## Состояние зоны на 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:447`=`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`.