231 KiB
Архив журнала зоны «Платформа» — записи паков P4–P6 с дофиксами (08–15.08.2026)
⚠ АРХИВ (вынесено оркестратором №17 16.08, слово владельца: журналы забивали контекст сессиям). Все паки здесь ИСПОЛНЕНЫ и ПРИНЯТЫ (P4 — D39.123 · P5 — D39.130 · P6+дофикс — D39.132). Живой журнал держит: текущее состояние · пинги оркестратора (входы P7). Инструкции отсюда не исполнять.
Дофикс P6 (15.08): фикс-лист приёмки ФП-1…ФП-8
Промт — PLATFORM_P6_DOFIX_SESSION_PROMPT.md. Мандат владельца исполнен буквально: каждая находка
проверялась по коду ДО починки, а не принималась на веру. Опровергнутых нет — подтвердились все, но
две подтвердились НЕ так, как их описала приёмка, и об этом сказано в строках ниже.
Записка-план: ID → статус → улика
| ID | Статус | Улика |
|---|---|---|
ФП-1 exit 2 у status |
ЗАКРЫТ | Подтверждён чтением движка: cmd/tmctl/render.go renderStatusJSON печатает отчёт и возвращает сентинел при flagged>0, main.go exitCode отображает его в 2. Платформа звала status на ТРЁХ путях — расчёт, ре-синк, счётчик перед спавном — и каждый читал ненулевой код как «движок не ответил». Чинено одним знанием (ingest.CompletedWithFlags): exit 2 при разбираемом отчёте = ответ, exit 2 с нечитаемым stdout = отказ, всякий иной ненулевой = отказ. Дев-путь выровнен. Пины runner.TestAFlaggedBookStillAnswersAboutItsMoney, TestExitTwoWithNothingToReadIsStillARefusal, TestAReportPrintedByAFailedCallIsNotAnAnswer, ingest.TestTheDevelopmentStatusReadsAFlaggedBookToo и сквозной runs.TestTheMoneyOfAFlaggedBookIsSettledThroughTheRealExitContract. PD-224 |
| ФП-1 «какие ещё команды выходят 2» | ЗАКРЫТ ЧТЕНИЕМ | Сентинел CompletedWithFlags рождается в движке в четырёх местах, принадлежащих трём командам (render.go:70 результат прогона, :553 status --json, :639 status человеку, :691 redrive). Платформа зовёт translate (код читается реконсилятором, OutcomeFlagged), status (починен) и manifest — а renderManifest возвращает nil для любого документа, который печатает, и другого пути к сентинелу у manifestCmd нет. redrive платформа не зовёт вовсе. Вывод записан доккомментом у runner.Manifest, вместе с тем, что случится, если движок это изменит: неклассифицированный код интейк уже читает как вину деплоя и файл пользователя не трогает |
| ФП-2 порядок апгрейда | ЗАКРЫТ | Подтверждён: шаг 2 звал голый tmctl migrate до установки нового бинаря — no-op старым. Порядок переписан: остановка демона (закрывает окно между списком и миграцией) → список → новый бинарь на версионированный путь → migrate ЕГО явным путём → переключение TM_PLATFORM_ENGINE_BIN → старт. «ДВА факта» приведены к трём. PD-225 |
| ФП-3 гейт миграции не запинен | ЗАКРЫТ | pgstore.TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList: по книге на каждый блокер, каждый по отдельности переводит вердикт в «нельзя», и список, который читает цикл деплой-заметки, содержит только свободную книгу. Живой прогон изолирован ИСКУССТВЕННО (его холд закрыт руками), потому что живой прогон всегда держит деньги — иначе один предикат прятался бы за другим. Три посадки, три падения. PD-226 |
| ФП-4 нечитаемый маркер хоронит потолок | ЗАКРЫТ | Подтверждён чтением: перечит причины стоял только на разбираемой ветке. Ветки слиты в одну (case err == nil, errors.Is(err, runner.ErrBadMarker)) — маркер на диске означает «юнит кончился» в обоих прочтениях, а решает о конце общий перечит. Пин runs.TestACeilingSurvivesAMarkerThatCannotBeRead (журнал с ceiling{scope:day} + маркер {not json → paused/daily_ceiling), посадка возвращает ровно то, что видела приёмка: failed/пусто. PD-227 |
| ФП-5 куки без Secure рядом с боевым OIDC | ЗАКРЫТ, форма выбрана иначе | Подтверждён: гейт ловил только связку с DEV_LOGIN. Легитимный сценарий, о котором спрашивал промт, СУЩЕСТВУЕТ — локальный провайдер по http на стенде, где Secure/__Host- не работают в принципе, — поэтому глухой отказ был бы неверен. Судит собственный адрес деплоя: TM_PLATFORM_OIDC_REDIRECT_URL и есть публичный URL платформы, значит http:// там = стенд (запускается), всё прочее — отказ на буте. Пин config.TestCookiesWithoutSecureAreRefusedNextToAProductionProvider (4 отказных формы + 2 законных), посадка падает. PD-228 |
| ФП-6 незнакомый scope потолка | ВЗЯТ | Приор верен и подтверждён кодом: ReadUsage ключит аккаунтный halted-флаг ровно на credit_exhausted, а CeilingPause отдавал его всему, кроме day. Теперь называются только book и day, всё остальное = ceiling_unknown. Резюмируемость не меняется ни при одном из трёх, а ложного «кредит кончился» на аккаунте с деньгами больше нет. PD-229 |
ФП-6 сид не тримит --subject |
ВЗЯТ | Подтверждён чтением: демон тримит (config.Load), сид читал сырую переменную. Одна нормализация с обеих сторон; пин TestASubjectThatIsOnlyPaddingIsNoSubject — падённое значение обрывает прогулку ДО обращения к демону. PD-230 |
ФП-6 комментарий у гарда at >= |
ВЗЯТ, довод переписан | Приёмка права: pipeline/events.go штампует at := e.now() анонсирующего процесса, а непроецированная строка мёртвого прогона стирается ForgetEvents и анонсируется заново — со СВОИМ временем. Значит инверсии движок не порождает. Гард остаётся и обоснован тем, чем он есть на самом деле: потребитель at-least-once-потока, который ПРИСВАИВАЕТ, обязан не зависеть от порядка доставки. Довод приведён к факту и в коде, и в пинящем тесте. PD-231 |
| ФП-6 комментарий Resume про book-ceiling | ВЗЯТ, чурн — строкой | Приёмка права: движок останавливается на невместившемся РЕЗЕРВИРОВАНИИ, то есть до своего потолка, поэтому остаток положителен и ветка exhausted не срабатывает. Комментарий приведён к факту. Сам чурн (холд-спавн-пауза за клик, провайдерских денег не тратит) — строка PD-223, а не порог: размер резервирования — число движка, платформе не видное, и любой порог здесь был бы догадкой |
| ФП-6 exit 5 со стопом позже маркера | ВЗЯТ | Подтверждён: stoppedOnRequest отвергал по времени, и прогон, который пользователь отменил, закрывался failed. Exit 5 недостижим для прогона, кончившегося сам, а два штампа приходят с РАЗНЫХ часов (маркер пишет хост юнита, намерение — платформа). Теперь exit 5 при записанном намерении = stopped независимо от порядка. Гард «чистый финиш перебивает стоп» проверен отдельно и не сдвинулся: 0/2/3 отвечены раньше в том же switch — это пинится тремя строками того же теста. PD-233 |
| ФП-7 строки регистра | ЗАКРЫТ | PD-217…PD-222 — шесть находок приёмки, которые этим касанием не чинятся, плюс PD-223 своя. PD-198/PD-199 не тронуты. Ни одну из шести чинить сверх строк не стал: каждая либо требует решения вне зоны (PD-217, PD-218), либо лечится вместе с соседом (PD-219 — с читающей поверхностью P7), либо сегодня инертна (PD-220, PD-221) |
| ФП-8 базис тестов | ЗАКРЫТ | Числа пересчитаны командами (ниже), шапка P6 исправлена, откуда взялось «396» — не установлено. PD-234 |
Аддендум владельца 15.08 (day_usd) |
ИСПОЛНЕН | Раздел отдельным пунктом ниже: из ПРИМЕРА шаблона убран, ⚠-абзац переписан как опция оператора, платформенная обработка daily_ceiling/409 названа предохранителем. Кода не касается |
| Своё ревью (4 линзы) | 6 ПРИНЯТО И ПОЧИНЕНО, 5 СТРОКАМИ, 3 ОПРОВЕРЖЕНИЯ ПРИНЯТЫ | Таблица «находка → диспозиция» ниже. Самая тяжёлая — моя же: фикс ФП-4 сделал ветку решающей, а глоток ошибки перечитки в ней остался (PD-236) |
Аддендум оркестратора к ФП-6а (решение владельца 15.08) — отдельным пунктом
ceilings.day_usd: 5 убран из ПРИМЕРА платформенного шаблона книги (deploy/README.md), остался
только book_usd. Довод владельца, записанный там же: трату прогона уже жёстко ограничивает
купленный объём — платформа держит холд и передаёт движку --ceiling-usd флаш к нему, — поэтому
дневная ось поверх этого второй лимит на ту же трату, а стоит она дороже, чем даёт: остановленный ею
прогон резюмировать нельзя (409), и пользователю эта пауза необъяснима, потому что лимит не его и он
его не видит.
Движок и его day_usd не тронуты. ⚠-абзац переписан так, чтобы дневной лимит читался как ОПЦИЯ
оператора, а не дефолт, и там же сказано, что платформенная обработка daily_ceiling/409 из P6
остаётся ПРЕДОХРАНИТЕЛЕМ ровно на случай, когда оператор впишет лимит руками: она делает такую
остановку различимой и честной, а не дефолтом, который её вызывает. Код не менялся — это правка
рекомендации, и day_usd в фикстуре render_test.go остаётся намеренно: там он стоит за «ключ
оператора, который рендер обязан пронести нетронутым».
Адверсариальное ревью дофикса (author≠reviewer, механизмом)
Четыре линзы субагентами по осям, названным промтом, плюс своя: шов против РЕАЛЬНОГО движка · деньги (пересчёт расчёта флагнутого прогона из сырого леджера двумя путями) · конфиг-отказы (security, другая модель-семья — Fable) · автомат состояний. Каждая работала на живом стенде и в КОПИИ репо вне его; рабочее дерево ревьюеры не трогали.
Денежная ось отчиталась цифрами: холд 3.000000 и его возврат гасят друг друга, списание 1.250000
одно, и четыре независимых счёта сходятся — свёртка сырых строк credit_ledger, грант − (committed − baseline), account_balances и сумма ReadAccount: 8 750 000 micro-USD везде.
| Находка | Линза | Диспозиция |
|---|---|---|
Неудачная перечитка причины хоронит потолок как failed; тот же глоток заново вооружает перезапуск |
автомат состояний | ПРИНЯТА, починена, запинена — PD-236. Мой же фикс ФП-4 сделал ветку решающей, а глоток остался; ошибка чтения теперь возвращается наверх |
| Exit 2 берётся по коду, без согласия отчёта (измерено: чужой отчёт РАССЧИТЫВАЕТ 2.5 USD) | деньги | ПРИНЯТА, починена, запинена — PD-237 |
| Документ, который не манифест, приезжает пустым манифестом → удаление загрузки | шов | ПРИНЯТА, починена, запинена — PD-238 (+ различение «глав нет» и «глав нет, а юниты есть») |
Manifest держится на непроверяемом инварианте чужой зоны |
шов | ПРИНЯТА, починена, запинена — PD-239: правило читается, а не предполагается |
Status читает stdout без потолка (найдено двумя линзами независимо) |
шов + деньги | ПРИНЯТА, починена, запинена — PD-240; написание пина вскрыло вторую половину: дренаж бесконечного писателя не возвращается никогда |
| Подтест-пассажир в пине гейта ФП-5 (зелен при удалённом гейте) | security | ПРИНЯТА, строка убрана — PD-235 |
Стоп пользователя переименовывается в paused/credit_exhausted, если потолок приехал в тот же дрейн |
автомат состояний | ПРИНЯТА строкой — PD-241: порядок «причина раньше намерения» ратифицирован в P6, молча переворачивать не стал; денежно-видимая половина — PD-203 |
ceiling_unknown проходит гейт резюма, отказывающий daily_ceiling |
автомат состояний | ПРИНЯТА строкой — PD-242 (не регресс: прежнее значение резюмировалось так же) |
DecodeStatus принимает null/{} за отчёт |
шов | ПРИНЯТА строкой — PD-243; денежные пути перекрыты (PD-40 и новая проверка согласия), остаётся ETA |
settle молча возвращает nil при Engine == nil |
деньги | ПРИНЯТА строкой — PD-244 (в проде недостижимо) |
| У дев-супервизора нет потребителя вне тестов | шов | ПРИНЯТА строкой — PD-245; комментарий приведён к факту, судьбу пути решать оркестратору |
Гейт ФП-5 обойти не удалось: 24 формы callback, *_FILE, схемные трюки |
security | ОПРОВЕРЖЕНИЕ ПРИНЯТО — гейт устоял; ложных отказов законных стендов тоже не нашлось (прокси с TLS отказывается ПРАВИЛЬНО: браузер ходит по https, Secure там работает) |
(B) exit 5 + намерение: полный перебор 216 клеток outcome против до-фиксной версии |
автомат состояний | ОПРОВЕРЖЕНИЕ ПРИНЯТО — расходятся ровно две клетки, обе exit 5: одна начинает записывать код выхода, вторая и есть починка |
| PD-212 (Go выходит 2 при панике) расширяется ли новой терпимостью | шов | ОПРОВЕРГНУТО ревьюером — паника не оставляет целого документа, а неполный отвергается; строка остаётся про путь прогона |
Живые пробы этого круга
- Гейт ФП-5 — живым демоном, не только тестом. С боевым callback:
TM_PLATFORM_INSECURE_COOKIES=1+ полный OIDC +TM_PLATFORM_OIDC_REDIRECT_URL=https://app.example.org/auth/callback→ERROR fatal, exit 1, сервис не поднялся. С локальнымhttp://127.0.0.1:8099/auth/callbackтот же демон проходит конфигурацию и стартует. То есть отказ ловит именно ту форму, которую нашла приёмка, и не ловит законный стенд. - Exit-контракт — настоящими процессами. Пины
runnerгоняют не мок, а/bin/sh-движок, печатающий отчёт и выходящий 2/1/11/13; сквозной денежный тестrunsподменяет движок скриптом, чьи ответ и код лежат в двух файлах, — так что путь от кода выхода до строки леджера проходится целиком, а не по обе стороны стаба. - Батарея дофикса —
make checkпри живом PG (~/.local/pgsql, порт 55433) и обоих гейтах окружения: EXIT=0, 17 пакетов, линтер 0 issues, скипов 0,make vuln— без уязвимостей.
Числа: командой, а не памятью (ФП-8)
git grep -h '^func Test' HEAD -- 'platform/**/*_test.go' | wc -l # 359 — состояние лендинга P5
grep -rh '^func Test' --include=*_test.go platform | wc -l # 416 — дерево после дофикса
То есть P6 + дофикс = +57 тестовых функций; на конце P6 в дереве был 401 (счёт приёмки — сходится:
дофикс добавил 15, из них 6 — по находкам собственного ревью). Плюс 2 фаззера (^func Fuzz), в этот
счёт не входящие и исполняемые сид-корпусом.
Батарея (go test ./... -race -count=1 -v при обоих гейтах окружения):
grep -cE '^--- (PASS|FAIL|SKIP)' <лог> # 418 верхнеуровневых прогонов (416 тестов + 2 фаззера)
grep -cE '^ --- ' <лог> # 139 подтестов
grep -c -- '--- SKIP' <лог> # 0
make check EXIT=0, 17 пакетов, линтер 0 issues, make vuln — без уязвимостей. Стенд:
PG 18.4 ~/.local/pgsql порт 55433, свой tmctl из КОПИИ backend/ вне репозитория.
Посадки (мутация → что упало)
| Мутация | Упало |
|---|---|
runner.Status: вернуть ошибку на ЛЮБОЙ ненулевой код |
TestAFlaggedBookStillAnswersAboutItsMoney; сквозной денежный — сообщением «холд флагнутого прогона всё ещё открыт: 3.000000», то есть ровно симптомом приёмки |
Supervisor.Status: то же на дев-пути |
TestTheDevelopmentStatusReadsAFlaggedBookToo |
reconcile: вернуть перечит причины на одну ветку |
TestACeilingSurvivesAMarkerThatCannotBeRead — failed/пусто вместо paused/daily_ceiling |
outcome: убрать case ingest.OutcomeStopped |
TestAGracefulSignalIsAStopOnlyWhenThisPlatformAskedForOne |
CeilingPause: вернуть credit_exhausted по умолчанию |
TestTheCeilingScopeDecidesWhichPauseTheRunGets |
config.Load: убрать гейт INSECURE_COOKIES+OIDC |
TestCookiesWithoutSecureAreRefusedNextToAProductionProvider |
seed: убрать TrimSpace |
TestASubjectThatIsOnlyPaddingIsNoSubject |
BooksForMigration: убрать предикат живого прогона |
TestEachOfTheThreeBlockersAlone… — bk-live объявлен мигрируемым |
| …убрать предикат резюмируемого | то же на bk-paused |
| …убрать предикат незакрытого холда | то же на bk-hold |
freshPausedReason: вернуть падение на снапшот |
TestAnEndingIsNeverDecidedFromAReadThatFailed — ошибка приходит уже от записи, а не от чтения |
Status: брать exit 2 по коду и разбору, без согласия отчёта |
TestExitTwoIsTakenOnlyFromAReportThatAgreesWithIt (4 подтеста) |
DecodeManifest: убрать проверку manifest_version |
TestADocumentThatIsNotAManifestIsNotAnEmptyManifest |
Parse: читать «глав нет» в одиночку |
TestAManifestThatContradictsItselfNeverCostsTheUpload |
Manifest: вернуть «любой ненулевой = отказ» |
TestAManifestThatCompletesWithFlagsIsStillAManifest |
drain: дренить и за капом вместо убийства процесса |
TestAnEndlessStatusIsRefusedRatherThanRead — виснет и падает по таймауту |
Пятнадцать из пятнадцати. Непинябельных в этом круге нет.
Чего я НЕ сделал — явным списком
- Контракт
openapi.yamlи его зеркало — не тронуты (PD-199 ждёт слова владельца, правит S4).ceiling_unknownиdaily_ceilingна провод по-прежнему не проецируются. tmctl migrateживьём не гонялся — движковой команды всё ещё нет в HEAD. Порядок вdeploy/README.mdисправлен и стал исполнимым, но проверен ЧАСТИЧНО: список--migratableна стенде, самmigrate— нет. Самолечение поschema_mismatchвслепую не строил (PD-201).- Читающая поверхность (P7), эскроу (строка 136), брокер — нет.
- PD-217…PD-222 и PD-241…PD-245 не чинены — заведены строками, каждая с причиной, почему не этим касанием.
- Порядок «причина паузы раньше намерения стопа» не переворачивал (PD-241): это ратифицированное решение P6, и его смена двигает клетки за пределами фикс-листа — вопрос оркестратору.
- Промт дофикса не трогал (
PLATFORM_P6_DOFIX_SESSION_PROMPT.md— артефакт оркестратора).
Вопрос на ратификацию, добавленный этим кругом
К трём вопросам P6 добавляется один: порядок «потолок раньше стопа» в outcome (PD-241). Сегодня
прогон, который пользователь остановил, показывается как paused/credit_exhausted, если в тот же
дрейн приехал потолок, — и этим зажигает аккаунтный halted-флаг у аккаунта с деньгами. Обе половины
— и статус, и флаг (PD-203) — про одно: что именно платформа обязана показать, когда две правды
пришли вместе.
P6 (14.08): потребительская половина шва · интейк формы Б · дев-стенд
Промт — PLATFORM_P6_SESSION_PROMPT.md, плюс аддендум оркестратора по ресёрчу деплоя (принят
эхом, отдельный пункт ниже). Всё, что здесь заявлено, проверено ИСПОЛНЕНИЕМ; там, где проверка
ждёт, это сказано словом «ждёт», а не пропущено.
Записка-план: ID → статус → улика
| ID | Статус | Улика |
|---|---|---|
| П-15 (а) exit-коды | ЗАКРЫТО | ingest/exit.go — ОДИН словарь shell-контракта на зону (реконсилятор, интейк, дев-супервизор). 4 = paused двумя независимыми каналами · 5 без намерения = прерывание и перезапуск · 10–19 = полоса. PD-113 и PD-152 закрыты, PD-196 закрыт потребительской половиной. Пины: TestWhatTheUnitDidBecomesTheProductStatus (13 строк таблицы) · TestOutcomeOfCoversTheWholeExitContract · TestAGracefulSignalIsAStopOnlyWhenThisPlatformAskedForOne |
| П-15 (б) фолд присваиванием | ЗАКРЫТО | таблица unit_resolutions (миграция 00015) по тройке (chapter, unit, wave); счётчики глав — count(*), не +1. Пин pgstore.TestAUnitAnnouncedTwiceIsCountedOnce (новый seq с тем же юнитом — то, что курсор поглотить не может) |
| П-15 (в) 1.1 · scope · outcome | ЗАКРЫТО | StreamVersion = "1.1", Ceiling.Scope, Outcome как ОДИН словарь для терминальной строки и кода выхода. Дневной потолок различим: внутренняя причина daily_ceiling, резюм отвечает 409 с диагностикой. Пины TestTheCeilingScopeDecidesWhichPauseTheRunGets · TestAResumeOfARunTheEnginesDailyCeilingStoppedIsRefused |
| П-15 (г) дев-супервизор | ЗАКРЫТО | outcomeOf удалён, дев-путь читает ту же ingest.OutcomeOf; два словаря exit-кодов в зоне больше не существуют |
| П-15 watch: foreign-hello | ВОСПРОИЗВЕДЁН и ЗАКРЫТ | дефект оказался тяжелее наблюдения: чужой поток усыновлялся, его ceiling материализовался на нашу попытку (ложная paused), а собственный hello потом валил проекцию в карантин НАВСЕГДА. Платформа теперь НАЗЫВАЕТ поток (TM_TRACE_ID), и называет его при создании строки попытки, а не при спавне. PD-200, PD-210 |
| П-14 интейк формы Б | ЗАКРЫТО | рендер в ОДНОМ месте (books.provision), из TM_PLATFORM_BOOK_TEMPLATE, работой с YAML-УЗЛОМ (комментарии и незнакомые ключи оператора живы, значения — строки), O_EXCL. Битый шаблон = класс, который ЖДЁТ. Проверено настоящим tmctl manifest: 3 главы, exit 0 |
| П-16 дев-стенд | ЗАКРЫТО | tmplatformctl seed ходит по HTTP теми же дверями, что пользователь; дев-вход с четырьмя гардами непроходимости в проде + пятым после ревью; рецепт страницей в deploy/README.md. Прогнано на стенде целиком |
| Деплой-порядок v15 | ЗАПИСАН, проверка ЖДЁТ | правило «эмиттер-бинарь не на стенд до tmctl migrate» + порядок в deploy/README.md, шаг сделан ИСПОЛНИМЫМ (tmplatformctl books --migratable). Сам tmctl migrate не гонялся — движковая половина не заленджена; помечено ждущим, НЕ сделанным (PD-201) |
| Попутные PD | ЗАКРЫТЫ 141 · 163 · 164 · 165; 154 ПЕРЕ-ДИСПОЗИЦИРОВАН | PD-165 закрыт ДВУМЯ разными ответами: относительный CTL_BIN отвергается на буте (воспроизведено: systemd 259 не стартует юнит), а $ ЗАМЕРЕН и оказался безопасен — экранирование было бы ошибкой |
| Гигиена доков | ЗАКРЫТО | README · STACK_DECISIONS · BACKLOG · регистр (секции, порядок, веса, шапка) · PLATFORM_DIRECTION — с ПРЕДЛОЖЕНИЕМ пере-подписи, а не тихой правкой |
Аддендум оркестратора (ресёрч деплоя 14.08) — отдельным пунктом, как просили
- «Прогоны остановлены» = нет живых юнитов И нет открытых резюмируемых попыток. Записано в
деплой-док и сделано ИСПОЛНИМЫМ:
tmplatformctl booksпечатает колонку MIGRATE и причину отказа,--migratable— только безопасные каталоги, по одному на строку, чтобы шаг деплоя был циклом, а не абзацем. Блокируют ТРИ разных факта, и они названы порознь, потому что снимаются по-разному: живой прогон · резюмируемый прогон (его попытка запинена к СТАРОМУ бинарю, и миграция заставила бы резюм перейти на новый — риск пере-оплаты уже купленных вызовов) · незакрытый холд (расчёт читаетcommitted_usdтем же запиненным бинарём, после миграции цифру уже не прочитать). - Расчёт денег зовёт ЗАПИНЕННЫЙ бинарь. Проверено чтением и исполнением: код был уже верен
(
s.engineBinary(l)на всех трёх путях — расчёт, ре-синк, чтение счётчика перед спавном), но ПИНА не было, а фейк движка выбрасывал аргумент. Фейк теперь записывает, чем его звали; пинruns.TestTheMoneyOfAFinishedRunIsReadWithTheBuildItRanWith, посадка «зватьCfg.EngineBinary» падает с именем нового бинаря в сообщении. - Самолечение по машиноразличимой ошибке — НЕ построено, и вот что построено вместо. Пока я
работал, параллельная бэкенд-сессия завела в дереве движка exit-код 13 (
schema_mismatch) и классRefusalSchemaMismatch— незакоммиченными. Вслепую я не строил и автоматику «поймала →migrate→ повтор» не делал: она означает, что ПЛАТФОРМА исполняет мутирующую команду движка, и это решение под ратификацию вместе с движковой половиной. Что сделано: код 13 заведён в словаре и отображён на класс, который ЖДЁТ человека и не тратит бюджет попыток, — то есть книга на хосте посреди апгрейда не отклоняется, а дожидаетсяmigrate. ⚠ Номер прочитан из НЕЗАКОММИЧЕННОГО дерева движка; если он приедет другим, менять одну строкуingest.ExitSchemaMismatch, и ошибка в обе стороны недеструктивна (неизвестный код полосы и так падает в безопасный класс). PD-201.
Живые пробы (команды и что они показали)
Стенд: PG 18.4 ~/.local/pgsql порт 55433 · демон tmplatformd на 127.0.0.1:8099 · свой tmctl,
собранный из КОПИИ backend/ вне репозитория · фейк движка с ПРАВИЛОМ потолка (умеет отказать —
иначе он ничего не проверяет, урок D39.123).
| Что проверялось | Чем | Результат |
|---|---|---|
| Рендер даёт конфиг, который движок ГРУЗИТ | tmctl manifest --config <рендер> --json |
exit 0, chapters_total: 3, book_id = id платформы; комментарий оператора и его ceilings в файле целы, плейсхолдер book_id: WILL_BE_REPLACED заменён, не продублирован |
| Полоса отказов на НАСТОЯЩЕМ движке | прогон без ключей провайдера на стенде | маркер exit-code/exited/10 → прогон failed, холд вернулся ЦЕЛИКОМ ($30 → $30), файл цел, ceiling_arg = 90000 (3 главы × $0.03) |
| Потолок книги (PD-113) | фейк с правилом, --usd 0.10 |
журнал: ceiling{scope:book} + finished{ceiling}, exit 4 → карточка paused/credit_exhausted, прогресс 4/6 ИЗ ПОТОКА, расчёт $0.08 из $5.10 |
| Дневной потолок и резюм | тот же фейк, SCENARIO=daily-ceiling |
хранится paused/daily_ceiling, на проводе paused_reason: null, повторный резюм — 409 и НИ ОДНОГО спавна |
| exit 5 без намерения | systemctl --user stop по живому юниту |
маркер exited/5 → попытка 1 interrupted, попытка 2 открыта, прогон translating |
| exit 5 с намерением | POST /v0/runs/{id}/stop |
маркер exited/5 → stopped, второй попытки нет |
| Дев-вход + сид целиком | tmplatformctl seed --url … |
аккаунт dev/dev@stand, грант, загрузка через ЖИВОЙ POST /v0/books, интейк not_started, 3 chapters |
| Гейт дев-входа | демон с TM_PLATFORM_DEV_LOGIN И боевым OIDC |
fatal: … cannot be enabled on a deployment that has an OIDC provider — не стартует |
$ в ExecStopPost (PD-165) |
systemd-run --user --property=… --setenv=dir=EXPANDED |
путь доехал ЛИТЕРАЛЬНО (…/dollar$dir/), то есть экранирование в $$ было бы ошибкой |
Относительный CTL_BIN (PD-165) |
systemd-run с ./tmplatformctl |
«neither a valid executable name nor an absolute path» — юнит не стартует вовсе |
⚠ Найдено ЖИВОЙ пробой, а не чтением: внутренняя причина daily_ceiling уезжала на провод, где
контракт знает одно значение и клиент, сгенерированный по спеке, её бы отверг. Починено
(contractPausedReason), строка PD-199 — вопрос владельцу контракта. Второе: сид упёрся в CSRF-слой
(X-TM-Client обязателен на unsafe-запросе с кукой) — ровно в ту дверь, которую обязан правильно
открывать фронт; это и есть причина, по которой сид ходит по HTTP, а не пишет строки сам.
Посадки
Первый круг — 20 из 20 пойманы, но не с первого захода: три посадки ПЕРЕЖИЛИ, и каждая означала слепой пин, а не безобидность мутации.
| Пережившая посадка | Почему пин был слеп | Что сделано |
|---|---|---|
mine := offset > 0 в тейлере |
тест ставил чужой поток ПЕРЕД своим hello, а чужой hello сам выключает mine — мутация не наблюдалась |
пин переписан на реальный сценарий: хинт попадает ВНУТРЬ чужого потока (после его hello), и мутация даёт sequence gap → карантин здорового прогона |
O_EXCL → O_TRUNC в рендере |
при существующем файле открытие не достигается — гарду это не свойство | тест переписан: первая ходка падает на ДВИЖКЕ (книга остаётся в интейке), шаблон удаляется, вторая ходка обязана его не искать. Ловится снятие short-circuit. ⚠ Сам O_EXCL НЕ пинится и это названо в тесте: две одновременные ходки пишут одинаковые байты, наблюдать нечего |
PauseRun всегда рапортует успех |
я запустил посадку по несуществующему имени теста | перезапущено против TestAPauseFromAnOldSnapshotDoesNotCloseARunOverALiveAttempt — падает |
Второй круг (находки панели) — 6 из 6, седьмая названа непиняемой: фолбэк причины в ветке
исчерпания бюджета недостижим по построению (в restart попадают только ветки, где причины нет),
и это записано в самом коде, а не заявлено закрытым.
Адверсариальное ревью (author≠reviewer, механизмом)
Четыре независимых ревьюера по диффу с установкой ОПРОВЕРГАТЬ, из них опровергатель другой модели-семьи (Fable) на security-оси — норма D39.120. Линзы: деньги и гонки · безопасность дев-входа · соответствие потребителя реальному движку и судьба файла пользователя · повторное использование, общность, честность комментариев.
| Находка | Линза | Диспозиция |
|---|---|---|
Имя провайдера dev не зарезервировано: TM_PLATFORM_OIDC_PROVIDER=dev кладёт чужие sub в namespace дев-аккаунта |
безопасность (Fable) | ПРИНЯТА, закрыта. Четвёртое из четырёх заявленных свойств было ЛОЖНЫМ без этого. Load отвергает имя, пин + посадка. PD-206 |
TM_PLATFORM_DEV_LOGIN=" " монтирует вход с личностью «пробел» |
безопасность (Fable) | ПРИНЯТА, закрыта тримом + пином |
Дев-вход не получает второй CSRF-слой (X-TM-Client нужен только при уже имеющейся куке) |
безопасность (Fable) | ПРИНЯТА как остаток, названа в коде и строкой PD-205: на стенде последствие ничтожно, но «POST, значит безопасно» — слабее, чем звучит. Тот же класс у боевого /auth/login |
| Ветка дев-входа стоит ПЕРВОЙ в switch — один регресс конфига даст ей перекрыть боевой OIDC | безопасность (Fable) | ПРИНЯТА, добавлен избыточный гард в main.go; он по построению недостижим, поэтому теста не имеет — сказано вслух |
| Грант дев-входа безусловен (боевой путь его придерживает у неподтверждённой личности) | безопасность (Fable) | ПРИНЯТА как намеренная: аккаунт с нулём кредита делает каждый прогон отказом. Не кран: строка леджера пишется только при INSERT личности — проверено в коде |
daily_ceiling стирался двумя путями → ложный аккаунтный флаг + обход гарда резюма |
деньги и гонки | ПРИНЯТА, закрыта. Третье значение ceiling_unknown + прогон с названной причиной не перезапускается. PD-209 |
Окно усыновления между ДОПУСКОМ и спавном; coalesce отказывался чинить усыновлённое имя |
деньги и гонки | ПРИНЯТА, закрыта. Имя даётся при создании строки попытки. PD-210 |
Побочно: Begin стал недостижим, и с ним тихо выключился chunker_version |
деньги и гонки | ПРИНЯТА, закрыта переносом эффекта в effect, пин + посадка. PD-210 |
| Апсерт юнита без гарда по времени: переанонс старой резолюции затирает новую | деньги и гонки | ПРИНЯТА, закрыта одним where; пин + посадка. PD-211 |
| Паника движка неотличима от «завершено с флагами» (Go-рантайм выходит РОВНО 2) | шов | ПРИНЯТА, НЕ чинится здесь: лечение — recover или другой номер, это движковая сторона. PD-212, запрос уходит строкой единого бэклога |
Форма манифеста не версионируется → будущий дрейф даст ChaptersTotal=0 → удаление файла |
шов | ПРИНЯТА как латентная, строка PD-213. Сегодня формы совпадают поле-в-поле; лечить сверкой manifest_version, где незнакомая версия даёт НЕ-деструктивный класс |
Чужая hello валит Tail до проверки «чья строка» → карантин здорового прогона навсегда |
шов | ПРИНЯТА, строка PD-214: порядок проверок в apply лечится отдельно, класс не денежный |
Код 13 (schema_mismatch) отсутствовал в словаре платформы |
шов | ПРИНЯТА, закрыта (см. аддендум п.3) |
| Сид рапортовал «кредит начислен» о начислении, которого не было | повторное использование | ПРИНЯТА, закрыта: зовёт существующий хелпер write. Проверено на стенде — второй прогон печатает no-op. PD-208 |
| Петля перезапусков без счётчика и бэк-оффа; exit 12 терминален, хотя лок проходит сам | деньги и гонки | ПРИНЯТЫ как остатки, PD-215 и PD-216: лечатся вместе (перезапуск на 12 без бэк-оффа — это и есть PD-215) |
gopkg.in/yaml.v3 — последний релиз по этому пути модуля |
повторное использование | ПРИНЯТА как заметка: переезд решается ОБЕИМИ зонами сразу, записано в STACK_DECISIONS §28 |
| Воскрешение прогона устаревшим снапшотом (нашёл сам при аудите диффа) | самоаудит | ЗАКРЫТА RestartInput.OnlyIfLive; пин + посадка «stopped → translating». PD-207 |
Чего я НЕ сделал — явным списком
- Читающая поверхность (SSE, главы/юниты/замечания, банк) — пак P7, заведена строкой П-17.
Хранилище под первую половину уже есть:
unit_resolutionsнесёт диспозицию каждого юнита. - Эскроу денег шва (строка 136) и вместе с ним PD-154 — денежный промт, строка П-18.
- Самолечение по
schema_mismatch— аддендум п.3 выше: вслепую не строю. - Спека
openapi.yaml— не трогал: её правит S4-сессия фронта. Отсюда PD-199. ReadUsage— не трогал, хотя ревью подтвердило: аккаунтный halted-флаг зажигается по паузе ОДНОГО прогона (PD-203). Это продуктовая семантика уже построенной ручки; правка — на ратификацию, а не тихо в паке про шов.- Порядок проверок в
applyтейлера (PD-214), версионирование манифеста (PD-213), бэк-офф перезапусков (PD-215/216) — приняты строками, не построены: класс PD-83. O_EXCLрендера — не пинится и не заявлен пинённым.- Шаг
tmctl migrateна стенде — ждёт движковую половину.
Вопросы на ратификацию
daily_ceilingна проводе. Контракт перечисляет одно значениеPausedReason, платформа различает два (плюсceiling_unknown). Сейчас незнакомая контракту причина едет какnull— ровно то, что спека сама велит клиенту рендерить. Назвать причину в спеке (тогда правка — одна функцияcontractPausedReason) или подтвердитьnull? PD-199.PLATFORM_DIRECTION§3. Две строки не исполнены:oapi-codegen(«ВЗЯТЬ — доказано») иsqlc(«до первого хендлера» — срок пройден, PD-44 открыт с P1). Зона не переписывает ратифицированное направление сама: в §3 добавлен баннер статуса с доводами за и против пере-подписи. Решение за оркестратором.- Второй гейт батареи. Живая проба рендера против НАСТОЯЩЕГО движка сделана постоянным тестом
(иначе клейм «движок это грузит» протухнет молча), и она гейтится
TM_PLATFORM_TEST_ENGINE_BIN+TM_PLATFORM_TEST_BOOK_TEMPLATE— той же формой, что у БД-тестов. На голом клоне это ОДИН честный скип. Если приёмка считает скипы при одном лишь DSN, число будет не ноль — и это не регресс, а новая ось проверки; форму гейта готов поменять по слову.
Третий раунд (14.08): ре-чек оркестратора FP5-9…FP5-11 и хвосты
Все три находки подтвердились на коде до правки; возражений нет. Две из трёх — мои вчерашние фиксы, которые ЗАЯВЛЕНЫ шире, чем сделаны, и это ровно тот класс, за который приёмка уже била.
FP5-9 [HIGH] Гейт тулчейна не отказывал 1.26.5 — и три дока это утверждали
Проверено исполнением: make version-check GO_VERSION=go1.26.5 на прежнем правиле проходил.
Регекс go1\.26\.([5-9]|[0-9]{2,}) принимал ровно ту версию, ради отказа от которой floor и
поднимали, а GO_MIN_VERSION жил только в тексте сообщения. Тот же регекс ставил 1.26.10 НИЖЕ
1.26.9. Клейм «хост на 1.26.5 получит отказ» был ложен в трёх местах (журнал ×2, STACK_DECISIONS,
коммент Makefile) — гейт «поднят» был только на словах.
Сделано, двумя независимыми механизмами:
make version-checkСРАВНИВАЕТ версии (sort -V), а не матчит; префиксыrc/develотвергаются отдельной веткой — пререлиз не несёт фиксов, которые обещает его номер. Цель отделена отtools-checkи берёт версию из переменнойGO_VERSION, чтобы её можно было судить версиями, которых на хосте нет.go.modполучилtoolchain go1.26.6. Решение и обоснование (оркестратор просил решить): floor ЯЗЫКА (go 1.26.4) остаётся общим с движком — один стенд собирает оба модуля; директиваtoolchainподнимает только тулчейн, то есть stdlib, которым линкуется сетевой модуль. Её читает ВСЯКАЯ сборка, включая ту, что не зовёт make: приGOTOOLCHAIN=auto(дефолт) хост скачает 1.26.6 вместо того, чтобы собрать уязвимый бинарь, при=local— остановится с ошибкой, называющей версию. Обе ветки — тот ответ, который нужен; ни одна не зависит от того, прочитал ли кто-то Makefile. Цена названа: офлайн-хост с=localи только 1.26.5 теперь не соберёт зону — это и есть гейт.
Пин — на СРАВНЕНИЕ, а не на прогон: новый пакет internal/gates гоняет version-check таблицей
версий, которых на этой машине нет (1.26.4/1.26.5/1.25.9/1.9.9/rc/devel — отказ;
1.26.6/1.26.7/1.26.10/1.27.0/2.0.0 — приём), и вторым тестом требует, чтобы go.mod и
GO_MIN_VERSION называли одну версию. Две посадки падают. Три ложных клейма переписаны на описание
механизма. Строка PD-197.
FP5-10 [HIGH] Гард корня не переживал unmount и маскировался бутовым MkdirAll
Тоже подтверждено: Stat(BooksDir) отвечает «есть» про пустой mountpoint, оставшийся после
размонтирования тома, смонтированного РОВНО в BooksDir, — а бут безусловным MkdirAll пересоздаёт
корень после рестарта. То есть мой вчерашний фикс закрывал только форму «каталог удалён вместе с
родителем» и не закрывал ту, ради которой писался.
Сделано по кандидату оркестратора — сентинел провижининга: файл .tmplatform-books в корне,
который пишет ПЕРВАЯ загрузка (markStorage, O_EXCL, поэтому две одновременные загрузки обе правы)
и не пишет больше никто — в частности, не пишет бут. Решение «хранилище на месте» принимается по
СЕНТИНЕЛУ (storageIsThere), а не по существованию каталога: ни unmount, ни MkdirAll его не
подделывают. Хост, который никогда не принимал загрузок, отвечает «нет» — безопасная сторона: у него
нет книг, которые можно отклонить.
Пин — books.TestAnUnmountedVolumeLooksLikeAnEmptyRootAndStillIsNotTheBooksFault: удаляем всё под
корнем и пересоздаём сам корень (ровно то, что оставляют unmount и бут), десять циклов ожидания —
книга жива, бюджет цел, движок не спрошен, сентинел не воскрес. Первая редакция этого пина посадку
ПЕРЕЖИЛА (мутация «не писать сентинел вовсе»): без сентинела всё ждёт, и тест этого не отличал —
добавлено утверждение, что принятая загрузка сентинел ПИШЕТ, иначе терминальность крэш-окна (FP5-3)
теряется. Обе посадки падают. PD-192 дополнена.
FP5-11 Правило ревизии применено не ко всем писателям
Пять писателей жизненного цикла ПРОГОНА остались на revision + 1 против правила, которое дофикс
ввёл для трёх интейковых: старт, реоткрытие, пауза (runs.go) и два закрытия (sink.go). Значит
staleness библиотеки, которую вчера закрыли на статусах интейка, оставалась на статусах прогона:
книга не-максимума аккаунта уходила translating — библиотека не двигалась.
Сделано: все пять берут nextRevisionOfThisBooksLibrary. Пин —
pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary (вторая книга держит максимум 41,
прогон стартует на первой; проверяются обе половины жизненного цикла — старт и пауза). Посадка
падает.
⚠ Мотивированное исключение, названное вслух: два писателя books.revision НЕ переведены —
прогресс-путь синка (bumpBook на unitDone/progress). Причина не вкусовая: unitDone вычисляет
chapters.revision из books.revision + 1 ДО инкремента, и прыжок книги через пол развязал бы
книжную и главную шкалы, а это отдельная scope-семантика контракта. Она и не нужна: прогресс
существует только у книги, у которой ИДЁТ прогон, а старт прогона теперь сам поднимает книгу на
уровень библиотеки — дальше +1 монотонен и виден. Если правило когда-нибудь понадобится и там,
оно приедет вместе с проверкой главной шкалы, а не молча.
Хвосты ре-чека
- (а) Коммент
PauseRunобещал проверку «в том же стейтменте», а стоит отдельныйselect … for updateв той же транзакции. Текст приведён к коду (транзакция и её замки). - (б) Комменты
parse.go(«terminal on the first answer») описывали до-дофиксное поведение на пути, который УДАЛЯЕТ файл пользователя, — самое опасное место для устаревшего текста. Переписаны: через бюджет попыток идёт КАЖДЫЙ ответ движка, удаляет только последний. - (в) Остаток FP5-5 закрыт кодом, а не строкой: проход, которому осталось меньше, чем нужно
разбору, книгу НЕ НАЧИНАЕТ (клейм берётся внутри
Parse, поэтому отложенная книга не тратит ничего) и говорит об этом одной строкой лога вместо ошибки на книгу. Пин —books.TestAPassTooShortForAParseStartsNoneAtAll, посадка падает. - (г) Мета-пин systemd-гейта перечислителен — принято как названный остаток: он ловит возврат к сверке сообщений по словам systemd и требует, чтобы гейт вообще спрашивал менеджер, но исчерпать все будущие формулировки перечислением нельзя.
Посадки третьего раунда
| Посадка (что откатывается) | Пин, который обязан упасть | Итог |
|---|---|---|
| гейт снова матчит регексом (принимает 1.26.5) | gates.TestTheToolchainGateComparesVersionsRatherThanMatchingThem |
поймана |
go.mod снова без toolchain |
gates.TestGoModPinsTheSameToolchainTheBatteryDemands |
поймана |
| гард снова смотрит на существование корня | books.TestAnUnmountedVolumeLooksLikeAnEmptyRootAndStillIsNotTheBooksFault |
поймана |
| сентинел не пишется загрузкой | тот же | ПЕРЕЖИЛА → пин усилен → поймана |
старт прогона снова revision + 1 |
pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary |
поймана |
| проход снова начинает книгу, на которую нет времени | books.TestAPassTooShortForAParseStartsNoneAtAll |
поймана |
Батарея третьего раунда
make checkс живым PG 18.4 (стенд~/.local/pgsql, порт 55433) под-race: EXIT=0, 16 пакетов, скипов 0, линтер 0 issues,make vuln— чисто.- Тесты
^func Testисполнением: 354 → 359 (+5, удалённых 0). - Посадки: 6 из 6 (одна со второго захода, см. таблицу).
Записка-план третьего раунда: ID → статус → улика
| ID | Статус | Улика |
|---|---|---|
| FP5-9 | ЗАКРЫТО | version-check сравнивает (sort -V) + toolchain go1.26.6 в go.mod; internal/gates ×2 пина, две посадки падают; три клейма переписаны. PD-197 |
| FP5-10 | ЗАКРЫТО | сентинел .tmplatform-books, пишет только загрузка; books.TestAnUnmountedVolumeLooksLikeAnEmptyRootAndStillIsNotTheBooksFault; две посадки падают. PD-192 дополнена |
| FP5-11 | ЗАКРЫТО с названным исключением | пять писателей прогона на nextRevisionOfThisBooksLibrary; pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary; посадка падает. Прогресс-путь синка НЕ тронут — обоснование выше. PD-122 дополнена |
| хвост (а) | ЗАКРЫТО | текст PauseRun приведён к коду |
| хвост (б) | ЗАКРЫТО | текст parse.go про терминальность приведён к коду. PD-198 |
| хвост (в) | ЗАКРЫТО кодом | проход не начинает книгу без полного бюджета; books.TestAPassTooShortForAParseStartsNoneAtAll; посадка падает. PD-189 дополнена |
| хвост (г) | ПРИНЯТО как остаток | перечислительность мета-пина названа, не «закрыта» |
Дофикс-2 (13.08): кросс-семейное ревью САМОГО дофикса
Пропуск, который я закрыл только по вопросу владельца: дофикс приёмки был проверен исполнением (пины · посадки · батарея · живая проба), но НЕ был отревьюен независимо — author≠reviewer для дофикс-диффа не соблюдался. Запущены два ревьюера другой семьи (Fable 5, я Opus), read-only по дереву, каждый со своей линзой — «деньги и гонки» и «интейк, потеря данных, правдивость чисел». Вернули пять дефектов с воспроизведением, одну гипотезу и два расхождения отчётности. Все подтверждены мной на коде до правки; возражений нет.
Н1 [medium-high, интейк] Пропавший КОРЕНЬ хранилища читался как вина каждой книги
os.Stat(workdir) даёт ENOENT и когда пропал каталог одной книги, и когда не смонтирован сам
BooksDir. Первое — терминальный конец этой книги (и лечение крэш-окна отказа, FP5-3), второе —
беда хоста. Читая второе как первое, ОДИН проход свипа терминально отклонял ВСЕ книги в интейке с
source_unreadable — причиной, которая винит файл пользователя, терминальна по устройству и не
имеет обратного хода (ни перепарса, ни удаления, PD-175). То есть мой же фикс FP5-3 создал путь
массового уничтожения загрузок из-за размонтированного тома.
Сделано: ErrStorageGone отличён от ErrDirectoryGone — корень спрашивается ПЕРЕД тем, как винить
книгу; причина storage_unavailable никогда не терминальна, бюджет не тратит (движка не звали) и на
книге не хранится. Предикат waitsForTheDeployment собрал оба таких случая в одном месте.
Пин — books.TestAVanishedStorageRootIsNotEveryBooksFault (две книги, снесённый корень, десять
циклов ожидания: обе живы, бюджет цел, движок не спрошен ни разу, том вернулся — следующий проход
разбирает). Посадка падает. Строка PD-192.
Н2 [minor, ревизия] Пол ревизии глотал весь статусный проход книги
Продолжение FP5-1 на соседнем пути: вставка научилась брать greatest(max, пол) + 1, а смены
статуса остались на revision + 1. Книга, загруженная ДО чужой отменённой загрузки, идёт
uploading → parsing → not_started тремя инкрементами СВОЕГО счётчика и всё ещё под полом —
greatest(max, пол) не двигается вовсе, а клиент по контракту отбрасывает чтение, которое не выше
применённого. Экран, только что загрузивший книгу, показывает её «приходящей», пока библиотеку не
сдвинет постороннее событие. Монотонность при этом не нарушена — это staleness, и именно поэтому
прошлый круг её не заметил.
Сделано: три писателя статуса (StartParsing, FinishParse, RejectBook) берут следующий номер
БИБЛИОТЕКИ, а не свой + 1 (nextRevisionOfThisBooksLibrary, владелец читается из обновляемой
строки). Пин — books.TestAStatusChangeIsVisibleEvenUnderTheLibrarysFloor, посадка падает. PD-122
переформулирована: остаток — собственный счётчик области (двое ОДНОВРЕМЕННЫХ писателей могут
вычислить одно и то же число; значение при этом растёт, поэтому клиент перезапрашивает).
М1 [medium, деньги] PauseRun — третий закрывающий путь без гарда живой попытки
Тот же класс, что PD-181 и FP5-2, на пути, который прошлый круг не проверил: пауза закрывала прогон,
не спрашивая, жива ли ещё закрываемая попытка И этого ли она прогона (апдейт попытки шёл даже без
run_id). Сценарий ревьюера: два поколения реконсилятора при перекрывающемся деплое, у старого ниже
PerChapter в конфиге → его проход считает остаток исчерпанным и паузит прогон, который уже
рестартован в живую попытку 2. Итог — прогон paused/credit_exhausted под живым тратящим движком, а
открытый холд попытки 2 не виден НИ в ListLiveRuns (прогон завершён), НИ в UnsettledRuns (попытка
не завершена).
Сделано: тот же exists (… a.id = $4 and a.run_id = runs.id and a.ended_at is null), что у соседей,
плюс run_id в апдейте попытки. Пин — pgstore.TestAPauseFromAnOldSnapshotDoesNotCloseARunOverALiveAttempt
(проверяет и то, что прогон жив, и то, что холд живой попытки виден в списке). Посадка падает. PD-193.
М2 [low, проекция] Синк законченной попытки усыновлял чужой хендшейк
Форма, которой до P5 не существовало: стоп ДО спавна оставляет попытку закрытой и без
engine_run_id. Устаревший материализатор с такой попыткой принимал hello попытки, которая её
заменила, биндил чужой engine_run_id на себя и материализовал тот же журнал второй раз — счётчики
глав и юнитов удваивались. Деньги не двигались.
Сделано: бинд отказан для законченной попытки (ended_at is null в CAS). Пин —
pgstore.TestAMaterializerOfAnEndedAttemptDoesNotAdoptTheNextAttemptsEngine, посадка падает. PD-194.
М3 [low, деньги] Возврат холда «прогона, который не запускался», ждал ответа движка
Ревьюер дал это гипотезой; я перепроверил и подтвердил. settle сперва требовал успешного
tmctl status, и только потом ветка «юнита не было — вернуть холод целиком». Но хост, который
производит эту ситуацию, — ровно тот, где движок не запускается: Status падает на каждом проходе,
и деньги прогона, который не начинался, остаются зарезервированными навсегда (видимыми, но
запертыми).
Сделано: ветка «нет юнита и нет базовой линии» идёт ДО вызова движка; ReleaseUnspawned
перепроверяет строку под замком, поэтому снапшот, который успели заспавнить, отвергается там и
settle идёт обычным путём. Пин —
runs.TestTheHoldOfARunThatNeverStartedComesBackOnAHostWhoseEngineCannotAnswer (движок отвечает
«no such file» на любой запрос; холд обязан вернуться, движок — не быть спрошенным). Посадка падает.
PD-195.
Хвосты того же круга
- Н3.
StartParsingи обаabandonвAcceptшли наcontext.WithoutCancelбез своего дедлайна: отсоединённый — не значит бесконечный, повисший запрос держал бы горутину запроса, которого уже нет. Переведены наwriteCtx(30 с). Пина нет и не заявляется: это граница, а не наблюдаемое поведение — чтобы его пинить, нужен стор, умеющий висеть. - Н4. Опечатка оператора в рукописном
book.yamlпо-прежнему стоит файла пользователя: движок отвечает exit 1 на всё, через пять циклов книга отклоняется какsource_unreadableи исходник удаляется. FP5-4 закрыл только «конфигурации нет вовсе». Заведено отдельной строкой риска PD-196, а не спрятано в комментарий: настоящее лечение — различающий код выхода или--dry-runна стороне движка, это не правка платформы. - Н5. Пин FP5-7 сверял ДВЕ точные формулировки старого стринг-матча — третье написание прошло бы мимо. Теперь тест разбирает файл гейта парсером и смотрит только КОД (комментарии там цитируют ту самую строку — текстовый скан принял бы историю за дефект), запрещает в коде слова systemd и требует, чтобы гейт вообще спрашивал менеджер. Посадка «вернуть матч по сообщению» падает.
- Отчётность. Ревьюер не смог проверить клейм «посадки 10 из 10»: списки лежали в скрэтчпаде сессии, а не в дереве. Исправлено — списки посадок теперь ниже, в самом журнале.
- Ловушка счёта, в которую ревьюер едва не попал и которую стоит знать:
git grep '^func Test' HEAD -- platformдаёт 262 — лишняя строка из код-блока в архивном доке; счёт по*_test.goдаёт 261.
Списки посадок (то, чего не хватало отчёту)
| Раунд | Посадка (что откатывается) | Пин, который обязан упасть | Итог |
|---|---|---|---|
| Дофикс | вставка снова читает только max(revision) |
TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision |
поймана |
| Дофикс | FinishUnspawnedStop без гарда живой попытки |
TestAStaleUnspawnedStopDoesNotCloseAResumedRun |
поймана |
| Дофикс | пропавший каталог снова = «нет конфигурации» | TestABookWhoseDirectoryIsGoneIsRejectedRatherThanLeftWaiting |
поймана |
| Дофикс | ожидание конфигурации снова жжёт бюджет | TestWaitingForAConfigurationDoesNotBringDeletionCloser |
поймана |
| Дофикс | проход интейка снова под бюджетом прогонов | TestOneBookInTheSweepGetsABudgetAParseCanLiveIn |
поймана |
| Дофикс | просроченный дедлайн снова 500 | TestAnUploadThatOutlivesItsDeadlineIsNotAnInternalError |
поймана |
| Дофикс | гейт systemd снова по тексту сообщения | TestTheSystemdGateAsksAboutTheCapabilityAndNotAMessage |
поймана |
| Дофикс | пауза перебивает стоп | TestAStopDuringSettlementOutranksThePause |
поймана |
| Дофикс | потолок частей формы снова n > cap |
TestAFormWithTooManyPartsIsRefusedAtTheCap |
ПЕРЕЖИЛА → пин переписан → поймана |
| Дофикс | wire-тест ревизии без различающей силы | TestTheCardProjectsTheRevisionTheStoreGaveAndInventsNone |
поймана |
| Дофикс-2 | пропавший корень снова читается как вина книги | TestAVanishedStorageRootIsNotEveryBooksFault |
поймана |
| Дофикс-2 | смена статуса снова revision + 1 |
TestAStatusChangeIsVisibleEvenUnderTheLibrarysFloor |
поймана |
| Дофикс-2 | пауза снова без гарда живой попытки | TestAPauseFromAnOldSnapshotDoesNotCloseARunOverALiveAttempt |
поймана |
| Дофикс-2 | синк снова биндит законченную попытку | TestAMaterializerOfAnEndedAttemptDoesNotAdoptTheNextAttemptsEngine |
поймана |
| Дофикс-2 | возврат холда снова ждёт ответа движка | TestTheHoldOfARunThatNeverStartedComesBackOnAHostWhoseEngineCannotAnswer |
поймана |
| Дофикс-2 | гейт systemd снова сверяет сообщение (усиленный пин) | тот же | поймана |
Батарея дофикса-2
make checkс живым PG 18.4 под-race: EXIT=0, 15 пакетов, скипов 0, линтер 0 issues.- Тесты
^func Testисполнением: 349 → 354 (+5 пинов, удалённых 0). - Посадки: 6 из 6 (таблица выше, нижние шесть строк).
- ⚠
make vulnпокраснел между двумя прогонами ОДНОГО дня, и не из-за дерева. База адвизори опубликовала пять уязвимостей stdlib против Go 1.26.5 —net/http,crypto/tls,net/url,encoding/xml,encoding/asn1(GO-2026-6218 / 6090 / 6089 / 6088 / 5972), все закрыты в 1.26.6, и govulncheck трассирует две из них в пути, которые эта служба зовёт (напримерpgstore.Open → pgx.ParseConfig → asn1.Unmarshal). Поставил 1.26.6 в~/.local(sha256 сверен с go.dev), поднялGO_MIN_VERSIONв Makefile 1.26.5 → 1.26.6 с обоснованием на месте, обновил пин вSTACK_DECISIONS.md. На 1.26.6 батарея зелёная и скан чист. НА РАТИФИКАЦИЮ: хост сборки, оставшийся на 1.26.5, должен получать отказ. ⚠ Правка 14.08: в этой редакции он его НЕ получал — гейт сравнивал регексом и принимал 1.26.5 (ре-чек, FP5-9). Как floor держится теперь — раздел «Третий раунд» выше.
Записка-план дофикса-2: ID → статус → улика
| ID | Статус | Улика |
|---|---|---|
| Н1 | ЗАКРЫТО | ErrStorageGone + waitsForTheDeployment; books.TestAVanishedStorageRootIsNotEveryBooksFault; посадка падает. PD-192 |
| Н2 | ЗАКРЫТО | nextRevisionOfThisBooksLibrary у трёх писателей статуса; books.TestAStatusChangeIsVisibleEvenUnderTheLibrarysFloor; посадка падает. PD-122 переформулирована |
| М1 | ЗАКРЫТО | гард живой попытки в PauseRun + run_id в апдейте попытки; pgstore.TestAPauseFromAnOldSnapshotDoesNotCloseARunOverALiveAttempt; посадка падает. PD-193 |
| М2 | ЗАКРЫТО | ended_at is null в бинде; pgstore.TestAMaterializerOfAnEndedAttemptDoesNotAdoptTheNextAttemptsEngine; посадка падает. PD-194 |
| М3 | ЗАКРЫТО (гипотеза ревью подтверждена мной) | ветка «не спавнился» ДО вызова движка; runs.TestTheHoldOfARunThatNeverStartedComesBackOnAHostWhoseEngineCannotAnswer; посадка падает. PD-195 |
| Н3 | ЗАКРЫТО БЕЗ ПИНА (честно) | writeCtx на StartParsing и обоих abandon; граница, а не поведение — пин не заявляется |
| Н4 | СТРОКОЙ | PD-196: exit 1 движка на опечатку в book.yaml стоит файла; лечение — на стороне движка |
| Н5 | ЗАКРЫТО | пин FP5-7 разбирает КОД гейта парсером, а не текст файла; посадка падает |
| Отчётность | ЗАКРЫТО | списки посадок перенесены из скрэтчпада в журнал (таблица выше) |
| Тулчейн | НА РАТИФИКАЦИЮ | Go 1.26.5 → 1.26.6 по пяти адвизори stdlib; make vuln чист, батарея зелёная |
Дофикс P5 (11.08): фикс-лист приёмки FP5-1…FP5-8
Пак принят УСЛОВНО с восемью находками (одна high). Ниже — что сделано по каждой, чем проверено и что осталось. Каждое число снято исполнением, команда рядом. Возражений НЕТ: все восемь подтвердились на коде до правки — три из них были прямыми последствиями МОИХ же решений, принятых в разных файлах и не сверенных между собой.
FP5-1 [HIGH] Пол ревизии библиотеки игнорировался на вставке
Приёмка права, и посылка перепроверена мной на дереве, не по памяти: DeleteUpload поднимал
users.library_revision, ListBooks читал greatest(max, пол), а nextLibraryRevision — ТОЛЬКО
max(books.revision). Книга, загруженная после отменённой загрузки, входила на или ниже пола, и
одна ревизия отвечала за три разных состояния библиотеки: до отменённой загрузки, во время неё и
после прихода следующей книги. Клейм «PD-122 закрыта в половине добавления» был ЛОЖЕН — тесты
покрывали только свежий аккаунт, где пол ещё ноль.
Сделано: вставка берёт greatest(max(revision), users.library_revision) + 1. Пин —
books.TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision, и он гоняет именно ту
последовательность, которой не было: книга → отмена → книга; проверяет и ревизию БИБЛИОТЕКИ, и
ревизию самой новой книги (иначе её карточка устарела бы в момент появления). PD-122 пере-формулирована:
открытым остаётся один случай — смена статуса книги, которая не самая новая, не двигает ни одну из
половин.
FP5-2 [деньги] FinishUnspawnedStop без гарда живой попытки
Тот же гард, что FinishRun получил кругом раньше (PD-181), на соседнем пути отсутствовал: путь
закрытия «стопа до спавна» проверял только unit_name is null. Устаревший проход мог закрыть уже
резюмированный прогон, и холд второй попытки выпадал из обоих списков; приёмка добавила к этому
следствие, которого я не назвал: каждый следующий резюм отвечал бы 409 навсегда, потому что
продолжать пришлось бы уже завершённый прогон.
Сделано: попытка обязана быть живой и принадлежать этому прогону (ended_at is null and run_id = $).
Пин — runs.TestAStaleUnspawnedStopDoesNotCloseAResumedRun; посадка «снять гард» падает.
Строка PD-186.
FP5-3 Крэш-окно «снос каталога → запись строки»
Клейм «порядок самоизлечивающийся» был ложен, и ложным его сделала МОЯ ЖЕ вторая правка того же
круга: снесённый каталог читается как «нет конфигурации», а эту причину я тем же паком сделал
НЕтерминальной — книга оставалась в parsing навсегда, ожидая конфигурацию, которую положить некуда.
Диспозиция F11 («порядок выбран самоизлечивающийся») переписана: она стояла на посылке, которая к
моменту записи уже не была верна.
Сделано: пропавший КАТАЛОГ отличён от отсутствующей конфигурации (ErrDirectoryGone) и терминален
сразу — движка при этом не зовём вовсе. Пин —
books.TestABookWhoseDirectoryIsGoneIsRejectedRatherThanLeftWaiting. Строка PD-187.
FP5-4 Ожидание конфигурации жгло бюджет разбора
ClaimParse считает КАЖДУЮ заявку, а книга без конфигурации заявляется раз в грацию бесконечно.
После пяти циклов ожидания бюджет исчерпан — и первый же ответ движка становится терминальным
мгновенно, вместе с удалением исходника. Движок отдаёт exit 1 и на опечатку в book.yaml, так что
цена ошибки оператора — файл пользователя.
Сделано: попытка возвращается (RefundParseAttempt), когда движок не был спрошен вовсе; возврат
условен на клейме, как и все прочие записи, заканчивающие проход. Пин —
books.TestWaitingForAConfigurationDoesNotBringDeletionCloser: после пятнадцати циклов ожидания
бюджет цел, и первый отказ движка после этого НЕ терминален и ничего не удаляет. Строка PD-188.
FP5-5 Бэкстоп гнал разбор под бюджетом прохода
Проход интейка шёл под теми же двумя минутами, что и проход прогонов, а разбор внутри — это тот же вызов движка, которому очередь даёт пятнадцать минут. Большая книга убивалась дедлайном свипа, убийство читалось как «хост не может запустить движок», попытка списывалась — и так каждый проход, пока книга не отклонялась ЗА СВОЙ РАЗМЕР. Вторую половину приёмка назвала точно: запись отказа шла на уже просроченном контексте и терялась, то есть цикл был бесконечным.
Сделано: у прохода интейка свой бюджет (jobs.JobTimeout + 1m), у каждой книги внутри прохода —
свой (jobs.JobTimeout), а терминальные записи (FinishParse/RejectBook/возврат попытки) идут на
контексте, переживающем дедлайн вызова. Пин — books.TestOneBookInTheSweepGetsABudgetAParseCanLiveIn
(фейк движка читает дедлайн, который ему выдали). Строка PD-189.
FP5-6 Просроченный дедлайн загрузки уходил 500
Сделано: 408 problem+json. Выбор кода назван вслух: медленный клиент — не сломанный сервис, а 408 по
RFC 9110 §15.5.9 означает ровно «полный запрос не пришёл за время, которое сервер готов был ждать», и
сообщает клиенту, что лечение — повтор. Кода 408 в перечне операции нет, поэтому он внесён в тот же
пакет вопросов владельцу контракта, что PD-172/PD-174/PD-180. Пин —
httpapi.TestAnUploadThatOutlivesItsDeadlineIsNotAnInternalError. Строка PD-190.
FP5-7 Скип-гард systemd-тестов пинил СООБЩЕНИЕ
Приёмка права дважды, и вторая половина — исправление МОЕЙ ошибки в фактах. Гейт существовал и скипал по подстроке «Failed to connect to bus», а systemd 259 отвечает «Failed to connect to user scope bus» — гейт перестал гейтить от чужой правки строки. Моя строка PD-178 при этом утверждала, что формы скипа для systemd в зоне НЕТ ВОВСЕ: это было неверно, и я записал предположение как факт.
⚠ И вторая посылка PD-178 устарела прямо в ходе дофикса: на этом стенде пользовательский менеджер
systemd теперь ЖИВ (systemctl --user show → Version=259.5), и все три теста проходят — тогда
как несколькими часами раньше в этой же сессии его не было (Linger=no, sudo нет, ручной запуск
выходил кодом 1). То есть состояние менеджера ПЛАВАЕТ между сессиями стенда, и строка переписана
именно так.
Сделано: гейт спрашивает СПОСОБНОСТЬ — доходит ли процесс до своего менеджера, — и скипает громко и
поимённо, симметрично форме БД-тестов. Пин —
runner.TestTheSystemdGateAsksAboutTheCapabilityAndNotAMessage: он падает, если гейт снова начнёт
сверять текст. PD-178 закрыта.
FP5-8 Хвосты
- (а) деньги. Стоп, пришедший в окно расчёта, отвечал
paused/credit_exhausted— «кончились деньги» вместо «владелец остановил». Пауза теперь отказывает при висящем интенте (ErrStopRequested), реконсилятор заканчивает прогон стопом. Пинruns.TestAStopDuringSettlementOutranksThePause. Строка PD-191. - (б) PD-153 дополнена: у СТОП-пути окно закрыто с обеих сторон (заявка не выдаётся прогону с
интентом; закрытие «стопа до спавна» спрашивает systemd, если у попытки есть базовая линия). Само
окно PD-153 — грация от
started_at— не тронуто. - (в) Потолок частей формы пропускал N+1 (
n > capвместоn >= cap). Пинhttpapi.TestAFormWithTooManyPartsIsRefusedAtTheCap— и ПЕРВАЯ его редакция посадку ПЕРЕЖИЛА: она сверяла только код 400, а форма, у которой просто кончились части, тоже 400, так что обе версии потолка проходили. Переписан на наблюдаемое следствие: файл стоит ровно на частиcap+1, и тест требует, чтобы интейку его НЕ отдали (in.read == 0) — при потолке на единицу больше файл дочитывается и книга принимается. Посадка теперь падает. - (г) Выключатель metrics-листенера был недостижим через окружение: пустое значение переменной
неотличимо от неустановленного и берёт дефолт. Теперь выключает СЛОВО (
off/none), и это записано там же, где документирован сам выключатель. - (д) Wire-тест ревизии вернул различающую силу: фикстура снова несёт РАЗНЫЕ числа (книга 41, прогон 12), и тест утверждает то, за что отвечает этот слой — он отдаёт число стора и не выдумывает своё. Правило, что число берётся из книги, пинится в сторе.
- (е)
helpметрикиqueue_depthназывает все состояния, которые она считает, включаяpending. - (ж) Счёт тестов в таблице задач пересчитан исполнением.
Батарея дофикса
make checkс живым PostgreSQL 18.4 под-race: все пакеты зелёные, включаяinternal/runner(менеджер systemd на стенде жив — см. FP5-7), линтер 0 issues.- Скипы: 0 при живом менеджере; при мёртвом — три ИМЕНОВАННЫХ скипа вместо трёх падений.
- Тесты
^func Testисполнением: 340 → 349, добавленных 9, удалённых 0, переименован 1 (TestTheCardsRevisionIsTheBooksAndNotTheRuns→TestTheCardProjectsTheRevisionTheStoreGaveAndInventsNone, FP5-8д — свойство то же, имя стало отвечать за то, что тест на самом деле утверждает). Против HEAD: 261 → 349. Команда:commдвух списков^func Test(HEAD и дерево) — единственная строка расхождения и есть это переименование. - Посадки дофикса: 10, поймано 10 — но честно: первый круг дал 9 из 10, пережившая посадка
(в) показала, что пин сверял не то следствие; переписан пин, не мутация. Списки —
mut9.json. make vuln: чисто.
Живая проба дофикса на собранном бинаре (go build ./cmd/tmplatformd, боевой PostgreSQL 18.4,
сессия заведена прямой вставкой в стенд и убрана после пробы):
- FP5-6: сырым сокетом отправлена шапка
POST /v0/booksсContent-Length: 100000и одной частью, дальше клиент «думает» 4 секунды приTM_PLATFORM_UPLOAD_DEADLINE=2s. Ответ —HTTP/1.1 408 Request Timeout,Content-Type: application/problem+json, тело{"type":"about:blank","title":"The upload did not finish in time","status":408}, соединение закрыто. Юнит-тест это же место видит через фейковый ридер: живой прогон подтверждает, что дедлайн РЕАЛЬНО срабатывает на соединении, а не только маппинг ошибки. - FP5-8г: с
TM_PLATFORM_METRICS_ADDR=offв печати эффективной конфигурации стоитvalue=off source=environment, в логе —no TM_PLATFORM_METRICS_ADDR: this instance exposes no metrics, слушателя на порту нет (ss -ltnпуст). Со значением-адресом скрейп отдаёт 200. - FP5-8е:
# HELP tm_platform_queue_depth Jobs in the queue that have not finished (pending, available, running, scheduled or retryable)— из живого скрейпа. Там же видно 9 семейств зоны из десяти:sweep_unfinished_total— счётчик с лейблами и серии до первого инкремента не имеет, это нормальная семантика prometheus, а не пропавшая метрика. - Заодно:
tm_platform_http_requests_total{code="200",method="GET",route="GET /v0/books"}— лейбл маршрута это ПАТТЕРН (F9), иGET /v0/booksживым Bearer'ом отвечает 200.
Записка-план дофикса: ID → статус → улика
| ID | Статус | Улика |
|---|---|---|
| FP5-1 | ЗАКРЫТО | nextLibraryRevision = greatest(max(revision), users.library_revision)+1; books.TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision; посадка «снова только максимум» падает |
| FP5-2 | ЗАКРЫТО | гард живой попытки в FinishUnspawnedStop (ended_at is null and run_id = $2); runs.TestAStaleUnspawnedStopDoesNotCloseAResumedRun; посадка падает. PD-186 |
| FP5-3 | ЗАКРЫТО | ErrDirectoryGone проверяется ПЕРЕД ErrNotProvisioned и терминален; books.TestABookWhoseDirectoryIsGoneIsRejectedRatherThanLeftWaiting; посадка падает. PD-187 |
| FP5-4 | ЗАКРЫТО | RefundParseAttempt условно на клейме; books.TestWaitingForAConfigurationDoesNotBringDeletionCloser; посадка падает. PD-188 |
| FP5-5 | ЗАКРЫТО | свой бюджет у прохода интейка и у каждой книги, терминальные записи на пережившем контексте; books.TestOneBookInTheSweepGetsABudgetAParseCanLiveIn; посадка падает. PD-189 |
| FP5-6 | ЗАКРЫТО кодом, вопрос владельцу контракта СТРОКОЙ | os.ErrDeadlineExceeded → 408; httpapi.TestAnUploadThatOutlivesItsDeadlineIsNotAnInternalError; посадка падает. PD-190 |
| FP5-7 | ЗАКРЫТО, PD-178 переписана и закрыта | гейт спрашивает способность соединиться с менеджером; runner.TestTheSystemdGateAsksAboutTheCapabilityAndNotAMessage; посадка падает. Менеджер на стенде сейчас ЖИВ (259.5) — три теста прошли |
| FP5-8а | ЗАКРЫТО | PauseRun отказывает при висящем интенте (ErrStopRequested); runs.TestAStopDuringSettlementOutranksThePause; посадка падает. PD-191 |
| FP5-8б | СТРОКОЙ | PD-153 дополнена: стоп-путь закрыт с обеих сторон; само окно грации не тронуто |
| FP5-8в | ЗАКРЫТО со второго захода | n >= cap; пин переписан на «файл за потолком не отдан интейку»; посадка падает |
| FP5-8г | ЗАКРЫТО | выключатель metrics-листенера — СЛОВО (off/none), пустая переменная неотличима от неустановленной; config/effective_test.go |
| FP5-8д | ЗАКРЫТО | фикстура снова несёт разные числа (книга 41, прогон 12); httpapi.TestTheCardProjectsTheRevisionTheStoreGaveAndInventsNone; посадка падает |
| FP5-8е | ЗАКРЫТО | help у queue_depth называет все состояния, включая pending |
| FP5-8ж | ЗАКРЫТО | числа в таблице задач пересчитаны исполнением (см. батарею выше) |
Приёмка P5 оркестратором (11.08): ПРИНЯТ УСЛОВНО — дофикс-раунд ДО лендинга (прецедент P4)
Что подтверждено исполнением. Батарея пере-прогнана мной на СВЕЖЕПОДНЯТОМ стенде (zonky PG
18.4, рецепт §«Postgres без root»): все пакеты зелёные, ВКЛЮЧАЯ internal/runner — на текущем
стенде пользовательский менеджер systemd ЖИВ (user@1000.service active, systemd 259), и три
теста PD-178 прошли; линтер 0 issues; тестов 340, удалённых против HEAD 0 (comm пуст).
Живые числа проб и мутационных прогонов read-only не пере-снимаются — приняты «со слов» с
пометкой. Ревью 10 агентами (6 линз + скептики): 8 находок подтверждено (1 high), 0
опровергнуто, 9 минорных. Задеты деньги и данные пользователей ⇒ по прецеденту P4 пак
принимается УСЛОВНО, лендинг после дофикса и ре-чека.
Фикс-лист (движение — запиской-планом ID → статус → улика, D39.121):
| ID | Находка | Лечение |
|---|---|---|
| FP5-1 HIGH | Пол ревизии библиотеки игнорируется на вставке (pgstore/books.go:99): nextLibraryRevision читает только max(books.revision), а DeleteUpload поднимает users.library_revision; книга, загруженная после отменённой загрузки, входит НА/НИЖЕ текущей ревизии — три разных состояния библиотеки под одной ревизией, revision-keyed refetch слеп. Клейм закрытия PD-122 ложен; тесты покрывают только свежий аккаунт |
вставка учитывает пол (greatest); тест со сценарием «отмена → повторная загрузка»; PD-122 переформулировать |
| FP5-2 деньги | FinishUnspawnedStop без гарда живой попытки (pgstore/sink.go:362): закрытие по устаревшему снапшоту РЕЗЮМИРОВАННОГО прогона — холд попытки-2 вне ListLiveRuns И вне UnsettledRuns, движок без надзора, последующий резюм вечно 409. Ровно та гонка, от которой FinishRun уже получил гард (PD-181) |
тот же гард «закрываемая попытка ещё живая»; пин + посадка |
| FP5-3 | Крашокно removeDir → RejectBook (books/parse.go:188): смерть между ними = книга навечно в parsing (ENOENT читается как not_configured, терминального пути нет) — клейм «порядок самоизлечивающийся» ложен, диспозиция F11 стояла на ложной посылке |
терминальность переживает краш (запись/маркер до сноса либо «каталога нет вовсе» = терминал); диспозицию F11 переписать |
| FP5-4 | not_configured-проходы жгут бюджет разбора (parse.go:138): ClaimParse инкрементит parse_attempts безусловно ⇒ после ~5 циклов ожидания конфигурации ПЕРВЫЙ отказ движка терминален мгновенно — включая удаление исходника при опечатке book.yaml (exit 1 = «вина файла») |
not_configured не тратит бюджет; пин «ожидание не приближает удаление» |
| FP5-5 | Свип-бэкстоп гонит разбор под общим бюджетом прохода 2 мин против 15 мин очереди (cmd/tmplatformd/runner.go:151): восстановление большой книги убивается дедлайном свипа, каждый цикл жжёт попытку, а reject на просроченном контексте не записывается — kill-loop |
у бэкстоп-разбора свой бюджет ≥ jobs.JobTimeout (пара тестом, как claimGrace); reject на свежем контексте |
| FP5-6 | Просроченный дедлайн загрузки уходит 500 (httpapi/v0.go:404) — код вне спеки и вне декларации PD-180 |
замаппить в честный код problem+json; внести в пакет вопросов PD-180 |
| FP5-7 | Скип-гард systemd-тестов пинит сообщение, которого systemd 259 не даёт (runner/systemd_test.go:25: ждёт «Failed to connect to bus», стенд говорит «…user scope bus…») — на стенде без менеджера тесты ПАДАЮТ, а не скипаются; PD-178 не знает об этом гарде и устарела фактами (менеджер сейчас ЖИВ, батарея зелёная) |
гейт по СПОСОБНОСТИ (проба соединения), скип поимённо — симметрия DSN-формы; PD-178 переписать: «состояние менеджера плавает между сессиями стенда» |
| FP5-8 хвосты | (а) стоп в окне settle отвечает paused/credit_exhausted вместо stopped (reconcile.go:543) · (б) PD-153-окно: полный возврат холда под стартующим движком через новый стоп-путь — дополнить строку · (в) потолок частей формы пропускает N+1 · (г) выключатель metrics-листенера недостижим через env · (д) wire-тест ревизии потерял различающую силу (фикстура 12→41 сравняла прогон с книгой) · (е) help queue_depth не называет pending · (ж) счёт тестов в таблице задач устарел |
по весу: (а)(б)(д) — фиксы с пинами; (в)(г)(е)(ж) — мелкие правки/честные строки |
Развилка book.yaml (вопрос зоны) и четыре контракт-вопроса — на мне, ратифицирую при лендинге
дофикса. Дерево остаётся незакоммиченным до ре-чека.
Ре-чек дофикса-2 (13.08, оркестратор; дата испр. 14.08 — стояло «11.08» ошибочно, дофикс-2 датирован 13.08): батарея воспроизведена зелёной (354 теста, Go 1.26.6, стенд ~/.local/pgsql:55433), НО 3 находки подтверждены (2 high) — третий раунд по списку ниже, лендинг после него:
| ID | Находка | Лечение |
|---|---|---|
| FP5-9 HIGH | Гейт тулчейна НЕ отказывает 1.26.5: Makefile:33 — регекс go1\.26\.([5-9]|…) принимает 1.26.5, GO_MIN_VERSION=1.26.6 живёт только в echo; go.mod — go 1.26.4 БЕЗ toolchain; клейм «хост на 1.26.5 получит отказ» ложен в трёх доках (журнал ×2, STACK_DECISIONS §, коммент Makefile) |
регекс/сравнение версий чинится по-настоящему (+ toolchain go1.26.6 в go.mod — реши и обоснуй); пин на функцию сравнения, не на прогон; три клейма поправить |
| FP5-10 HIGH | PD-192 не закрывает заявленный сценарий: гард — только Stat(BooksDir) (parse.go:235); unmount тома, смонтированного РОВНО в BooksDir, оставляет пустой mountpoint → Stat успешен → терминальный reject ВСЕХ книг с удалением; безусловный MkdirAll на буте (runner.go:129) маскирует пропажу после рестарта |
различать «хранилище пропало» от «книги нет» устойчиво к обеим формам (кандидат: сентинел-файл провижининга в BooksDir, который бут НЕ пересоздаёт; нет сентинела → ErrStorageGone, не терминалить, бюджет не жечь); пин на unmount-форму |
| FP5-11 | Н2 закрыт не для всех писателей статуса: пять писателей жизненного цикла прогона остались на revision+1 (pgstore/runs.go:108,517,620 · sink.go:318,396) против собственного правила дофикса (books.go:111) — staleness библиотеки на статусах прогона |
те же «следующий номер библиотеки», что у трёх интейковых; пин на прогонный переход у книги не-максимума аккаунта |
| хвосты | (а) коммент PauseRun «в том же стейтменте» против фактического SELECT FOR UPDATE (runs.go:580) — текст к коду; (б) комменты parse.go:41-52 «terminal on the first answer» описывают до-дофиксное поведение на пути удаления файла; (в) остаток FP5-5: в одном проходе бэкстопа полный бюджет получает только первая книга — вторая жжёт попытки на обрезанном контексте (узкий триггер — честная строка либо бюджет на КНИГУ); (г) мета-пин systemd-гейта перечислителен — остаток названный, не дефект |
(а)(б) — правки текста; (в) — строка или фикс; (г) — знать |
Сессия P5 (11.08): загрузка книги · стоп и резюм · наблюдаемость · форма регистра
Каждое число ниже снято исполнением, команда стоит рядом. Дерево не коммичено, в индексе ничего
не держу; зона записи — только platform/.
Таблица задач
| Задача промта | Что сделано | Чем проверено |
|---|---|---|
1. POST /books (П-9, PD-72) |
Потоковый multipart (r.MultipartReader), пер-маршрутный потолок тела, свой дедлайн чтения, каталог книги под TM_PLATFORM_BOOKS_DIR, статусы uploading → parsing → not_started | rejected с писателями, разбор = tmctl manifest --json, свип-бэкстоп интейка |
44 теста (пак + пины дофикса): 22 internal/books + 11 httpapi/intake_test.go + 6 pgstore/intake_test.go + 5 на шов манифеста (runner/manifest_test.go). 21 посадка пака, живая проба на боевом бинаре |
⚠ половина 1: кто пишет book.yaml |
НЕ построена — вопрос ниже. Построен ШОВ: books.ErrNotProvisioned, книга без конфигурации отказывает разбором с причиной not_configured |
TestABookWithNoEngineConfigurationWaitsRatherThanDies |
| 2. Стоп и резюм (PD-140) | POST /v0/runs/{id}/stop пишет намерение (runs.stop_requested_at, миграция 00014) ДО сигнала; /resume переиспользует reopen реконсилятора и ставит спавн в очередь, а не в запрос; реконсилятор классифицирует маркер по намерению, не рестартит стопнутый прогон и повторяет стоп живому юниту |
32 теста (пак + пины дофикса): 28 runs/control_test.go + 4 httpapi/control_test.go. 25 посадок пака, живая проба |
| 3. Наблюдаемость (П-11) + PD-114 | prometheus/client_golang v1.24.1 на отдельном слушателе; 10 семейств метрик зоны, включая запросы и задержки по паттерну маршрута; бюджет свипа НА ПРОГОН (PD-169); печать эффективной конфигурации с источником и редакцией секретов и денег |
12 тестов (пак + пин дофикса): 5 internal/metrics + 6 config/effective_test.go + 1 internal/jobs. 8 посадок пака, живая проба |
| 4. Регистр секциями (пинг №16) | Таблица разложена на 11 секций (открытые по весу · принятый риск · закрытые ратификацией · закрытые по эрам P1–P5), построчная форма сохранена | python3 docs/scripts/counts.py --check — зелено; список ID до и после перекладки совпадает посимвольно |
⚠ ВОПРОС НА РАТИФИКАЦИЮ: кто пишет book.yaml при интейке
Промт гейтит эту половину явно, и она НЕ построена. Что известно и чего стоит каждый вариант:
Факт. Каталог новой книги создаёт платформа (иначе POST /books некуда писать), а разбор —
tmctl manifest --config <каталог>/book.yaml; движок требует в конфиге book_id, пару языков,
пути pipeline/models/source_file и хотя бы один потолок (backend/internal/config/book.go
проверяет всё это на загрузке). D39.110 §2b говорит «платформа book.yaml не правит» — в контексте
того решения речь шла о ПОТОЛКАХ, которые платформа передаёт аргументом прогона, а не о создании
файла для книги, которой ещё не существует.
| Вариант | Что делает платформа | Цена |
|---|---|---|
| А. Оператор кладёт файл (то, что построено) | Ничего: каталог и исходник создаются, конфигурации ждём; без неё разбор отказывает и после бюджета попыток книга становится rejected с причиной not_configured |
Загрузка через UI не доходит до конца без ручной работы оператора НА КАЖДУЮ книгу; бета с чужими пользователями на этом не живёт. Зато D39.110 §2b соблюдён буквально |
| Б. Рендер из деплой-шаблона при интейке (кандидат промта) | Один раз, при создании каталога, рендерит book.yaml из шаблона деплоя (TM_PLATFORM_BOOK_TEMPLATE): языки и жанр из BookIntake, source_file — имя, которое платформа же и записала, потолки и пути пайплайна — из шаблона оператора. Дальше файл принадлежит оператору: платформа его не читает и не трогает |
Платформа становится писателем файла движка ОДИН раз. Нужен шаблон в деплое (ещё одна переменная и ещё один файл, который можно забыть). Если оператор потом файл поправил — ничего не происходит, платформа туда не возвращается |
| В. Движок сам заводит книгу | Платформа зовёт новую $0-команду движка (tmctl init --source-lang zh --target-lang ru …), движок пишет свой конфиг сам |
Архитектурно чище всех: файл движка пишет движок, и требования к полям живут там же, где проверяются. Но это строка ЕДИНОГО бэклога и релиз движка, то есть П-9 стоит до неё |
Что предлагает зона: Б как бета-меру и В как правильную форму, строкой единого бэклога. При Б
шов уже готов — books.ErrNotProvisioned заменяется рендером в ОДНОМ месте
(books.Service.manifest), остальной интейк не меняется. Ратифицирует оркестратор; тихо
интерпретировать эту развилку сессия не стала.
Вторым вопросом того же корня: название книги. BookIntake поля title не несёт, манифест
движка названия не даёт вовсе (он даёт id, счёты и идентичность разреза). Сегодня заголовок берётся
из ИМЕНИ ЗАГРУЖЕННОГО ФАЙЛА (книга.epub → «книга»), пустое остаётся пустым — синтезировать
«Книга 1» значит показать читателю ярлык, которого никто не писал. Если ратифицируется Б, имя
логично брать оттуда же; если владелец хочет поле title в форме — это правка спеки, не зоны.
Дизайн стопа: намерение, а не догадка по коду выхода
Кандидат-дизайн промта взят целиком; вот его границы.
- Что записывается.
runs.stop_requested_atпишется ОДНИМ оператором с проверкой владения и живости (pgstore.RequestStop:update … from books where owner_id = $2 and finished_at is null returning …плюс имя юнита живой попытки). Ноль строк — два разных ответа, и они разделены вторым запросом ТОЛЬКО на этом пути: видит владелец прогон →ErrRunNotLive(409), не видит →ErrNoRun(404). Идемпотентно черезcoalesce: второй стоп не сдвигает ПЕРВУЮ отметку, потому что она — улика о том, что случилось раньше. - Порядок. Запись коммитится ДО обращения к systemd. Запинено не косвенно: фейк раннера зовёт
хук ВНУТРИ своего
Stop, хук читаетstop_requested_atиз БД и требует, чтобы она уже была (TestTheStopIsRecordedBeforeSystemdIsAsked). Посадка «спросить systemd раньше» падает. - Гонка «стоп против самостоятельного финиша». Намерение перекрашивает исход, только если оно
РАНЬШЕ маркера (
stoppedOnRequest:!l.StopRequestedAt.After(m.At)), а чистые коды движка (0 · 2 · 3) намерение перебивают всегда — завершённый перевод не должен показываться отменённым. Три пина: стоп →stopped, маркер раньше стопа →failed, чистый выход →ready. - Три следствия, каждое запинено. Прогон с намерением НЕ перезапускается реконсилятором (иначе деньги уходят на работу, которую владелец отменил) · живой юнит с намерением получает стоп ПОВТОРНО (закрывает «платформа умерла между записью и вызовом») · стоп до спавна заканчивает прогон и возвращает холд ЦЕЛИКОМ (проверено балансом, не статусом).
- Чего дизайн НЕ закрывает, и это названо: штатная перезагрузка хоста. Там намерения нет ни у
кого, маркер честно скажет
exited/1, и прогон закроется какfailedвместо перезапуска. Различить это может только движок различимым кодом выхода graceful-stop (строка 165 единого бэклога) — платформенной догадки здесь быть не должно. PD-152 остаётся открытой этой половиной.
Резюм по контракту: stopped/paused → новая попытка с ОСТАТКОМ бюджета (потолок − RunSpent);
awaiting_bank → 409 «подпись неполна», и она неполна у любого деплоя, потому что банк сегодня не
материализуется вовсе (компаньон §3); нечего продолжать → 202 с прогоном КАК ЕСТЬ, без переписывания
его состояния (прогон, который остановил пользователь, остаётся stopped, а не становится paused
из-за того, что резюм не нашёл денег). Механика перезапуска переиспользована: restart
реконсилятора и Resume — один reopen; решение «паузить» вынесено к вызывающим, потому что
реконсилятор паузит ЖИВОЙ прогон, а резюм работает с уже завершённым и переписывать ему исход не
вправе.
Наблюдаемость: что выбрано и почему не stdlib
expvar рассмотрен первым (норма §1 «stdlib прежде библиотеки») и этой работы не несёт: нет
лейблов — «запросы по маршруту и коду» не выражаются вовсе; нет гистограмм — на вопрос о задержке
остаётся среднее, единственная статистика, которая прячет хвост; его JSON не читает ни один скрейпер.
Сэкономил бы он зависимость, а стоил бы написания недостающих трёх руками — ровно того самописного
пути, который та же норма и запрещает. Взят prometheus/client_golang v1.24.1 (релиз
24.07.2026, пин сверен живьём через proxy.golang.org), OpenTelemetry отклонён как тяжёлый для
одной VM: коллектор процессом, протокол экспорта настройкой и всё равно scrape-эндпоинт на конце.
Эталон оси (половина PD-115) — практики именования Prometheus плюс четыре золотых сигнала, внесены в
ENGINEERING_STANDARDS §2. Разбор — STACK_DECISIONS §24.
Отдаётся ОТДЕЛЬНЫМ слушателем (TM_PLATFORM_METRICS_ADDR, дефолт 127.0.0.1:9464), не маршрутом
под /v0: экспозиция несёт операционную форму деплоя, а второй модели авторизации ради скрейпера
зона не заводит — тот же довод, что сделал админ-поверхность CLI (§10). Риск принят строкой PD-179 и
записан в deploy/README.md. Величины снимает СВИП, а не скрейп: коллектор, ходящий в Postgres на
каждый запрос, отдал бы нагрузку на контрол-плейн тому, у кого есть доступ к порту.
Метрики зоны: глубина очереди · возраст самого старого открытого холда · попытки в карантине · живые прогоны · отставание тейлера в байтах · книги в интейке по статусу · длительность свипа и счётчик проходов, не уложившихся в бюджет (PD-169) · запросы и задержки по паттерну маршрута.
Что нашла ЖИВАЯ ПРОБА, чего не нашли тесты
Проба гоняет боевые tmplatformd/tmplatformctl против живого Postgres реальными HTTP-вызовами;
подменены две чужие стороны и обе процессом — движок (как в P4) и systemd (стенда с пользовательским
менеджером нет, PD-178). Нашла две вещи, и обе исправлены:
- Резюм спавнил движок ВНУТРИ запроса — запрос висел, пока прогон не кончится. На фейке это
видно как таймаут 120 с; на настоящем systemd висело бы меньше, но по-прежнему секундами:
spawnAttemptчитает отсчёт книги (tmctl status— секунды CPU движка) и ходит к systemd. Исправлено: резюм ставит спавн в ОЧЕРЕДЬ (EnqueueRunNow), как это делает допуск прогона; реконсилятор — бэкстоп, если запись потерялась. Пин переписан на новое свойство, посадка «спавнить в запросе» падает. - Идентификатор книги тёк в INFO-логи (
book accepted,book parsed,book rejected) — против нормыENGINEERING_STANDARDS§Наблюдаемость («id пользователя/книги в логи не текут») и против собственной дисциплины раннера, который в INFO называет ПРОГОН и никогда книгу. Снято; на ERROR путь книги остаётся в двух местах намеренно (терминальный отказ разбора и неудавшееся удаление каталога) — это открытый класс PD-139, строка дополнена. Пин —TestNoBookIdentifierReachesAnInfoLine.
Прогон пробы после исправлений (числа с экрана):
| Шаг | Наблюдение |
|---|---|
| печать конфигурации | 29 настроек с источником; DSN — (set)/environment, обе денежные — (an amount; not logged); утечек значений в лог 0 |
POST /v0/books (38 символов) |
201, status: parsing, title: 蛊真人 из имени файла, character_count: 38 (руны, не байты) |
| статусы | parsing → not_started, chapter_count: 2 из манифеста, каталог книги: book.yaml, source.txt |
| файл больше 64 КиБ | 413 problem+json (PD-72) |
| источник, который движок не разобрал | rejected, каталог книги удалён |
POST .../runs (2 главы) |
202, юнит создан, движок запущен, холд $0.06 |
| стоп живого прогона | 202, и в САМОМ ответе прогон ещё translating — стоп ПРИНЯТ, а не завершён (движок дописывает чекпойнт). Через свип: юнит погашен, маркер run_…-1.exit записан, книга и прогон → stopped, finished_at проставлен, расчёт: баланс 9.990000 (списано $0.01 — то, что отчитал движок), холд закрыт |
| резюм | 202; прогон → translating, попытка 2 с НОВЫМ юнитом, холд $0.05 = потолок $0.06 минус потраченное |
| резюм живого прогона | 409 · стоп несуществующего прогона — 404 |
| метрики | queue_depth 0 · live_runs 1 · oldest_open_hold_seconds 2.63 · quarantined_attempts 0 · tailer_lag_bytes 0 · books_in_intake{uploading,parsing} 0/0 · sweep_duration_seconds_count{runs} 8 · http_requests_total{code="201",route="POST /v0/books"} 2 |
| гигиена логов | сумм, argv движка и id книг — 0; ERROR-строк — 0 |
⚠ В пробе book.yaml кладёт СТЕНД, играя оператора, — это и есть та половина, что стоит на
ратификации. Заодно этим прогоняется путь «поломка деплоя ретраится, а не отвергает книгу»: первая
попытка разбора отказывает как not_configured, следующий свип разбирает книгу уже с конфигурацией.
Батарея, посадки и счёт
make checkс живым PostgreSQL 18.4 под-race: все пакеты зелёные, кромеinternal/runner(три systemd-теста, PD-178 — разбор ниже), линтер 0 issues.- Скипы:
go test ./... -count=1 -v | grep -c -- '--- SKIP'= 0. make vuln: No vulnerabilities found.- Тесты
^func Testисполнением: 261 → 340, добавленных 79, удалённых 0 (comm -23списка HEAD и списка дерева — пусто). - Посадки: 74 прогона в КОПИИ зоны вне репозитория (скрипт восстанавливает копию после каждой;
списки —
mut1…mut6.jsonрабочего каталога сессии). Итог: 65 пойманы своим кругом, 7 пережили, 2 не применились по образцу — и все девять разобраны, а не подчищены:- ДВЕ пережили по-настоящему, и обе были дефектами ТЕСТА, а не кода: «брошенная загрузка чистится на отменённом контексте» (тест не отменял контекст, то есть проверял не то свойство) и «отмена загрузки роняет ревизию» (тест сравнивал с числом ДО загрузки вместо того, которое клиент мог увидеть ВО ВРЕМЯ неё). Оба теста усилены — первый отменяет контекст ровно так, как ушедший клиент, второй читает библиотеку из середины аплоада, — и пере-посадки ПОЙМАНЫ;
- три пережили как СЛАБЫЕ мутации:
_ = unitне меняет порядка вызовов; переименование веткиawaiting_bankэквивалентно, потому что обе ветки отвечают отказом; «заявка разбора отдаётся назад» не моделировала снятия заявки. Все три переписаны в настоящие (перестановка вызовов, «пуститьawaiting_bankв резюмируемые», снятие условия свежести) и ПОЙМАНЫ; - одна пережила на УСТАРЕВШЕЙ копии (посадка на
Unwrapметрик гонялась до того, как телеметрия была вписана в цепочку теста дедлайна) — пере-посадка ПОЙМАНА; - две не применились: образец встречался дважды либо расходился пробелами; переписаны и ПОЙМАНЫ.
⚠ Чего эта цифра НЕ значит (урок PD-142/PD-151): поимённого соответствия «каждый пин — своя
посадка» нет и не заявляется. 62 посадки против 73 новых тестов покрывают несущие свойства —
деньги, гонки снапшота, переходы статусов, потолки, редакцию логов, гарды файловой системы; не
покрыты вспомогательные, где тест утверждает форму ответа или таблицу значений.
Несущие посадки: потолок тела возвращается к дефолту · дедлайн загрузки не расширяется ·
Unwrapснят · стоп классифицируется по коду выхода · гард гонки «стоп vs финиш» снят · стопнутый прогон рестартится · рестарт стирает свежий интент · закрытие неспавненного стопа не перепроверяет юнит · право на спавн выдаётся завершённому прогону · спавн игнорирует интент · резюм берёт второй холд · резюм даёт полный потолок · резюм спавнит в запросе · резюм судит деплой раньше владения · конфликт второго живого прогона снова 500 · прогон стартует на книге вне интейка · книга, отклонённая движком, сохраняет файл · поломка деплоя считается виной книги · бюджет попыток снят · заявка разбора не compare-and-set и не спейсит ретраи · свип сносит каталог живой книги · новая книга входит с ревизией 0 · отмена загрузки роняет ревизию · пол ревизии игнорируется · ревизия прогона берётся из своей колонки (два места) · корневой гард каталога принимает любой путь · символы считаются байтами · строка книги пишется после тела · сумма и секрет печатаются · лейбл метрики — путь вместо маршрута · настройка читается без записи · свип без бюджета на прогон · вердикт остановленной попытки переписывается резюмом.
⚠ Оспаривание посылки: батарея зоны на этом стенде не может быть зелёной (PD-178)
Промт требует «0 FAIL». Три теста internal/runner падают, и это свойство СТЕНДА, а не пака: на
хосте нет пользовательского менеджера systemd (/run/systemd/system есть, PID 1 — systemd 259,
но user@1000.service не поднят, Linger=no, sudo нет; попытка поднять менеджер руками выходит
кодом 1 без вывода). Проверено, что это не регрессия: git archive HEAD platform, распакованный в
копию вне репозитория, даёт те же три падения с тем же текстом «Failed to connect to user scope bus».
Форма зоны для внешнего предусловия существует и она другая — БД-тесты СКИПАЮТСЯ вслух по
TM_PLATFORM_TEST_DSN, и make check называет скипы поимённо. Приводить одно к другому сессия не
стала: правка теста ради зелени запрещена (D39.121), решение — оркестратора.
⚠ Отсюда же ограничение живой пробы, названное прямо: systemd-клеймы P4 этим паком НЕ пере-мерены
(cgroup прогона, --collect, поведение маркера на трёх исходах). Проба подменяет systemd процессом,
моделируя ровно ту семантику, от которой зависит стоп: юнит переживает создателя, ExecStopPost
отрабатывает с SERVICE_RESULT/EXIT_CODE/EXIT_STATUS, stop = SIGTERM главному процессу.
⚠ Стенд с P4 при этом изменился: там был systemd 255, здесь 259.
Регистр: закрыто, сужено, заведено
- Закрыто (4): PD-72 (потолок тела приехал с маршрутом и своим тестом) · PD-140 (ручки стопа и резюма) · PD-114 (печать эффективной конфигурации) · PD-169 (бюджет свипа на прогон плюс метрика).
- Сужено, но открыто (4 + 1): PD-152 (пользовательский стоп закрыт, ребут — за движком) · PD-115 (ось наблюдаемости получила эталон; ops и конфигурация — нет) · PD-122 (добавление книги двигает ревизию области; удаление — по-прежнему нет) · PD-162 (терминальное состояние появилось у ИНТЕЙКА; клин живого прогона на удалённом каталоге не тронут) · PD-139 дополнена вторым местом.
- Заведено (14, из них 4 закрыты тем же паком): PD-172 «файл последним» не записано в контракте · PD-173 у
rejectedнет причины на проводе · PD-174POST /booksотвечает 404 там, где спека кода не даёт (и резюм — 503) · PD-175 квоты интейка нет · PD-176requestTooLargeне доходит через обёртки · PD-177character_countдля не-UTF-8 — оценка · PD-178 systemd-тесты падают, а не скипаются · PD-179/metricsбез аутентификации (принятый риск) · PD-180 201 несётparsing, а неuploading, плюс отказы интейка вне спеки · PD-181…PD-184 (кросс-семейное ревью, закрыты здесь же) · PD-185finalizingбез писателя. - Регистр: 185 строк (
python3 docs/scripts/counts.py): 132 закрыто · 1 ратификацией · 4 риском · 48 открыто (1 major — PD-113, прежний). - Форма файла по пингу №16: 11 секций, построчная форма
| PD-N | … |сохранена,--checkзелёный. Проверено, что ни одна строка не потеряна: список ID до и после перекладки сортированно совпадает (diff <(grep -o '^| PD-[0-9]* |' до) <(… после)— пусто).
Вопросы владельцу контракта (через оркестратора)
- Порядок частей формы.
POST /booksтребует, чтобыfileшёл ПОСЛЕДНИМ (PD-172). Это свойство потокового приёма, а не прихоть: строка книги пишется до тела, а языки в ней обязательны. Тем же правилом специфицирована браузерная загрузка S3. Спека молчит — вносить? - Причина отказа.
rejectedбез причины на проводе (PD-173) не даёт экрану различить «файл не тот» и «наш движок был недоступен», а это разные советы пользователю. Поле заводить не право зоны; в БД причина есть. - Инстанс без интейка отвечает на
POST /booksохраняемым 404 (PD-174) — по форме зоны для несмонтированных маршрутов, но кода этого спека не перечисляет (класс PD-112). Там же: резюм на деплое без маркер-команды отвечает 503, а 0.2.1 ратифицировала 503 только для СТАРТА прогона. - «Responds immediately; the book enters
uploading» недостижимо как класс (PD-180): по HTTP ответ не может уйти раньше, чем прочитано тело, поэтому 201 несётparsing. Плюс отказы интейка, которых спека не описывает вовсе: число частей формы, длина текстового поля, порог 413 и обрыв соединения по дедлайну маршрута.
Кросс-семейное ревью (D39.120 п.1а): что нашла ДРУГАЯ модель после трёх своих
⚠ Сначала о разрыве, который это закрывает. Три ревьюера выше запускались без указания модели и унаследовали модель сессии — то есть все четверо (автор и трое) были одной семьи. D39.120 п.1(а) просит в панели ≥1 опровергателя ДРУГОЙ моделью на оценочных линзах; механические линзы (сверка провода, посадки, батарея) от смены модели не выигрывают, а «деньги и гонки» — линза именно оценочная. Пробел назван владельцем, не найден сессией. Запущены два ревьюера Fable 5 (слепой поиск по диффу и атака на денежные инварианты), обоим предписано воспроизводить исполнением. ⚠ Оговорка из той же ноты, п.1(в): Fable — та же семья Anthropic, поэтому семейный слепой участок этим НЕ снимается; первичны пере-ран и исполнение, а внешняя калибровка — не право сессии.
Результат: другая модель нашла 5 дефектов, которые три однофамильца пропустили, и два из них — уничтожение пользовательских данных. Все воспроизведены исполнением, все закрыты, каждый с пином и посадкой.
| Находка | Чем это было | Как закрыто |
|---|---|---|
| Грация клейма (10 мин) КОРОЧЕ таймаута задания очереди (15 мин) — обе константы поставил этот же пак, в разных файлах, не сверив | В окне 10–15 минут свип отдаёт клейм второму парсеру: два tmctl manifest на одной директории, проигравший умирает на эксклюзивном локе движка с exit 1 — а exit 1 у движка означает «источник не разобрать». Книга отклонялась ТЕРМИНАЛЬНО, исходник пользователя удалялся. Воспроизведено тестом ревьюера |
Грация написана как jobs.JobTimeout + 5m, пара утверждается тестом; терминальная запись возможна только пока клейм ЕЩЁ НАШ (parse_started_at = <наш>). PD-183 |
| Один exit 1 движка = удаление загрузки | Движок маппит на exit 1 ВСЕ свои отказы (его собственный комментарий): полный диск, лок от ручного tmctl оператора, украденный клейм. Первый же такой отказ уничтожал файл безвозвратно — ре-парса и удаления в контракте нет |
Отказ ИСТОЧНИКА идёт через тот же бюджет попыток, что и поломка деплоя; удаление — только на терминальном шаге. Пин расширен: первый отказ обязан оставить файл на месте |
not_configured был терминальным — а конфигурацию не пишет никто, пока развилка не ратифицирована |
Значит на СЕГОДНЯШНЕМ деплое любая загрузка умирала бы в течение часа, с удалением, из-за пробела, которого пользователь не видит, а оператор закрывает одним файлом | not_configured НЕ становится терминальным никогда: книга ждёт в parsing с целым исходником, и это видно метрикой интейка. ⚠ Тест на этом свойстве ПЕРЕВЁРНУТ намеренно — он пинил «пять попыток и отказ», и это и было ошибкой |
| Устаревший снапшот до-финиширует РЕЗЮМИРОВАННЫЙ прогон (деньги) | Маркер завершённой попытки остаётся на диске; проход со старым снапшотом закрывал вернувшийся к жизни прогон, и холд попытки 2 выпадал из ОБОИХ списков — ни живых прогонов, ни несведённых. Достижимо стало ровно с появлением резюма | FinishRun пишет, только если закрываемая попытка ещё живая, и возвращает признак «закрыл». PD-181 |
| Стоп закрывал прогон под ЖИВЫМ движком (деньги) | «Заявка отдана назад» ≠ «процесса нет»: systemd-run, убитый после запроса, оставляет движок работать — зона это уже знает и ради этого хранит базовую линию, но стоп-путь читал пустое имя юнита как «его не было» |
Ненулевая базовая линия при пустом имени = надгробие спавна; имя юнита детерминировано, платформа спрашивает systemd Alive и живому юниту шлёт стоп. PD-182 |
| Заявка на спавн игнорировала висящий интент стопа (мой комментарий утверждал обратное) | Стоп, попавший в окно, пока воркер читает отсчёт книги, не мешал старту движка | stop_requested_at is null — в том же CAS; комментарий приведён к коду |
TM_PLATFORM_UPLOAD_DEADLINE мог пережить грацию свипа |
Оператор, поднявший дедлайн выше часа, получал удаление строки и каталога из-под ещё идущей загрузки | Пара проверяется на буте |
| Полностью принятая загрузка выбрасывалась, если клиент ушёл до 201 | Телефон уснул — файл и минуты линии в корзину | StartParsing идёт на контексте, переживающем запрос |
Лейбл method — токен вызывающего |
Для маршрута кардинальность закрыта, для метода — нет | Закрытый список, прочее — (other). PD-184 |
runner.Manifest подвисал/убивал движок на превышении потолка |
Правку я собирался внести раньше, и она НЕ доехала (скрипт упал на другой ассерции) — поймано только здесь | Остаток дренится, превышение — честная ошибка |
Статус finalizing без писателя |
Слово контракта, которого не пишет ни один путь — тот же класс, что интейк-статусы до этого пака | Строкой PD-185, не кодом: писателя выдумывать не право зоны |
Посадок этого круга: 12, поймано 11. Пережила одна и она названа, а не подчищена: «отказ сносит каталог ПОСЛЕ строки, а не до» — свойство про ПОРЯДОК двух побочных эффектов, наблюдаемое только через смерть процесса между ними; конечное состояние в обоих порядках одинаково, и юнит-тестом это не ловится. Порядок выбран самоизлечивающийся (сначала каталог, потом строка) и обоснован в коде.
⚠ Что этот круг говорит о предыдущем. Три однофамильных ревьюера прошли по тем же файлам и не увидели ни рассинхрона двух констант, поставленных в одном паке, ни того, что «exit 1 = вина файла» уничтожает данные на первом же транзиенте. Это ровно та коррелированная слепота, ради которой D39.120 просит другую модель, — и здесь она подтверждена эмпирически, а не принята на веру.
Записка-план после ревью: ID → статус → улика (D39.121)
| ID | Что | Статус | Улика |
|---|---|---|---|
| R1 | Право на спавн выдаётся законченному прогону (деньги, критично) | ЗАКРЫТО | гард в RecordSpawn; TestTheQueueWorkerDoesNotStartARunThatWasAlreadyStopped; посадка «снять гард» падает |
| R2 | Рестарт стирает свежий интент стопа (деньги) | ЗАКРЫТО | RestartRun берёт строку прогона for update и отдаёт ErrStopRequested; TestAStopPressedWhileTheReconcilerRestartsIsNotLost; посадка падает |
| R3 | Закрытие неспавненного стопа роняет только что стартовавший движок (деньги) | ЗАКРЫТО | FinishUnspawnedStop с перепроверкой под замком; TestARunSpawnedWhileTheSweepWasClosingItIsNotAbandoned; посадка падает |
| R4 | Спавн не читает интент стопа (деньги) | ЗАКРЫТО | отказ в spawnAttempt; тот же пин, что у R1; посадка падает |
| R5 | Интейк и телеметрия голодают на общем дедлайне | ЗАКРЫТО | у каждого прохода свой бюджет (cmd/tmplatformd/runner.go); прогон пробы: sweep_duration_seconds_count{intake} растёт вместе с {runs} |
| R6 | Свип сносит каталог книги, которую не удалил | ЗАКРЫТО | abandon возвращается на ошибке удаления; TestTheSweepNeverRemovesTheSourceOfABookItCouldNotDelete; посадка падает |
| R7 | Отмена загрузки роняет ревизию библиотеки назад | ЗАКРЫТО | пол users.library_revision; TestCancellingAnUploadDoesNotWindTheLibraryBack; две посадки падают |
| R8 | Бюджет разбора считает тики, а не время | ЗАКРЫТО | заявка держится; пин расширен проверкой «до грации не делается ничего»; посадка падает |
| R9 | Таймаут задания очереди — молчаливая минута River | ЗАКРЫТО | JobTimeout выбран явно; jobs_test.go пинит вид и политику заданий |
| R10 | runner.Manifest подвисает на превышении потолка |
ЗАКРЫТО | остаток дренится, превышение — ошибка; TestAManifestThatIsNotJSONIsAnError и соседние гоняют реальный процесс |
| R11 | Ревизия прогона на проводе — из его колонки | ЗАКРЫТО | стор отдаёт книжную на всех путях; pgstore.TestEveryRunTheStoreHandsOutCarriesItsBooksRevision, runs.TestEveryRunCarryingAnswerUsesTheBooksRevision; посадка падает |
| R12 | Резюм судит деплой раньше владения | ЗАКРЫТО | порядок переставлен; TestOwnershipIsJudgedBeforeTheDeploymentsHealth |
| R13 | Резюм второго живого прогона книги → 500 | ЗАКРЫТО | ErrRunInFlight → 409; TestResumeIsRefusedWhenTheBookHasAnotherLiveRun |
| R14 | Два резюма подряд → 404 проигравшему | ЗАКРЫТО | проигравший перечитывает прогон; TestTwoResumesOfOneRunTakeOneHoldAndBothAnswer |
| R15 | Каталог-сирота, если строка исчезла между телом и parsing |
ЗАКРЫТО | Accept убирает за собой и на этой ветке |
| R16 | 201 несёт parsing, а не uploading |
СТРОКОЙ, не кодом | PD-180: по HTTP ответ не может уйти раньше тела; uploading наблюдаем параллельным чтением, пин есть |
| R17 | Отказы интейка, которых спека не описывает (части формы, длина поля, порог 413, обрыв по дедлайну) | СТРОКОЙ | PD-180, вопросом владельцу контракта одним пакетом с PD-172/PD-174 |
| R18 | 503 на резюме у несконфигурированного деплоя | СТРОКОЙ | PD-174 расширен: класс тот же, что у ратифицированного 503 на старте |
| R19 | Любая загрузка сегодня приходит к rejected, и выхода из rejected нет |
СТРОКОЙ | PD-175 расширен; корень — развилка book.yaml, вопрос выше |
| R20 | Пути книг в ERROR-логах интейка | СТРОКОЙ | PD-139 дополнена; на INFO/WARN идентификаторов нет, и это запинено |
| R21 | requestTooLarge не доходит через обёртки |
СТРОКОЙ | PD-176: 413 отдаётся штатно, соединение закрывается обычным путём под ReadTimeout |
| R22 | Порядок блокировок в RestartRun (строка прогона бралась последней) |
ЗАКРЫТО попутно | R2 берёт её сразу после книги — документированный порядок восстановлен |
| F1 | Грация клейма короче таймаута задания: свип крадёт парс, книга терминально отклоняется с удалением файла | ЗАКРЫТО | claimGrace = jobs.JobTimeout + 5m; books.TestTheClaimGraceOutlivesTheQueuesJobTimeout; посадка падает |
| F2 | Один exit 1 движка уничтожал загрузку | ЗАКРЫТО | отказ источника идёт через бюджет; удаление только на терминальном шаге; посадка «отклонять с первого раза» падает |
| F3 | not_configured терминален — то есть каждая сегодняшняя загрузка обречена |
ЗАКРЫТО (тест перевёрнут намеренно) | TestABookWithNoEngineConfigurationWaitsRatherThanDies; посадка «отклонять после бюджета» падает |
| F4 | Устаревший снапшот до-финиширует резюмированный прогон, холд вне списков | ЗАКРЫТО | гард живой попытки в FinishRun; TestAStaleSweepDoesNotReFinishAResumedRunFromTheOldAttemptsMarker; посадка падает |
| F5 | Стоп закрывает прогон под живым движком после отданной заявки | ЗАКРЫТО | Alive по детерминированному имени юнита; TestAStopDoesNotCloseARunWhoseGivenBackClaimLeftAnEngineRunning; посадка падает |
| F6 | Заявка на спавн игнорирует интент стопа | ЗАКРЫТО | условие в CAS; TestAStopCommittedWhileTheWorkerReadsTheMeterStopsTheSpawn; посадка падает |
| F7 | Дедлайн загрузки мог пережить грацию свипа | ЗАКРЫТО | проверка пары на буте; config.TestAnUploadDeadlineLongerThanTheSweepsGraceIsRefused; посадка падает |
| F8 | Принятая загрузка выбрасывалась, если клиент ушёл до 201 | ЗАКРЫТО | контекст переживает запрос; TestAnUploadSurvivesAClientThatHangsUpAfterTheLastByte; посадка падает |
| F9 | Лейбл method — токен вызывающего |
ЗАКРЫТО | закрытый список; посадка падает |
| F10 | runner.Manifest подвисает на превышении потолка |
ЗАКРЫТО | дренаж остатка + ошибка; правка была задумана раньше и НЕ доехала — поймана только кросс-семейным кругом |
| F11 | Порядок «каталог до строки» в отказе | ⚠ ПЕРЕСМОТРЕНО дофиксом → FP5-3 | диспозиция «порядок самоизлечивающийся» стояла на посылке, которую тот же пак отменил (not_configured стал НЕтерминальным), поэтому крэш-окно оставляло книгу в parsing навсегда; закрыто отдельной причиной ErrDirectoryGone (PD-187), пин и посадка есть |
| F12 | finalizing — статус без писателя |
СТРОКОЙ | PD-185 |
Что НЕ делалось (по промту)
Эскроу/uncertain/closing (строка 136) · формула потолка PD-158 и SpendBound PD-159 · ставка
$0.03 · эмиттер-сторона движка и словарь events.go · П-2 · платёжный провайдер · удаление аккаунта
(PD-107) · счётчик ревизии ОБЛАСТИ целиком (PD-122 сужена, не закрыта) · правка контракта (четыре
вопроса выше вместо неё).
Ратификация приёмкой P4 (оркестратор №15, 09.08)
Вердикт: P4 + дофикс + V2 ПРИНЯТЫ и ЗАЛЕНДЕНЫ — D39.123, код d29e30c (53 файла, +9867/−170,
только platform/). Три раунда, 13 агентов приёмки; каждое заявление пере-ранено исполнением.
Метод и что подтвердилось. 7-агентный воркфлоу (слепой дифф · опровергатель-Opus D39.120 ·
пере-ран батареи · 10 моих посадок · 47 wire-проверок контракта · охотник-деньги ·
охотник-systemd с РЕАЛЬНЫМ tmctl из HEAD) → фикс-лист F1–F8 → ре-чек 4 контурами (обе пробы
приёмки, пере-мутации, PD-158/159 адверсариально на Opus, охота по дельте) → V2 → финальный
ре-чек (5/5 пере-мутаций убиты поимённо). Реальный движок принял платформенный argv целиком,
имена полей status --json совпали с парсером побайтно, exit-контракт и маркер сошлись; четыре
systemd-клейма пере-мерены; батарея EXIT=0 с живым PG 18.4 под -race, скипов 0; тестов
105→261 (+156/−0); дерево на лендинге байт-равно проверенному.
Пять MAJOR приёмки и PD-159 селф-ревью — все закрыты с пинами; разборы в разделах «Дофикс P4» и «Ре-чек V2» ниже. Несущие: аргумент потолка против кумулятивной семантики движка (F1; вскрыт тем, что сквозная проба шла на фейке без кумулятива) · дедлок books↔run_attempts 258/300 с вечным карантином проекции на транзиентном 40P01 (F2; ложно закрытая PD-129 пере-открыта и закрыта честно) · settle по устаревшему снапшоту (F5) · двойная оплата при отложенном расчёте (PD-159, найдена селф-ревью сессии — воспроизведена приёмкой на до-фиксном дереве).
Ратифицировано (D39.123 п.2): linger-privilege-модель · ПОПРАВКА формулы потолка:
committed + прирост, БЕЗ reserved (PD-158) — сессия была права, отказавшись молча исполнять
формулу моего пинга: исполнение обеих против гейта движка показало, что пинг переплачивал
запасом ровно на leftover-reserved · карантин ПРОЕКЦИИ (PD-105-уточнение) · ставка $0.03 ·
default_chapters = верх шкалы · 503 в контракт (0.2.1; PD-112 закрыт) · PD-114 → задача
«печать эффективной конфигурации» · PD-115 — направление принято. PD-113 — единственный
открытый major, до эмиттера; его цена после PD-158 выросла (стоп по потолку = ровно исчерпание
холда). Движку заведены строки 165 (различимые exit-коды потолка/стопа) и 166 (оценка $/глава).
Урок приёмки в обе стороны: (а) фейк шва обязан моделировать СЕМАНТИКУ чужой стороны, не только форму — обе жёсткие находки (F1, N1-stop→failed) прошли бы фейком; класс закрыт кумулятивным ceilingJudge в пинах; (б) моя ошибка нумерации (PD-167 = коммент 00009, не 00011) поймана сессией — исправлена в строке; (в) две посадки сессии, пережившие первую редакцию пина, задекларированы, не подчищены — норма «заявление=команда» работает в обе стороны.
Дофикс P4 (09.08): фикс-лист приёмки — F1…F8, N1…N4
Пак принят УСЛОВНО с пятью MAJOR. Ниже — что сделано по каждому пункту, чем это проверено и что осталось. Каждое число здесь снято исполнением; команда стоит рядом с числом.
F1 [MAJOR, деньги/шов] Аргумент потолка — по ратифицированной формуле
Приёмка права, и её посылка перепроверена мной на HEAD ОБЕИХ зон, не по памяти:
backend/cmd/tmctl/invocation.go объявляет флаг словами «It caps the book's CUMULATIVE
committed+reserved spend, not this run's increment», а backend/internal/store/ledger.go Reserve
сравнивает SUM(committed_usd + reserved_usd) книги с c.BookUSD на КАЖДОЙ резервации. Движковая
проба приёмки пере-прогнана в КОПИИ бэкенда (в чужую зону не писал):
TestAcceptSecondRunDeniedWhenCeilingIsPassedAsIncrement — при committed $3.00 потолок 3.0 даёт
ReserveDeniedBook, 6.0 — ReserveOK.
Сделано: runs.meter (committed, reserved) читается ОДНИМ вызовом status --json перед стартом
(bookMeter), аргумент = committed + прирост (meter.bookCap). ⚠ Слагаемое reserved из
ратифицированной формулы НЕ добавляется — это названное отклонение в консервативную сторону, разбор
и вопрос оркестратору ниже, PD-158. reserved_usd
внесён в аллоулист ingest.StatusReport указателем — отсутствие ≠ ноль ровно по той же причине,
что и у committed, только ошибка зеркальная: ноль вместо реального резерва даёт потолок НИЖЕ того, на
чём книга уже стоит. Рестарт идёт тем же путём и получает свежий отсчёт (спавн один на все случаи).
Фактически ушедшее значение хранится: run_attempts.ceiling_arg_micro_usd (миграция 00011) — оно не
равно ceiling_micro_usd, и после того как счётчик книги сдвинулся, восстановить его нечем.
Пины (посадки — ниже): TestTheSecondRunOfABookIsGivenTheCumulativeCapAndNotItsOwnIncrement,
TestAResumeIsGivenACapComputedFromTheMeterAsItStandsNow. Оба гоняют фейк ceilingJudge, который
судит --ceiling-usd по правилу движка, включая проход восстановления: открытие книги на запись
обнуляет остаточный reserved_usd, после чего потолок на уровне committed или ниже отвергается
первой же резервацией. Это и есть ответ на «почему батарея пака этого не поймала»: её фейк лишь
записывал аргумент, а фейк, который не может отказать, не проверяет ничего. Резюмный пин отделён от
арифметики специально: в нём смоделирован ОСТАВЛЕННЫЙ резерв убитого процесса ($0.20), и он
утверждает ДВА факта — что потолок пересчитан по свежему отсчёту и что остаток его не раздул
(PD-158).
Проба приёмки на этом дереве (её файл, только Reserved дописан в отчёты фейка):
TestAcceptSecondRunCeilingArgument — PASS, run2 args: … --ceiling-usd 6.000000 при
committed книги $3.
Устаревшие посылки, названные приёмкой, сняты:
runner/engine.goбольше не пишет, что флага нет и что копировать форму из чужого дерева нельзя: флаг заленден (0e69bc1), форма ратифицирована, поэтому--ceiling-usd {{usd}}стал дефолтом конфигурации (TM_PLATFORM_ENGINE_CEILING_ARGостаётся переопределением для сборки с другим написанием).ErrCeilingNotWiredоставлен: тип по-прежнему допускает пустое значение, а пустой шаблон не должен значить «без потолка». ⚠ Уточнено ревью доков: он не «недостижим» — переменная из одних пробелов проходит подстановку дефолта и даёт пустой шаблон, то есть отказ в рантайме при одном WARN на буте (PD-165 — тот же класс, что относительныйTM_PLATFORM_CTL_BIN).ingest/resync.goбольше не пишет, что у статуса нет фазового сплита:progress{draft,edit}внесён в аллоулист и материализуетсяApplyStatusтеми же четырьмя счётчиками, что и поток. Этим снято ⚠ затирания фаз резюнком — не гейтом, а тем, что затирать стало нечем. Гейт «поток идёт → ре-синк не зовём» оставлен, но его обоснование переписано честно: он теперь про ЦЕНУ (каждый вызов статуса — секунды CPU на пере-нарезку) и про то, что поток свежее.
F2 [MAJOR] Инверсия блокировок была жива; транзиентный сбой уходил в карантин
Порядок написан в ОДНОМ месте и стал глобальным — pgstore.lockBook:
books → runs → run_attempts → account_balances → reservations. Книга блокируется первой в
RunSink.Apply, RestartRun и StartRun (последний добавлен не «за компанию»: он берёт баланс до
update books, и если бы книгу первым брал только рестарт, пара StartRun ∥ RestartRun дала бы ту
же инверсию на другой паре строк). closeReservation уже брал баланс раньше резервации — проверено,
не тронуто. Собственной сверкой всех транзакций пакета найден ещё один нарушитель — DeleteBook
(резервации, потом книга); цикла для него сегодня нет, но именно такое рассуждение записанный
инвариант и должен заменять, поэтому книга взята первой и там.
Классификация ошибки материализации вынесена в runs.quarantines и стала явной:
транзиентные SQLSTATE (40P01/40001, pgstore.IsTransient) и собственная отмена — повтор
следующим свипом; карантин остаётся только логическим ошибкам (пропасть, конфликт payload, битая
строка, строка длиннее буфера).
Пины. Конкурентный оставлен (TestAMaterializerAndAReconcilerOnOneRunDoNotDeadlock, 60×3 через
реальные API), но он пробует, а не доказывает: посадка «снять lockBook из RestartRun» его
ПЕРЕЖИЛА — рестарт по-настоящему берёт замок один раз за прогон, и окно слишком узкое. Поэтому
написаны два пина, утверждающие порядок ПРЯМО: TestTheMaterializerTakesTheBookBeforeTheAttempt и
TestARestartTakesTheBookBeforeTheAttempt — тест держит замок книги, ждёт, пока операция реально
заблокируется, и спрашивает строку попытки через for update nowait. Свободна — значит операция
ждёт книгу, ничего не держа. Занята — значит она взяла попытку по дороге, то есть в том порядке,
который и дедлочит. ⚠ Ожидание блокировки читается из pg_stat_activity, а не из pg_locks:
ждущий строку ждёт на transactionid держателя, а у таких строк pg_locks.database пуст, поэтому
запрос по своей базе их не видит (проверено — первая редакция теста падала именно так).
F3, F4 [MAJOR, пины] Два свойства, построенных и не запиненных
- Нечитаемый отсчёт ОТКАЗЫВАЕТ спавну (сердце PD-124):
TestAnAttemptWhoseMeterCannotBeReadIsNotStartedAtAll— три формы нечитаемости (вызов упал · нетcommitted_usd· нетreserved_usd), в каждой утверждается, что юнит не создан И право на спавн не заклеймлено (unit_nameпуст), значит свип повторит; хвост показывает, что после починки движка та же попытка стартует. - Устаревший exit-маркер стирается ДО старта:
TestAStaleExitMarkerIsClearedBeforeTheUnitStarts— маркер создаётся до спавна, после спавна обязан отсутствовать, свип обязан оставить прогон живым и холд открытым.
F5 [MAJOR, деньги] Расчёт по устаревшему снапшоту
pgstore.ReleaseUnspawned перечитывает run_attempts.unit_name for update в ТОЙ ЖЕ транзакции,
что и деньги, и отказывает (ErrAttemptSpawned), если юнит есть. Реконсилятор тогда откладывает
расчёт: следующий проход видит юнит в снапшоте и считает по базовой линии попытки. Выбран SQL-гард,
а не перечитывание в Go, потому что гард атомарен с самим возвратом холда.
Пин: TestAStaleSnapshotDoesNotGiveBackTheHoldOfAnAttemptThatSpent — в проходе со старым снапшотом
холд не возвращается целиком, следующий свип списывает ровно потраченные $0.50.
⚠ Проба приёмки TestAcceptStaleSnapshotReleasesTheHoldOfARunThatActuallySpent на этом дереве не проходит как написана и это не
регрессия: она требует, чтобы деньги разрешились в ТОМ ЖЕ вызове reconcile, а исправление
откладывает их на следующий проход. Замеренная разница: было — баланс 10.000000 (весь холд назад,
списано ноль), стало — 7.000000 при 3.000000 в резерве (деньги на месте и видимы), после
следующего свипа — 9.500000.
F6, F7 [MINOR]
- Относительный
TM_PLATFORM_STATE_DIRотвергается на буте (filepath.IsAbs), пинTestARelativeStateDirectoryIsRefusedAtBoot. - «Метка давности» ре-синка построена:
runs.last_resync_at(миграция 00012) пишется КАЖДЫМ ре-синком, пинTestAResyncRecordsWhenItWasTaken(нет метки до первого · метка равна времени вызова · вторая переписывает первую). ⚠ Названная девиация: на провод метка не выходит — в контракте v0 у прогона поля свежести нет, и заводить его самовольно не право зоны (PD-150).
F8 [MINOR, отчёт/регистр]
Все пять исправлений внесены: числа тестов пересчитаны исполнением и приведены с командой (105 → 261,
удалённых 0); арифметика «36 новых» исправлена на 27 + 9 (после ратификации PD-112) и подтверждена
скриптом по таблице; заявление «каждый из 22 пинов проверен посадкой» снято как непроверяемое
(PD-142); строка PD-105 приведена к коду — карантинится ПОПЫТКА (её проекция), жизненный цикл
прогона продолжается; PD-122 дополнен: ревизия не двигается и на ДОБАВЛЕНИИ книги (AddBook пишет
revision = 0), класс тот же.
Новые строки регистра (N1…N4) и ратификации
Заведены четыре по фикс-листу плюс две собственные (PD-156 — ниже, PD-151 — числа отчёта):
PD-152 (стоп → failed: реальный tmctl ловит SIGTERM и выходит кодом 1, так что
ветка stopped достижима только для смерти ОТ сигнала — проба пака показала stopped на фейке,
который именно так и умирал) · PD-153 (грация спавна от started_at, а не от времени заявки) ·
PD-154 (settled_at может остаться NULL между Settle и MarkSettled; потребителей нет,
деньги целы) · PD-155 (смена StateDir осиротляет маркеры — строка внесена в deploy/README.md).
Ратификации оркестратора внесены в регистр: PD-112 закрыт ратификацией (503 вносится в спеку
правкой владельца контракта) · PD-114 переформулирован в задачу «печать эффективной конфигурации
на старте с редакцией секретов», env-only остаётся · PD-115 — направление «по внешнему эталону на
ось» ратифицировано, носитель работы — строка · PD-129 пере-диспозиция: строка была закрыта
ЛОЖНО, действительно закрыта дофиксом (PD-145) · default_chapters = верх шкалы остаётся, кода не
касается · PD-113 остаётся открытым до эмиттера.
Батарея и посадки дофикса
make checkс живым PostgreSQL 18.4 под-race: 13 пакетов, 0 FAIL, линтер 0 issues.go test ./... -count=1 -v | grep -c -- '--- SKIP'= 0.make vuln: No vulnerabilities found.- Тесты: 105 → 261, удалённых 0 (команда — раздел «Батарея и самопроверка» ниже).
- Посадки: 24 из 24 пойманы (скрипт восстанавливает файл после каждой):
bookCapвозвращает прирост · записывается прирост вместо аргумента · отсутствующийreserved_usdчитается нулём · материализатор берёт попытку первой · рестарт берёт попытку первой · транзиентный сбой карантинится · нечитаемый отсчёт читается нулём · очистка маркера удалена · расчёт доверяет снапшоту (ReleaseвместоReleaseUnspawned) · относительныйStateDirпринимается · метка ре-синка не пишется · метка пишется только однажды · ре-синк снова кладёт один агрегат · шаблон потолка теряет дефолт · отказ деплоя проверяется ПОСЛЕ вызова движка · остаточная резервация раздувает потолок ·DeleteBookберёт резервации раньше книги · отложенный расчёт не ограничен верхней границей · транзиентный сбой карантинится (через РЕАЛЬНЫЙ путь) · повторная заявка перезаписывает базовую линию · рестарт Postgres карантинит проекцию · сброшенное соединение карантинит проекцию · ретрай пересчитывает переданный потолок · счётчик назад списывает молча. ⚠ Две посадки в первой редакции ПЕРЕЖИЛИ и это записано, а не подчищено: «рестарт берёт попытку первой» (конкурентный пин её не ловил — написаны прямые пины порядка) и «метка пишется только однажды» (тест ставил метку один раз — добавлена вторая проверка). - Контрактная проба приёмки (47 wire-проверок,
-overlayповерх пакетаhttpapi) пере-прогнана на этом дереве целиком: PASS, формы не сломаны. - Гоночная проба приёмки
TestAcceptConcurrentStartRunTakesExactlyOneHold: PASS (8 конкурентныхStartRun→ 1 прогон, 1 холд) — она проверяет то, чтоlockBookвStartRunмог бы изменить.
Что нашла собственная сверка диффа (после фиксов, до сдачи)
Две правки, обе внесены и обе — про то, что дофикс мог испортить рядом:
DeleteBookбрал резервации раньше книги — единственный оставшийся нарушитель порядка, который я записал как глобальный. Цикла для него сегодня нет (проверено перебором пар: удаление трогает только ЗАКРЫТЫЕ резервации, аStartRunвставляет новую), но инвариант с необъявленным исключением перестаёт быть инвариантом — книга взята первой и там, и это запинено (TestDeletingABookTakesItBeforeItsReservations; вспомогательныйassertBookFirstобобщён на любую вторую строку — попытку или резервацию).- Отказ ДЕПЛОЯ проверялся после вызова движка. Перенеся чтение отсчёта в начало
spawnAttempt, я поставил его ПЕРЕД дешёвыми отказами («нечем записать конец юнита», «нечем передать потолок»), и инстанс с неполной конфигурацией платил бы секундами CPU движка за каждый прогон на каждом свипе, чтобы прийти к ответу, зависящему только от конфигурации. Вынесено вruns.runnable(), вызывается первым; пинTestAMisconfiguredDeploymentIsRefusedWithoutAskingTheEngine(посадка «убрать ранний отказ» падает — 15-я в списке выше).
Формула потолка: почему reserved в неё не входит (PD-158, ратифицировано 09.08)
Сдавалось как ВОПРОС с консервативным кодом; ре-чек V2 ратифицировал формулу зоны, пере-мерив обе
против гейта движка — та, что с reserved, переплачивала запасом ровно на leftover-reserved.
Первая формула — committed + reserved + прирост — читает reserved_usd из status --json. Но
store.Open, путь ЗАПИСИ, которым идёт каждый translate, выполняет recoverReservations и
обнуляет reserved_usd книги ДО того, как будет судима первая резервация
(backend/internal/store/store.go:88, тело — :214); tmctl status идёт read-only и этот проход
намеренно не делает — движок пишет об этом сам (store.go:108: «after a run crashes, reserved_usd
stays non-zero until the next WRITE command … status will show this leftover as reserved»).
Значит цифра, которую платформа кладёт в потолок, к моменту сравнения уже стёрта, и прогон получает на её величину БОЛЬШЕ, чем его холд: движок останавливается позже, расчёт упирается в потолок холда, леджер пишет «capped at the hold» — и это читается как перерасход движка, хотя это наша арифметика. Путь достижим на каждом резюме после падения, потому что падение и оставляет резервацию.
Что сделано: bookCap считает committed + прирост. Отклонение в КОНСЕРВАТИВНУЮ сторону — более
узкий потолок может только остановить прогон раньше, перерасхода не даёт, — названо в коде и
запинено, и сдано ВОПРОСОМ, а не решением: отклонять ратифицированное молча не право зоны. Ответ
пришёл ре-чеком V2 — формула принята. Цифра reserved
по-прежнему читается и обязательна: она и есть доказательство, что отклонение безопасно — на спавне
другого писателя нет (эксклюзивный лок плюс один живой прогон на книгу), значит любой reserved по
построению остаток.
Найдено при F1 и заведено, а не построено
PD-157: дневной потолок книги --ceiling-usd не перекрывает. Ратификация D39.122 говорит это
прямо, и движок требует хотя бы один из book_usd/day_usd (backend/internal/config/book.go:250),
а book.yaml пишет ОПЕРАТОР — платформа его не правит (D39.110 §2b) и в book add лишь проверяет
наличие. Значит книга с низким day_usd останавливает прогон на лимите, которого платформа не
выбирала: движок выходит кодом 1, прогон приезжает failed, деньги пользователя целы, причина ему
не видна. В status --json дневной фигуры нет вовсе, так что и диагностировать это платформа
сегодня не может. Не строил: фикс-лист этого не просил, а закрывается оно либо проверкой при
заведении книги, либо словом контракта о том, кто владеет потолками book.yaml у книг под
платформой — второе не право зоны.
Селф-ревью дофикса: три верификатора, что нашли
Прогнаны три независимых ревьюера по разным линзам — один по фикс-листу и диффу, один НАМЕРЕННО без отчёта («вне карты»), один по claim-fidelity доков против кода. Все трое работали read-only, мутации гоняли в копиях дерева.
Найдено и ПОЧИНЕНО в этом же дереве:
- PD-159, деньги, major. Отложенный расчёт прогона оплачивал работу СЛЕДУЮЩЕГО прогона той же
книги, а тот платил за неё ещё раз. Нашли ДВА верификатора независимо, каждый воспроизвёл
исполнением; я воспроизвёл третьим ($2.10 за прогон, стоивший $0.10) ПЕРЕД починкой. Причина —
расчёт читает пожизненный счётчик КНИГИ в момент повтора, а завершённый-но-нерассчитанный прогон
не мешает начать новый. Починено верхней границей
SpendBound(наименьшая базовая линия среди попыток книги, стартовавших позже — она снята после остановки этой попытки и до того, как та что-либо добавила, то есть ТОЧНАЯ граница). - PD-160, пин. «Транзиентный сбой не карантинит» пинилось на чистой функции; удаление ветки, которая её ВЫЗЫВАЕТ, переживало батарею. Написан пин, который гонит НАСТОЯЩИЙ дедлок через весь путь.
- PD-161, деньги. Повторная заявка права на спавн перезаписывала базовую линию — при
systemd-run, убитом ПОСЛЕ подачи запроса, попытка платила бы разницу от цифры, включающей её же работу.
Найдено, заведено и НЕ построено (каждое — строкой с названным лечением): PD-152 расширен живым
замером — штатная перезагрузка закрывает все прогоны как failed, потому что ExecStopPost
отрабатывает и движок выходит кодом 1 (путь строки 138 покрывает только потерю питания) · PD-162
удалённый каталог книги заклинивает прогон навсегда с открытым холдом · PD-168 бюджет перезапуска
считается по текущей ставке (замерено: $5.50 вместо $2.50 при удвоении) · PD-163 ревизия области
читается вторым запросом · PD-164 неразбираемый маркер вечно валит реконсиляцию · PD-165
относительный TM_PLATFORM_CTL_BIN и $ в quoteArgv · PD-166 chunker_version теряется ·
PD-167 расхождение док↔код в комментарии миграции · PD-169 бюджет свипа один на все прогоны ·
PD-170 асимметрия Settle с PD-97.
Найдено в ДОКАХ и исправлено: тело F1 противоречило собственному разделу PD-158 · описание фейка
ceilingJudge отстало от кода · §21 STACK_DECISIONS не догнал PD-158 · старый раздел «Сессия P4»
утверждал, что флага потолка в HEAD нет · числа тестов и посадок разъезжались между журналом и
регистром · PD-158 приписывал D39.122 буквальную формулу, которой в решении нет (она из пинга) ·
PD-122 требовал «свою миграцию», хотя колонка users.library_revision существует с 00001 и не
используется ни одним путём · «ErrCeilingNotWired недостижим через конфигурацию» — переменная из
пробелов до него доходит.
Ре-чек V2 (оркестратор №15, 09.08): четыре хвоста, три — в новом коде дофикса
V2-1 [MAJOR] — моя же классификация была неполна. IsTransient знала только про дедлок и
сериализацию, поэтому ШТАТНЫЙ рестарт Postgres (57P01/57P02/57P03, класс 08, сетевой сброс) всё ещё
карантинил проекцию живого платного прогона НАВСЕГДА — пути снятия карантина в дереве нет. Клейм
PD-145 «карантин остаётся только логическим ошибкам» был в этой части неверен и пере-сформулирован.
Теперь покрыты класс 08, 57P0x, pgconn.SafeToRetry и любой net.Error; ошибки чтения ФАЙЛА при
этом остаются логическими — *fs.PathError не удовлетворяет net.Error, поэтому битый журнал
по-прежнему останавливает проекцию, как и должен. Шесть новых кейсов в таблице пина, две посадки.
V2-2 [MINOR, деньги] — PD-161 был закрыт наполовину. В БД значения сохранялись, а движку на
ретрае уходил потолок, пересчитанный по СВЕЖЕМУ счётчику, то есть включающий трату собственного
«призрака»; форензик-колонка 00011 на этом пути лгала (замер приёмки: handed 3.400000 против stored
3.000000 — воспроизведён посадкой на моём дереве до буквы). Теперь ретрай ПЕРЕДАЁТ сохранённый
аргумент. Пин расширен до handed == stored, плюс отдельный пин на гард в БД
(TestASecondClaimOnOneAttemptKeepsWhatTheFirstRecorded) — после правки в Go посадка на coalesce
переставала ловиться, и свойство без собственного пина не считается закрытым.
V2-3 [MINOR, доки у денег] — три текста утверждали отменённую формулу (reconcile.go про
рестарт, комментарий поля Reserved, строка PD-144) и два — несуществующий отказ загрузчика на
пустое переопределение. Всё приведено к факту. Туда же: комментарий миграции 00011 нёс отменённую
формулу, а 00009 обещал перечитывание файла, которого тейлер не делает. Обе исправлены, отпечатки в
migrations.sha256 обновлены — с явной причиной в шапке файла: обе миграции написаны этим паком,
нигде не применялись, а миграция с лгущим комментарием хуже сдвинутого отпечатка до релиза.
⚠ Ре-чек назвал застывшим комментарий 00011 под номером PD-167, тогда как предмет PD-167 — 00009;
исправлены обе, расхождение в нумерации названо в строке.
V2-4 [NOTE] — счётчик книги ниже собственной базовой линии попытки списывал $0 молча. Клампить в ноль правильно (платить за подмену БД проекта код решать не вправе), молчать — нет: добавлен WARN, не INFO, с фактом и без цифр.
Что дофикс НЕ трогал (осознанно)
Ручки стопа/резюма (PD-140) · эскроу и uncertain (строка 136) · POST /books (П-9) · собственный
счётчик ревизии области (PD-122) · грация от времени заявки (PD-153) — заведены строками, не
построены: фикс-лист их не просил, а расширять пак на приёмке — это то, за что заведён PD-83.
Сессия P4 (08–09.08): раннер — юнит-на-прогон, очередь, реконсилятор, тейлер, первые ручки /v0
Каждое число ниже — с командой, которой получено. Дерево не коммичено; в индексе ничего не держу.
Работа 1 — транзиентный systemd-юнит на прогон
internal/runner/runner.go (юнит), marker.go (маркер выхода), engine.go (инвокация движка).
Привилегия-модель решена ДО постройки, и второй вариант отвергнут по СВОЙСТВУ, а не по вкусу.
Выбран linger-пользователь: юниты создаются в СОБСТВЕННОМ менеджере платформы, новых привилегий в
рантайме ноль. Polkit-вариант research/25 непригоден: polkit авторизует ГЛАГОЛ, а не свойства —
StartTransientUnit отдаёт выбор ExecStart=/User= вызывающему, а транзиентный СИСТЕМНЫЙ юнит без
User= идёт от root, то есть правило выдаёт сервису root и сузить это нечем. Плюс до systemd v257 в
запрос авторизации не попадает даже ИМЯ юнита (systemd issue #17224 → PR #34651, влит 09.10.2024),
так что на стенде (systemd 255, systemctl --version) «узкое правило» не узкое вовсе. Разбор —
STACK_DECISIONS §15.
Замерено на стенде, не выведено из доки (все команды — systemd-run --user, стенд без root):
| Свойство | Как замерено | Результат |
|---|---|---|
| Юнит переживает того, кто его создал | спавнер-скрипт стартует юнит и выходит | ActiveState=active, маркер success |
--collect ⇒ состояние systemd не истина |
systemctl --user show после выхода |
LoadState=not-found, ExecMainStatus=0 для прогона, вышедшего с кодом 3 |
| Маркер несёт исход | ExecStopPost на трёх исходах |
exit-code/exited/3 · success/killed/TERM · oom-kill/killed/TERM |
Лимиты в app.slice НЕ применяются |
cat app.slice/cgroup.subtree_control |
пусто; лист без memory.max; процесс, потрогавший 400 МиБ, пережил MemoryMax=64M |
| Лимиты в своём срезе применяются | то же в tm-runs.slice |
memory.max=67108864, pids.max=32, тот же процесс — oom-kill, Memory peak: 64.0M |
%-спецификаторы в --property= не раскрываются |
ExecStopPost с 100%_done/%n |
доехали буквально ⇒ экранировать % не нужно и было бы неверно |
ProtectHome=yes прячет /run/user/<uid> |
проба под юнитом | Permission denied ⇒ своя же шина недостижима; ProtectHome=tmpfs+BindPaths ⇒ сокет виден |
Шине хватает DBUS_SESSION_BUS_ADDRESS |
env -u XDG_RUNTIME_DIR |
работает; без обоих — Failed to connect to bus |
Ответ PD-13 живёт в cgroup ПРОГОНА и измерен. ⚠ И там же найдено собственным флейком: MemoryMax
БЕЗ MemorySwapMax=0 не ограничивает прогон — ядро выдавливает страницы в своп (у среза
swap peak: 692.5M), процесс выживает и тормозит до скорости диска. Для перевода это хуже отказа:
тормозящий часами прогон продолжает платить за каждый прошедший вызов. Снято: MemorySwapMax=0 идёт
вместе с MemoryMax (runner.go), три прогона пина подряд зелёные, три посадки «убрать своп-лимит»
подряд красные.
Пиннинг версии (строка 139): run_attempts.engine_binary пишется ДО создания юнита, новая попытка
его НАСЛЕДУЕТ, резюм исполняет именно его, а переход на другую сборку требует явного
TM_PLATFORM_RESUME_MAY_CHANGE_ENGINE. ⚠ Первая редакция закрывала эту строку НАПОЛОВИНУ — путь
читался каналом ремонта и не исполнялся резюмом; поймано моей же пост-сверкой диффа с промтом, не
батареей (обе половины компилировались и всё было зелёным). PD-143.
Работа 2 — очередь River + воркер
go.mod: github.com/riverqueue/river v0.42.0 + riverdriver/riverpgxv5 (пин из STACK_DECISIONS
подтверждён). internal/runs/queue.go.
Холд берётся ДО спавна и в ОДНОЙ транзакции с созданием прогона, попытки и записи очереди
(pgstore.StartRun, runs.go:66): холд без прогона — деньги, зарезервированные ни за чем; прогон без
холда — трата незарезервированного; запись очереди без обоих — воркер, спавнящий движок против книги,
у которой нет бухгалтерии. Очередь вставляет своё задание через колбэк с той же pgx.Tx.
Гард «один живой прогон на книгу» — исполнением: частичный уникальный индекс
runs_one_live_per_book мапится в доменную ErrRunInFlight (не сырой SQLSTATE), ручка отдаёт 409.
Живая проба: второй POST .../runs на ту же книгу → 409, Reserved не сдвинулся.
MaxAttempts: 1 у задания — намеренно против рефлекса очередей: повтор здесь не доделывает работу, а
спавнит ВТОРОЙ движок; восстановление — работа реконсилятора. Живая проба: повторная доставка задания
на живой прогон не создала второго юнита (len(started) = 1).
Работа 3 — реконсилятор
internal/runs/reconcile.go. На КАЖДЫЙ выход юнита — через ExecStopPost-маркер, не D-Bus: сигнал,
посланный в момент, когда платформа лежит (а она лежит несколько раз в неделю по замыслу), теряется;
файл — нет. На БУТЕ тот же свип перезапускает прерванные прогоны (строка 138): истина — каталоги книг
и Postgres, systemd спрашивается только «жив ли юнит», и это улика, не вердикт.
Разграничитель «не стартовал ещё» / «умер без слова» — время (spawnGrace 60 с). Перезапуск даёт
новой попытке ОСТАТОК бюджета (budget − RunSpent), иначе один прогон потратил бы потолок дважды;
если остатка нет — paused/credit_exhausted, не failed.
Расчёт — из фигуры движка, прочитанной ПОСЛЕ выхода процесса (status --json, ратифицированный
канал ремонта; лока нет, гадать не о чем). Не прочиталась — резервация остаётся ОТКРЫТОЙ и попадает в
UnsettledRuns для следующего свипа. ⚠ Эскроу-половина research/25 (write-ahead intent, uncertain,
closing) НЕ строилась: это строка 136 и её собственный промт.
Резюнк — честно редкий и честно грубый: по умолчанию 5 минут (TM_PLATFORM_RESYNC_EVERY), и
только там, где чинить нечего — курсор не двигался ЛИБО материализация в карантине. Причина в цене:
каждый вызов заново ингестит и режет исходник (1.4–1.5 с CPU на книге 23 МБ, строка 100).
Работа 4 — тейлер events.jsonl
internal/ingest/tail.go. Курсор (engine_run_id, seq) коммитится в ОДНОЙ транзакции с эффектом
(pgstore.RunSink.Apply), байтовое смещение — хинт. Файла ещё нет (эмиттер = строка 103) — тейлер
спокойно ждёт (ErrNoJournal не ошибка свипа).
PD-105 ратифицирован этим паком и реализован: дубль seq — НОРМА, пропускается идемпотентно и
без фатала; тот же seq с ДРУГИМ payload — ErrPayloadConflict → карантин ПРОЕКЦИИ (не прогона:
движок тратит уже зарезервированные деньги, и наша неспособность читать его журнал — не повод это
выбросить). Пропасть остаётся ошибкой. Строка PD-105 реестра обновлена.
Частичная последняя строка не читается как событие (писатель ещё пишет). Журнал пер-книжный и
append-only, поэтому резюм дописывает ВТОРОЙ hello: читатель ведёт область и не судит чужие строки.
Работа 5 — ручки /v0 по контракту 0.2.0
internal/httpapi/v0.go: GET /books · GET /books/{bookId} · GET /books/{bookId}/run-options ·
POST /books/{bookId}/runs · GET /usage. Прогресс/статус прогона отдаётся внутри карточки книги —
отдельной ручки прогона в спеке НЕТ, и выдумывать её я не стал.
CeilingBounds: максимум = Balance КАК ЕСТЬ (холд — дебет в момент взятия), подрезан И остатком
книги; max_chapters: 0 легален. default_chapters = верх шкалы — продуктовая политика платформы
(D39.115 §4), компромисс назван вслух в коде и вынесен вопросом ниже.
Интейк: дев-инструмент tmplatformctl book add (Go-подкоманда, а не шелл-скрипт: тестируется).
POST /books (multipart) НЕ взят — обоснование строкой П-9 зонного бэклога.
Пересчёт «главы→доллары» — константа платформы $0.03, провенанс: exp08 v2 ($10.94 на 500 глав =
$0.0219) через ревизию D30.4 (+15–25% → $0.0252–0.0274), округление ВВЕРХ. Движковой поверхности
оценки не выдумывал; потребность — строка П-10.
Работа 6 — регистр
Закрыты: PD-43 (денежный контур получил вызывающих) · PD-81 · PD-82 · PD-97 · PD-99 · PD-105. Заведены PD-108…PD-116 (из них PD-108/110/111/116 — дефекты, найденные и закрытые внутри этого же пака; PD-112…115 открыты вопросами). Строки эмиттер-стороны (PD-60/61) не тронуты.
Сквозная проба на боевом бинаре (не тестами)
Поднят tmplatformd против живого PG, с фейковым tmctl ($0, говорит ровно две поверхности:
инвокацию с потолком и status --json).
| Шаг | Наблюдение |
|---|---|
book add + GET /v0/books |
книга в библиотеке, status: not_started |
GET run-options |
min 1 / max 333 / default 333 — $10 / $0.03 = 333, подрезано 500 главами книги |
POST runs (100 глав) |
202, форма Run по спеке; юнит tm-run-<id>-1.service active running |
| потолок, дошедший до движка | --ceiling-usd 3.000000 = 100 × $0.03 |
второй POST на ту же книгу |
409 |
/v0/usage при открытом холде |
remaining_percent: 70 (холд — дебет) |
| прогресс из журнала | draft 4/10, edit 0/10 — пофазно, без затирания резюнком |
| выход юнита | книга и прогон → ready, finished_at проставлен |
| расчёт | грант $10 → движок отчитался $0.42 → баланс 9.580000 (не потолок $3) |
| гигиена логов | grep -c 'ceiling-usd|3.000000|bk_' daemon.log = 0 |
systemd после --collect |
юнитов 0 |
Ратифицированное свойство D39.106 — прогон переживает деплой — проверено убийством платформы:
pkill -9 по имени процесса → API отвечает 000, юнит active running, журнал за 6 с вырос
10 → 13 строк. Платформа поднята заново → карточка показала draft 21/300 (догнала всё, что было
записано, пока её не было), через 6 с — 24/300, юнит ОДИН, в логе новой платформы
grep -c 'run unit started' = 0 (второго движка не создано). Остановка через systemd → маркер
success/killed/TERM → статус stopped (не failed).
Батарея и самопроверка
make checkсTM_PLATFORM_TEST_DSN(живой PostgreSQL 18.4,-race): все 13 пакетов зелёные, линтер 0 issues. Прогнана дважды подряд.- Скипы:
go test ./... -count=1 -v | grep -c -- '--- SKIP'= 0. make vuln: No vulnerabilities found.- Дифф
^func TestИСПОЛНЕНИЕМ: 105 → 261, удалённых 0, добавленных 156 (число после дофикса 09.08). Команда, которой считано:H=$(git grep -h '^func Test' HEAD -- 'platform/**/*.go' | sed 's/(.*//' | sort -u)·T=$(grep -rh '^func Test' platform --include=*_test.go | sed 's/(.*//' | sort -u)·comm -23 <(echo "$H") <(echo "$T")— пусто, то есть удалённых ноль. ⚠ Исправлено приёмкой (PD-151): здесь стояло «105 → 203, +98», и это было неверно на момент написания — приёмка пересчитала 242/+137/−0, дофикс с ре-чеком довели до 261/+156/−0. Шапка журнала при этом была права. ⚠ Две мои промежуточные редакции этого счёта были неверны, и это записано, а не подчищено: сначала pathspec не тот, потом грep без*.go— второй ловил КОПИЮ теста, вставленную в этот же журнал приёмкой P1 (platform-PROGRESS.md:683), и рисовал ложное «удалёнTestHoldAndSettleOnTheSameAttemptDoNotDeadlock». Тест на месте:credits_test.go:308. - Посадки (мутации): 12 из 12 в самопроверке; 139 в аудите ревью — 107 поймано, 32 пережили
(8 не ослабления), по ним написано 22 новых пина. ⚠ Заявление «каждый проверен своей
посадкой» СНЯТО приёмкой (PD-142/PD-151): прогонов посадок было девять (9/10, затем 9/9), и
поимённого соответствия пин↔посадка сессия не вела — 22 ими не покрываются. Итого посадок,
проверенных поимённо: 33 моих в паке (ниже) и 24 в дофиксе (раздел «Дофикс P4»), все
пойманы. Каждая восстанавливалась после прогона:
холд вне транзакции допуска · мапинг
runs_one_live_per_book· high-water mark в синке ·greatestу spend · payload-конфликт · детект пропасти · частичная строка как событие · область чужого потока (обе стороны:mine := trueиmine := false) · срез прогона · половина шкалы (та самая ошибка «минус Reserved») · грация спавна · потолок какfailed· argv в INFO-логе ·MemorySwapMax· compare-and-set права на спавн. ⚠ Одна посадка ПЕРЕЖИЛА первую редакцию пина и это записано, а не подчищено: high-water mark проверялся событиемprogress, аprogress— присваивание, то есть идемпотентен сам по себе. Настоящий пин — считающий эффект (unit_done):TestARedeliveredCountingEventDoesNotCountTwice.
Девиации и то, чего не сделал
503на старте прогона не входит в перечисленные спекой статусы операции — PD-112, вопрос владельцу контракта. Каждый разрешённый код в этой ситуации соврал бы.- Стоп по потолку сегодня отдаётся как
failed— PD-113, контрактно видимо. Движок возвращает потолок ошибкой (errReserveCeiling, сверено в HEAD), события потолка нет (строка 103). Ветка под событие построена и запинена; выдумывать порог «сколько процентов потолка = стоп» я не стал. POST /booksне взят — П-9, с обоснованием. PD-72 закрывается вместе с ним.- Эскроу/
uncertain/closingне строились — строка 136, следующий денежный промт. - Стоп/резюм прогона как ручки контракта не в списке работ промта — не строились; банк-стоп заканчивает попытку и прогон, резюм = новый прогон.
Supervisor.Statusдев-пути починен (PD-108) — он в моей зоне и это мой канал ремонта.
Адверсариальное ревью (author≠reviewer), и почему это главный результат пака
Четыре независимых верификатора, ни один не читал отчёт (его на тот момент не существовало): (1) промт+дифф вслепую · (2) охота за дефектами ВНЕ карты · (3) сверка провода с openapi 0.2.0 · (4) сила пинов посадками. Каждому запрещено писать в репозиторий; эксперименты — в копиях.
Нашли три ДЕНЕЖНЫЕ ошибки и три запирающие прогон. Все закрыты в этом дереве, каждая с пином.
| Находка | Чем это было | Как закрыто |
|---|---|---|
| PD-124 (major) | Расчёт брал committed_usd — ПОЖИЗНЕННУЮ сумму КНИГИ (SUM(committed_usd) … WHERE book_id) — как трату прогона. Второй прогон книги оплачивал первый заново; после того как сумма книги перерастает потолок, каждый прогон стоит ровно свой потолок. Воспроизвели ДВА верификатора независимо |
Попытка пишет базовую линию книги ДО старта (миграция 00010) и платит разницу; линию не прочитать = не стартуем |
| PD-125 (major) | Перезапуск при отложенном расчёте брал ВТОРОЙ холд и терял первый навсегда: списки его не находили | Перезапуск спрашивает AttemptReservationOpen; список несведённых ключуется на завершённости ПОПЫТКИ |
| PD-126 (major) | Юнит, который не удалось создать, оставлял «право на спавн» — и каждый свип перезапускал прогон, съедая потолок по свипу за раз | Неудавшийся Start снимает право; повторяется та же попытка |
| PD-127 (major) | Одна нечитаемая строка журнала возвращала ошибку ДО чтения маркера: прогон навсегда translating с висящим холдом |
Любая ошибка журнала = карантин ПРОЕКЦИИ, жизненный цикл продолжается |
| PD-128 | Грация спавна мерилась от старта ПРОГОНА ⇒ у перезапущенной попытки её не было | Мерится от старта попытки |
| PD-129 | Инверсия порядка блокировок books↔runs между материализатором и финишером ⇒ взаимоблокировки |
Книга блокируется первой везде |
| PD-130/131 | Любая ошибка чтения журнала выглядела как «догнали»; хендшейк не обязан был нести seq 1 |
Только настоящий EOF; seq != 1 = плохой хендшейк |
| PD-132 | Холд прогона, который так и не стартовал, не возвращался | Нет линии и нет юнита ⇒ холд возвращается целиком |
| PD-133/134/135 | Инстанс без движка не отдавал даже библиотеку; пиннинг версии писался и не читался; прерванный прогон вставал paused без причины |
Чтения монтируются отдельно; EngineBinary читается и используется; PauseRun вместо FinishRun |
| PD-117…121 | Потолок ниже минимума схемы отвечал 409 вместо 400 · Usage.paused_reason недостижим через путь реконсилятора · карточка несла ревизию ПРОГОНА (отстающую) · чужой курсор пагинации принимался · «exhausted» при остатке, на который прогон стартует |
Все пять закрыты, каждая с пином |
| PD-136/138 | Деплой-юнит нёс обе диспозиции сразу; go.mod не тидинут |
Переписано; go mod tidy |
| PD-142 | Аудит силы пинов: 139 посадок, 107 поймано, 32 пережили (8 из них — не ослабления: эквивалентный код или страховка DDL). Пережившие — не дефекты кода, а отсутствующие пины, часть на свойствах, объявленных закрытыми | 22 новых пина написаны; ⚠ «каждый проверен своей посадкой» СНЯТО приёмкой (PD-142/PD-151) — прогонов посадок было девять. Самые весомые: блокировка строки попытки под КОНКУРЕНЦИЕЙ (восемь горутин на одно событие — последовательная доставка посадку не ловила) и CSRF на контрактной поверхности (снятие переживало всё, а это старт платного прогона с амбиентной кукой) |
Оставлено открытым и названо: PD-122 (Library.revision не монотонна при удалении книги — сегодня
недостижимо, ручки удаления нет; правильное решение — свой счётчик области, это своя миграция и
несколько путей записи, поэтому НЕ сделано, а объявлено гейтом) · PD-137 (упорядочение на
пользовательский менеджер — UID site-specific, инструкция в шапке юнита) · PD-139 (пути книг в
ERROR-логах — диспозицию надо принять осознанно) · PD-140 (Service.Stop не подключён: ручек
стопа/резюма в скоупе промта не было) · PD-141 · PD-112/113/123.
Что ревью подтвердило, а не опровергло: привилегия-модель (с одним уточнением, принятым:
«никакое правило не сузит» верно для ТРАНЗИЕНТНЫХ юнитов; шаблонный системный юнит с фиксированным
User=, запускаемый StartUnit, — вариант, которого мой разбор не касался) · арифметика
CeilingBounds (баланс КАК ЕСТЬ, без второго вычитания холдов — проверено на живом PG пятью
раскладами) · отсутствие кросс-аккаунтных чтений и стартов · инвариант balance == SUM(ledger)
(сломать не удалось) · правила тейлера по областям и курсору.
Возражение ревью, которое я принял только частично. Верификатор предложил различать стоп по
потолку через ceiling_pct из status --json. Не взял: ceiling_pct считается против
book_ceiling_usd, то есть против потолка ИЗ book.yaml, а наш потолок приходит аргументом прогона
(строка 145), которого в HEAD НА ТОТ МОМЕНТ не было — значит смысл поля зависел от ещё не залендённой
формы (флаг заленден 09.08, 0e69bc1; альтернатива от этого не становится верной — ceiling_pct
по-прежнему считается против потолка из book.yaml, а не против переданного аргумента). Взять его
сейчас значило бы построить дискриминатор на поведении, которого я не могу проверить. Записано в
PD-113 как названная и оценённая альтернатива.
Повторная сквозная проба на боевом бинаре после починок (чистая база, фейковый движок с НАКОПИТЕЛЬНЫМ счётчиком книги, как у настоящего): грант $10.00 → прогон 1 (движок насчитал $0.40) → баланс 9.600000 → прогон 2 на ТОЙ ЖЕ книге (счётчик книги $0.40 → $0.80) → баланс 9.200000, резервов ноль. До починки второй прогон списал бы $0.80.
Вопросы (канал вопросов промта; НЕ тихая интерпретация)
503вне перечня спеки (PD-112) — вносить в спеку или назвать другой код?default_chapters= верх шкалы. Когда книга дороже баланса, верх шкалы резервирует ВЕСЬ баланс под одну книгу, и вторую начать нельзя (D39.110 §2в). Альтернатива — доля от максимума; какая именно — вопрос продуктовый, не инженерный.- Конфигурация (вопрос ВЛАДЕЛЬЦА 08.08, PD-114). Ответ по существу: единого stdlib-пути в Go
нет; мейнстрим — либо 12-factor «только окружение» (наш выбор, записан в доккомменте
config.go, деплой уже использует systemd-нативныеEnvironmentFile=+LoadCredential=), либо слоёнаяфлаги > env > файл > дефолты(koanf/viper). Ниже нормы у нас другое: 27 переменных (грепнуто), ноль флагов у демона, нет печати эффективной конфигурации при старте. Вопрос владельца при этом нашёл реальный дефект в этом же паке: дефолт «$/глава» лежал в двух местах — исправлено, резолвится только вconfig. - Абстрактный вопрос владельца, и он подтверждается фактом (PD-115).
ENGINEERING_STANDARDS§2 называет внешнюю версионированную базовую линию ровно для ОДНОЙ оси — безопасности (ASVS 5.0 L2 + API Top-10 2023). Всё остальное — собственная проза зоны. Разница видна эмпирически: PD-57/PD-58 нашлись именно сверкой с RFC 9700/9207 и NIST SP 800-63B. Там, где эталона нет, сверять не с чем — грепнуто на 08.08: метрик и трейсинга ноль, процедуры бэкапа/восстановления вdeploy/нет, SLO не заданы. Предложение: §2 получает по эталону на ось; ратификация — оркестратора.