1463 lines
190 KiB
Markdown
1463 lines
190 KiB
Markdown
# Журнал зоны «Платформа»
|
||
|
||
> Весь прогресс платформы — ЗДЕСЬ (решение владельца 04.08): пинги, итоги сессий, открытые
|
||
> вопросы, предложения на ратификацию. В `docs/PROGRESS.md` платформа не пишет; оркестратор
|
||
> читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4).
|
||
|
||
## Текущее состояние
|
||
|
||
- **P5 ПОСТРОЕН и ДОФИКШЕН по приёмке — дерево передаётся на лендинг (11.08).** `POST /v0/books`
|
||
потоково с пер-маршрутным потолком тела и своим дедлайном чтения · статусы
|
||
`uploading → parsing → not_started | rejected` получили писателей, разбор — $0-команда движка
|
||
`tmctl manifest` · `POST /v0/runs/{id}/stop|resume` с намерением стопа в Postgres ДО сигнала ·
|
||
метрики Prometheus на отдельном слушателе · печать эффективной конфигурации.
|
||
- **Приёмка 11.08 (8 находок) → дофикс → кросс-семейное ревью дофикса (5 находок) → ре-чек
|
||
оркестратора (3 находки, 2 high) → ТРЕТИЙ РАУНД ЗАКРЫТ (14.08)**: разделы «Третий раунд»,
|
||
«Дофикс-2» и «Дофикс P5» ниже, движение — записками-планами. Батарея на стенде `~/.local/pgsql`
|
||
(порт 55433) под `-race`: EXIT=0, 16 пакетов, линтер 0 issues, скипов 0, `make vuln` чист, тестов
|
||
**261 → 359**. Посадки: 10/10 · 6/6 · 6/6 (две пережившие за все раунды — оба раза виноват был
|
||
слепой пин, переписан пин, не мутация); списки — в журнале.
|
||
⚠ **Тулчейн 1.26.6 НА РАТИФИКАЦИЮ**, и вчерашний клейм про него был ЛОЖЕН: гейт принимал 1.26.5
|
||
(регекс вместо сравнения). Теперь floor держат два механизма — `make version-check` сравнивает
|
||
версии, `go.mod` несёт `toolchain go1.26.6`, который читает всякая сборка; при `GOTOOLCHAIN=auto`
|
||
хост скачает нужный тулчейн, при `=local` остановится. Само сравнение запинено (`internal/gates`).
|
||
**ВОПРОС на ратификацию — кто пишет `book.yaml` при интейке**: половина П-9 стоит на нём, раздел
|
||
«Развилка» ниже. Дерево не коммичено.
|
||
- **P4 «раннер» + дофикс + V2 ПРИНЯТЫ и ЗАЛЕНДЕНЫ — D39.123, код `d29e30c`** (ревью-шапка ниже):
|
||
транзиентный systemd-юнит на прогон · очередь River с холдом-до-спавна в одной транзакции ·
|
||
реконсилятор с базовой линией расчёта · тейлер `events.jsonl` с карантином проекции · пять
|
||
ручек `/v0` контракта **0.2.1** · дев-интейк. Батарея с живым PG 18.4 под `-race` зелёная,
|
||
скипов 0, тестов зоны **261**. Формула аргумента потолка — `committed + прирост` (PD-158,
|
||
ратифицирована ПОПРАВКОЙ к пингу оркестратора).
|
||
- **Регистр — 198 строк** (`python3 docs/scripts/counts.py`): 145 закрыто · 1 закрыт ратификацией
|
||
(PD-59) · 4 риском · 48 открыто (**1 major — PD-113**: стоп по потолку отдаётся `failed` до
|
||
эмиттера). Форма — 13 секций (пинг оркестратора №16).
|
||
- **Промт P5 ВЫДАН оркестратором 10.08** (`PLATFORM_P5_SESSION_PROMPT.md`, D39.128): П-9
|
||
`POST /books` (+PD-72 потолок тела; развилка «кто пишет book.yaml при интейке» — вопросом в этот
|
||
журнал ДО стройки той половины) · стоп/резюм-ручки PD-140 (различение «потолок vs авария» —
|
||
за эмиттером движка, строки 103/165, его промт выдан параллельно) · наблюдаемость П-11 +
|
||
печать конфигурации PD-114 · DEFECT_REGISTER секциями (пинг №16). Запуск — по слову владельца.
|
||
Эскроу/uncertain (строка 136) — следующим денежным промтом, НЕ в P5.
|
||
- Эры P0–P3 (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены
|
||
(D39.107/109/112/114); разделы — в [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md).
|
||
Стек и рецепт стенда — `STACK_DECISIONS.md`; критерии приёмки — `ENGINEERING_STANDARDS.md`.
|
||
|
||
## Третий раунд (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) — гейт «поднят» был только на словах.
|
||
|
||
Сделано, двумя независимыми механизмами:
|
||
|
||
1. `make version-check` СРАВНИВАЕТ версии (`sort -V`), а не матчит; префиксы `rc`/`devel` отвергаются
|
||
отдельной веткой — пререлиз не несёт фиксов, которые обещает его номер. Цель отделена от
|
||
`tools-check` и берёт версию из переменной `GO_VERSION`, чтобы её можно было судить версиями,
|
||
которых на хосте нет.
|
||
2. `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 (11.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). Нашла две вещи, и обе исправлены:
|
||
|
||
1. **Резюм спавнил движок ВНУТРИ запроса** — запрос висел, пока прогон не кончится. На фейке это
|
||
видно как таймаут 120 с; на настоящем systemd висело бы меньше, но по-прежнему секундами:
|
||
`spawnAttempt` читает отсчёт книги (`tmctl status` — секунды CPU движка) и ходит к systemd.
|
||
Исправлено: резюм ставит спавн в ОЧЕРЕДЬ (`EnqueueRunNow`), как это делает допуск прогона;
|
||
реконсилятор — бэкстоп, если запись потерялась. Пин переписан на новое свойство, посадка
|
||
«спавнить в запросе» падает.
|
||
2. **Идентификатор книги тёк в 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-174 `POST /books` отвечает 404 там, где спека кода не даёт (и резюм —
|
||
503) · PD-175 квоты интейка нет · PD-176 `requestTooLarge` не доходит через обёртки ·
|
||
PD-177 `character_count` для не-UTF-8 — оценка · PD-178 systemd-тесты падают, а не скипаются ·
|
||
PD-179 `/metrics` без аутентификации (принятый риск) · PD-180 201 несёт `parsing`, а не
|
||
`uploading`, плюс отказы интейка вне спеки · PD-181…PD-184 (кросс-семейное ревью, закрыты
|
||
здесь же) · PD-185 `finalizing` без писателя.
|
||
- Регистр: **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]* |' до) <(… после)` — пусто).
|
||
|
||
### Вопросы владельцу контракта (через оркестратора)
|
||
|
||
1. **Порядок частей формы.** `POST /books` требует, чтобы `file` шёл ПОСЛЕДНИМ (PD-172). Это
|
||
свойство потокового приёма, а не прихоть: строка книги пишется до тела, а языки в ней
|
||
обязательны. Тем же правилом специфицирована браузерная загрузка S3. Спека молчит — вносить?
|
||
2. **Причина отказа.** `rejected` без причины на проводе (PD-173) не даёт экрану различить «файл не
|
||
тот» и «наш движок был недоступен», а это разные советы пользователю. Поле заводить не право
|
||
зоны; в БД причина есть.
|
||
3. **Инстанс без интейка** отвечает на `POST /books` охраняемым 404 (PD-174) — по форме зоны для
|
||
несмонтированных маршрутов, но кода этого спека не перечисляет (класс PD-112). Там же: резюм на
|
||
деплое без маркер-команды отвечает 503, а 0.2.1 ратифицировала 503 только для СТАРТА прогона.
|
||
4. **«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` мог бы изменить.
|
||
|
||
### Что нашла собственная сверка диффа (после фиксов, до сдачи)
|
||
|
||
Две правки, обе внесены и обе — про то, что дофикс мог испортить рядом:
|
||
|
||
1. **`DeleteBook` брал резервации раньше книги** — единственный оставшийся нарушитель порядка,
|
||
который я записал как глобальный. Цикла для него сегодня нет (проверено перебором пар: удаление
|
||
трогает только ЗАКРЫТЫЕ резервации, а `StartRun` вставляет новую), но инвариант с необъявленным
|
||
исключением перестаёт быть инвариантом — книга взята первой и там, и это запинено
|
||
(`TestDeletingABookTakesItBeforeItsReservations`; вспомогательный `assertBookFirst` обобщён на
|
||
любую вторую строку — попытку или резервацию).
|
||
2. **Отказ ДЕПЛОЯ проверялся после вызова движка.** Перенеся чтение отсчёта в начало `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, мутации
|
||
гоняли в копиях дерева.
|
||
|
||
**Найдено и ПОЧИНЕНО в этом же дереве:**
|
||
1. **PD-159, деньги, major.** Отложенный расчёт прогона оплачивал работу СЛЕДУЮЩЕГО прогона той же
|
||
книги, а тот платил за неё ещё раз. Нашли ДВА верификатора независимо, каждый воспроизвёл
|
||
исполнением; я воспроизвёл третьим ($2.10 за прогон, стоивший $0.10) ПЕРЕД починкой. Причина —
|
||
расчёт читает пожизненный счётчик КНИГИ в момент повтора, а завершённый-но-нерассчитанный прогон
|
||
не мешает начать новый. Починено верхней границей `SpendBound` (наименьшая базовая линия среди
|
||
попыток книги, стартовавших позже — она снята после остановки этой попытки и до того, как та
|
||
что-либо добавила, то есть ТОЧНАЯ граница).
|
||
2. **PD-160, пин.** «Транзиентный сбой не карантинит» пинилось на чистой функции; удаление ветки,
|
||
которая её ВЫЗЫВАЕТ, переживало батарею. Написан пин, который гонит НАСТОЯЩИЙ дедлок через весь
|
||
путь.
|
||
3. **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`.
|
||
|
||
### Девиации и то, чего не сделал
|
||
|
||
1. **`503` на старте прогона не входит в перечисленные спекой статусы операции** — PD-112, вопрос
|
||
владельцу контракта. Каждый разрешённый код в этой ситуации соврал бы.
|
||
2. **Стоп по потолку сегодня отдаётся как `failed`** — PD-113, контрактно видимо. Движок возвращает
|
||
потолок ошибкой (`errReserveCeiling`, сверено в HEAD), события потолка нет (строка 103). Ветка под
|
||
событие построена и запинена; выдумывать порог «сколько процентов потолка = стоп» я не стал.
|
||
3. **`POST /books` не взят** — П-9, с обоснованием. PD-72 закрывается вместе с ним.
|
||
4. **Эскроу/`uncertain`/`closing` не строились** — строка 136, следующий денежный промт.
|
||
5. **Стоп/резюм прогона как ручки контракта** не в списке работ промта — не строились; банк-стоп
|
||
заканчивает попытку и прогон, резюм = новый прогон.
|
||
6. **`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.
|
||
|
||
### Вопросы (канал вопросов промта; НЕ тихая интерпретация)
|
||
|
||
1. **`503` вне перечня спеки** (PD-112) — вносить в спеку или назвать другой код?
|
||
2. **`default_chapters` = верх шкалы.** Когда книга дороже баланса, верх шкалы резервирует ВЕСЬ
|
||
баланс под одну книгу, и вторую начать нельзя (D39.110 §2в). Альтернатива — доля от максимума;
|
||
какая именно — вопрос продуктовый, не инженерный.
|
||
3. **Конфигурация (вопрос ВЛАДЕЛЬЦА 08.08, PD-114).** Ответ по существу: единого stdlib-пути в Go
|
||
нет; мейнстрим — либо 12-factor «только окружение» (наш выбор, записан в доккомменте `config.go`,
|
||
деплой уже использует systemd-нативные `EnvironmentFile=`+`LoadCredential=`), либо слоёная
|
||
`флаги > env > файл > дефолты` (koanf/viper). Ниже нормы у нас другое: **27 переменных**
|
||
(грепнуто), **ноль флагов** у демона, **нет печати эффективной конфигурации** при старте.
|
||
Вопрос владельца при этом нашёл реальный дефект в этом же паке: дефолт «$/глава» лежал в двух
|
||
местах — исправлено, резолвится только в `config`.
|
||
4. **Абстрактный вопрос владельца, и он подтверждается фактом (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](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-only `status` показывает leftover-reserved, который `store.Open` зануляет до первой судимой резервации; включение переплачивало бы запасом сверх холда (исполнено обеими формулами против гейта). `reserved_usd` читается обязательным полем как гард присутствия и улика leftover. Значение ниже уже потраченного откажет первой же резервации. День-потолок не перекрывается. Опция по желанию зоны: движок готов провести `--ceiling-usd` и в `status`, чтобы мониторинг видел действующий потолок capped-прогона (сегодня status показывает книжный) — скажите, заведём строку.
|
||
|
||
|
||
|
||
## Пинг оркестратора №16 — 09.08.2026 (форма DEFECT_REGISTER, строка 167)
|
||
|
||
Реестр дефектов дорос до 171 строки одной таблицей; докс-аудит D39.125 (worksheet `docs/archive/reports/DOC_AUDIT_INVENTORY_2026-08-09.md`, жалоба «зонная изоляция прячет половины тем») просит форму: разложить таблицу на СЕКЦИИ (open по весу · accepted-risk · fixed по эрам паков), сохранив построчную форму `| PD-N | … |` — `docs/scripts/counts.py` ключуется формой строки, не позицией, секции его не ломают. Заведите П-строкой в свой BACKLOG, исполнение — попутно следующим паком, НЕ срочно. Вопросы — через владельца.
|