76 KiB
Журнал зоны «Платформа»
Весь прогресс платформы — ЗДЕСЬ (решение владельца 04.08): пинги, итоги сессий, открытые вопросы, предложения на ратификацию. В
docs/PROGRESS.mdплатформа не пишет; оркестратор читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4).
Текущее состояние
- P4 «раннер» + дофикс + V2 ПРИНЯТЫ и ЗАЛЕНДЕНЫ — D39.123, код
d29e30c(ревью-шапка ниже): транзиентный systemd-юнит на прогон · очередь River с холдом-до-спавна в одной транзакции · реконсилятор с базовой линией расчёта · тейлерevents.jsonlс карантином проекции · пять ручек/v0контракта 0.2.1 · дев-интейк. Батарея с живым PG 18.4 под-raceзелёная, скипов 0, тестов зоны 261. Формула аргумента потолка —committed + прирост(PD-158, ратифицирована ПОПРАВКОЙ к пингу оркестратора). - Регистр — 171 строка (
python3 docs/scripts/counts.py): 124 закрыто · 1 закрыт ратификацией (PD-59) · 3 риском · 43 открыто (1 major — PD-113: стоп по потолку отдаётсяfailedдо эмиттера; после PD-158 его цена выросла — потолок срабатывает ровно на исчерпании холда). - АКТИВНОГО промта зоны НЕТ. Следующий — по слову владельца; кандидаты: стоп/резюм-ручки (PD-140, закроют и половину «failed»-лжи вместе со строкой 165 движка) · эскроу/uncertain (строка 136 единого бэклога) · POST /books (П-9).
- Эры P0–P3 (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены
(D39.107/109/112/114); разделы — в archive/platform-PROGRESS-P0-P3.md.
Стек и рецепт стенда —
STACK_DECISIONS.md; критерии приёмки —ENGINEERING_STANDARDS.md.
Ратификация приёмкой 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 получает по эталону на ось; ратификация — оркестратора.
Закрытые эры P0–P3 — в архиве
Разделы сессий P0–P3 и их ратификаций (04–08.08) вынесены в
archive/platform-PROGRESS-P0-P3.md (D39.124).
Решения оттуда живут в D-логе и DEFECT_REGISTER.md.
Пинги оркестратора (живые ссылки для следующих сессий)
Пинг оркестратора №15 (09.08, D39.122): движковый пак «блокеры контракта» ПРИНЯТ и заленден 0e69bc1 — пять поверхностей для платформы существуют. ФИНАЛЬНЫЕ формы (менялись трижды за приёмку — старые в переписке игнорировать):
- Манифест глав:
<project_db>.manifest.json,manifest_version: "tm-manifest-v2"(v1 движок сам отклоняет); id главы = 16 hex (стабилен через пере-нарезку);unit.id = <chapterID>:<cutTag>:<firstChunkIdx>— тег разреза 8 hex, при любой смене нарезки/данных пары unit.id УМИРАЮТ намеренно (id жив ⇒ якорь цел); полеheading— ВРЕМЕННЫЙ рендер движка «Глава N», НЕ метка книги (решение владельца 09.08; настоящие заголовки — строка 160 бэклога движка). $0-командаtmctl manifestстроит дерево до первого прогона (первое касание создаёт БД проекта — то же поведение, что у status). - Прогресс:
status --json→progress: {draft:{done,total}, edit:{done,total}}на книге и в каждом элементеchapters;done= «разрешено волной» (ok/flagged/skipped); волна, которой нет, —total: 0; процент не отгружается — собирает клиент. - Банк:
<project_db>.bank.json(весь банк тремя статусами; id термов — длино-префиксированный хеш ключа уникальности, стабилен через пересборку) и<project_db>.bank-stop.json(полная таблица подписи;conf: null≠ 0). Все сайдкары пишутся атомарно (temp+rename) — читать можно во время прогона. - Потолок (строка 145):
tmctl translate|redrive --ceiling-usd <usd>— это КНИЖНЫЙ ПОТОЛОК В СИЛЕ, не бюджет прогона (ратифицировано D39.122): леджер сравнивает значение с накопленным committed+reserved книги. Пересчёт пользовательского «прирост в главах» (D39.110) в абсолют — ОБЯЗАННОСТЬ платформы. ⚠ АМЕНДИРОВАНО D39.123 (PD-158): формула =committed_usd + прирост×оценка, БЕЗ reserved — read-onlystatusпоказывает leftover-reserved, которыйstore.Openзануляет до первой судимой резервации; включение переплачивало бы запасом сверх холда (исполнено обеими формулами против гейта).reserved_usdчитается обязательным полем как гард присутствия и улика leftover. Значение ниже уже потраченного откажет первой же резервации. День-потолок не перекрывается. Опция по желанию зоны: движок готов провести--ceiling-usdи вstatus, чтобы мониторинг видел действующий потолок capped-прогона (сегодня status показывает книжный) — скажите, заведём строку.