2024 lines
272 KiB
Markdown
2024 lines
272 KiB
Markdown
# Журнал зоны «Платформа»
|
||
|
||
> **Что это.** Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше.
|
||
> Отработавшие эры вынесены срезами в [`archive/`](archive/) — читать только по конкретной ссылке.
|
||
|
||
## ⚠ ПИНГ ОРКЕСТРАТОРА №23 (10.09) — ЧЕТЫРЕ АДРЕСА В ВАШЕЙ ЗОНЕ ПОСЛЕ ДВИЖКОВОГО ПАКА И РАТИФИКАЦИИ ДВУХ ОСТАНОВОК
|
||
|
||
Зона ваша, рукой не трогаю. Всё ниже пере-снято моим прибором сегодня; каждый адрес — от корня репозитория.
|
||
|
||
**(1) Два комментария утверждают о движке неверное — ПРОЗА протухла, КОД у вас верен.**
|
||
`platform/internal/runs/reconcile.go:1534` и `platform/internal/pgstore/runs.go:320` пишут «the engine
|
||
catches SIGTERM and exits 1». Движок на пойманном сигнале отдаёт **5** (`backend/cmd/tmctl/main.go:100`
|
||
= «5 graceful stop»), и ваш собственный код это знает: `platform/internal/ingest/exit.go:34` = `ExitStopped = 5`,
|
||
`reconcile.go:1134` сверяется именно с ним. То есть врут только комментарии — но их цитируют доки, и ложь
|
||
уезжает дальше. Тот же текст, вероятно, в `platform/internal/runner/control_test.go` (у меня адрес из
|
||
чужого замера, сам не открывал).
|
||
|
||
**(2) `platform/internal/runner/runner.go:56-58` и `:163-164` — НЕ ошибка, а ЦЕЛЬ; удалять НЕЛЬЗЯ.**
|
||
Там сказано, что движок «finishes the in-flight chunk before exiting» и что SIGTERM даётся ему, «so it can
|
||
finish the chunk it is paying for». Сегодня это ЛОЖНО: у движка одна кнопка и она жёсткая
|
||
(`backend/cmd/tmctl/main.go:207` отменяет контекст прогона, летящие вызовы рвутся). Но **владелец 10.09
|
||
ратифицировал ровно то, что ваш комментарий описывает** — акт `D39.234` п.1: первое нажатие мягкая
|
||
остановка (новых единиц не раздаём, начатое доигрываем), второе — сегодняшняя жёсткая. ⇒ это случай «док
|
||
впереди кода»: комментарий станет истинным, когда сядет пак. Пометьте его как цель, а не правьте под
|
||
сегодняшнее дерево.
|
||
|
||
**(3) ⛔ ЧИСЛОВОЙ КОНФЛИКТ, который включится в ту же секунду, как SIGTERM станет мягким.**
|
||
`platform/internal/runner/runner.go:59` = `stopGrace = 10 * time.Minute` (600 с), и он же уезжает в
|
||
`TimeoutStopSec`. Законное ожидание ОДНОГО вызова у движка — до `attempt_max_s` **1240 с**
|
||
(`backend/configs/models.yaml`, deepseek), и владелец это ожидание ратифицировал (`D39.230` п.3, потолок
|
||
подтверждён `D39.234` п.1б). ⇒ мягкий SIGTERM при нынешнем грейсе означает SIGKILL по ЗАКОННОМУ ожиданию,
|
||
а SIGKILL не оставляет ни пометки `cancelled`, ни сеттла оценки — деньги списаны, следа нет. Грейс обязан
|
||
вырасти выше потолка ожидания ПЛЮС время жёсткой фазы, либо второй сигнал шлёте вы сами по своему
|
||
продуктовому дедлайну.
|
||
|
||
**(4) То, что вы просили, движок публикует — но не туда, где вы это возьмёте.**
|
||
`platform/internal/pgstore/credits.go:235` просит «publish the count and sum of estimated-price rows beside
|
||
committed_usd, which is what would let this side say "at most Y"». Движок их считает и печатает —
|
||
`backend/internal/pipeline/status.go:321` (`estimated_rows` / `estimated_usd` в `status --json`) — но по ШВУ
|
||
они не едут: в кадрах `backend/internal/runevents/runevents.go` слова `estimated` **0 хитов** (контроль:
|
||
`committed` в том же файле — **6**; денежный кадр несёт один `committed_micro_usd`, `runevents.go:244`).
|
||
С вашей стороны читателя тоже нет: в `platform/internal` вне тестов `estimated` — **2 хита, оба
|
||
комментарии** (контроль: `committed_micro` — 1 хит; go-файлов вне тестов прибор прочёл **81**). Носитель —
|
||
строка бэклога **382**; чинится ПАРНО с движком.
|
||
|
||
**Что из этого следует для очереди.** Пак движка «мягкая остановка» и платформенная половина лендятся
|
||
ПАРОЙ и порознь не лендятся (`D39.234` п.2). Платформенная половина, как я её вижу: `mode` у `/stop`,
|
||
грейс выше потолка ожидания, путь второго сигнала (`systemctl kill --signal=SIGTERM`, юнит не трогая),
|
||
чтение оценочных строк из потока. Промт ещё не написан — если у вас есть довод против любой из четырёх
|
||
позиций, скажите ДО того, как я его напишу.
|
||
|
||
## ПРАВДА У ДВЕРИ — ОТЧЁТ (11.09, `textmachine-8e`)
|
||
|
||
> Предмет: платформа говорила человеку не то, что произойдёт. Три ряда — **`PD-455`** (форма обещает
|
||
> старт, в котором дверь откажет) · **`PD-162`** (прогон над пропавшим каталогом берёт холд и висит
|
||
> вечно) · **`PD-448`** (двойной клик по «продолжить»; строить было ЗАПРЕЩЕНО, заказан разбор).
|
||
> Вход: HEAD `05f690e`, дерево зоны чистое, батарея на входе зелёная и снятая ДО первой правки —
|
||
> `MAKE-EXIT=0 · 20 пакетов ok · 0 FAIL · линтер 0 issues · 5 скипов`. Сессия не коммитит: дерево
|
||
> передано оркестратору №23.
|
||
|
||
### Комплектность против заказа — по пунктам §4 промта
|
||
|
||
| Пункт | Исход | Чем предъявлено |
|
||
|---|---|---|
|
||
| §4.1 `PD-455` — форма обязана знать про живой прогон | **сделано** | предикат с двумя привязками + `blocked.code: run_in_flight`; пины; живая проба на стенде |
|
||
| §4.2 `PD-448` — исследуй и предложи, НЕ строй | **разбор, не строил** | замер воспроизводимости, три пути с ценой, рекомендация, расхождение канона отдельным пунктом |
|
||
| §4.3 `PD-162` — отказ ДО денег | **сделано у ОБЕИХ денежных дверей**, ряд остаётся открытым остатком | пины с контрольным прогоном; живая проба; ряд пере-написан |
|
||
| §4.4 интейк-гигиена | **не делал** (подписанный пропуск) | одна строка про следующий предмет `PD-175` — ниже |
|
||
| §4.5 якоря регистра | **сделано**, и их оказалось не четыре, а пятнадцать | `counts.py --lint` до и после |
|
||
| §5 самопроверка исполнением | **сделано** | батарея · `make vuln` · мутации на изолированной копии · живая проба · адверсариальный проход |
|
||
|
||
### `PD-455` — одно определение, две привязки
|
||
|
||
Правило допуска по фактам КНИГИ стояло инлайном в `Start` тремя проверками (статус · материализованное
|
||
дерево глав · живой прогон), и форма заказа о них не знала: вердикт считался из остатка книги и баланса,
|
||
а `ErrRunInFlight` жил только у двери. Человек видел `covers_all`, жал и получал 409.
|
||
|
||
Сделано: три проверки вынесены в **один предикат** `platform/internal/runs/runs.go:655`=`func startable`,
|
||
у него две привязки РАЗНОЙ авторитетности — `Start` зовёт его под замком книги (решает), `Order` зовёт
|
||
на опрашиваемом пути (`runs.go:304`, совещательно и заведомо устаревает). Мерило пака — «сколько
|
||
ОПРЕДЕЛЕНИЙ придётся тронуть, если правило изменится» — выполнено: одно.
|
||
|
||
Три решения, которые я принял внутри этой свободы, и довод каждого:
|
||
|
||
1. **`PricedBook` НЕ расширен.** Рядом с ним стоит письменное возражение (`platform/internal/pgstore/books.go:1476`:
|
||
«It is a second read beside ReadBookForRun rather than more columns on it, because the two answer
|
||
different questions of different shapes»). Форма делает ВТОРОЕ чтение — `ReadBookForRun`, ту самую
|
||
запись, которой судит дверь. Возражение этим исполнено, а не переступлено: две разной формы записи
|
||
остаются двумя, и определение по-прежнему одно. Цена, которую я принял: +1 запрос на КАЖДЫЙ опрос
|
||
формы (рядом уже три чтения, одно из них с пер-главным агрегатом по `units`). Это моё суждение, а не
|
||
замер: бенчмарка я не снимал.
|
||
2. **`verdict` не тронут.** Он отвечает про ДЕНЬГИ, и баланс книгу действительно покрывает; четвёртое
|
||
значение закрытого enum сломало бы генерённых клиентов. Рост пошёл через `Blocked.code`, который канон
|
||
предавторизует.
|
||
3. **Порядок двух причин в `blocked`.** Член один, кандидатов два, и они не равны: свой живой прогон —
|
||
это «старта нет вовсе», чужой холд — «шкала короче, чем мог бы позволить баланс». Свой прогон
|
||
выигрывает: сказать второе, когда верно первое, значит отправить человека останавливать ЧУЖОЙ прогон,
|
||
после чего клик всё равно откажет. Пин: `httpapi.TestARunOfThisBooksOwnOutranksAnotherBooksHold`.
|
||
|
||
Контракт: минор **0.14.0** ратифицирован оркестратором (`D39.244`) по моему пингу — `Blocked.code`
|
||
получил значение `run_in_flight`, смысл схемы расширен с «что держит шкалу короче» на «почему старт не
|
||
предлагается так, как обещал бы один баланс», `book_id` при этой причине — ЭТА ЖЕ книга. Константа
|
||
`httpapi.ContractVersion` переведена на `0.14.0` тем же деревом; гейт `gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||
зелёный.
|
||
|
||
### `PD-162` — отказ до денег, и вторая дверь, которой ряд не называл
|
||
|
||
Проверка каталога книги стоит на допуске ДО холда (`runs.go:707`=`func (s *Service) sourceThere`, зовётся
|
||
из `Start` после предиката книги и перед чтением счёта). ⚠ **Трудности, которую промт объявлял главной,
|
||
действительно нет** (§4.3 промта): каталог создаётся на интейке ДО строки в БД, а исключение первого
|
||
прогона принадлежит ЖУРНАЛУ внутри каталога (`journalSize` мапит ENOENT в нулевой офсет) — так что
|
||
`os.Stat(workdir)` на допуске не отвергает ничего законного, и никакой ветви-исключения я не строил.
|
||
|
||
**Что я нашёл сверх заказа: резюм — вторая денежная дверь, и клин там тот же.** Измерено исполнением ДО
|
||
правки: тест `runs.TestAResumeOverAMissingDirectoryIsRefusedBeforeTheHold` на дереве без гарда дал
|
||
`a resume over a missing directory answered <nil>, want ErrSourceGone` — то есть резюм над пропавшим
|
||
каталогом ВОЗВРАЩАЛ прогон и брал холд остатка бюджета. Оркестратор подтвердил, что это в границах пака
|
||
(«предмет `PD-162` — прогон, который никогда не поедет, а не конкретная ручка»), и гард стоит теперь и там
|
||
(`platform/internal/runs/reconcile.go:1721`, перед `reopen`).
|
||
|
||
**Вина книги и вина хоста разведены и отвечают РАЗНЫМИ словами** — это урок `PD-192`, который зона
|
||
уже оплатила один раз. Итоговое правило (третья и четвёртая строки приехали дофиксом по ревью):
|
||
|
||
| что с каталогом | ответ | почему так |
|
||
|---|---|---|
|
||
| его нет, книга ПОД корнем интейка, сентинел на месте | `ErrSourceGone` → 409 `book_not_ready` + `cause: source_gone` (на резюме — 409 `run_not_resumable` + та же причина) | это и вправду конец этой книги, и ожидание не поможет — так причина и говорит |
|
||
| его нет вместе с СЕНТИНЕЛОМ корня (форма размонтирования) | `ErrStorageUnavailable` → 503 | вина ХОСТА; сказать здесь «ваша книга непригодна» — ровно тот дефект, который в интейке отклонил все книги тома |
|
||
| его нет, а книга лежит ВНЕ корня интейка (`tmplatformctl book add --workdir`, инстанс без `BooksDir`) | `ErrStorageUnavailable` → 503 | сентинела на том томе нет вовсе: чужой здоровый маркер уликой про этот том не является, и платформа не объявляет конец книги на основании каталога, которого не создавала |
|
||
| путь ЕСТЬ, но это не каталог (файл, симлинк на файл) | `ErrStorageUnavailable` → 503 | `os.Stat` на файле успешен, движок открывает каталог; писала этот файл не платформа |
|
||
| путь есть и НЕ ЧИТАЕТСЯ (права, I/O) | `ErrStorageUnavailable` → 503, в сообщении операция и errno БЕЗ пути | ошибка уходит в ERROR-строку, а норма зоны §2 запрещает id книги в логах |
|
||
|
||
Предикат корня не скопирован, а **вызван**: `books.storageIsThere` стал экспортируемым
|
||
`platform/internal/books/books.go:574`=`func StorageIsThere(booksDir string) bool`, а «книга под нашим
|
||
корнем?» — `books.go:594`=`func Owns(booksDir, dir string) bool`; методы остались обёртками в одну
|
||
строку. Копия была бы вторым экземпляром правила, у которого первая редакция стоила зоны всех книг
|
||
хоста — и мутация, ломающая `StorageIsThere`, красит не только мой тест, но и ДВА чужих пина `PD-192`
|
||
в `internal/books`, что и есть доказательство, что предикат один.
|
||
|
||
**Ряд остаётся ОТКРЫТЫМ, и остаток пере-написан честно:** класс шире удаления каталога — прогон, УЖЕ
|
||
живущий, который не поедет никогда (запинен к старой сборке движка, без `reserved_usd`, беспризорный
|
||
деплой), по-прежнему висит с холдом до прихода человека. Это половина эскроу (`П-18`), автоматического
|
||
терминального вердикта у реконсилятора нет и он не строился.
|
||
|
||
### `PD-448` — разбор, а не стройка
|
||
|
||
⚠ Ниже — ТОЛЬКО разбор; кода по этому ряду я не написал ни строки.
|
||
|
||
**Механика, снятая чтением (адреса пере-сняты сегодня).** `Resume` читает состояние прогона ДО замка
|
||
книги: `platform/internal/runs/reconcile.go:1652`=`s.Store.ReadRunForResume`, затем
|
||
`reconcile.go:1663`=`unlock, err := s.lockBook(ctx, l.BookID)`, и только потом судит по `l.Status`
|
||
(`reconcile.go:1696`) — по значению, прочитанному ДО замка. Из этого следуют два разных проигравших:
|
||
|
||
* **проигравший ВНУТРИ окна** (его чтение случилось до коммита победителя) держит устаревший
|
||
`stopped`, доходит до `reopen`, его вставка `run_attempts` падает на уникальном индексе
|
||
`run_attempts_run_id_attempt_no_key` → `ErrNoRun` → `Resume` отвечает **прогоном**
|
||
(`reconcile.go:1736`=`return s.Store.ReadRun(ctx, userID, runID)`), и холд не берётся: транзакция
|
||
откатывается целиком;
|
||
* **проигравший СНАРУЖИ окна** (его чтение случилось уже после коммита победителя) видит
|
||
`translating`, падает в `default:` и получает `ErrNotResumable` — «the run cannot be continued: it
|
||
is translating». **Вот это и есть дефект ряда.**
|
||
|
||
⇒ **Окно дефекта — НЕ гонка за индекс, а интервал между чтением состояния и взятием замка.** И отсюда
|
||
же объяснение, почему изолированно тест зелёный, а в полном пакете под нагрузкой красный: замок книги
|
||
(`platform/internal/runs/bank.go:400`=`func (s *Service) lockBook`) — ВНУТРИПРОЦЕССНЫЙ мьютекс, так что
|
||
два резюма одного демона идут друг за другом, и под голоданием по процессору горутины стартуют дальше
|
||
друг от друга — проигравший чаще успевает прочитать уже переведённое состояние. Формулировка в шапке
|
||
теста («both calls pass the state check together and race for attempt N+1») описывает состояние ДВУХ
|
||
экземпляров демона; в одном процессе это последовательность, а не гонка.
|
||
|
||
**Воспроизводимость — замер сегодня, субагентом, инструментом и с числом прогонов** (вывод в
|
||
`scratchpad/pd448/`, 67 файлов):
|
||
|
||
| что запускал (всё — `go test ./internal/runs/ -race`) | раз | красных | контроль |
|
||
|---|---|---|---|
|
||
| изолированно `-count=10 -run '^TestTwoResumes…$'`, load 0.17 | 10 | **0** | в выводе 10 строк `=== RUN` и 10 `--- PASS` с этим именем, других `---` нет |
|
||
| полный пакет на простаивающей машине, `-count=1` | 3 | **0** | в каждом прогоне 183 верхнеуровневых теста, целевой `--- PASS` = 1, `--- FAIL` = 0 |
|
||
| `GOMAXPROCS=1` (`-cpu 1`) | 5 | **0** | в каждом `=== RUN` = 1, `--- PASS` = 1 |
|
||
| **под нагрузкой** (32 CPU-жруна на 8 ядрах, load 29–50), подмножество `TestTwoResumes\|TestResumeIsRefused` | 20 | **4** | `--- PASS` 16, `--- FAIL` 4 |
|
||
| полный пакет под нагрузкой | 4 | **не измерено** | все четыре уперлись в `-timeout 9m` на ДРУГИХ тестах; целевой в каждом успел пройти зелёным до обрыва |
|
||
|
||
Текст всех четырёх падений ОДИН и тот же, и он читается, а не считается цветом:
|
||
`control_test.go:898: resume 1: runs: the run cannot be continued: it is translating, want the run back`
|
||
— то есть падает ровно утверждение «проигравший обязан ответить прогоном», и источник ошибки — ветка
|
||
`default` из разбора выше. ⚠ **Запись ряда «полный пакет упал 2 из 2» сегодня НЕ пере-проверена:**
|
||
полный пакет под нагрузкой ни разу не дошёл до конца, и это названо «не измерено», а не «не
|
||
воспроизвелось».
|
||
⚠ Замер шёл на МОЁМ дереве (гард каталога в `Resume` уже стоял). Путь гонки он не трогает, а если и
|
||
влияет, то в сторону РЕЖЕ: лишний `os.Stat` внутри замка отодвигает коммит победителя, то есть чаще
|
||
оставляет проигравшего в «хорошей» ветке. Числа выше поэтому — нижняя граница, а не верхняя.
|
||
|
||
**Чего сегодня нет, чтобы различить два случая.** Ни `LiveRun`, ни `run_attempts` не несут ни
|
||
заявителя, ни ключа идемпотентности: колонки таблицы — `id · run_id · attempt_no · engine_run_id ·
|
||
started_at · ended_at · exit_code · last_seq` (`platform/internal/pgstore/migrations/00002_readmodel.sql:72-87`,
|
||
и ни один из четырёх поздних альтеров этого не добавил). То есть у проигравшего состояние побайтово то
|
||
же, что у законного резюма часового прогона.
|
||
|
||
**Три пути промта, с ценой каждого:**
|
||
|
||
| путь | что делает | цена | закрывает ли предмет |
|
||
|---|---|---|---|
|
||
| **(а)** `Idempotency-Key` на `resumeRun` | второй клик под ТЕМ ЖЕ ключом реплеит ответ первого | контрактный минор (параметр операции) + клиент обязан прислать один ключ на оба клика | **не полностью:** при ОДНОВРЕМЕННЫХ кликах второй получает `409 key_in_flight` + `Retry-After` (`platform/internal/httpapi/idempotency.go:110`), то есть снова ошибку; прогоном он ответит лишь на ПОВТОРЕ |
|
||
| **(б)** claim-токен или колонка в транзакции рестарта | записать, КТО и чем открыл попытку, и дать проигравшему это увидеть | **миграция** + суждение по временному окну («открыто секунду назад резюмом») | да, но окно — это лотерея под другим именем |
|
||
| **(в)** перенести чтение состояния ПОД замок | убрать интервал | одна строка | ⛔ **делает хуже ДЕТЕРМИНИРОВАННО:** проигравший тогда видит `translating` ВСЕГДА |
|
||
|
||
**Рекомендация — четвёртый путь, которого в промте нет: сделать резюм идемпотентным ПО СУЩЕСТВУ.**
|
||
Ветка `case "translating"` в том же switch (`reconcile.go:1696`) отвечает **прогоном**, а не отказом —
|
||
с единственным исключением «стоп уже запрошен» (`l.StopRequestedAt != nil`; поле есть,
|
||
`platform/internal/pgstore/runs.go:322`), потому что такой прогон сворачивается, а не продолжается, и
|
||
202 там был бы ложью. Тогда оба проигравших — и внутри окна, и снаружи — отвечают прогоном, и мерить
|
||
больше нечего: гонки не остаётся, потому что исходы совпали.
|
||
Цена: **одна строка кода и одна строка канонной таблицы.** Ни миграции, ни ключа, ни нового состояния.
|
||
Семантика честная: `POST /resume` означает «пусть этот прогон идёт»; если он уже идёт — требование
|
||
выполнено, это ординарная идемпотентность записи.
|
||
**Довод ПРОТИВ, который я вижу сам:** клиент теряет различие «я продолжил» и «оно и так шло», а канон
|
||
этим свойством дорожит строкой выше — `paused` отвечает 409 именно потому, что 202 был бы «тихим
|
||
no-op». Разница, на которой я стою: у `paused` НИЧЕГО не переоткрывается и лекарство другое (новый
|
||
прогон), а у `translating` состояние ровно то, которого просил пользователь.
|
||
**Чего я НЕ знаю:** как это увидит фронт — у него сегодняшний `409 run_not_resumable` описан таблицей
|
||
канона, а его кода я не читал (чужая зона). И не знаю, есть ли деплой с ДВУМЯ экземплярами демона: при
|
||
одном экземпляре внутрипроцессный замок делает «оба прошли проверку вместе» невозможным, при двух —
|
||
возможным, и тогда путь (г) закрывает и эту форму тоже, а (а) и (б) — нет.
|
||
|
||
**ДИСПОЗИЦИЯ ОРКЕСТРАТОРА №23, пришла в тот же день.** Четвёртый путь ПРИНЯТ как направление, строит
|
||
его СЛЕДУЮЩИЙ платформенный пак — не этот. Довод принятия оказался не тем, с которого я начинал:
|
||
сомнение было в том, что идемпотентный `202` ломает канонную гарантию «202 = работа реально
|
||
пере-открыта», а проверка показала, что зона её уже НЕ держит — дерево отдаёт `202` проигравшему, у
|
||
которого пере-открыл не он, и это закреплено шапкой самого теста. Значит выбор был не между починкой
|
||
и обходом, а между двумя смыслами глагола. Критерий, по которому это лечение: обход оставляет ответ
|
||
зависимым от интерливинга и молчит об этом, лечение делает его ИНВАРИАНТНЫМ К ПОРЯДКУ.
|
||
**Три вещи поехали в ряд `PD-448`, чтобы следующий пак их не потерял** — и каждую я пере-проверил
|
||
своим прибором, а не принял на слово: вырез «стоп уже запрошен» обязателен (`l.StopRequestedAt`,
|
||
`pgstore/runs.go:322`; слово `stop_requested` уже на проводе, `httpapi/v0.go:242`); проверка re-pass
|
||
стоит ДО свитча (`reconcile.go:1687` против `:1696`) и после новой ветки должна пропускаться, иначе
|
||
двойной клик по re-pass снова получит отказ; и остаток окна, который правка НЕ закрывает —
|
||
`stop_requested` читается в том же незамкнутом снимке, а `Stop` замка книги не берёт вовсе (первый же
|
||
его оператор — `RequestStop`), так что стоп между снимком и ответом даст один `202` на сворачивающийся
|
||
прогон: один кадр, денег не трогает. Канон при этом НЕ двинут осознанно: строка таблицы — минор
|
||
`0.15.0`, и бамп сегодня покрасил бы гейт версии на сданном дереве, где константа несёт `0.14.0`.
|
||
|
||
### ⚠ Расхождение канона и дерева — отдельным пунктом (правит оркестратор)
|
||
|
||
**1. `translating` у `resumeRun`.** Канон обещает `409 run_not_resumable` («nothing; it is already
|
||
running» — таблица §`resumeRun` в `docs/architecture/14-api-contract/openapi.yaml`), а отгруженный код
|
||
проигравшему гонки за уникальный индекс возвращает ПРОГОН, 202 (`reconcile.go:1736`). То есть одна
|
||
ручка отвечает на одно состояние двумя разными способами в зависимости от того, где её застали. Это
|
||
расхождение СТАРШЕ моего пака и им не создано; рекомендация (г) как раз и сводит обе стороны к одному
|
||
ответу.
|
||
|
||
**2. Отказ резюма над пропавшим каталогом — ввёл Я, называю прямо.** `Resume` теперь может ответить
|
||
`409 book_not_ready` + `cause: source_gone` (`reconcile.go:1721`), а таблица §`resumeRun` перечисляет
|
||
ответы по СТАТУСУ прогона и такого отказа не знает. Генерённого клиента это не ломает: операция
|
||
объявляет общий `409 Conflict`, а `book_not_ready` — законное значение `ErrorCode`, которое эта же
|
||
поверхность уже отдаёт на соседней ручке. Но текст канона неполон, и строка таблицы нужна.
|
||
|
||
### Одна строка про `PD-175` (§4.4)
|
||
|
||
Следующим предметом ряда я считаю **ВЫХОД ИЗ `rejected`**, а не свип сирот и не потолок аплоадов.
|
||
Довод: у отклонённой книги сегодня выхода нет ВООБЩЕ — единственный переход в `not_started` стоит в
|
||
`platform/internal/pgstore/books.go:360` и требует `status = 'parsing'`, то есть перепарса не существует,
|
||
а ручки удаления нет в контракте вовсе (`PD-122`). Такая строка живёт в библиотеке человека навсегда, и
|
||
цена этого — доверие на КАЖДОЙ неудачной загрузке, тогда как сироты и потолок диска стоят операторского
|
||
диска, который в закрытой бете ограничен её же размером. Свип и ретеншен при этом остаются за гейтом
|
||
открытой регистрации (`D39.176` п.1), а счётный потолок аплоадов — это квота под другим именем, и он
|
||
запрещён.
|
||
|
||
### Якоря регистра (§4.5): их было не четыре, а пятнадцать
|
||
|
||
Промт называл три уехавших якоря в зоне плюс четвёртый, линтером не ловимый (без токена, внутри
|
||
`PD-162`). Все четыре починены. **Остальные одиннадцать убили МОИ СОБСТВЕННЫЕ переезды строк** —
|
||
восемь на первом круге правок и ещё три после дофикса по ревью, — и это ровно то правило зоны, по
|
||
которому «якорь, убитый твоим переездом, чинит тот, чей переезд его убил». Прибор до и после:
|
||
`python3 docs/scripts/counts.py --lint` из корня репозитория — **23 проблемных якоря → 12**, и группа
|
||
«по КОРНЮ ЦЕЛИ … internal 8» из выдачи исчезла целиком.
|
||
⚠ Двенадцатый живой ✗ — **не мой к починке**: он лежит в `docs/architecture/05-decisions-log.md:2661`
|
||
(зона оркестратора) и указывает В мою: `platform/internal/runs/runs.go:451` → `:479`. Убил его мой
|
||
переезд, поэтому адрес назван ему пингом, а файл я не трогаю.
|
||
⚠ И один урок про сам прибор: якорь БЕЗ токена (`path:line` без `=`) линтер не проверяет вовсе, так
|
||
что мои собственные такие ссылки (`v0.go:610` в ряду `PD-455`) уехали МОЛЧА и были пере-сняты рукой.
|
||
Ставя новый якорь, ставь токен.
|
||
|
||
### Находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО
|
||
|
||
| находка | что сделано | чем предъявлено |
|
||
|---|---|---|
|
||
| форма заказа не спрашивала правило допуска (`PD-455`) | один предикат `startable`, две привязки; на проводе `blocked.code: run_in_flight` | пины (ниже) + живая проба на стенде: `blocked":{"code":"run_in_flight","book_id":"bk_7JDPZD3S7J22XS3I"}` и 409 на втором клике |
|
||
| **резюм — ВТОРАЯ денежная дверь с тем же клином** (ряд её не называл) | гард `sourceThere` и там, перед `reopen` | до правки тест дал `a resume over a missing directory answered <nil>, want ErrSourceGone`; после — зелёный 8/8 |
|
||
| **мой собственный `fmt.Errorf` унёс бы путь книги в ERROR-лог** (класс `PD-139`) | путь выброшен, оставлены операция и errno | пин `runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath`; в логе стенда `tmstand-truth` встречается 2 раза, обе — эхо конфига на буте |
|
||
| мои переезды строк убили 8 чужих якорей (3 были сломаны до меня) | пере-снял все 11 в своей зоне + 12-й без токена внутри `PD-162` | `counts.py --lint`: 23 → 12 проблемных, группа «internal 8» исчезла |
|
||
| 12-й якорь моего переезда живёт в ЧУЖОЙ зоне | НЕ правлю, сообщаю адрес оркестратору | `docs/architecture/05-decisions-log.md:2661`: `platform/internal/runs/runs.go:451` → `:479` |
|
||
| `PD-455` покинул класс тревог гейта регистра | id убран из `alarmBaseline` с причиной, ТЕМ ЖЕ деревом (инструкция самого гейта) | `ALARM PD-count: 14 … (baseline 14)`, строка `ALARM PD-455 LEFT` исчезла, тест зелёный |
|
||
| форма молчит про ПРОПАВШИЙ каталог | НЕ чинил, завёл ряд `PD-466` с доводом, почему опрос — не место для `os.Stat` | ряд в регистре |
|
||
| носитель числа скипов батареи (`STACK_DECISIONS` §«Гейты батареи») называет не все условия | дописал два измеренных условия | два прогона одной смены назвали РАЗНЫЕ условия для одного и того же теста — оба вывода в отчёте ниже |
|
||
| **рецепт стенда называл сторону НАОБОРОТ**: «относительные `pipeline:`/`models:` — отличие ШАБЛОНА», хотя относительные они в ОРИГИНАЛЕ, а в шаблоне обязаны быть абсолютными | сторона названа явно, добавлена проверка на месте и цена ошибки | замерено: рабочий шаблон стенда несёт АБСОЛЮТНЫЕ пути (строки 20–21), и с ним `books.TestTheRenderedConfigurationIsOneTheEngineActuallyLoads` зелёный против свежесобранного `tmctl`; по этой строке приёмка получила восемь красных живых проб, которые были дефектом шаблона, а не дерева |
|
||
|
||
### Какой тест что пинит (норма зоны §3 п.4)
|
||
|
||
| свойство несущего пути | пин |
|
||
|---|---|
|
||
| форма несёт отказ двери про живой прогон, и дверь отвечает то же | `runs.TestTheFormSaysWhatTheDoorWillRefuseOverABookThatIsAlreadyRunning` |
|
||
| на проводе это `blocked.code: run_in_flight` с `book_id` ЭТОЙ книги | `httpapi.TestTheFormCarriesTheRunInFlightThatWillRefuseTheClick` |
|
||
| свой живой прогон важнее чужого холда в одном члене `blocked` | `httpapi.TestARunOfThisBooksOwnOutranksAnotherBooksHold` |
|
||
| старт над пропавшим каталогом отказывает ДО холда | `runs.TestABookWhoseDirectoryIsGoneIsRefusedBeforeTheHold` |
|
||
| резюм над пропавшим каталогом отказывает ДО холда | `runs.TestAResumeOverAMissingDirectoryIsRefusedBeforeTheHold` |
|
||
| размонтированный том — вина деплоя, и книгу она не винит | `runs.TestAVanishedBooksVolumeIsTheDeploymentsFaultAndNotTheBooks` |
|
||
| нечитаемый каталог — тоже деплой, и путь книги в ошибку не попадает | `runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath` |
|
||
| два отказа выходят на провод по-разному (409+`source_gone` против 503) | `httpapi.TestAVanishedSourceAndAVanishedVolumeAnswerDifferently` |
|
||
| объявленная версия контракта = ратифицированный канон | `gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` (существующий) |
|
||
|
||
### Числа, снятые ПОСЛЕ последней правки
|
||
|
||
* **Батарея с ЧЕТЫРЬМЯ гейтами** (`TM_PLATFORM_TEST_DSN` на стендовый Postgres · `ENGINE_BIN` собран
|
||
из ТЕКУЩЕГО `backend/` по `PD-432` · `BOOK_TEMPLATE` стенда · достижимый пользовательский systemd):
|
||
`MAKE-EXIT=0` · **20 пакетов `ok` · 0 `FAIL`** · линтер `0 issues` · `sqlc diff` чисто · 5 скипов.
|
||
(Перечислены все двадцать: `cmd/tmplatformctl` `cmd/tmplatformd` `auth` `backup` `books` `config`
|
||
`exports` `gates` `httpapi` `ingest` `jobs` `login` `metrics` `money` `pgstore` `pricing`
|
||
`readmodel` `reqid` `runner` `runs` — «зелёная батарея» без полного списка ничего не значит.)
|
||
* **`make vuln`** (отдельная цель, в `check` не входит, блокер лендинга по §2 стандарта):
|
||
`No vulnerabilities found.`, `VULN-EXIT=0`.
|
||
* ⚠ **Оба числа ПЕРЕ-СНЯТЫ ПОСЛЕ ДОФИКСА по верификатору** (дофикс тронул `internal/gates` и
|
||
`cmd/tmplatformd`, то есть код, а не только доки): батарея снова `MAKE-EXIT=0 · 20 ok · 0 FAIL ·
|
||
линтер 0 issues · 5 скипов`, `make vuln` снова `No vulnerabilities found.`. Числа совпали с
|
||
до-дофиксными — но это ИЗМЕРЕНО, а не унаследовано: правка кода после снятых чисел обнуляет их
|
||
независимо от того, насколько она мала.
|
||
* **Скипы: 5, и условий у них ДВА, а не одно** — это отдельная находка сегодняшнего дня.
|
||
Четыре пробы `internal/runner` скипаются словами «this deployment's pipeline enables the bank
|
||
contour and names `<репо>/backend/configs/mining-contrast.zh.txt`, which is not on this host»
|
||
(артефакт чужой зоны, его отсутствие — не поломка платформы). Пятая,
|
||
`runner.TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`, умеет скипнуться ВТОРЫМ
|
||
условием — `listen tcp 127.0.0.1:11434: bind: address already in use`, — и два честных прогона одной
|
||
смены назвали для неё РАЗНЫЕ условия. Носитель числа (`STACK_DECISIONS` §«Гейты батареи») знал ноль
|
||
из двух; оба дописаны туда же вместе с предупреждением, что по одному прогону условия не считают.
|
||
* **Денежные пины не флейковые:** четыре теста × 8 прогонов под `-race` = **32 строки `--- PASS`,
|
||
0 `--- FAIL`** (`go test ./internal/runs/ -race -count=8 -run '<четыре имени>'`).
|
||
* **Живая проба на стенде** — свой демон (порт 18080, свой `BOOKS_DIR` под `$HOME`, своя база
|
||
`tmstand_truth`), книга заведена ЖИВЫМ интейком через `tmplatformctl seed`, и порт сверен с PID
|
||
своего демона перед сидом (шаг 3а рецепта). ⛔ `TM_PLATFORM_BOOKS_DIR` ни секунды не смотрел на
|
||
`<репо>/books`; проверка стоит в самом скрипте стенда, а не в намерении.
|
||
- форма над книгой в покое: `"blocked":null`, `verdict":"covers_all"`;
|
||
- первый клик: `202`, баланс 25.000000 → 22.862733 (холд взят);
|
||
- форма над той же книгой С ЖИВЫМ ПРОГОНОМ:
|
||
`"blocked":{"code":"run_in_flight","book_id":"bk_7JDPZD3S7J22XS3I"}` — то есть **ровно то, что
|
||
дверь и ответила** вторым кликом: `409 run_in_flight`;
|
||
- каталог книги удалён (руками, внутри своего стенда) → `409 book_not_ready` + `cause: source_gone`,
|
||
**баланс 25000000 до и 25000000 после** — холд не взят;
|
||
- сентинел `.tmplatform-books` убран (форма размонтирования) → `503 service_unavailable`, баланс тот
|
||
же, и в логе ERROR-строка `run refused: this deployment cannot start runs`;
|
||
- ⚠ **Живая движковая половина на этом хосте ДОСТИЖИМА, и «5 скипов» читать как «сюда не
|
||
добраться» нельзя:** приёмка №23 прогнала пробы `internal/runner` с шаблоном, чьи
|
||
`pipeline:`/`models:` абсолютные, и получила **42 PASS · 0 FAIL** при тех же пяти скипах;
|
||
условие одно — артефакт контраста, строка бэклога **251**, чужая зона. Числа приёмки, не мои.
|
||
- ⚠ $0: движок на стенде провайдерских ключей не имеет (`.env` рядом с `book.yaml` нет, ключей в
|
||
окружении 0), прогон честно умер `exit-code`, и холд вернулся сам.
|
||
|
||
### Мутационные посадки — 18 штук, выживших НЕТ
|
||
|
||
Изоляция по зонному стандарту §3 п.3: копия взята ВМЕСТЕ с каноном
|
||
(`cp -a --parents platform docs/architecture/14-api-contract <куда>/`), перед каждой правкой
|
||
скрипт утверждает `test -f go.mod` и что путь — копия, а не дерево. **База копии зелёная**
|
||
(`internal/runs` ok 57 с · `httpapi` ok 2.8 с · `gates` ok 0.46 с · `books` ok 25 с, exit 0), и
|
||
вердикты судятся по ДЕЛЬТЕ против неё.
|
||
⚠ Первый прогон базы упёрся в `-timeout 20m` на `internal/books` — под ЧУЖОЙ нагрузкой (load 46,
|
||
соседний замер), — и скрипт ОТКАЗАЛСЯ судить мутации против красной базы. Числа ниже со второго
|
||
прогона. Та же ловушка поймала и адверсариальный проход: его первая батарея была красной по
|
||
таймауту трёх пакетов из-за параллельной батареи, а не из-за дерева.
|
||
|
||
| # | что сломано | падает | ТЕКСТ падения (по нему и засчитано) |
|
||
|---|---|---|---|
|
||
| 1 | дверь перестаёт спрашивать про живой прогон | форма | `the form says <nil> over a book that is being translated, want the door's run_in_flight` |
|
||
| 2 | форма перестаёт НЕСТИ ответ двери | форма | то же сообщение с другой стороны |
|
||
| 3 | провод предпочитает чужой холд своему прогону | `httpapi` ×1 | `blocked = map[book_id:bk_other code:credit_held], want this book's own run` |
|
||
| 4 | `blocked` называет чужую книгу | `httpapi` ×2 | `blocked.book_id = , want the book being asked about` |
|
||
| 5 | допуск перестаёт смотреть на каталог | `runs` ×5 | `a start over a missing directory answered <nil>, want ErrSourceGone` |
|
||
| **6** | **каталог смотрится ПОСЛЕ холда, а не до** | `runs` ×5 | **`a refused start moved money: {Balance:10000000 Reserved:0} -> {Balance:6180172 Reserved:3819828}`** — денежный пин не вакуумен: на кону были настоящие $3.819828 |
|
||
| 7 | резюм перестаёт смотреть на каталог | `runs` ×1 | `a resume over a missing directory answered <nil>, want ErrSourceGone` |
|
||
| 8 | пропавший ТОМ свалить на книгу | `runs` ×2 | `answered … its source directory is not on this deployment, want ErrStorageUnavailable` |
|
||
| 9 | пропавший КАТАЛОГ свалить на деплой | `runs` ×2 | `answered … the book storage is not mounted on this deployment, want ErrSourceGone` |
|
||
| **10** | **маркер СВОЕГО корня читается как улика про ЛЮБОЙ том** | `runs` ×1 | `a start over a book outside the root answered … not on this deployment, want the deployment's` — пин находки ревью |
|
||
| 11 | сентинел хранилища перестаёт спрашиваться | `runs` ×1 **плюс два ЧУЖИХ пина `PD-192`** в `internal/books` | `the host is at fault, not the book` |
|
||
| 12b | путь, который не каталог, проходит дверь | `runs` ×1 | `a start over a FILE in the book's place answered runs: stat journal: …` |
|
||
| 13 | пропавший источник перестаёт быть видом `book_not_ready` | `runs` ×1 | `the refusal is not a book_not_ready: runs: its source directory is not on this deployment` |
|
||
| 14 | причина `source_gone` замолкает у СТАРТА | `httpapi` ×1 | `cause = <nil>, want "source_gone" — without it the client offers waiting` |
|
||
| 15 | РЕЗЮМ отвечает словами старта | `httpapi` ×1 | `code = book_not_ready, want "run_not_resumable"` |
|
||
| **16** | **оба новых значения провода переименованы** | `httpapi` ×4 **и гейт** | `this build serves blocked.code: runInFlight and the canon's enum does not carry it: [credit_held run_in_flight]` — до дофикса эта посадка ВЫЖИВАЛА |
|
||
| 17 | объявленная версия контракта уезжает от канона | `gates` ×1 | `this build announces contract 0.13.1 and the ratified canon is 0.14.0` |
|
||
|
||
**Итог: 18 посадок (17 + одна пере-посаженная), 17 пойманы, 0 выживших**, и каждая засчитана по
|
||
ТЕКСТУ падения, а не по цвету.
|
||
⚠ **Четыре посадки в ПЕРВОЙ редакции не собрались** — снятие блока оставляло неиспользованными `facts`,
|
||
импорт `books`, переменную `st`, — и «ни один тест не упал» там означало «пакет не собрался», то есть
|
||
НЕ ИЗМЕРЕНО, а вовсе не «дыра». Пере-посажены так, чтобы менялось РЕШЕНИЕ, а идентификаторы
|
||
оставались в деле, и только тогда засчитаны. Это тот самый случай, когда вердикт по неправой причине
|
||
отличается от поимки лишь тем, прочёл ли кто-нибудь текст.
|
||
⚠ **Посадка 11 — улика «предикат ВЫЗВАН, а не скопирован»:** ломая `books.StorageIsThere`, она красит
|
||
и мой тест, и ДВА чужих пина `PD-192`. Копия так бы не покраснела.
|
||
⚠ **Посадка 1 показала границу:** снятие `HasLiveRun` из предиката красит только ФОРМУ — дверь
|
||
по-прежнему отвергает вторую покупку, потому что за ней стоит частичный уникальный индекс
|
||
`runs_one_live_per_book` (`pgstore.StartRun`, ветка `isUnique`). У двери есть второй рубеж, у формы
|
||
его не было — это и есть предмет `PD-455` одной фразой.
|
||
|
||
### Греп ОТКРЫТЫХ рядов регистра по СВОИМ ПОЛНЫМ путям (норма зоны §3 п.8)
|
||
|
||
Прибор: греп по полным путям семи тронутых файлов с разбором таблицы регистра — **34 открытых ряда**
|
||
цитируют мои файлы. Диспозиции:
|
||
|
||
* **тронуты механизмом — четыре:** `PD-455` закрыт · `PD-162` сужен, остаток пере-написан · `PD-448`
|
||
разобран и остаётся открытым · `PD-466` заведён этим паком.
|
||
* **назван, но не закрыт — один:** `PD-139` (путь каталога книги утекает в ERROR-логи внутри обёрнутых
|
||
ошибок). Моя новая ветка МОГЛА стать его третьим носителем и не стала — путь выброшен из ошибки, пин
|
||
`runs.TestADirectoryThatCannotBeReadIsTheDeploymentsAndCarriesNoPath`. Сам ряд я не лечил: два его
|
||
существующих носителя — тейлер и обёртки спавна — мой дифф не трогает.
|
||
* **`PD-175`** — §4.4, одна строка выше.
|
||
* **остальные 28 — совпадение по ИМЕНИ ФАЙЛА, а не по предмету** (реконсилятор, свип, телеметрия,
|
||
интейк, читающая модель). Проверяемо: в `reconcile.go` мой дифф — ОДИН хунк на 8 строк
|
||
(`git diff -- platform/internal/runs/reconcile.go | grep -E '^@@'` даёт ровно одну строку,
|
||
`@@ -1713,6 +1713,14 @@`), в `books.go` — экспорт одной функции, в `runner.go` — одно поле
|
||
конфигурации. Оставлены открытыми с этой причиной.
|
||
|
||
### Что сказал адверсариальный проход (author≠reviewer, исполнением)
|
||
|
||
Направленный второй читатель по пяти осям §5 промта, со своей копией дерева и своими посадками.
|
||
**Девять находок, четыре существенные, и три из них я сам не видел.** Что сделано с каждой:
|
||
|
||
| находка прохода | вердикт | что сделано |
|
||
|---|---|---|
|
||
| **F3.** `sourceThere` спрашивал сентинел КОРНЯ ИНТЕЙКА про любую книгу — а книга от `tmplatformctl book add --workdir` живёт на томе, которого платформа не писала. Проход показал пробой: том такой книги пропал, корень интейка здоров ⇒ ответ был «конец КНИГИ». **Это `PD-192` заново, моими руками** | принята целиком | сентинел спрашивается только под своим корнем (`books.Owns`); где спрашивать нечего — ответ деплоя. Пин `runs.TestABookOutsideTheIntakesRootIsNotDeclaredDeadByAnotherVolumesMarker` |
|
||
| **F2.** Оба новых значения провода (`run_in_flight`, `source_gone`) не были запинены НИЧЕМ: все мои ассерты сравнивали провод с той же Go-константой. Проход переименовал обе — **вся батарея осталась зелёной** (класс `PD-1`) | принята целиком | ассерты переписаны на ЛИТЕРАЛЫ + гейт со ВТОРЫМ независимым источником: `gates.TestTheBlockedVocabularyServedIsTheOneTheCanonEnumerates` читает enum `Blocked.code` из канона и сверяет в обе стороны |
|
||
| **F1.** Отказ резюма выходил словом СТАРТА (`book_not_ready` на `runId`), которого таблица §`resumeRun` не знает, и не был покрыт ни одним тестом | принята | резюм отвечает своим словарём — `409 run_not_resumable` + `cause: source_gone`; это форма, какой канон уже пользуется для двух осей, перекрывающих таблицу. Пин `httpapi.TestAResumeOverAGoneSourceIsRefusedInTheResumeHandlesOwnWords`. Строка канонной таблицы всё равно нужна — пункт оркестратору |
|
||
| **F5.** «Одно определение» верно для предиката, но отображение «ответ предиката → член провода» — вторая рукописная вещь с МОЛЧАЛИВЫМ хвостом: новый отказ завтра оставит форму с `covers_all` | принята | хвост стал громким: незнакомый отказ пишет ERROR-строку оператору. Компайл-тайм это не ловит (предикат отвечает `error`), и я это говорю, а не прячу |
|
||
| **F8.** `os.Stat` успешен на ФАЙЛЕ: путь, который есть, но не каталог, проходил дверь | принята | `IsDir()`, ответ деплоя; пин `runs.TestAFileWhereTheBooksDirectoryShouldBeIsRefusedToo` |
|
||
| **F9.** Фраза «the wire this build serves is the 0.13.1 shape» осталась в настоящем времени над константой `0.14.0` | принята | время исправлено |
|
||
| **F4.** Свойство «путь книги не течёт в ERROR» запинено у `sourceThere` и сломано строкой позже на том же вызове: `journalSize` (`internal/runs/spawn.go:301`) отдаёт `*fs.PathError` целиком, и он уходит в `default:` → 500 | **не лечил, назвал** | это носитель ряда `PD-139`, у которого СВОЯ диспозиция («решение, а не побочный эффект»); я записал в ряд и своё решение по своей ветке, и второй носитель адресом. Класс закрыт на ОДИН носитель из двух, и в отчёте это сказано так |
|
||
| **F6.** `bank.go` несёт посимвольную копию первой ветки предиката | **не лечил, назвал** | копируется не ПРАВИЛО, а предложение: сам предикат `readyToTranslate` там и вызывается, а живой-прогон половина у двери банка законно ДРУГАЯ (исключение `awaiting_bank`, ратифицированное находкой P9). Сводить их значило бы либо сломать исключение, либо втащить внутрь флаг «я дверь банка» |
|
||
| **F7.** Реконсилятор при своём респавне каталог не спрашивает, и окно между проверкой и спавном остаётся | **граница, а не дефект** | ровно то, что абзац «ОТКРЫТЫМ остаётся» ряда `PD-162` и говорит; половина живого прогона — эскроу (`П-18`) |
|
||
|
||
⚠ **Проход также подтвердил три моих утверждения исполнением, а не чтением:** автоматического
|
||
терминального вердикта живому прогону в дельте нет (грепы по `+367` добавленным строкам
|
||
`reconcile.go`); `PricedBook` не тронут (`pgstore/books.go` вообще отсутствует в `git status`);
|
||
контракт-первичность соблюдена — канон уехал на 0.14.0 РАНЬШЕ кода, коммитом `ba8542a`.
|
||
⚠ И назвал ловушку прибора, которую я знал по чужому опыту, а он встретил сам: его ПЕРВАЯ батарея была
|
||
красной по таймауту трёх пакетов — из-за параллельной батареи на той же машине, а не из-за дерева;
|
||
на чистом перепрогоне те же пакеты дали 48 с / 148 с / 123 с.
|
||
|
||
|
||
### Чего НЕ удалось / не измерено (§10)
|
||
|
||
* **Полный пакет `internal/runs` под нагрузкой ни разу не дошёл до конца** — четыре попытки уперлись в
|
||
`-timeout 9m` на ДРУГИХ тестах (`TestALiftedQuarantineMaterializesTheJournalAgainFromTheCursor`,
|
||
`TestARestartHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince`, `TestADeadlockDoesNotStopTheProjection`).
|
||
Значит запись ряда `PD-448` «упал 2 из 2 полным пакетом» сегодня **не пере-проверена**; гонка
|
||
воспроизведена подмножеством. Это «не измерено», а не «не воспроизвелось».
|
||
* **Цена второго чтения на пути формы не замерена.** Я утверждаю, что +1 индексный запрос на опрос
|
||
приемлем рядом с уже стоящим там пер-главным агрегатом, но бенчмарка не снимал — это суждение.
|
||
* **Ветка «стат каталога упал не-ENOENT» проверена только правами** (`chmod 000` на корне): EIO,
|
||
залипшее сетевое монтирование и симлинк-в-никуда я не воспроизводил.
|
||
* **Гонка «каталог удалён МЕЖДУ проверкой и спавном» остаётся** — проверка сужает окно, а не закрывает
|
||
его; это уже половина живого прогона, то есть эскроу (`П-18`), и её пак не брал.
|
||
* **Фронт я не читал** (чужая зона), поэтому не знаю, как он сегодня рисует `blocked` и что сделает с
|
||
новым значением кода; канон обязывает клиента терпеть незнакомое значение, но проверить это я не мог.
|
||
* **Число скипов батареи снято дважды и дало РАЗНЫЕ условия** для одного и того же теста (см. ниже) —
|
||
считать условия по одному прогону нельзя, и я не знаю, сколько их всего.
|
||
|
||
### Что я сам считаю слабым местом сделанного
|
||
|
||
1. **Форма по-прежнему может обещать старт над пропавшим каталогом** (`PD-466`). Я разрезал правило на
|
||
«факты БД спрашивают обе площадки» и «диск спрашивает только дверь», и довод у разреза есть
|
||
(опрос — не место для `os.Stat` зависшего монтирования), но это именно РАЗРЕЗ: половина обещания
|
||
формы осталась неправдой, и я назвал её, а не закрыл.
|
||
2. **`Options.Refusal` — поле типа `error` в структуре-значении.** Оно даёт слою провода решать
|
||
словарь (что правильно: коды живут в `httpapi`), но это необычная форма, и следующий читатель может
|
||
принять её за «ошибку чтения формы», а не за «ответ двери». Комментарий это говорит; форма всё равно
|
||
на любителя.
|
||
3. **Обе главные находки прохода — мои СЛЕПЫЕ ПЯТНА, и они одного рода.** Я построил фикстуру
|
||
`onTheVolume` под свою же картину мира (книга лежит под корнем интейка) и посадил двенадцать
|
||
мутаций — но ни фикстура, ни каталог мутаций не могли найти случай, которого в моей картине не
|
||
было: книгу ВНЕ этого корня. И мутировал я ЛОГИКУ, ни разу не тронув СЛОВАРЬ, поэтому «переименуй
|
||
оба новых значения провода» в мой каталог не попало — а именно эта посадка и выживала. Вывод, к
|
||
которому я пришёл не сам: свои мутации проверяют то, что автор считает важным, и ровно поэтому
|
||
второй читатель не роскошь.
|
||
4. **Мой предикат не покрывает третью площадку, которая судит те же факты** — дверь правок банка
|
||
(`internal/runs/bank.go:124-142`) читает `readyToTranslate` и `HasLiveRun` своим кодом, потому что у
|
||
неё СВОЁ правило (исключение для `awaiting_bank`, ратифицированное находкой P9). Я сознательно её не
|
||
тронул: свести их в один предикат значило бы либо сломать исключение, либо втащить в предикат флаг
|
||
«я — дверь банка», после которого «одно определение» становится лозунгом. Но факт остаётся: слово
|
||
«одно определение» верно для ДВУХ площадок из трёх, и я предпочитаю сказать это прямо.
|
||
|
||
### Дофикс по верификатору приёмки (11.09, тот же день) — один ВЫЖИВШИЙ мутант и одно тихо-зелёное
|
||
|
||
Проход верификатора по замороженной копии: девять посадок, семь чистых поимок, **один выживший и одно
|
||
тихо-зелёное**. Оба — вне моей карты находок, оба однострочные, и оба ломали ровно тот механизм, что
|
||
пак объявил главным. Обе находки я пере-проверил исполнением, прежде чем чинить.
|
||
|
||
1. **ВЫЖИВШИЙ: единственная проводка `BooksDir` в дверь не была утверждена ничем.**
|
||
`cmd/tmplatformd/runner.go:39` — единственное производственное присваивание, а соседний тест-свидетель
|
||
несёт комментарий «Mutation caught: dropping any assignment in runsConfig» и **`BooksDir` не
|
||
утверждал**. Посадка «снять строку» пережила полную батарею. Цена: с непроведённым корнем
|
||
`books.Owns("", …)` ложен для КАЖДОЙ книги, и книга, чей собственный каталог пропал, отвечает `503`
|
||
и операторским «this deployment cannot start runs» вместо `409 book_not_ready`+`source_gone` — то
|
||
есть путаница `PD-192` возвращается одной выпавшей строкой, молча, при целых деньгах. Запинено
|
||
своим случаем в `TestTheOperatorsRunnerKnobsReachTheReconciler`; посадка теперь краснеет текстом
|
||
«BooksDir is "", want the intake's …: the run door cannot tell whose fault a missing directory is».
|
||
⚠ И класс тут тот же, что мы записали нормой сегодня: **комментарий теста утверждал ШИРЕ, чем тест
|
||
делал.** Теперь утверждение и комментарий сошлись — покрыты все одиннадцать полей.
|
||
2. **ТИХО-ЗЕЛЁНОЕ В МОЁМ ЖЕ НОВОМ ГЕЙТЕ.** Регулярка `(?s)\n Blocked:\n.*?\n +enum: \[…\]`
|
||
телом схемы не ограничена: с удалённым из канона enum она лениво дотягивалась до СЛЕДУЮЩЕГО
|
||
`enum:` и зачитывала `Usage.state` семьюдесятью строками ниже — красное по неправой причине; а если
|
||
те же два слова положить под соседнюю схему, гейт отвечал **`--- PASS` над каноном, который их
|
||
больше не ратифицирует**. Чтение теперь ограничено телом схемы (`enumOfSchema`), и у самого
|
||
читателя есть пин на трёх синтетических документах — `TestTheSchemaEnumReaderStopsAtTheSchemasOwnBody`.
|
||
Проверено посадкой верификатора на копии: удаление enum из канона даёт теперь МОЙ текст
|
||
«schema "Blocked" carries no enum», а не чужие значения.
|
||
⚠ Горькая деталь: этот гейт я построил ровно как противоядие от своего слепого пятна по сигналу
|
||
прохода — и построил его с собственным слепым пятном того же рода. Прибор, проверяющий словарь,
|
||
сам обязан иметь пин; теперь имеет.
|
||
3. **Носитель числа скипов нёс ДВА разных числа.** Мой новый блок говорил «5 скипов», а нетронутая
|
||
строка того же раздела — «ожидание при всех четырёх: … **скипов 0**» (замер 29.08). Читатель,
|
||
попавший на вторую, объявил бы приёмку при пяти скипах или счёл бы пять поломкой. Разведено по
|
||
УСЛОВИЯМ: ноль относится к дереву 29.08, где не было ни артефакта контраста (строка бэклога 251),
|
||
ни занятого локального адреса провайдера; счёт скипов сверяется с блоком, который эти условия
|
||
называет.
|
||
4. **Якорь `PD-448` на сам тест уехал** (`control_test.go:846` → `:881`) — уехал ДО пака и промтом не
|
||
назывался, починен заодно. Плюс мой собственный `contract_test.go:16` → `:17`, который сдвинул
|
||
импорт, добавленный этим же дофиксом.
|
||
|
||
⚠ **Чего в этом списке НЕТ и почему.** Верификатор назвал пятым пунктом отсутствие фразы «работа
|
||
завершена, править не планирую» в зонном журнале — **фраза там была и есть**, в конце моей
|
||
секции (на момент проверки — `platform-PROGRESS.md:481`, после этой вставки она уехала ниже; греп по
|
||
файлу даёт ТРИ хита: сама фраза, её упоминание в этом абзаце и пак 08.09). Проверил прибором, прежде
|
||
чем «чинить»: приёмку своей работы тоже надо проверять, иначе в журнал уедет вторая копия той же
|
||
фразы и следующая смена будет гадать, какая из них настоящая.
|
||
|
||
**Работа завершена, править не планирую.** Дерево — 14 путей, все в `platform/` (13 изменённых плюс
|
||
новый `internal/runs/admission_test.go`), вне зоны не тронуто ничего; `docs/PROGRESS.md`,
|
||
`docs/experiments/`, `backend/docs/` в дереве — работа соседних смен. Сессия не коммитит: передано
|
||
оркестратору №23 вместе с тремя пунктами, которые правит он (якорь в журнале решений · строка
|
||
канонной таблицы §`resumeRun` · расхождение по `translating`, старше этого пака).
|
||
|
||
|
||
## ДОРАБОТКА ПО ИНВАРИАНТУ `D39.240` — ОТЧЁТ (11.09, `textmachine-5c`)
|
||
|
||
> Три предмета: строки **401** (вторая половина), **394**, **392**. Вход HEAD `d61469f`, дерево зоны
|
||
> на входе чистое. Записка-план ниже по странице; порядок исполнен как объявлен.
|
||
|
||
### 401 — отметка о расчёте больше не остаётся пустой навсегда
|
||
|
||
Расчёт — ДВА действия: `Settle` закрывает резервацию своей транзакцией, `MarkSettled` ставит
|
||
`settled_at` вторым оператором. Смерть процесса между ними оставляла леджер верным, а столбец пустым
|
||
**навсегда**: рабочий список ключуется на ОТКРЫТОЙ резервации, а её уже нет — прогон выпадает из
|
||
всех списков, которые могли бы к нему вернуться.
|
||
|
||
**Сделано:** отметка не запоминается, а ВЫВОДИТСЯ. `Store.StampSettledRuns` одним оператором
|
||
доштамповывает прогоны, чьи жизнь и деньги кончились («прогон завершён И у него нет открытых
|
||
резерваций»), и фаза расчёта зовёт его последним действием прохода. Ни новой колонки, ни нового
|
||
состояния; «навсегда» стало «до следующего прохода». Число починенного идёт WARN-ом — это факт о
|
||
деплое (процесс умер посреди расчёта), а не о прогонах.
|
||
|
||
⚠ **Названная цена:** отметка ставится временем ПОЧИНКИ, а не моментом закрытия денег; точное время
|
||
живёт в `closed_at` самой резервации. Прогон, починенный здесь, опаздывает штампом не больше чем на
|
||
один проход.
|
||
|
||
⛔ **Своя находка адверсариального прохода — своя же правка чуть не стала дороже дефекта.** Запрос
|
||
фильтрует `finished_at is not null and settled_at is null`, и ни один существующий индекс `runs` ему
|
||
не помогает: один частичный по `finished_at is null` (противоположная половина), второй по книге. ⇒
|
||
починка была бы **последовательным сканом `runs` на КАЖДОМ тике**, а `runs` только растёт. Заведён
|
||
частичный индекс миграцией `00035`, предикат в предикат: в здоровом деплое множество ПУСТО, потому
|
||
что обычный путь ставит отметку в том же проходе, — индекс держит ровно то, что оставил сбой.
|
||
|
||
### 394 — «убили своим грейсом» больше не читается как поломка деплоя
|
||
|
||
**Замерено исполнением, с контрольной посадкой.** Юнит нашей формы (`TimeoutStopSec=3`, процесс ловит
|
||
SIGTERM и не выходит) даёт маркер `{"result":"timeout","code":"killed","status":"KILL"}`; тот же юнит
|
||
с процессом, который по SIGTERM выходит, даёт `{"result":"exit-code","code":"exited","status":"5"}`.
|
||
Тройка `timeout/killed/KILL` и есть «сработал НАШ `TimeoutStopSec`», и прибор эти случаи различает.
|
||
|
||
**Сделано:** `timeout` выведен из группы «поломка деплоя» в `interrupted`. Довод — не вкус, а
|
||
невыполнение довода самой группы: там написано «the next spawn dies the same way», что верно для
|
||
OOM (следующий процесс упрётся в тот же лимит) и ложно для убийства по грейсу (следующий процесс
|
||
никто не останавливает, работа предшественника в чекпоинтах). Контракт при этом НЕ трогается:
|
||
`interrupted` — существующее значение, и его определение дословно описывает этот случай («retrying IS
|
||
the remedy, and finished work is not bought again»).
|
||
|
||
⛔ **Проверено прибором по требованию оркестратора: в эту развилку `timeout` приходит ДВУМЯ ветвями, а
|
||
не одной.** Замер трёх состояний намерения: намерение СТАРШЕ маркера → `outcome` отдаёт `stopped` и
|
||
до причины отказа не доходит вовсе; намерения НЕТ → `failed`; намерение МЛАДШЕ маркера → `failed`.
|
||
Последнее — не угол: часы разные (маркер несёт хостовые, намерение платформенные), и «нажали стоп
|
||
после убийства» — обычное состояние. Обе достигающие ветви хотят одного ответа, поэтому правка одна,
|
||
но запинены обе.
|
||
|
||
⚠ **Чего НЕ сделано и почему** — не переведён в `stopped`. Грейс срабатывает только после того, как
|
||
остановку у systemd попросили, но при перезагрузке хоста менеджер останавливает ВСЕ юниты, и прогон,
|
||
не успевший свернуться, получил бы `stopped` и остался мёртвым после штатного ребута. Это ровно
|
||
асимметрия, ради которой написан `interruptedBySomeoneElse`.
|
||
|
||
### 392 — гонка имени юнита: ВОСПРОИЗВЕДЕНА, потом закрыта
|
||
|
||
Строка описывала механизм грубее, чем он есть. Бóльшая часть окна уже была закрыта: `reopen`
|
||
отказывает рестарту при записанном намерении, а подзапрос читает «последнюю не кончившуюся попытку»,
|
||
то есть при любом порядке КОММИТОВ ответ верен. **Настоящая дыра — в изоляции READ COMMITTED:**
|
||
`RequestStop` был одним UPDATE; когда он ждёт замок строки `runs` (его держит рестарт, пока кончает
|
||
одну попытку и открывает следующую), Postgres после разблокировки пере-проверяет `WHERE` против
|
||
НОВОЙ версии строки, **а подзапрос в `RETURNING` считается по снимку начала оператора**.
|
||
|
||
**Предъявлено исполнением на живой БД, в обе стороны:**
|
||
```
|
||
до правки: рестарт закоммитил "unit-NEW"; RequestStop ответил "unit-OLD"; живой юнит "unit-NEW"
|
||
после правки: рестарт закоммитил "unit-NEW"; RequestStop ответил "unit-NEW"
|
||
```
|
||
**Сделано:** `RequestStop` стал транзакцией — `select … for update of r` по строке прогона, затем
|
||
чтение имени и запись намерения. В READ COMMITTED блокирующее чтение после взятия замка пере-читает
|
||
последнюю зафиксированную версию, поэтому каждый следующий оператор транзакции видит рестарт, который
|
||
был в полёте. Имя юнита и намерение принадлежат одному моменту.
|
||
⚠ Ожидание замка правкой НЕ добавлено: одиночный UPDATE ждал того же замка. Изменилось не то, ждёт ли
|
||
вызов, а что он видит, проснувшись.
|
||
|
||
### Мутация — пять посадок на копии, засчитано по ТЕКСТУ
|
||
|
||
Копия жила в `~/.cache/tm-5c/mut2/platform` (не в общем скретчпаде), перед каждой правкой утверждались
|
||
`test -f go.mod` и точный `pwd`, после — `diff -rq` с деревом «идентично» и копия удалена.
|
||
|
||
| посадка | что сломано | вердикт и **текст** |
|
||
|---|---|---|
|
||
| A (392) | снято блокирующее чтение, назад к одному оператору | КРАСНО: «the stop answered "unit-OLD" while the live unit is "unit-NEW": the caller would signal a unit that is already dead…» |
|
||
| B (394) | `timeout` возвращён в группу поломок деплоя | КРАСНО ×2: «a "timeout" kill is reported to the client as "service_error", want "interrupted"» и то же во второй ветви |
|
||
| C (401) | из фазы расчёта убрана починка | КРАСНО: «a run whose money had closed is still carrying no settled mark after a full sweep…» |
|
||
| D (401) | починка штампует и ЖИВЫЕ прогоны | ⛔ **ВЫЖИЛА** на первом заходе |
|
||
| D′ (401) | то же, против пере-строенной фикстуры | КРАСНО: «a run that is still LIVE was stamped settled … because it happened to hold nothing» |
|
||
|
||
⛔ **Выжившая посадка — дефект В МОЁМ СОБСТВЕННОМ ПИНЕ, и он важнее трёх пойманных.** Починка
|
||
исключает живой прогон ДВАЖДЫ — потому что у него открыт холд, и потому что его жизнь не кончилась.
|
||
Первое исключение ПРЯЧЕТ второе: моя зеркальная фикстура держала прогон с открытым холдом, поэтому
|
||
снятие условия «прогон завершён» не меняло ничего. Фикстура пере-строена на состояние, где условия
|
||
расходятся, и это состояние ОБЫЧНОЕ, а не угловое: **между попытками** — свип рассчитал кончившуюся
|
||
попытку (резервация закрыта) и ещё не открыл следующую; прогон жив и не держит ничего, и только
|
||
«кончился ли прогон» удерживает починку от него. На пере-строенной фикстуре посадка краснеет, и
|
||
краснеет ИМЕННО во второй подветви — то есть первая действительно её прятала.
|
||
|
||
### Числа, сняты ПОСЛЕ последней правки
|
||
|
||
```
|
||
$ TM_PLATFORM_TEST_DSN=…55433 TM_PLATFORM_TEST_ENGINE_BIN=… TM_PLATFORM_TEST_BOOK_TEMPLATE=… \
|
||
TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
|
||
MAKE-EXIT=0 · пакетов `ok` 20 · строк FAIL 0 · линтер «0 issues» · скипов 5
|
||
gofmt / go vet / sqlc diff чисты · ALARM PD-count: 15 (baseline 15) — не сдвинут
|
||
скипы поимённо, условие у всех ОДНО и названное — нет `configs/mining-contrast.zh.txt`:
|
||
TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses ·
|
||
TestALivePreviewWritesNothingAndALiveApplyWrites ·
|
||
TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing ·
|
||
TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber ·
|
||
TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem
|
||
```
|
||
|
||
### ⚠ Правка ЧУЖОГО теста, вызванная заказанной сменой поведения — объявляю по `D39.183`
|
||
|
||
`TestWhyARunFailedDecidesWhetherARetryIsWorthOffering` (`internal/runs/reconcile_test.go`) держал ряд
|
||
«killed by the stop timeout → `service_error`». После правки 394 он честно покраснел в батарее — не
|
||
подогнан, а **описывал поведение, которое заказано сменить** (строка 394, ратифицировано письмом
|
||
оркестратора «делай, обе половины»). Ряд приведён к новому поведению, и вместе с ним исправлен
|
||
КОММЕНТАРИЙ теста, который нёс тот самый довод, что правка и опровергает.
|
||
**Куда уехала гарантия:** в `TestAKillByOurOwnGraceIsNotReportedAsABrokenDeployment`, где живёт и
|
||
довод, и замер маркера, и контроль `oom-kill`, который обязан ОСТАТЬСЯ `service_error`. Остальные
|
||
шесть рядов старого теста не тронуты — довод группы для них выполняется.
|
||
|
||
### Что НЕ удалось
|
||
|
||
**1. Цепочка «свип → `Stop` → настоящий systemd» по-прежнему не пройдена ни одним тестом.** Объявлено
|
||
заранее в записке-плане и остаётся верным: такого стенда в зоне нет, и я его не строила. Предмет 392
|
||
живёт в изоляции транзакции, и ЕГО стенд есть — доказательство полное для гонки и неполное для цепочки.
|
||
|
||
**2. Ущерб по 392 предъявлен на уровне СТОРА, а не продукта.** Тест показывает, что `RequestStop`
|
||
возвращал мёртвое имя; что обработчик после этого шлёт сигнал в пустоту и пользователь держит `202` —
|
||
следствие, выведенное чтением, а не поставленный сценарий.
|
||
|
||
**3. Гонка воспроизведена НА ФИКСТУРЕ, а не на настоящем рестарте.** Транзакция-соперник пишет те же
|
||
три оператора, что и `reopen` (замок строки прогона, конец попытки, вставка следующей), но это моя
|
||
запись, а не вызов `ReopenRun`. Если `reopen` однажды перестанет брать замок строки `runs` первым,
|
||
фикстура продолжит воспроизводить старую форму гонки, а не новую.
|
||
|
||
**4. По 394 не проверено исполнением, что `timeout` не приходит от НАЧАЛЬНОГО тайм-аута.** У наших
|
||
юнитов `Type=simple` (дефолт `systemd-run`), для которого стартовая фаза завершается сразу, поэтому
|
||
`timeout` может прийти только от остановочной, — но это РАССУЖДЕНИЕ о systemd, а не мой замер.
|
||
Сконструировать стартовый тайм-аут на `Type=simple` я не пробовала.
|
||
|
||
**5. Взяты три предмета из десяти.** 398, 399, 400 и три носителя мусора на диске не тронуты по
|
||
границам захода; 400 и мусор — ещё и потому, что я их не читала.
|
||
|
||
## ДОРАБОТКА ПО ИНВАРИАНТУ `D39.240` — ЗАПИСКА-ПЛАН (11.09, `textmachine-5c`)
|
||
|
||
> Три предмета из улова отменённого пака, открытые владельцем словом «если остались баги или
|
||
> рефакторинг про останов — доделать просто»: строки **392**, **394** и **401** (вторая половина).
|
||
> Инвариант: остановка ЖЁСТКАЯ, деньги терять допустимо, но **без гонок, без половинчатых состояний,
|
||
> с верным возобновлением**. Вход HEAD `d61469f`, дерево зоны на входе чистое.
|
||
> ⛔ Границы заданы оркестратором и приняты: **398** и **399** (логика служебного обхода), **400** и
|
||
> носители мусора на диске (**401**а/б/в) НЕ берутся — последние два потому, что я их не читала, и
|
||
> давать себе предмет, которого не смотрела, под видом «доделать» нельзя.
|
||
|
||
### 392 — гонка имени юнита: механизм назван точно, и он тоньше, чем строка
|
||
|
||
Строка говорит «между коммитом намерения и вызовом systemd свип успевает рестартовать». Разбор кода
|
||
показывает, что бóльшая часть этого окна УЖЕ закрыта, и закрыта хорошо: `reopen` отказывается
|
||
рестартовать прогон с записанным намерением (`ErrStopRequested`), а `RequestStop` читает имя юнита
|
||
подзапросом «последняя не кончившаяся попытка» — то есть при любом порядке КОММИТОВ ответ верен.
|
||
|
||
⇒ **Настоящая дыра — не в порядке коммитов, а в изоляции READ COMMITTED.** `RequestStop` сегодня один
|
||
UPDATE. Когда он блокируется на замке строки `runs`, который держит идущий `reopen`, Postgres после
|
||
разблокировки пере-проверяет `WHERE` против НОВОЙ версии строки (EvalPlanQual) — но **подзапрос в
|
||
`RETURNING` считается по снимку, взятому в начале оператора**. Значит намерение ложится на новую
|
||
попытку, а имя юнита возвращается от СТАРОЙ. Обработчик шлёт сигнал мёртвому юниту, живой работает,
|
||
пользователь держит `202`.
|
||
|
||
**Чинить так:** `RequestStop` становится транзакцией — `select … for update` по строке прогона (в
|
||
READ COMMITTED он после взятия замка пере-читает последнюю зафиксированную версию), затем чтение
|
||
имени живой попытки, затем UPDATE. Тогда имя юнита и запись намерения принадлежат одному моменту.
|
||
**Предъявлять исполнением**, а не рассуждением: тест на настоящем Postgres, где рестарт и стоп идут
|
||
параллельно, и утверждение — какое имя юнита вернулось.
|
||
|
||
⚠ **Про стенд, честно и заранее.** Цепочку «свип → `Stop` → настоящий systemd» по-прежнему не гоняет
|
||
ни один тест, и я такого стенда не строю: он не влезает в этот заход. Но предмет 392 живёт НЕ там —
|
||
он живёт в изоляции транзакции, и её стенд в зоне есть (живой Postgres). ⇒ доказательство будет
|
||
полным для гонки и неполным для цепочки; второе пойдёт в «что не удалось», как и предупредил
|
||
оркестратор.
|
||
|
||
### 394 — «убили своим грейсом» читается как поломка деплоя
|
||
|
||
**Замерено исполнением** (юнит нашей формы, `TimeoutStopSec=3`, процесс ловит SIGTERM и не выходит):
|
||
маркер, который пишет systemd, — `{"result":"timeout","code":"killed","status":"KILL"}`.
|
||
**Контрольная посадка на том же стенде** (тот же юнит, процесс выходит по SIGTERM) —
|
||
`{"result":"exit-code","code":"exited","status":"5"}`. То есть тройка `timeout/killed/KILL` и есть
|
||
«сработал НАШ `TimeoutStopSec`», и прибор эти два случая различает.
|
||
|
||
Сегодня она попадает в общий список с `oom-kill` и даёт `failed` + `service_error`
|
||
(`reconcile.go`, `failureReason`). ⛔ **И довод, записанный над этим списком, ДЛЯ `timeout` не
|
||
работает**: там сказано «an out-of-memory kill and a stop-timeout SIGKILL both told the client that
|
||
retrying would help, which for those two is exactly false: the next spawn dies the same way». Для OOM
|
||
это верно — следующий процесс упрётся в тот же лимит. Для убийства по грейсу неверно: следующий
|
||
процесс НИКТО не останавливает, и работа его предшественника лежит в чекпоинтах, то есть повтор не
|
||
покупается заново.
|
||
|
||
⇒ **Делаю: `timeout` уходит из группы `service_error` в `interrupted`** — «ретрай и есть лекарство» —
|
||
и довод пишется на месте, вместе с тем, почему общий довод не покрывает этот случай.
|
||
⛔ **ОБЪЯВЛЯЮ ГРОМКО: это правка ПРОДУКТОВО ВИДИМОГО поведения поверх решения с записанным доводом.**
|
||
Она в моей зоне и по предмету строки, но если оркестратор считает, что вопрос «предлагать ли
|
||
пользователю повтор» — не мой, пусть скажет, и я откачу этот пункт, оставив вторую половину.
|
||
⚠ **Чего я НЕ делаю и почему:** не перевожу такой прогон в `stopped`. Соблазн есть — грейс срабатывает
|
||
только после того, как остановку у systemd попросили, — но при перезагрузке хоста менеджер
|
||
останавливает все юниты, и прогон, не успевший свернуться, получил бы `stopped` и остался бы мёртвым
|
||
после штатного ребута. Это ровно та асимметрия, ради которой написан `interruptedBySomeoneElse`.
|
||
|
||
### 401 (вторая половина) — отметка о расчёте может остаться пустой навсегда
|
||
|
||
Расчёт и отметка — ДВА действия: `Settle` закрывает резервацию своей транзакцией, `MarkSettled`
|
||
ставит `settled_at` отдельным оператором (`reconcile.go`, четыре места). Смерть процесса между ними
|
||
оставляет леджер верным, а столбец пустым — **навсегда**, потому что рабочий список `UnsettledRuns`
|
||
ключуется на ОТКРЫТОЙ резервации, а она уже закрыта. Читателей у столбца вне тестов ноль (пере-снято:
|
||
три писателя, одна очистка, ноль чтений; контроль — 15 упоминаний в тестах).
|
||
|
||
**Чинить самолечением, а не новым состоянием.** Отметка ВЫВОДИМА: «прогон кончился И у него нет
|
||
открытых резерваций». Значит фаза расчёта одним оператором доштамповывает такие прогоны, и «навсегда»
|
||
превращается в «до следующего прохода». Ни новой колонки, ни нового смысла.
|
||
⚠ Альтернатива — удалить столбец, у которого ноль читателей, — отвергается: он несёт СМЫСЛ, который
|
||
охраняет ветка `--release-hold` («a field that lies is a field the next reader believes»), и удаление
|
||
было бы решением про будущее, которого я не знаю.
|
||
|
||
### Порядок и почему такой
|
||
|
||
**401 → 394 → 392.** От дешёвого и однозначного к тому, что меняет продуктовое поведение, и дальше к
|
||
тому, что требует конкурентного стенда: если заход придётся сдавать неполным, неотданным останется
|
||
предмет с самым узким радиусом, а не самый дешёвый.
|
||
|
||
### Что предъявляю исполнением
|
||
|
||
1. **392** — конкурентный тест на живом Postgres: рестарт и стоп в параллель, утверждение про
|
||
ВОЗВРАЩЁННОЕ имя юнита; мутация — вернуть одиночный UPDATE, ждать красноты по тексту.
|
||
2. **394** — маркер грейс-убийства уже замерен (выше, с контрольной посадкой); в батарее — пин на
|
||
разбор этого маркера, с контролем на `oom-kill`, который обязан ОСТАТЬСЯ `service_error`.
|
||
3. **401** — тест: расчёт прошёл, отметка не поставлена (симулируем смерть между двумя действиями),
|
||
следующий проход её ставит; контроль — живой прогон отметку НЕ получает.
|
||
|
||
### Мандат самопроверки
|
||
|
||
Мутация обязательна и засчитывается по ТЕКСТУ падения, а не по цвету. Отдельным заходом —
|
||
адверсариальный проход по своей готовой работе. Секция «что не удалось» непустая по построению:
|
||
цепочка «свип → systemd» в ней уже есть.
|
||
|
||
## ДОФИКС ПО ОТМЕНЁННОМУ ПАКУ: `--no-block` С ПИНОМ (11.09, `textmachine-5c`)
|
||
|
||
> Взят владельцем ОТДЕЛЬНО от отменённого пака и взят ровно в той форме, которую зона просила: правка
|
||
> вместе с пином, а не правка с довеском. Довод, который дошёл дословно: без пина в коде остаётся
|
||
> утверждение «asking again is free and idempotent», которое держится на недокументированном
|
||
> поведении systemd и которое никто не проверяет.
|
||
> ⛔ **Границы захода:** только это. Ни режима остановки, ни второго сигнала, ни кадра событий — пак
|
||
> отменён и остаётся отменённым.
|
||
|
||
### Что сделано, тремя правками
|
||
|
||
**1. `Runner.Stop` больше не ждёт конца прогона** (`platform/internal/runner/runner.go`). Замер, ради
|
||
которого правка и берётся, уже в журнале решений: блокирующая форма вернулась через **15,058 с**
|
||
(по SIGKILL, юнит с `TimeoutStopSec=15`), `stop --no-block` — через **0,014 с**. Комментарий над
|
||
функцией несёт оба следствия и НЕ несёт третьего, которого нет (см. правку 3).
|
||
|
||
**2. Пин на то, что раньше держалось на удаче** —
|
||
`runner.TestARepeatedStopNeitherSignalsNorExtendsTheGrace`, живой, на настоящем systemd. Он
|
||
спрашивает у systemd ровно те два свойства, которые УТВЕРЖДАЕТ комментарий свипа: повторный `stop` на
|
||
юните в `deactivating` (а) не доставляет второго сигнала и (б) не перезапускает `TimeoutStopSec`.
|
||
Числа прогона: три стопа в t+0 / t+2 / t+4 против грейса 6 с → процесс получил **1** сигнал, юнит
|
||
умер на **t+6,06 с**.
|
||
⚠ **Контроль встроен в сами числа, отдельного прогона не требуется, и это сказано в теле теста:**
|
||
«один сигнал» не читается как отсутствие, потому что тот же лог доказывает, что фикстура сигналы
|
||
СЛЫШИТ (первый стоп доставил); а юнит не «просто не убит» — он УБИТ, и утверждение о том, по чьим
|
||
часам: по первому стопу (t+6), тогда как перезапущенный таймер дал бы t+10.
|
||
|
||
**3. Комментарий свипа перестал утверждать без основания** (`platform/internal/runs/reconcile.go`,
|
||
ветка пере-выдачи стопа): «free and idempotent» теперь названо ЗАМЕРЕННЫМ свойством systemd, с обеими
|
||
половинами и с ценой потери каждой, и со ссылкой на пин.
|
||
|
||
### Мутационная проверка — выполнена, на КОПИИ, три посадки
|
||
|
||
⚠ Это то, чего смена не сделала в отменённом паке и записала в «что не удалось». Здесь сделано.
|
||
Копия дерева зоны жила в `~/.cache/tm-5c/mut/platform` (не в общем скретчпаде — его чистит не только
|
||
свой процесс), перед каждой правкой утверждались `test -f go.mod` и точный `pwd`; после прогона копия
|
||
сверена с деревом (`diff -rq` по `internal/runner` — идентично) и удалена.
|
||
|
||
| посадка | что сломано | вердикт и **текст** падения |
|
||
|---|---|---|
|
||
| M1 | снят `--no-block` у `Stop` | КРАСНО, `TestTheSoftStopDoesNotWaitForTheRunToEnd`: «the stop waits for the unit to be gone: [systemctl --user stop tm-run-X-1.service]» |
|
||
| M3 | пере-выдача ДОСТАВЛЯЕТ (`systemctl kill --signal=SIGTERM` вместо `Stop`) | КРАСНО: «the process received **3** signals from three stops, want exactly 1» |
|
||
| M4 | часы грейса идут от ПОЗДНЕГО стопа (первый стоп сдвинут на t+4) | КРАСНО: «the unit died at **t+10,09s** against a 6s grace: a repeated stop restarted the stop timeout» |
|
||
|
||
⭐ **Каждая посадка засчитана по ТЕКСТУ, а не по цвету:** в каждом случае сообщение называет ровно то,
|
||
что сломано. И две половины утверждения оказались независимо различающими — M3 краснит только счёт
|
||
сигналов, M4 только часы.
|
||
⚠ **M1 живой пин НЕ ловит, и это названо, а не обойдено:** без `--no-block` первый стоп блокируется на
|
||
весь грейс, остальные два приходят уже на мёртвый юнит, и числа сходятся прежние. Флаг ловит
|
||
argv-пин, который для того и написан отдельно — живой тест скипается на хосте без пользовательского
|
||
менеджера systemd, и флаг на таком хосте остался бы непроверенным вовсе.
|
||
⚠ **Флейковость проверена, а не предположена:** живой пин прогнан **5 раз подряд** — 5 зелёных,
|
||
разброс смерти 6,04–6,12 с при пороге 8 с, счёт сигналов 1 во всех пяти. Пин не маргинален ни по
|
||
одной из двух осей.
|
||
|
||
### Числа
|
||
|
||
```
|
||
$ TM_PLATFORM_TEST_DSN=…55433 TM_PLATFORM_TEST_ENGINE_BIN=… TM_PLATFORM_TEST_BOOK_TEMPLATE=… \
|
||
TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
|
||
MAKE-EXIT=0 · пакетов `ok` 20 · строк FAIL 0 · линтер «0 issues» · скипов 5
|
||
gofmt / go vet / sqlc diff чисты · ALARM PD-count: 15 (baseline 15) — не сдвинут
|
||
скипы поимённо, условие у всех ОДНО и названное — нет деплой-артефакта
|
||
`configs/mining-contrast.zh.txt`: TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses ·
|
||
TestALivePreviewWritesNothingAndALiveApplyWrites · TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing ·
|
||
TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber · TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem
|
||
|
||
$ go test ./internal/runner/ -run TestARepeatedStopNeitherSignalsNorExtendsTheGrace -count=1 -v (×5)
|
||
5 зелёных · сигналов 1 во всех пяти · смерть юнита 6,04 / 6,09 / 6,09 / 6,10 / 6,12 с при пороге 8 с
|
||
```
|
||
⚠ **Новый пин ВНУТРИ батареи отработал, а не проскочил:** цель `check` гоняет `-v` и печатает КАЖДЫЙ
|
||
`--- SKIP`; скипов ровно пять, и пина среди них нет, при нуле падений. Числа выше — из отдельных
|
||
прогонов той же командой, потому что `t.Logf` в сводку `make check` не попадает.
|
||
⚠ Гейты стенда закрыты все четыре намеренно: без `TM_PLATFORM_TEST_DSN` та же батарея печатает те же
|
||
`ok` и прячет ~370 тестов (эррата `08.09-д`).
|
||
|
||
### Что НЕ удалось
|
||
|
||
**1. Живой пин не покрывает саму правку, и это названо выше, а не обойдено** — снятие `--no-block`
|
||
ловит только argv-пин. Разделение осознанное (живой тест скипается без пользовательского менеджера
|
||
systemd), но означает: на хосте без systemd свойство «стоп не ждёт» проверено, а «systemd инертен к
|
||
повтору» — нет, и это не чинится в зоне.
|
||
|
||
**2. Пин говорит о ЮНИТЕ, а утверждение живёт в СВИПЕ.** Тест проверяет `Runner.Stop` напрямую;
|
||
никакой тест не гоняет пере-выдачу ЧЕРЕЗ реконсайлер против настоящего systemd — такого стенда в зоне
|
||
нет. То есть цепочка «свип → `Stop` → systemd» на живом юните не пройдена ни разу, и если однажды
|
||
свип начнёт звать не `Stop`, а что-то другое, пин этого не заметит.
|
||
|
||
**3. Ущерб от блокировки не предъявлен исполнением — ни одна половина.** Что запрос повиснет, а свип
|
||
запишет ложный клин, взято ЧТЕНИЕМ кода (`reconcileOne`, комментарий про израсходованный бюджет) плюс
|
||
замером самой блокировки. Сценария «блокирующий стоп внутри прохода довёл прогон до отсрочки» я не
|
||
ставила; на предмет правки это не влияет, но утверждение о механизме ущерба остаётся выведенным.
|
||
|
||
**4. Прочие девять адресов улова не тронуты** — они вне границ захода и живут строками бэклога
|
||
**392**–**397**.
|
||
|
||
## ПАК «ДВЕ ОСТАНОВКИ» — ОТМЕНЁН ВЛАДЕЛЬЦЕМ, РАБОТА ОТКАЧЕНА (10.09, `textmachine-5c`)
|
||
|
||
> Промт `docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md` (редакция `ac9a24d`), вход HEAD `ac9a24d`.
|
||
> **Отменён владельцем по размаху**, а не по качеству: две остановки того не стоят. Новый инвариант
|
||
> объявлен тем же словом: **стоп остаётся ЖЁСТКИМ, терять деньги ДОПУСТИМО, но остановка обязана быть
|
||
> КОРРЕКТНОЙ** — без гонок и половинчатых состояний, с правильным возобновлением. Предмет переехал с
|
||
> «не сжечь деньги» на «не оставить мусор».
|
||
>
|
||
> ⚠ **Эта секция — не отчёт о построенном, а НАЙДЕННОЕ.** Код зоны возвращён к HEAD целиком (кроме
|
||
> константы версии контракта, см. ниже). Здесь остаётся то, что зона УЗНАЛА о собственном дереве:
|
||
> оно пережило пак и стоит дороже, чем стоил бы код.
|
||
|
||
### Что откачено и что оставлено
|
||
|
||
Возвращены к HEAD **26** путей в `platform/`. Удалены девять моих новых файлов:
|
||
`internal/gates/stopgrace_test.go` · `internal/httpapi/stopmode_test.go` ·
|
||
`internal/pgstore/migrations/00035_stop_mode.sql` · `internal/pgstore/migrations/00036_estimated_and_stop_ledger.sql` ·
|
||
`internal/pgstore/stopmode_test.go` · `internal/runner/stop_systemd_test.go` · `internal/runner/stop_test.go` ·
|
||
`internal/runs/estimated_test.go` · `internal/runs/stopmode_test.go`.
|
||
|
||
**Оставлено ОДНО и по прямому указанию оркестратора:** `internal/httpapi/capabilities.go`, константа
|
||
`ContractVersion`. Это чужой долг, взятый попутно: канон уехал на `0.13.1` актом `D39.235` п.6, а
|
||
константа осталась на `0.13.0`, и гейт `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||
краснел с момента ратификации — базовая линия зоны была КРАСНОЙ на входе, и не по вине пака.
|
||
⚠ В ходе пака константа стояла на `0.14.0` (свой минор: `mode` у `/stop` плюс `Run.stop_mode`, по
|
||
ратифицированному порядку «код первым»); с отменой пака этого минора нет, поэтому при откате она
|
||
приведена к **`0.13.1`** — к тому, чему билд действительно служит.
|
||
|
||
### ⛔ Что зона узнала о СЕБЕ — это и есть результат смены
|
||
|
||
Ниже разделено на «замерено исполнением», «прочитано в коде» и «латентно» намеренно: смешивать их
|
||
дороже обычного, потому что по ним заводятся строки. Адреса — по HEAD `ac9a24d`.
|
||
|
||
**1. `Runner.Stop` блокируется на весь грейс — ЗАМЕРЕНО.** `internal/runner/runner.go:195` зовёт
|
||
`systemctl --user stop` без `--no-block`, `runCommand` (`:106-112`) своего тайм-аута не имеет.
|
||
Пробник на транзиентном юните с `TimeoutStopSec=15`, процесс ловит SIGTERM и не выходит (systemd 259):
|
||
блокирующая форма вернулась через **15,058 с** (по SIGKILL), `stop --no-block` — через **0,014 с**,
|
||
юнит в `deactivating`. Сегодня движок на жёстком сигнале умирает за секунды, поэтому это невидимо.
|
||
⚠ **Испр. 11.09 по эррате `10.09-з`:** носитель последствия НЕ `WriteTimeout` — тридцать секунд стоят у
|
||
слушателя МЕТРИК (`cmd/tmplatformd/main.go:204-208`, комментарий там прямо говорит «for once a WriteTimeout
|
||
too: nothing here streams»), а у API-слушателя его НЕТ намеренно (`internal/httpapi/serve.go:36`, «No
|
||
WriteTimeout» — срезал бы SSE). Пере-снято моей рукой: во всей зоне вне тестов `WriteTimeout`
|
||
УСТАНАВЛИВАЕТСЯ **в одном месте** — `main.go:208`. Контроль, что прибор спрашивал существующее:
|
||
`grep -rn WriteTimeout platform/ --include=*.go | grep -v _test.go` на `1c5bd2a` даёт четыре строки, из
|
||
которых три — комментарии, и ДВЕ из них объясняют, почему у API-слушателя его нет.
|
||
⭐ И механизм, на который поправка меняет ущерб, в зоне УЖЕ ЗАРЕГИСТРИРОВАН: `PD-103` — «зависший Postgres
|
||
паркует хендлеры и ждущих в пуле, пока клиент сам не уйдёт», ровно потому что `WriteTimeout` у сервера
|
||
отсутствует по проекту (SSE) и `TimeoutHandler` в цепочке нет. Блокирующий `Stop` — тот же класс, другой
|
||
источник блокировки; ⇒ поправка садится на существующий носитель, а не заводит новое утверждение. ⇒ ущерб не «оборванный `202`», а **зависший запрос с удержанной горутиной обработчика**
|
||
(контракт описывает этот вызов АСИНХРОННЫМ) плюс ложный клин свипа (п.2).
|
||
|
||
**2. Израсходованный бюджет прогона свип считает ПРОВАЛОМ реконсиляции — прочитано в коде.**
|
||
`reconcileOne` (`internal/runs/reconcile.go:89-90`) заворачивает каждый прогон в `s.runBudget()`
|
||
(дефолт 60 с), и комментарий на `:92-115` говорит буквально «SPENDING the budget counts as a failure
|
||
even when nothing reported one». ⇒ блокирующий `Stop` внутри свипа (`:580`) даёт `ReconcileFailures++`,
|
||
отсрочку и через `StalledAfter` ЛОЖНУЮ тревогу оператору, плюс съеденную долю прохода для всех
|
||
остальных аккаунтов. ⚠ Прежняя формулировка оркестратора («свип встанет до двадцати минут») неверна
|
||
по механизму и исправлена актом `D39.236` п.5 по возражению этой смены.
|
||
|
||
**3. Повторный `stop` на юните в `deactivating` ИНЕРТЕН — ЗАМЕРЕНО, и это несущее свойство.**
|
||
Три стопа в t+0 / t+2 / t+4 против грейса 6 с: процесс получил **один** сигнал, юнит умер на
|
||
**t+6,04 с** (не t+10). То есть пере-выдача не шлёт второго сигнала и не перезапускает
|
||
`TimeoutStopSec`. ⭐ Ценность не в хорошей половине, а в том, что комментарий свипа
|
||
(`reconcile.go:576-579`) УЖЕ утверждает «asking again is free and idempotent», и это утверждение
|
||
держится на недокументированном поведении systemd, которое до этой смены никто не проверял.
|
||
⚠ И вторая половина того же комментария — «it is the only thing that closes the first case» —
|
||
**не выполняется**, если юнит уже начал останавливаться: пропущенный первый SIGTERM свип не чинит,
|
||
юнит стоит в `deactivating` до SIGKILL по грейсу (сегодня — до десяти минут прогона, который ничего
|
||
не делает, с зарезервированным холдом).
|
||
|
||
**4. Два SIGTERM подряд Go-процесс видит как ОДИН — ЗАМЕРЕНО, ратифицировано `D39.238`.**
|
||
gap 0 мс → оба сигнала увидены в **0 из 20** прогонов (в длинной серии слиплись 40 из 40);
|
||
gap 1 мс → **20 из 20**; буфер принимающего канала **8** — коалесценция живёт НИЖЕ канала, в
|
||
рантаймовом БИТЕ на сигнал. ⚠ Первый мой прибор давал «2 из 2» и подтверждал удобное: `kill` шёл из
|
||
шелла, и запуск двух процессов сам создавал зазор. Прибор чинен, число перевернулось.
|
||
Норма, выведенная из этого оркестратором: **замер, чей результат подтверждает удобное, проверяется на
|
||
ПРИБОР прежде, чем на предмет.**
|
||
|
||
**5. ⛔ `finish` рассчитывает деньги из ДО-ДРЕНАЖНОЙ выписки — ЛАТЕНТНО, но класс настоящий.**
|
||
`reconcile.go:955-958`: расчёт внутри `finish` объявлен оппортунистическим и получает тот самый `l`,
|
||
который проход прочитал ДО дренажа журнала. `finish` вручную освежает `l.Status`/`l.PausedReason`
|
||
перед вызовом (`:~918`) — то есть автор знал про класс и закрыл ровно два поля. **Любое число, которое
|
||
дренаж ЭТОГО ЖЕ прохода записал в строку попытки, для этого расчёта невидимо.**
|
||
⚠ Граница честности: на HEAD ничего не сломано — `settle` не читает из выписки ни одного поля, которое
|
||
пишет дренаж. Класс предъявлен исполнением только потому, что пак добавил такое поле: строка попытки
|
||
читала 9 оценочных строк, выписка — 0, и расчёт взял число, которое случайно знал запасной канал.
|
||
Поле ушло с паком, класс остался. Родственник в этом же файле уже стоил зоне дефекта: `freshRunState`
|
||
(`reconcile.go:~660`) написан ровно потому, что выписка не может знать, что записал её собственный дренаж.
|
||
|
||
**6. Обработчик `/stop` шлёт сигнал по имени юнита из СВОЕЙ ЖЕ выписки — прочитано в коде.**
|
||
`Service.Stop` (`reconcile.go:1541`) берёт `unit` из `RequestStop` и зовёт `Runner.Stop(ctx, unit)`.
|
||
Успел ли свип между этими двумя рестартовать попытку — сигнал уйдёт СТАРОМУ юниту. Самолечение есть
|
||
(следующий проход увидит `alive && StopRequestedAt != nil` и пере-выдаст стоп текущему, `:575-583`),
|
||
но приходит оно от СВИПА, а не от обработчика: окно равно одному тику, и всё это время пользователь
|
||
держит `202`, а прогон работает.
|
||
|
||
**7. Двойной клик по `/stop` — гонка по построению, сегодня безвредная.** `RequestStop`
|
||
(`internal/pgstore/runs.go:1298-1309`) — один UPDATE; под READ COMMITTED два одновременных нажатия
|
||
не различимы самим оператором, потому что `RETURNING` отвечает НОВОЙ строкой и не может отличить
|
||
собственную запись от чужой. Схлопывание в первый таймстамп делает оба нажатия одинаковыми, поэтому
|
||
вреда нет — ровно до той секунды, когда ответ или действие начнут зависеть от того, кто нажал первым.
|
||
Форма лечения, проверенная в отменённом паке: `select … for update` в одной транзакции с UPDATE.
|
||
|
||
**8. `stoppedOnRequest` сравнивает ДВОЕ РАЗНЫХ ЧАСОВ, и код это признаёт.** `reconcile.go:1137-1153`,
|
||
довод на `:~1000`: «the two clocks being compared are not even the same one — the marker carries the
|
||
host's, the intent the platform's». Сегодня цена ошибки — ЯРЛЫК. В любой конструкции, где от этого
|
||
ярлыка зависит ДЕЙСТВИЕ (перезапуск, возврат холда, возобновление), это гонка по построению.
|
||
|
||
**9. Каталог движка: потолка ожидания нет у пяти провайдеров из восьми — ЗАМЕРЕНО прибором.**
|
||
Программа на `config.LoadModels` + `Timeouts.Profile().DeadlineFor(B)` над снапшотом `git archive HEAD
|
||
backend` (ни байта записи в чужую зону): `attempt_max_s` задан у **3** (`deepseek` 1240 · `kimi` 1200 ·
|
||
`gemini` 1200), инертен у **5** (`zai` · `xai` · `openai` · `mistral` · `local`), и у них `DeadlineFor`
|
||
растёт с бюджетом линейно и сверху не ограничена. Максимум по каталогу при наибольшем бюджете
|
||
сегодняшней конфигурации (**35 200** ток. = редакторские `max(8000×2.2, 16000) = 17 600` с одним
|
||
удвоением по `regenerate_before_escalate: 1`) — **1240 с**, deepseek. Беcключевой `zai` перерастает его
|
||
при **43 400** ток., то есть при `edit_ceiling_out` ≈ 9 900 против сегодняшних 8 000 — одна строка
|
||
yaml, которая читается как ручка качества. Носитель — строка бэклога **383**.
|
||
|
||
**10. Dev и прод расходятся по грейсу в 20 раз, и это не объявлено нигде.**
|
||
`internal/ingest/supervisor.go:18` — `stopGrace = 30 * time.Second` (уезжает в `cmd.WaitDelay`, `:75`)
|
||
против `600 с` в проде (`internal/runner/runner.go:59`). Один и тот же движок против тех же платных
|
||
провайдеров получает на свёртку полминуты на dev-пути и десять минут на боевом.
|
||
|
||
**11. Пять открытых рядов реестра — про холды и остановку, и это один предмет, а не пять.**
|
||
Их печатает сам гейт зоны (`make check`, `internal/gates`, строка `ALARM PD-count: 15 open rows …
|
||
(baseline 15)`): **PD-418** (у settling-строки живого прогона нет ручки, а рантбук обещает обратное) ·
|
||
**PD-244** (единственный выход `settle`, оставляющий холд открытым МОЛЧА) · **PD-162** (книга с
|
||
пропавшим каталогом заклинивает прогон навсегда с открытым холдом) · **PD-465** (заявка на спавн
|
||
коммитится ДО подъёма юнита) · **PD-217** (незакрытый холд блокирует апгрейд движка бессрочно).
|
||
Под новым инвариантом это готовый предметный список, уже приоритизированный реестром.
|
||
|
||
**12. Что в дереве оказалось ЛУЧШЕ ожидаемого — и это тоже находка.** Гонка «кто пишет исход мёртвого
|
||
прогона» ЗАКРЫТА, и закрыта правильной формой: `FinishRun` возвращает `closed bool`, и `finish`
|
||
(`reconcile.go:903`, ветка `!closed` ~`:937`) разбирает случай «прогон уехал под этим проходом» явно;
|
||
второй расчёт получает `ErrNoReservation`, потому что резервация закрывается под `state = 'open'`.
|
||
Столкновение реконсайлера с `/stop` названо в трёх местах и в каждом закрыто: `finishStopped`
|
||
(`:678-685`), отказ рестарта по `ErrStopRequested` (`:1324`), пере-проверка заявки под замком в
|
||
`FinishUnspawnedStop` (`:~710`). ⇒ **модель «записать условно и прочитать вердикт» уже живёт в зоне**,
|
||
и новому инварианту стоит брать её образцом, а не изобретать другую.
|
||
|
||
**13. И одна развилка, которую новый инвариант обязан пересмотреть ЯВНО.**
|
||
`interruptedBySomeoneElse` (`reconcile.go:539`, функция `:1129`) ПЕРЕЗАПУСКАЕТ прогон по коду 5 без
|
||
записанного намерения. Это осознанное решение с записанным доводом (ребут против ручного
|
||
`systemctl stop`, асимметрия цен), а не дефект — но это главная развилка «правильного возобновления»,
|
||
и наследовать её молча нельзя.
|
||
|
||
### Числа смены
|
||
|
||
```
|
||
$ make check (все четыре гейта стенда закрыты: Postgres 55433 · движковый бинарь и шаблон из
|
||
`git archive HEAD backend` · достижимый пользовательский systemd · MemoryMax)
|
||
вход (до первой правки): MAKE-EXIT=2 · 19 `ok` · 1 FAIL · линтер «0 issues» · скипов 5
|
||
единственный красный — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||
(«this build announces contract 0.13.0 and the ratified canon is 0.13.1»), НЕ по вине смены
|
||
скипы 5, условие у всех одно и названное: нет деплой-артефакта `configs/mining-contrast.zh.txt`
|
||
|
||
ПОСЛЕ ОТКАТА (последний прогон, снят после последней правки):
|
||
MAKE-EXIT=0 · 20 `ok` · 0 FAIL · линтер «0 issues» · скипов 5
|
||
`gofmt`/`go vet`/`sqlc diff` чисты · `ALARM PD-count: 15 open rows … (baseline 15)` — не сдвинут
|
||
красный входа ПОГАШЕН правкой константы: канон `0.13.1`, билд `0.13.1`
|
||
```
|
||
⚠ Гейты стенда закрыты все четыре намеренно: без `TM_PLATFORM_TEST_DSN` та же батарея печатает те же
|
||
`ok` и прячет ~370 тестов (эррата `08.09-д`). Счёт скипов печатается рядом со счётом `ok`.
|
||
|
||
### Что НЕ удалось
|
||
|
||
**Ничего из заказанного не сдано — пак отменён на середине, и это не оценка работы, а факт.**
|
||
Построенное и зелёное на момент отмены (грейс суммой с названными слагаемыми, `--no-block`, путь
|
||
второго сигнала через наблюдение `SubState`, режим в записи намерения с атомарной заявкой на второй
|
||
сигнал, чтение минора `1.4` из обоих каналов, пометка оценки на строке расчёта, живой systemd-стенд)
|
||
откачено целиком. Из адверсариального прохода §5.5 успели отработать три направления из шести —
|
||
(а) гонка двух нажатий, (б) остановка мёртвого юнита, (д) пере-выдача не должна эскалировать; (в)
|
||
чтение мягко остановленного прогона реконсиляцией, (г) идемпотентность `/stop` и (е) удлинённое окно
|
||
`deactivating` до отмены проверены не были. Мутационная проверка СВОИХ новых пинов — та, которую
|
||
норма зоны требует отдельным заходом, — не выполнена ни для одного из них: до неё смена не дошла.
|
||
Поэтому ни один пин выше в дереве не остаётся, и ни на один нельзя ссылаться как на проверенный.
|
||
|
||
## Состояние зоны на 08.09.2026
|
||
|
||
| Вопрос | Ответ |
|
||
|---|---|
|
||
| последний заленджённый пак | «разрез приёма до готовности и правда о себе» (08.09, `ddcbf0c`, акт **D39.229**), канон контракта `0.13.0`. ⚠ Обе величины берутся ПРИБОРОМ, а не отсюда: `git log --oneline -1 -- platform/` и `grep '^ version:' ../../docs/architecture/14-api-contract/openapi.yaml` — эта строка стареет, они нет. ⭐ И стареет она БЫСТРЕЕ, чем кажется: её уже правил этот пак по §4.5, и лендинг того же пака сделал её неверной снова — читай прибором, а не глазами |
|
||
| пак в дереве, не закоммиченный | дофикс по паку «разрез приёма» (две позиции, 08.09) — отчёт ниже |
|
||
| открытые дефекты | `DEFECT_REGISTER.md` (счёт — `python3 docs/scripts/counts.py` от корня) |
|
||
| нормы и приёмка | `ENGINEERING_STANDARDS.md` · направление — `PLATFORM_DIRECTION.md` · стек и стенд — `STACK_DECISIONS.md` |
|
||
| незакрытые куски работы | `../BACKLOG.md` (`П-N`) |
|
||
| как разворачивается | `../deploy/README.md` |
|
||
|
||
## ДОФИКС ПО ПАКУ «РАЗРЕЗ ПРИЁМА» — ДВЕ ПОЗИЦИИ (08.09, `textmachine-fa`, после лендинга `ddcbf0c`)
|
||
|
||
> Заказ оркестратора №23: он закрывал пробел в СВОЕЙ приёмке (норма требует двух верификаторов по
|
||
> сданной работе, он их не поставил и поставил задним числом), и слепой верификатор нашёл две вещи в
|
||
> моей зоне. Обе я пере-проверила прежде, чем чинить. Правило остановки объявлено заказчиком заранее:
|
||
> дофикса ровно два, дальнейшее — строка бэклога.
|
||
|
||
**1. Рукописная тройка в резерве — класс `D39.216` ВНУТРИ пака, нанятого его убрать.**
|
||
`cutTailReserve()` возвращал `3 * s.write()`, где тройка — ручной счёт записей, следующих за разрезом,
|
||
и жила она только в докстринге. Замер подтверждён мной: **0 хитов в тестах** при 3 хитах в не-тестах.
|
||
Четвёртая пост-разрезная запись — и резерв тихо недокрывает, а последствие называет ⛔-абзац моего же
|
||
`stepLeaving`: оборванная терминальная запись, книга в `parsing` без задания до свипа.
|
||
|
||
⇒ Выбрана вторая из двух названных форм — **тройка ПИНИТСЯ**, а не выводится. Довод против первой,
|
||
чтобы он не пропал: вывести резерв структурно значит дать прогулке список её оставшихся шагов, то есть
|
||
вернуть тот самый рукописный перечень одним уровнем ниже. Вместо этого — сверка с ДРУГИМ выражением
|
||
того же факта: `UploadSettle` уже собирает прогулку из разреза и четырёх записей, ровно одна из
|
||
которых (`StartParsing`) идёт ДО разреза, значит резерв обязан равняться остатку прогулки после выноса
|
||
разреза и этой одной записи. **Два выражения делят константы, но не маршрут** — потому это сверка, а
|
||
не тавтология.
|
||
|
||
Предъявлено посадкой: пятая запись, добавленная в `UploadSettle` и НЕ добавленная в счёт, красит
|
||
`TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt` — и он **ЕДИНСТВЕННЫЙ красный** на трёх
|
||
пакетах (`books`, `config`, `httpapi`), текстом: «the cut reserves 1m30s … but the walk (4m0s) has
|
||
2m0s left once the cut and the write before it are taken out».
|
||
|
||
**2. `PD-464` нёс ровно ту протухшую фразу, которую пак снял с трёх других рядов.**
|
||
Диспозиция говорила «(08.09, в дереве, статус флипает лендинг)» при колонке статуса `fixed` и
|
||
состоявшемся лендинге. Замер: рядов с этой фразой в файле был **ровно один — мой собственный**.
|
||
Приведена к лендингу (`ЗАЛАНДЁН ddcbf0c, акт D39.229`); теперь фразы в файле **0** при 465 рядах,
|
||
колонок ≠ 7 — **0**, `counts.py --check` — **EXIT=0**.
|
||
|
||
⭐ **Класс, из-за которого это уцелело, стоит назвать: норма §4.5 сработала на ТРЁХ чужих рядах и не
|
||
сработала на ОДНОМ моём.** Ряд я писала сама и потому не подпала под собственную правку. Тот же класс
|
||
оркестратор поймал у себя часом раньше на строке 253. Общее правило: **проход по норме обязан включать
|
||
строки, которые этот же проход и создал** — иначе он чистит только унаследованное.
|
||
|
||
**Числа дофикса, сняты после последней правки кода:** `make check` со всеми четырьмя гейтами —
|
||
**MAKE-EXIT=0** · пакетов `ok` **20** · строк FAIL **0** · линтер «**0 issues**» · скипов **5** (условие
|
||
прежнее и названное). Моих файлов в дереве — **4, все в `platform/`**. ⚠ `git status` показывает **7**: остальные три
|
||
(`docs/BACKLOG.md`, `docs/ORCHESTRATOR_SESSION_PROMPT.md`, `docs/architecture/05-decisions-log.md`) —
|
||
живая работа оркестратора в ЕГО зоне, идущая параллельно. Не мои и не тронуты; называю их, чтобы «все в
|
||
моей зоне» не читалось как «в дереве больше ничего нет». Якоря дофикс НЕ сдвинул: 7 проблемных на
|
||
`HEAD` и 7 у меня, новых ноль (замер дифференциальный, как в основном отчёте).
|
||
⚠ Код выхода взят на этот раз ВЕРНО и подтверждён вторым источником: в прошлый раз я написала
|
||
`echo "MAKE-EXIT=$?"` после подстановки `$(git rev-parse …)`, и `$?` ловил код `git`, а не `make` —
|
||
напечатанный ноль не значил ничего. Теперь `st=$?` стоит сразу за `make`, и рядом вердикт самой цели:
|
||
`make check` СОХРАНЯЕТ `.check.log.<pid>` при провале и удаляет при успехе, лога нет.
|
||
|
||
**Попутно — прибрала свой мусор на общем стенде.** Убитые прогоны (в том числе мой, когда я гасила
|
||
зависший пакет) оставили в общем Postgres **34** скретч-базы ≈9,5 МБ каждая. Удалены все 34, отказов
|
||
**0**, осталось **0**. ⚠ Инструмент выбран самоохраняющийся: обычный `drop database` БЕЗ `with (force)`
|
||
— он ОТКАЖЕТ, если между листингом и дропом кто-то подключился, вместо того чтобы выдернуть базу из-под
|
||
чужого прогона. Верификатор не стал их трогать именно из-за этого риска, и это была верная осторожность.
|
||
|
||
## ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ОТЧЁТ (08.09, `textmachine-fa`)
|
||
|
||
> Промт `docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md`, вход HEAD `3f4680c`, дерево на входе чисто.
|
||
> Зона НЕ коммитит — дерево передано оркестратору №23 (`textmachine-a8`). Пак $0, платных вызовов **0**.
|
||
> **Работа завершена, править не планирую.** Сказано ПОСЛЕ адверсариального круга, а не до него: первая
|
||
> редакция этого отчёта несла ту же фразу при шести неисправленных дефектах, введённых этим паком.
|
||
> Дерево — 26 файлов, все в `platform/` (`git status --porcelain -- platform/`), 23 правленых и 3 новых.
|
||
|
||
### Исход по каждому пункту §4
|
||
|
||
| Пункт | Исход |
|
||
|---|---|
|
||
| §4.1 ограничитель параллелизма | **сделано**; форма — `x/sync/semaphore` на общей точке порождения, ожидание с деградацией в очередь. Предъявлено НАГРУЗКОЙ |
|
||
| §4.2 бюджет хвоста из кода | **сделано, форма Б (структурная)**; попутно вскрыт и закрыт ЧЕТВЁРТЫЙ промах суммы — квитанция не входила в неё |
|
||
| §4.3 рантбук | **сделано**; правок рантбука ДВЕ, вторая объявлена ниже с доводом |
|
||
| §4.4 три места неразличимого сбоя | **сделано все три**, каждое своим лечением; свип вылечен БЕЗ миграции |
|
||
| §4.5 правда о себе | **сделано**: шапка, два ряда флипнуты, маркер третьего починен, черты заэкранированы |
|
||
| §4.6 честная причина человеку | **закрыто по построению на моей стороне** — и ПРЕМИСА пака при этом опровергнута замером (ниже) |
|
||
| §4.7 живой гейт | **рецепт исполнен и РАБОТАЕТ**; довод зоны опровергнут, разрез впервые встретился с настоящим движком |
|
||
| §4.8 п.2 (комментарий `cutNow`) | **взят вместе с §4.4**, как и предписано |
|
||
| §4.8 п.4 (мёртвый `enqueue`) | **взят вместе с §4.1**: предикат сведён в одно названное место |
|
||
| §4.8 п.6 («ВСЕГДА» контракта) | **не беру — пинг оркестратору** с моим выбором из двух (ниже) |
|
||
| §4.8 остальное | **не делаю**, как объявлено паком |
|
||
| ⚠ сверх пака | константа контракта `0.12.0` → `0.13.0` — красное на входе, взято по явному указанию оркестратора (ниже) |
|
||
|
||
### Что стало с деревом — находка → что сделано → чем предъявлено
|
||
|
||
| Находка | Что стало с деревом | Чем предъявлено |
|
||
|---|---|---|
|
||
| §4.1 у синхронного входа нет ограничителя параллелизма | `internal/books/limit.go`: потолок на `semaphore.Weighted`, взводится в `s.manifest` — ОДНОЙ строке, через которую к движку идут все три входа. Дефолт — `DefaultMaxCuts = jobs.DefaultWorkers` (4), то есть число, подо что хост уже рассчитан, и носитель у него ОДИН. Конфиг `TM_PLATFORM_MAX_CUTS`; ноль отвергает читатель чисел (`loader.number`), а не отдельная проверка интейка. Наблюдаемость: 3 гейджа + 2 счётчика, публикует существующий телеметрический проход | `TestTheHostRunsNoMoreCutsAtOnceThanItsCapAllows` — 6 загрузок при потолке 2, пик **2**, и это НЕ вакуум: тест сперва дожидается контрольной величины «4 из 6 стоят в очереди» из счётчиков самого потолка · парный `TestWithRoomForEveryCutTheHostRunsThemAllAtOnce` — та же нагрузка при потолке 6 даёт пик **6** (иначе первый тест проходил бы и на фикстуре, где ничего не совпало по времени) · посадка M4 |
|
||
| упор в потолок не должен стоить пользователю загрузки | ожидание, а не отказ: не дождался ⇒ `errNotConclusive` ⇒ `201 parsing`, дорезает очередь. На очередном пути — claim обратно, НОЛЬ потраченных попыток и `river.JobSnooze`, то есть задание возвращается, не тратя единственную попытку (`giveBack` + `jobs.ErrTryAgainLater`) | `TestAnUploadThatRunsOutOfBudgetWaitingForASlotIsAcceptedRatherThanRefused` · `TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook` (claim ОТДАН и задание ПОСТАВЛЕНО — то, чего первая редакция не утверждала) · `TestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack` (оба исхода: упор в потолок и нехватка бюджета) · `TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt` · посадки M9, N7, N8 |
|
||
| §4.2 граница хвоста выводилась руками и трижды была неверна | ОДИН отсоединённый дедлайн на весь хвост (`walk`), каждый шаг берёт `min(свой бюджет, остаток)` (`step`). Шаг, добавленный завтра, границу не двигает ПО ПОСТРОЕНИЮ | `TestNoStepOfAnUploadsTailOutlivesTheWalk` — в т.ч. **50** вложенных шагов, каждый просит час · `TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget` (по дедлайну, который движок РЕАЛЬНО получил, а не по секундомеру) · `TestAStepOutsideAWalkKeepsItsOwnBudgetAndSurvivesItsCaller` · посадки M1, M2 |
|
||
| ⚠ ЧЕТВЁРТЫЙ промах той же суммы, найден моей же посадкой | квитанция идемпотентности (10 с) писалась ПОСЛЕ `Accept` и в сумму не входила ⇒ хвост был длиннее объявленного ровно на неё. Квитанция стала ТЕРМИНОМ: `UploadSettle = CutBudget + 4*writeBudget + ReceiptBudget` = **220 с** (ровно замер 07.09), носитель величины ОДИН — `books.ReceiptBudget`, тратит её `httpapi.settleCtx` | `TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget` · `TestTheReceiptSpendsTheShareTheIntakeSetAsideForIt` (httpapi) · посадки M3, M7'' |
|
||
| §4.3 рантбук молчал про таймаут ОТВЕТА прокси | `deploy/README.md`: молчание названо числом (220 с), требование к прокси — `TM_PLATFORM_UPLOAD_DEADLINE + 220 с` = 13 мин 40 с на дефолтах, цена ошибки названа (человек получает ошибку на ПРИНЯТОЙ книге и повтором делает вторую) | директивы сверены с вендор-доками ЭТОЙ сессией: nginx `proxy_read_timeout`, дефолт **60 с** (nginx.org, ngx_http_proxy_module) · HAProxy `timeout server` (docs.haproxy.org 3.0, индекс ключевых слов) |
|
||
| §4.4 `ClaimParse` в `cutNow` молчал о сбое БД | гонка и «хранилище не спросили» разведены: первая — INFO, вторая — ERROR, и текст называет следствие (книга `parsing` без задания до свипа) | `TestTheIntakeTellsALostRaceApartFromAStoreItCouldNotAsk` — обе фикстуры, и каждое сообщение проверено ОТСУТСТВУЮЩИМ в чужой |
|
||
| §4.4 ветвь `RowsAffected()==0 ⇒ задание НЕ ставить` не запинена | пин на ОБЕ стороны: чужой claim ⇒ задания нет и чужой claim цел; свой ⇒ задание есть и claim снят | `TestOnlyAClaimThatWasReallyGivenBackQueuesTheJobThatFinishesTheBook` · посадка M6' красная ТЕКСТОМ про задание |
|
||
| §4.4 свип `StuckIntake` считает от `added_at` (штамп ДО тела) | ⭐ вылечено БЕЗ миграции и без колонки: бутовый гейт сверял дедлайн с `min(UploadGrace, ClaimStale)`; добавлено ТРЕТЬЕ окно `books.ClaimGrace` (20 мин — самое узкое). Условие «свип забирает claim у идущей загрузки» стало недостижимо настройкой | посадка M5 красная текстом «does not fit the parse claim's grace» + `TestEveryWindowAnUploadMustFitInsideIsActuallyConsulted`. ⚠ **Исправление к первой редакции этой строки:** я написала, что у каждого из трёх окон свой случай, недостижимый двум другим — это БЫЛО НЕВЕРНО и найдено адверсариальным проходом. `ClaimGrace` сегодня самое узкое, поэтому все пять случаев ловит один терм, и два других можно было удалить из гейта при зелёной батарее. Вылечено выносом выбора окна в `intakeWindow(...)`, который тест кормит значениями, делающими каждое окно самым узким по очереди |
|
||
| §4.8 п.2 комментарий `cutNow` лгал о том, кто ставит задание | снят тем же движением, что и правка ветви | `grep -rn "the queue job is already enqueued" platform/ --include=*.go` → **0** при 200 осмотренных `.go` (единственный хит по дереву — цитата находки в этом журнале, и она историческая) |
|
||
| §4.8 п.4 предикат «кто ставит задание» размазан по двум местам | `cutsItsOwnUploads()` — одно названное место, читают оба; doc параметра `StartParsing` больше не выдаёт его за штатный путь | `grep "s.Engine != nil" internal/books/*.go` (без тестов, 4 файла) → **1 хит, и он внутри самого предиката**. ⚠ Рядом остаётся `s.Engine == nil` в `s.manifest` — это НЕ тот предикат, а nil-гард самого вызова, и он не решает, кто ставит задание |
|
||
| §4.5 шапка журнала лгала о том, где зона | пере-снята прибором: последний пак «деньги и правда» `fda0679`, коммитов в `platform/` после него 0, канон `0.13.0`; в шапку вписаны КОМАНДЫ, которыми числа берутся | `git log --oneline fda0679..HEAD -- platform/ \| wc -l` → **0** · `grep '^ version:' openapi.yaml` → `0.13.0` |
|
||
| §4.5 три ряда `open` при легшем лечении | `PD-424` и `PD-438` → `fixed`; у `PD-441` починен МАРКЕР, статус оставлен `open` и в ячейке названо, что держит его движковая половина (строка **331**). Фраза «в дереве» снята во всех трёх | `grep -c 'статус флипает лендинг'` → **0** при 465 рядах |
|
||
| §4.5 незаэкранированные `\|` | заэкранированы в трёх рядах (`PD-375`, `PD-422`, `PD-197`) | эскейп-аware счёт колонок: рядов с числом колонок ≠ 7 — **0** из 465 |
|
||
| `PD-464` (строка регистра, моя зона) | закрыт ОБЕИМИ половинами и переведён в `fixed` с диспозицией | см. ячейку ряда |
|
||
|
||
### §6 ось 4: существующее ПРЕЖДЕ велосипеда — что рассмотрено и чем отвергнуто
|
||
|
||
| Кандидат | Исход | Довод |
|
||
|---|---|---|
|
||
| **`golang.org/x/sync/semaphore`** | **ВЗЯТ** | `Acquire(ctx, 1)` — ровно нужная семантика: ждёт до слота или до конца контекста, очередь **FIFO** (в отличие от буферизованного канала, где поздний может обогнать раннего и часть загрузок ждала бы весь бюджет). `TryAcquire` у него не «барджит»: `success := s.size-s.cur >= n && s.waiters.Len() == 0` — быстрый путь отказывает, пока список ожидающих непуст, и слот у стоящих в очереди не ворует. ⚠ Прочитано в ИСХОДНИКЕ пинованной версии (`$(go env GOMODCACHE)/golang.org/x/sync@v0.22.0/semaphore/semaphore.go`, `TryAcquire`), а не по памяти: на этом свойстве держится довод про FIFO. Уже был в `go.sum` **косвенной** зависимостью той же версии `v0.22.0`; правка `go.mod` — перевод в прямые, БЕЗ смены версии (`git diff platform/go.mod`: одна строка вверх, одна вниз; `go.sum` −1 строка) |
|
||
| `errgroup.SetLimit` | отвергнут | ограничивает горутины, которые запускает САМА группа. Здесь группы нет и быть не может: вызывающие независимы и приходят из разных мест (HTTP-обработчик, воркер River, свип). Форма не подходит по существу, а не по вкусу |
|
||
| `netutil.LimitListener` | отвергнут | ограничивает СОЕДИНЕНИЯ на слушателе — то есть весь API разом, включая чтения, листинги и логин, из-за нагрузки на приём. И не накрывает ни воркера, ни свип: это ровно «потолок в маршруте», который пак запрещает |
|
||
| `MaxWorkers` очереди (уже стоит) | отвергнут как ЕДИНСТВЕННОЕ средство, но учтён как число | ограничить синхронный вход им нельзя: этот путь намеренно НЕ ставит задание, чтобы воркер не гонялся с разрезом за claim. Зато он назвал дефолт: 4 — то, подо что хост уже рассчитан, и потолок сказан один раз для всех способов запустить движок, а не только для того, что идёт через очередь |
|
||
| самописный счётчик / буферизованный канал | отвергнут | норма зоны «stdlib или устоявшаяся библиотека прежде своего», и здесь у своего есть конкретная цена — отсутствие FIFO |
|
||
|
||
### Посадки мутаций — вердикт по ТЕКСТУ падения, а не по цвету
|
||
|
||
Копия дерева с каноном (`cp -a --parents platform docs/architecture/14-api-contract`), базовая линия копии зелёная на всех четырёх пакетах.
|
||
|
||
| Посадка | Вердикт | Текст, по которому он вынесен |
|
||
|---|---|---|
|
||
| M1 разрез снова отсоединён от хвоста | **RED** | `the cut was granted 1m29.99s inside a 700ms walk` |
|
||
| M2 `step` перестаёт капать по хвосту | **RED** | `the walk is not capping it` + `adding a step moves the boundary` |
|
||
| M3 хвост съедает долю квитанции | **RED** | `want UploadSettle (3m30s) less the receipt's share (10s)` |
|
||
| M4 потолок не применяется вовсе | **RED** | контрольная величина легла в ноль: `{Limit:2 InFlight:0 Waiting:0 Waited:0 GaveUp:0}` |
|
||
| M5 гейт забывает окно claim'а | **RED** | `an upload deadline of 21m0s was accepted, though ... does not fit the parse claim's grace` |
|
||
| M6′ release ставит задание, ничего не вернув | **RED** | `a release that gave back nothing still queued a job (2 in total)` |
|
||
| M7 значение `ReceiptBudget` изменено | ⚠ **ВЫЖИЛА, и это ВЕРНЫЙ исход** | пере-сайзинг остаётся согласованным по обе стороны шва; ловить надо не значение, а ДРЕЙФ — см. M7'' |
|
||
| M7'' квитанция возвращается к своему литералу | **RED** (после того, как по находке M7 заведён пин) | `the receipt was given 30.0s, want the share the intake declared for it (10s)` |
|
||
| M9 занятый хост тратит попытку книги | **RED** | `a queued parse that found no slot answered <nil>, want the cap` |
|
||
| M10 два способа не получить claim свёрнуты обратно в один молчаливый `return` | **RED** | `losing the race said nothing` + `a claim that could not be asked for said nothing, so the book sits `parsing` with no job and nobody knows` + `was not logged at ERROR` |
|
||
|
||
**ТРЕТИЙ круг посадок — по коду, ПЕРЕПИСАННОМУ после адверсариального прохода.** Первые две редакции
|
||
двух пинов оказались тавтологичны, и посадка это показала, а не рассуждение.
|
||
|
||
| Посадка | Вердикт | Текст |
|
||
|---|---|---|
|
||
| N1 у ожидания удалена строка INFO | **RED** | `a cut that queued for a slot and got one said nothing` |
|
||
| N2 переименована причина `host_at_cut_capacity` | ⚠ ВЫЖИЛА → **N2′ RED** | первая редакция пина сверяла КОНСТАНТУ с самой собой и проходила при любом значении; переписана на литерал: `the reason is "MUTATED", want the stable "host_at_cut_capacity"` |
|
||
| N4 два гейджа потолка поменяны местами | **RED** | `the exposition is missing "tm_platform_cuts_in_flight 4"` |
|
||
| N5 из проводки выброшен `MaxCuts` оператора | **RED** | `MaxCuts is 0, want the operator's 7: TM_PLATFORM_MAX_CUTS does nothing` |
|
||
| N6 `cutsItsOwnUploads` всегда истинен | ⚠ ВЫЖИЛА → **N6′ RED** | счёт заданий РАЗЛИЧИТЬ НЕ МОЖЕТ (сломанный предикат ставит то же одно задание через релиз); переписано на лог с положительным контролем: `a deployment with no engine attempted a cut anyway` |
|
||
| N7 разрез перестаёт оставлять хвосту резерв | **RED** | `the intake's parse claim was NOT given back` + `the queue was handed 0 jobs` + `has spent 1 attempts` |
|
||
| N8 `giveBack` перестаёт возвращать задание очереди | **RED** | `the error does not tell the queue to bring the job back, so the single attempt is spent` |
|
||
| N9 разрез запускается даже когда места на него нет | **RED** | `a cut was started with no room for it (1 calls)` + `the upload is "not_started", want ` + `the pass does not say WHY it did not cut ("no_time_to_cut")` |
|
||
|
||
⚠ **Единственная выжившая, которую я НЕ чиню и объявляю:** смена значения `DefaultMaxCuts`. Это
|
||
сайзинг, а не свойство: изменённый потолок остаётся согласованным по всей системе, и «поймать» его
|
||
можно было бы только пином на литерал, то есть запретом менять число. Что запинено — ПРОВОДКА
|
||
(оператор получает своё число) и ЕДИНСТВЕННОСТЬ носителя (`DefaultMaxCuts = jobs.DefaultWorkers`).
|
||
|
||
⚠ **Первая редакция M6 и M7 НЕ КОМПИЛИРОВАЛАСЬ** (`declared and not used: tag`, `imported and not used`). Это не вердикт, а его отсутствие: посадка, которая не собралась, красит батарею по причине, не имеющей отношения к предмету. Обе пере-посажены компилирующимися.
|
||
|
||
### Классы и знаменатели — «закрыт в N из M», M посчитан командой
|
||
|
||
| Класс | Знаменатель | Как посчитан |
|
||
|---|---|---|
|
||
| входы, порождающие процесс движка на разрезе | **3 из 3** (интейк · `parseWorker` · свип `Sweep`) | `s.manifest` имеет РОВНО ОДНОГО вызывающего (`grep -rn 's\.manifest(' --include=*.go internal/ \| grep -v _test` → 1: `parse.go:178`), у `parseClaimed` их два (`Parse`, `cutNow`), у `Parse` — воркер `jobs.go:117` и `Sweep`. Все три сходятся в одну строку |
|
||
| порождения движка ВНЕ потолка | **1** — `internal/readmodel/readmodel.go:156` | `grep -rn '\.Manifest(' --include=*.go \| grep -v _test` → 2 вызывающих, один из них мой. Оставлен снаружи сознательно, довод — ниже |
|
||
| шаги хвоста под общим дедлайном | **все** (в `Accept` их 5 + квитанция) | построением, а не перечнем: `writeCtx` идёт через `step`, `step` капает по хвосту. Проверено на 50 шагах, которых в коде нет |
|
||
| ряды регистра с диспозицией «статус флипает лендинг» | **3 из 3** | `grep -c 'статус флипает лендинг'` → было 3, стало **0** |
|
||
| ряды с числом колонок ≠ 7 | **3 из 3** | эскейп-аware счёт: было 3, стало **0** при 465 осмотренных |
|
||
| открытые ряды регистра по МОИМ файлам | 10 путей осмотрено, совпадений — 20 рядов, из них МОЙ предмет **1** (`PD-464`, закрыт); остальные 19 — соседние классы, не тронутые этим паком | `grep` по ПОЛНЫМ путям (норма §3 п.8), контроль: открытых рядов всего **109** |
|
||
|
||
### Числа и команды — сняты ПОСЛЕ последней правки
|
||
|
||
```
|
||
$ python3 docs/scripts/counts.py --check → EXIT=1, и это ОЖИДАЕМО: ровно два расхождения,
|
||
✗ docs/PROGRESS.md: «открытых рядов регистра платформы — 112» против пере-счёта 110
|
||
✗ docs/PROGRESS.md: «... (major 3)» против пере-счёта 1
|
||
⚠ оба литерала — в ЧУЖОЙ зоне (`docs/PROGRESS.md`), туда не лезу. После лендинга и закрытия
|
||
PD-464 верные числа: **открытых 109, major 1, minor 36, info 72** (`counts.py` по дереву).
|
||
|
||
$ git log --oneline fda0679..HEAD -- platform/ | wc -l → 0
|
||
$ grep '^ version:' docs/architecture/14-api-contract/openapi.yaml → 0.13.0
|
||
$ go list ./... | wc -l → 20 (носитель скипов чинен первым движением)
|
||
|
||
$ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_ENGINE_BIN=… TM_PLATFORM_TEST_BOOK_TEMPLATE=… \
|
||
TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
|
||
MAKE-EXIT=0 · пакетов `ok` **20** · строк FAIL **0** · линтер «0 issues» · скипов **5**
|
||
⚠ Прогон ПОСЛЕДНИЙ — после адверсариального круга и после последнего добавленного пина. Прежние
|
||
редакции этих чисел (до круга) в отчёте не оставлены: они были верны и уже не про этот код.
|
||
```
|
||
|
||
⚠ **Скипы 5, и условие у всех одно, названное** (§10 ниже): нет деплой-артефакта
|
||
`backend/configs/mining-contrast.zh.txt`. Из четырёх гейтов батареи на этом хосте закрыты все:
|
||
Postgres · движковый бинарь + шаблон (собран из `git archive HEAD backend`, §4.7) · достижимый
|
||
пользовательский `systemd` · `MemoryMax` — судится самим `TestARunIsBoundedByItsOwnCgroup` (`STACK_DECISIONS` §«Гейты батареи»: прямой пробы у этого условия нет), и он ОТРАБОТАЛ: в полном перечне скипов, который печатает `check`, его нет, а `FAIL` в прогоне нет вовсе.
|
||
⚠ Числа Go-батареи сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются.
|
||
|
||
### Живой гейт (§4.7) — довод зоны опровергнут ИСПОЛНЕНИЕМ
|
||
|
||
Зона писала, что второй гейт батареи требует `$0`-пайплайна рядом с `backend/prompts/`, то есть записи в
|
||
чужую зону или полной копии дерева. **Проверено исполнением — неверно.** Рецепт:
|
||
|
||
```
|
||
$ git archive HEAD backend | tar -x -C $W # снапшот чужой зоны, ни байта записи в неё
|
||
$ cd $W/backend && go build -o $W/tmctl ./cmd/tmctl
|
||
$ sed -e 's|pipeline: ../configs/...|pipeline: $W/backend/configs/pipeline-c1.yaml|' \
|
||
-e 's|models: ../configs/...|models: $W/backend/configs/models.yaml|' \
|
||
$W/backend/example/book.yaml > $W/template.yaml # пути абсолютные
|
||
$ TM_PLATFORM_TEST_ENGINE_BIN=$W/tmctl TM_PLATFORM_TEST_BOOK_TEMPLATE=$W/template.yaml go test ./internal/books/
|
||
```
|
||
`TestTheRenderedConfigurationIsOneTheEngineActuallyLoads` — **PASS**.
|
||
|
||
⭐ **И где именно рассуждение зоны свернуло не туда:** «нужен `$0`-пайплайн» верно для теста
|
||
`internal/runner`, который гоняет `translate` и падает на `missing API keys`. Оно было ОБОБЩЕНО на гейт
|
||
целиком — а `manifest` есть `$0`-глагол и ключей не требует по `D20.4`, поэтому боевой `pipeline-c1.yaml`
|
||
(«платный») загружается и режет без единого ключа. То есть довод был верен про один тест и ложен про гейт,
|
||
и разница видна только исполнением.
|
||
|
||
### ⚠ Правки, вызванные заказанной сменой поведения (объявляю по `D39.183`)
|
||
|
||
1. **`TestAnUploadDeadlineIsRefusedUnlessTheWholeUploadFitsTheTighterWindow` → `...TightestWindow`.** Гейт
|
||
получил третье окно (§4.4, п.7 десятки) — прежний тест утверждал, что дедлайн `26m29s` ПРИНИМАЕТСЯ, и
|
||
после лечения это неверно. Тест не «починен под зелень»: он пере-написан строже — у каждого из трёх окон
|
||
свой случай, недостижимый двум другим, и посадка M5 краснит его именем нового окна.
|
||
2. **`ClaimGrace` экспортирована** (была `claimGrace`) — переименование затронуло 3 тестовых файла зоны
|
||
механически; утверждений не тронуто. Основание — то же, по которому экспортирована `UploadGrace`: бут
|
||
обязан отказывать конфигурации, которая её нарушает.
|
||
3. **Вторая правка рантбука сверх §4.3** — абзац про `TM_PLATFORM_MAX_CUTS`. Довод: §4.1 требует, чтобы
|
||
потолок был «конфигурируемым и наблюдаемым», а ручка, о которой рантбук молчит, оператору не доступна;
|
||
документировать ручку, которую этот же пак и завёл, — часть §4.1, а не «остальное про выкат».
|
||
|
||
### ⚠ Константа контракта `0.12.0` → `0.13.0` — что было сломано и кем
|
||
|
||
Батарея была КРАСНОЙ на входе, до единой моей правки: `internal/gates`
|
||
`TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` — «this build announces contract 0.12.0 and the
|
||
ratified canon is 0.13.0». Улика: файл-носитель (`internal/httpapi/capabilities.go:36`) в моём диффе
|
||
отсутствовал (`git diff --name-only HEAD | grep -i contract` → пусто). Канон увёл на `0.13.0` коммит
|
||
оркестратора `3d90943`, константу за собой не потянув; последняя правка константы — `6ceb133`, до него.
|
||
То есть гейт красен с момента ратификации, **сутки**, и заметила это входная сверка следующей сессии зоны.
|
||
Взято мной по ЯВНОМУ указанию оркестратора с названным основанием: ратифицированный порядок `D39.208` п.1
|
||
— код первым с честно красным гейтом, канон вторым; здесь порядок был обратный, и правка возвращает мир к
|
||
гейту, а не гейт к миру (`D39.183`, обслуживание). **Авторство ошибки — оркестратор, не прошлый пак:** на
|
||
`fda0679` канон и константа обе были `0.12.0`, гейт был зелёным, и число батареи в акте `D39.221` честное.
|
||
|
||
### §4.6 — ответ (а): закрыто по построению НА МОЕЙ СТОРОНЕ, но премиса пака опровергнута
|
||
|
||
Пункт 10 десятки предполагал, что причина отказа не доезжает. **Опровергнуто:** перечислены ВСЕ семь
|
||
пользовательских отказов приёма — `payload_too_large` и `request_timeout` (корневые коды), `malformed` ×2,
|
||
`unsupported_pair` ×2, `no_book`, `no_chapter_structure`, `too_long`, `missing_or_late`. Причины, не
|
||
выразимой перечислимым кодом, я не нашла; расширять `errors[]`/`cause.code` нечем, и минор не нужен.
|
||
⚠ **Собственную первую находку снимаю:** я решила, что слишком длинное поле формы уходит с ПУСТЫМ
|
||
`errors[]` — неверно, оно названо на месте чтения (`v0.go:843`, `ItemTooLong`), а ветвь `Invalid(w, r)` без
|
||
элемента до него не доходит.
|
||
|
||
⛔ **А вот премиса пака про клиента ЗАМЕРОМ НЕ ПОДТВЕРЖДАЕТСЯ, и следующая смена не должна её унаследовать.**
|
||
Пак пишет: «Таблица „код → русская фраза“ у клиента уже есть (`14-api-contract/README.md`, и фронт её
|
||
рисует)». Замер: `no_chapter_structure` в живых доках — **11 хитов при 1104 осмотренных `.md`**, и НИ ОДИН
|
||
не таблица фраз; в `14-api-contract/README.md` нет ни `no_book`, ни `no_chapter_structure`. В зоне фронта
|
||
`no_book`/`unsupported_pair` — **0 хитов при 8114 осмотренных `.ts`/`.tsx`**. Что там есть на самом деле —
|
||
правило «клиент диспетчеризует по СТАТУСУ и показывает одну нейтральную фразу» (README §вход) и решение
|
||
владельца 16.08 о машинном коде. ⇒ вывод пака (моя работа тут закончена, клиентская половина — фронт, а он
|
||
заморожен) остаётся ВЕРНЫМ, но не потому, что таблица есть, а потому, что платформа дала клиенту всё, по
|
||
чему её можно нарисовать. Разница существенна: с премисой пака работа выглядит сделанной у обеих сторон.
|
||
|
||
### Пинги оркестратору
|
||
|
||
1. ⛔ **Литералы в `docs/PROGRESS.md` под гардом `counts.py --check` протухли моим флипом — двигать их
|
||
тебе.** После лендинга верно: **открытых 109, major 1** (было «112 (major 3)»). Разница в три ряда:
|
||
`PD-424` и `PD-438` переведены в `fixed` по твоему же маркеру, `PD-464` закрыт этим паком. Гейт красен
|
||
ОЖИДАЕМО и ровно на этих двух строках — других расхождений он не даёт.
|
||
2. **§4.8 п.6, «ВСЕГДА» в дельте контракта — моё мнение, как просил пак: нужна ОГОВОРКА В КАНОНЕ, а не
|
||
структурная гарантия.** Довод из кода, а не из вкуса. Между `FinishParse` и `ReadBook` свип
|
||
материализатора может взять долг (он записан ИМЕННО `FinishParse`) и дописать строке `source_chars` и
|
||
`structure` (`readmodel.refresh` → `SaveStructure`). Но он НЕ МОЖЕТ ни снять вердикт разреза, ни
|
||
изменить `status`/`chapter_count`: единственный писатель `reject_reason` — `RejectBook`
|
||
(`pgstore/books.go:394`, один хит по всему коду), а статус двигают только `FinishParse`/`reject`.
|
||
⇒ расхождение возможно только В СТОРОНУ БОЛЬШЕГО: ответ либо уже несёт поверхностные поля, либо ещё
|
||
нет. Структурная гарантия потребовала бы держать что-то поперёк `FinishParse`→`ReadBook` на горячем
|
||
пути ради полей, которые клиент всё равно перечитывает карточкой. ⚠ И отдельно: **саму фразу «ВСЕГДА» я
|
||
в каноне не нашла** — `grep 'ВСЕГДА' 14-api-contract/README.md` даёт 0, `grep -i always` в
|
||
`openapi.yaml` — 12 хитов, все про другое (SSE-кадры, `about:blank`). Назови предложение адресом, и
|
||
если оно живёт не там, где я искала, мой довод надо перепроверить против него.
|
||
3. ⚠ **Премиса пака в §4.6 неверна — см. секцию выше.** «Таблица код → русская фраза у клиента уже есть, и
|
||
фронт её рисует» замером не подтверждается (0 хитов `no_book`/`unsupported_pair` при 8114 осмотренных
|
||
`.ts`/`.tsx`; в `14-api-contract/README.md` ни `no_book`, ни `no_chapter_structure`). Вывод пака устоял,
|
||
основание — нет. Стоит поправить, иначе следующая смена унаследует «у клиента всё готово».
|
||
4. **Мелкая неточность адреса в §4.7:** «а 35 строками ниже в том же журнале лежит рецепт снапшота» —
|
||
реально 261 строкой ниже. Адреса в дереве, которое я сдаю: довод — `platform-PROGRESS.md:452`, рецепт —
|
||
`:713` (на входном `HEAD` это были `:173` и `:434`; расстояние то же). На существо не влияет: рецепт там
|
||
и есть, и он работает.
|
||
5. **Твой вопрос «есть ли дешёвый способ закрыть сутки красноты между ратификацией и следующей сессией
|
||
зоны» — есть, и он в ТВОЕЙ зоне.** Пара «канон ↔ объявленная константа» сегодня судится только Go-тестом,
|
||
который гоняет зона. А `docs/scripts/counts.py --check` уже читает оба дерева, уже висит на зонном
|
||
pre-commit и уже срабатывает **именно на коммитах с D-логом или PROGRESS** — то есть ровно на
|
||
ратификационных. Добавить туда одну проверку — `grep '^ version:' openapi.yaml` против
|
||
`const ContractVersion` в `platform/internal/httpapi/capabilities.go` — стоит десятка строк и ловит
|
||
ровно тот класс, который стоил суток: он предупреждает того, КТО ДВИГАЕТ КАНОН, в момент движения.
|
||
Заказом не делаю (файл в `docs/`), рекомендацию записываю.
|
||
|
||
### Адверсариальный проход по СВОЕЙ готовой работе — восемь находок, и они были настоящие
|
||
|
||
Проход заказан §5.4 и выполнен субагентом (author ≠ reviewer) по готовому диффу, с направлением на
|
||
классы, которые уже стоили зоне денег. **Круги НЕ сошлись с первого раза: он нашёл восемь, и шесть из
|
||
них — дефекты, которые ввёл ЭТОТ пак.** Каждую я пере-проверила по коду прежде, чем чинить.
|
||
|
||
| # | Находка | Чем оказалась | Что сделано |
|
||
|---|---|---|---|
|
||
| **F1** | комментарий `giveBack` обещал повтор задания с бэкоффом | ⛔ ЛОЖЬ: `ParseArgs.InsertOpts` — `MaxAttempts: 1`, повтора нет вовсе; книга на занятом хосте ждала свип **20 минут** | заведён `jobs.ErrTryAgainLater`; воркер переводит его в `river.JobSnooze(RetryDelay)`, который НЕ тратит единственную попытку. Комментарий приведён к правде. Пин — `TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt` |
|
||
| **F2** | у выигравшего слот не проверялось, осталось ли время на разбор | ⛔ настоящий: слот, выигранный в конце бюджета, отдавал движку миллисекунды; убитый процесс читается как `parser_unavailable`, а он ТРАТИТ попытку — пять таких удаляют файл пользователя | `takeCutSlot(ctx, reserve)`: `worthStarting` до и ПОСЛЕ ожидания, `waitCtx` обрывает ожидание на резерв раньше. Резерв — `CutBudget` на очередном пути, `0` на интейке (там попытка не тратится) |
|
||
| **F3** | отказ бута при `MaxCuts < 1` | ⛔ МЁРТВЫЙ КОД: `l.number` уже отвергает всё непозитивное, и мой тест пинил чужой охранник, а не мой | ветвь удалена; в тесте названо, ГДЕ живёт отказ |
|
||
| **F4** | тест трёх окон | ⛔ ВАКУУМЕН для двух окон из трёх, и его комментарий утверждал обратное — ровно тот класс, который он якобы чинил | выбор окна вынесен в `intakeWindow(...)`; новый тест делает каждое окно самым узким по очереди. Ревьюер пере-мутировал независимо: теперь красный |
|
||
| **F5** | терминальная запись могла родиться истёкшей | ⛔ настоящий и злой: при спетом хвосте claim НЕ отдавался и задание НЕ ставилось — книга «принята», а доделать её некому 20 минут. Мой тест этого не утверждал | `stepLeaving` + `cutTailReserve`: слабину забирает РАЗРЕЗ, а не записи. Пин — `TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook` |
|
||
| **F6** | комментарий потолка обещал больше, чем потолок делает | верно: `readmodel` порождает те же процессы мимо него; плюс `DefaultMaxCuts` был вторым литералом числа воркеров | комментарий сужен до правды и называет, что осталось снаружи; `DefaultMaxCuts = jobs.DefaultWorkers` — один носитель, и `config.Runner.Workers` берёт его же |
|
||
| **F7** | шесть поверхностей пережили мутацию | верно все шесть | закрыты пинами (ниже), кроме значения `DefaultMaxCuts` — это САЙЗИНГ, и его смена не дефект; названо в §10 |
|
||
| **F8** | баннер `capabilities.go` противоречил себе | верно, и сломала его Я этой же сменой | баннер разводит два порядка: код первым (канон отстаёт) — ратифицированный, канон первым (код отстаёт) — тот, что стоил суток |
|
||
|
||
### ⛔ НАХОДКА №9 — класс, которого не ловит НИ батарея, НИ мутация
|
||
|
||
Поймана мной при починке F5: **моя починка была дефектной, и её дефект не имел цвета.**
|
||
|
||
`cutTailReserve` был КОНСТАНТОЙ `3 * writeBudget`, а бюджет записи в тестах — полем сервиса
|
||
(`s.writeBudget`, который фикстуры укорачивают, чтобы достать случаи, недостижимые за 30 секунд). В
|
||
фикстуре с хвостом 300 мс резерв оставался 90 с — больше всего хвоста ⇒ шаг разреза рождался истёкшим,
|
||
движок не звался НИКОГДА, и пакет `books` **зависал навсегда** на `<-first`.
|
||
|
||
⭐ **Почему это отдельный класс.** Батарея его не ловит, потому что зелёного вердикта просто не
|
||
наступает — но и красного тоже: прогон висит до таймаута `go test`, и в CI это читается как «долго», а
|
||
не как «сломано». Мутация его не ловит по той же причине: у посадки нет вердикта, есть тайм-аут.
|
||
Единственное, что его назвало — прогон с УКОРОЧЕННЫМ `-timeout` и чтение стека упавшего по нему
|
||
процесса (`limit_test.go:162`, `<-first`); по цвету он неотличим от медленной машины. Две вещи из этого:
|
||
- резерв сделан производным от бюджета В СИЛЕ (`s.cutTailReserve()` = `3 * s.write()`), иначе фикстура
|
||
молча моделирует не то;
|
||
- «нет места для разреза» больше не притворяется отказом хранилища: claim берётся на СВОЁМ бюджете
|
||
записи, разрез — на своём, и пустой разрез отвечает «вердикта нет» (`ReasonNoTimeToCut`), а не падает
|
||
внутри `ClaimParse`. Это тот же класс, что F1: диагноз, который называет не то, что случилось.
|
||
Запинено `TestAnUploadWithNoRoomLeftForACutSaysThatAndHandsTheBookOver` + посадка N9.
|
||
|
||
⚠ **И правило, которое стоит пережить пак:** число, которое фикстура умеет укорачивать, и число,
|
||
выведенное из него, обязаны быть выведены ОДИНАКОВО. Константа рядом с полем — это две величины,
|
||
которые совпадают в бою и расходятся в тесте, то есть ровно то, что фикстура сделать не может увидеть.
|
||
|
||
⚠ **Урок, который стоит пережить этот пак:** шесть из восьми находок — в коде, который я СДАВАЛА как
|
||
готовый, с зелёной батареей, десятью посадками и отчётом, где написано «круги сошлись». Батарея была
|
||
зелёной на всех восьми. Ловит их не цвет, а второй читатель, которому названо, ГДЕ у этого пака мягко.
|
||
|
||
### Якоря, убитые моим переездом — норма §3 п.8
|
||
|
||
Мои правки сдвинули строки в `internal/config/config.go`, `internal/pgstore/books.go`,
|
||
`internal/metrics/metrics.go` и `cmd/tmplatformd/runner.go` (везде вставки, сдвиг +6 в первых двух).
|
||
|
||
**Замер дифференциальный, а не «посмотрела»:** линтер на входном `HEAD` даёт **8** проблемных якорей,
|
||
моё дерево давало **26**. Чтобы отделить своё от унаследованного и от WIP чужой сессии, собрала
|
||
`git archive HEAD` в /tmp, подменила в копии ТОЛЬКО `platform/` своим и сравнила списки `comm`-ом.
|
||
⚠ Кап вывода линтера — 25 строк; на 26 проблемах обрезка читается как отсутствие, поэтому в копии
|
||
скрипта кап поднят до 500. Появившихся из-за меня — **18**.
|
||
|
||
| Где | Сколько | Что сделано |
|
||
|---|---|---|
|
||
| `platform/docs/DEFECT_REGISTER.md` | **14 из 14** | пере-наведены механически: токен найден в цели, адрес заменён; ни одного «руками» |
|
||
| `docs/PROGRESS.md`, `docs/architecture/05-decisions-log.md` | **4** | ЧУЖАЯ зона — ушли пингом с готовыми адресами и токенами |
|
||
|
||
⭐ **Находка из этого же хода:** экранирование `\|` в ячейке регистра (§4.5) **ломает якорь**, если черта
|
||
попала в его токен. `PD-422` держал `internal/runs/runs.go:436`=`resnapshot := book.BankMoved || book.HasPriorRun`;
|
||
после экранирования токен перестал совпадать с кодом. Вылечено укорочением токена до
|
||
`resnapshot := book.BankMoved` (единственный хит в файле). Счёт колонок и сверка токена тянут ячейку в
|
||
разные стороны — следующий, кто пойдёт экранировать черты, наступит на то же. Ушло пингом.
|
||
|
||
Итог: мой лес **12** проблемных якорей против **8** на `HEAD`; остаток — ровно те 4 чужой зоны.
|
||
|
||
### §10 — что НЕ удалось и что НЕ проверено (это разные исходы)
|
||
|
||
- **Скипов 5 (было 6), и условие у всех ОДНО и названное:** `backend/configs/mining-contrast.zh.txt` нет
|
||
на этом хосте, и это деплой-артефакт, которого нет в репозитории (снапшот `git archive HEAD backend`
|
||
его не несёт — `ls backend/configs` даёт `langpacks pairs models.yaml pipeline-*.yaml`, и всё).
|
||
Скипающиеся: `TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses`,
|
||
`TestALivePreviewWritesNothingAndALiveApplyWrites`,
|
||
`TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing`,
|
||
`TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber`,
|
||
`TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`. Шестой (`TestARestorePointCanActuallyBeRestored`)
|
||
закрыт: `pg_dump`/`pg_restore` есть в `~/.local/pgsql/bin`, переменные выставлены.
|
||
⚠ Это НЕ «не проверено» про мой предмет: ни один из пяти не касается разреза приёма — они про банк,
|
||
выдачу и снапшот-гард. Живой гейт МОЕГО предмета закрыт и зелёный.
|
||
- ⛔ **ЭТО МЕСТО БЫЛО «НЕ ПРОВЕРЕНО» И ОКАЗАЛОСЬ ДЕФЕКТОМ — оставляю как след.** Первая редакция отчёта
|
||
писала: «полагаюсь на то, что River повторит задание с бэкоффом; сколько попыток он даёт, я не
|
||
измеряла». Замер (адверсариальный проход, подтверждён мной по коду): `ParseArgs.InsertOpts` —
|
||
`MaxAttempts: 1`, повтора НЕТ ВООБЩЕ, задание просто списывается, и книга ждала свип 20 минут. То
|
||
есть моё «рассуждение, а не замер» было не осторожностью, а неверным утверждением в комментарии кода.
|
||
Вылечено `river.JobSnooze`, который возвращает задание не тратя единственную попытку. ⭐ Урок ровно
|
||
тот, что записан в каноне зоны: строка «не проверено» — это не смягчение, это место, где ещё не
|
||
посмотрели, и смотреть надо ДО сдачи.
|
||
- **НЕ ЗАПИНЕНО ИМЕНЕМ, но покрыто исполнением** (проверено посадками, не грепом по именам):
|
||
`cutTailReserve` — посадкой N7, `worthStarting` и `waitCtx` — обоими исходами
|
||
`TestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack` (ожидание обрывается на резерв раньше,
|
||
поэтому случай «упор в потолок» кончается за ~1,5 с, а не за весь бюджет), `engineNotAsked` — обоими
|
||
ветвями там же, `jobs.RetryDelay` — новым тестом очереди. Собственного теста по имени у них нет.
|
||
- **НЕ ПРОВЕРЕНО экспериментально: сколько памяти реально держит один `tmctl manifest`.** Дефолт 4 выбран
|
||
как число, под которое хост уже был рассчитан (`MaxWorkers` очереди), а не измерен на большой книге.
|
||
Ручка конфигурируема именно поэтому.
|
||
- **Опровержение премисы §4.6 сделано ГРЕПОМ ПО КОДАМ**, а не чтением рендера фронта: я искала строки
|
||
`no_book`/`unsupported_pair` в 8114 `.ts`/`.tsx`. Таблица, ключуемая иначе (например, по корневому
|
||
`code`), таким грепом не нашлась бы. Утверждаю ровно замеренное.
|
||
- **Одно новое условное сообщение НЕ запинено:** `«the parse claim could not be given back; the backstop
|
||
sweep takes the book»` в `giveBack` — ветвь, где отказ `ReleaseParseClaim` накладывается на упор в
|
||
потолок. Четыре остальных новых сообщения запинены ОБЕИМИ фикстурами (где обязано прозвучать и где
|
||
обязано молчать), это пятое — нет: чтобы его достать, нужен отказ хранилища ВНУТРИ уже насыщенного
|
||
потолка, и фикстуру такой конъюнкции я не построила. Называю прямо, а не выдаю шесть из семи за семь.
|
||
Остальные шесть — включая обе новые («движок не спрошен» и «не осталось места на разрез») — запинены
|
||
фикстурой, где сообщение обязано прозвучать, И фикстурой, где обязано молчать.
|
||
- **Ограничитель НЕ накрывает `readmodel`** (`internal/readmodel/readmodel.go:156` — второй и последний
|
||
вызывающий `Engine.Manifest`) и глаголы `export`/`status`. Это осознанная граница, а не пропуск:
|
||
материализатор и `status` идут внутри очереди, которая уже ограничена одним `MaxWorkers` на все три
|
||
типа заданий (`internal/jobs/jobs.go:170`), свипы последовательны, а `readEngine` накрыл бы ещё
|
||
`Status` на пути СТАРТА платного прогона (`internal/runs/spawn.go:242`) и связал бы запуск прогонов с
|
||
нагрузкой приёма. Единственным неограниченным источником процессов был синхронный интейк — он и закрыт.
|
||
⚠ Если приёмка считает, что хосту нужен потолок на ВСЕ порождения, это отдельная работа со своим
|
||
дизайном (развязка денежного пути), а не райдер к этому паку.
|
||
|
||
### Попутно: совместимость с новой секцией `bank.json` (пришло пингом от движковой зоны, проверено моим кодом)
|
||
|
||
Движковый пак добавил в `bank.json` секцию `consolidation` (полнота банка) и поле `never_asked`, версия
|
||
`tm-bank-v1` НЕ бампнута. **Пере-проверено на моей стороне, не принято на слово:**
|
||
- `bank.json` разбирает `internal/ingest/bank.go:78` `DecodeBank` — простой `json.Unmarshal`, неизвестный
|
||
член игнорируется. ⚠ Строгий декодер в зоне ЕСТЬ ровно один (`internal/httpapi/bank.go:137`,
|
||
`grep -rn DisallowUnknownFields --include=*.go` → **1 хит при 200 `.go`**), но он на ДРУГОМ пути — тело
|
||
запроса на правки ОТ КЛИЕНТА, где строгость требует сам канон. Пути не пересекаются ⇒ лендинг движка
|
||
приём банка не ломает.
|
||
- Читателей секции у зоны нет. ⚠ По подстроке их **2 при 186 `.go` в `internal/`**
|
||
(`internal/ingest/manifest.go:97`, `internal/pricing/pricing.go:112`) — и оба английская ПРОЗА про
|
||
«terminology consolidation» в денежных комментариях, а не чтение поля. Счёт по подстроке и счёт по
|
||
владению здесь расходятся на два: следующему, кто будет снимать этот ноль, читать хиты, а не число.
|
||
- ⛔ **Закон на будущее:** бит `complete` брать ГОТОВЫМ из артефакта, не выводить у себя (п.6 закона
|
||
входной двери шва). У движка он считается от среза рендер-паса, а срез классификатора полноты банка не
|
||
означает — самостоятельный вывод разошёлся бы с движковым молча.
|
||
|
||
Строку под читателя НЕ завожу: это следующий пак зоны, и решение оркестратора — не торопить.
|
||
|
||
### Вопросы оркестратору
|
||
|
||
- **Нужен ли ряд регистра на остаток §4.1** (порождения вне потолка: `readmodel` + `export`/`status`)?
|
||
Я его НЕ завела: это не дефект сегодняшнего поведения, а названная граница механизма, и заводить ряд
|
||
«мы решили иначе» — засорять регистр. Скажи, если хочешь ряд.
|
||
- **`ReasonHostAtCapacity` — константа, которая НИКОГДА не пишется в БД** (`reject` — единственный писатель
|
||
причины, а класс потолка возвращает claim до него). Я оставила её строкой рядом с пятью
|
||
`ReasonX`-константами, потому что читатель приходит за ними туда же, и написала это в комментарии.
|
||
Если считаешь, что не-хранимой причине там не место — скажу, куда унести.
|
||
|
||
## ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ЗАПИСКА-ПЛАН (08.09, `textmachine-fa`)
|
||
|
||
> Промт `docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md`, вход HEAD `3f4680c`, дерево на входе чисто
|
||
> (`git status --porcelain` — пусто). Зона НЕ коммитит. Пак $0.
|
||
> Baseline снят сам: `python3 docs/scripts/counts.py --check` → «Литералы сходятся с пере-счётом
|
||
> (8 проверок)», регистр 465 рядов / open 112 / major 3.
|
||
|
||
**Что беру и в каком порядке.** Сначала то, что стоит $0 и является предусловием остального (шапка
|
||
этого журнала, носитель числа пакетов, ряды регистра), потом код в порядке связности: бюджет хвоста —
|
||
ограничитель — три неразличимых сбоя, потому что первые два связаны структурно и чинить их по
|
||
отдельности значит ломать один другим. Живой гейт и рантбук — последними, они судят уже построенное.
|
||
|
||
**Разметка решений, принятых ДО кода — чтобы их можно было опровергнуть по этой записке.**
|
||
|
||
1. **Бюджет (§4.2) — форма Б, структурная.** Перечень шагов подвёл трижды, и четвёртый перечень был бы
|
||
той же заплатой (`D39.216`). Беру ОДИН отсоединённый контекст хвоста с дедлайном `UploadSettle`,
|
||
от которого наследуются все шаги: `context.WithTimeout` на потомке с более ранним дедлайном сам даёт
|
||
`min(шаг, остаток)`, поэтому добавленный шаг границу не двигает ПО ПОСТРОЕНИЮ, а не по внимательности
|
||
следующего автора. Пин утверждает САМО свойство (§5.3), а не сумму слагаемых.
|
||
2. **Ограничитель (§4.1) — на `books.Service.manifest`.** Замер входов, а не память: `Engine.Manifest`
|
||
зовут ДВА места (`internal/books/parse.go:440`, `internal/readmodel/readmodel.go:156`), а `s.manifest` —
|
||
ровно те три, что названы заказом (интейк · `parseWorker` · свип `Sweep`). Шире (`runner.readEngine`,
|
||
общий на `manifest`/`export`/`status`) НЕ ставлю, и довод замером: очередь у платформы ОДНА и уже
|
||
ограничена (`internal/jobs/jobs.go:170`, `MaxWorkers` дефолт 4) на все три типа заданий сразу, свипы
|
||
последовательны — то есть единственный неограниченный источник процессов на хосте это и есть
|
||
синхронный интейк; а `readEngine` накрыл бы ещё `Status`, который стоит на пути СТАРТА платного
|
||
прогона (`internal/runs/spawn.go:242`, `bookMeter`), и связал бы запуск прогонов с нагрузкой приёма.
|
||
Что осталось снаружи — называю в отчёте числом, а не умолчанием.
|
||
3. **Форма — ожидание, а не немедленный отказ.** Ожидание внутри уже стоящего `CutBudget` к хвосту
|
||
ничего не добавляет (приор оркестратора, проверяю кодом), а упор даёт штатную деградацию: не уложился
|
||
⇒ `errNotConclusive` ⇒ `201 parsing` ⇒ книгу доделывает очередь. Это строго лучше `503`: пользователь
|
||
получает книгу. Существующее прежде своего (§6): `x/sync/semaphore`, `errgroup.SetLimit`,
|
||
`netutil.LimitListener`, `MaxWorkers` — рассматриваю и отвергнутое называю с доводом.
|
||
4. **Свип `StuckIntake` (§4.4) — гипотеза лечения БЕЗ миграции.** `StartParsing` не штампует
|
||
`parse_started_at` (`internal/pgstore/books.go:213`), поэтому `coalesce(parse_started_at, added_at)`
|
||
в предикате свипа — это `added_at`, поставленный ДО прихода тела; бутовый гейт
|
||
(`internal/config/config.go:729`) сверяет дедлайн только с `min(UploadGrace, ClaimStale)` = 30 мин и
|
||
пропускает дедлайн до 26m29s, а `claimGrace` = 20 мин ⇒ условие достижимо настройкой. Кандидат —
|
||
добавить `claimGrace` третьим окном в тот же `min()`: колонки не нужно, форма гейта уже ровно эта.
|
||
Не выйдет — пинг, а не полумера молча (§4.8).
|
||
|
||
**Что считаю рискованным.** (а) Форма Б трогает контексты на ВСЁМ пути приёма — класс ошибок здесь
|
||
«тихо-зелёный»: путь продолжает работать, а гарантия исчезает, поэтому пин обязан быть структурным
|
||
(дедлайны шагов), а не «уложились по часам». (б) Ограничитель на общей точке касается и очередного
|
||
входа — нагрузочное предъявление обязано считать ОДНОВРЕМЕННЫЕ процессы, а не суммарные. (в) Фикстуры
|
||
зоны уже делали два разных числа одним (`D39.208` п.5): везде, где в фикстуре встречаются `writeBudget`,
|
||
`CutBudget` и `UploadSettle`, беру ТРИ РАЗНЫХ значения.
|
||
|
||
**Чего не делаю:** п.6 десятки (текст контракта) — пинг оркестратору; всё из §4.8.
|
||
|
||
## ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (06–07.09, `textmachine-bf`)
|
||
|
||
> Промт `docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md`, вход HEAD `e4097cb`, дерево на входе чисто.
|
||
> Зона НЕ коммитит — дерево передано оркестратору №23 (`textmachine-a8`).
|
||
|
||
### Что построено
|
||
|
||
| Заказ | Что стало с деревом | Чем предъявлено |
|
||
|---|---|---|
|
||
| §4.0 триаж журнала | журнал 5029 → **491** строки, из которых 385 — этот отчёт; две эры вынесены срезами в `archive/` | `carriers.py` по живому файлу — **0** находок при 491 осмотренных строках; вынос разобран построчно (ниже) |
|
||
| `PD-441` объявленная маржа | каждая строка `settlement` несёт БАЗИС; `runs` печатает `≥` перед суммой и легенду | два независимых пути по деньгам (ниже) · пины `TestEverySettlementSaysThatItsFigureIsAFloor`, `TestOnlyAnEndingTheEngineChoseIsSettledAsComplete`, `TestTheOperatorsTableSaysSpentIsAFloor…` |
|
||
| `PD-424` окно гонки | арбитр В ХРАНИЛИЩЕ: `AbandonOrder.ProofAttemptID` + `ProofSpawns`, сверка под книжной блокировкой | пины `TestAProofAboutTheAttemptBeforeThisOneIsRefused`, `TestAnAttemptClaimedWhileSystemdWasBeingAskedIsRefused` |
|
||
| `PD-438` видимость парковки | колонка `run_attempts.parked_at`, гейдж `tm_platform_parked_attempts`, ячейка PARKED в `runs` | пины `TestAParkedAttemptIsVisibleInTheRowTheGaugeAndTheListing`, `TestTheOperatorsTableSaysSpentIsAFloor…` |
|
||
| строки 285 + 325 | ⛔ **ПОСТРОЕНО, НО НЕ ГОТОВО — сработало правило остановки, см. секцию ниже.** Синхронный $0 `tmctl manifest` на приёме: fail fast и вход сужен по ЧИСЛУ ГЛАВ | 4 пина `failfast_test.go` + `TestOnlyTheHolderOfTheParseClaimCanDeleteARefusedIntake` + пин медленного разреза (круг 3) и три пина круга 4 (деплойные ветви · материализация · очередь) |
|
||
| строка 305 | `make check` при таймауте называет пакет, тест и причину | воспроизведено и предъявлено сквозным `make check` на копии |
|
||
| §4.4б | комментарий `THE FIX IS NOT IN THIS PACKAGE` приведён к поведению | греп по отозванной формулировке — 0 при 197 осмотренных `.go` |
|
||
|
||
### Заказ по пунктам — исход каждого
|
||
|
||
| Пункт промта | Исход |
|
||
|---|---|
|
||
| §4.0 триаж журнала | **сделано**; независимо пере-проверено оркестратором №23 мультимножеством и `carriers.py` по самим срезам |
|
||
| §4.1 `PD-441` | **сделано в достижимой половине**; вторая половина — движковая, названа поимённо и ушла строкой бэклога |
|
||
| §4.1 `PD-424` | **сделано**, и предмет оказался шире ряда: окон два |
|
||
| §4.1 `PD-438` | **сделано** |
|
||
| §4.2 строка 282 | **сознательно не делаю** — снято до выдачи, платформенная половина уже построена |
|
||
| §4.3 строки 285 и 325 | ⛔ **построено и ОСТАНОВЛЕНО**: пять кругов самопроверки, девять major из одиннадцати — в этом одном механизме, третий подряд промах бюджета хвоста. Предмет уезжает отдельным паком по правилу остановки оркестратора №23; семь незакрытых пунктов перечислены поимённо |
|
||
| §4.4 два малых дока | **не трогаю** — зона оркестратора; расхождения ушли пингами (ниже) |
|
||
| §4.4а строка 305 | **сделано**, предъявлено сквозным `make check` на копии |
|
||
| §4.4б комментарий | **сделано** |
|
||
| §4.5 запреты | **соблюдены**, проверено исполнением: контракт, `ENGINEERING_STANDARDS`, `PLATFORM_DIRECTION`, `backend/` — 0 изменённых файлов; коммитов 0; индекс пуст |
|
||
| пинги оркестратора: `PD-458`, `П-10` | **пере-сняты по дереву, оба подтвердились**; `PD-458` → `fixed`, `П-10` закрыта актом зоны |
|
||
|
||
### Два круга самопроверки — что нашёл каждый
|
||
|
||
⚠ **Круги сошлись на ТРЕТЬЕМ, а не на втором.** Второй я вела сама и объявила сходимость
|
||
преждевременно: адверсариальный субагент нашёл четыре major, три из них — дефекты, которые ввёл ЭТОТ
|
||
пак и которых не видел ни один мой пин.
|
||
|
||
| круг | находка | что сделано | чем предъявлено |
|
||
|---|---|---|---|
|
||
| 1 (мой, по готовой работе) | посадка «снят сравнитель попытки» ВЫЖИЛА — случай ловился чужим охранником | новой попытке дан тот же счёт заявок + сверка ТЕКСТА отказа | пере-посадка красная, текст называет именно сравнитель попытки |
|
||
| 1 | посадка «снят охранник `is null`» ВЫЖИЛА — два охранника прикрывали друг друга | добавлено обращение к записи напрямую | пере-посадка красная: `a second mark moved the stamp` |
|
||
| 1 | синхронный разрез добавил хвост, о котором бутовый гейт не знал | `UploadSettle = CutBudget + writeBudget + 30s`, `CutBudget` стал константой | пин + мутация, текст называет оба числа |
|
||
| 2 (по заказу и по первому кругу) | «≥» и PARKED были предъявлены ЧТЕНИЕМ КОДА, а не исполнением | заведён пин, читающий настоящий вывод команды | он же поймал мою ошибку в ожидании: `money.USD()` не печатает `$` |
|
||
| 2 | охранники `DeleteRefusedIntake` (стадия, claim, отсутствие прогона) не запинены | заведён пин | мутация «охранник claim'а всегда истинен» → `gave <nil>, want ErrNoBook` |
|
||
| 2 | дельта контракта врала про `character_count_exact` и `structure` | ⚠ **починка второго круга оказалась НЕВЕРНОЙ и пере-сделана четвёртым:** материализация с интейка снята, поэтому в `201` оба поля отсутствуют ВСЕГДА, а не «когда материализация удалась» | дельта переписана; пин `TestTheIntakeNeitherMaterializesNorEnqueuesWhenItsCutSucceeds` |
|
||
| 2 | числа отчёта устаревали четырежды | сняты заново после последней правки | команды и выводы — в разделах ниже |
|
||
| **3 (опровергатель, `fable`)** | ⛔ **пере-чтение строки для `201` шло на контексте, открытом ДО тела** (бюджет 30 с), а разрез длится до 90 с ⇒ после медленного разреза `201` нёс `parsing`/0 глав над строкой `not_started`/500 | свежий `writeCtx` для пере-чтения | пин `TestASlowCutStillAnswersWithTheRowAsItStandsAfterIt`; бюджет укорочен сеемым полем, а не ожиданием |
|
||
| 3 | ⛔ **три ветви деплойного класса НЕ отдавали claim и тратили попытку**, а одна писала `rejected`, который `201` выносил пользователю | все три при `atIntake` возвращают `errNotConclusive`, не трогая `defer_`/`reject` | пин `TestADeploymentFaultLeavesNoVerdictAboutTheFile`: `parse_attempts = 0`, `parse_started_at is null` |
|
||
| 3 | ⛔ **синхронная материализация читательской поверхности** (до 5 мин) держала запрос и не входила в `UploadSettle` | на интейке не материализуем — долг записан, забирает свип; `UploadSettle = CutBudget + 3×writeBudget` с перечнем шагов | гейт `internal/config` читает саму константу |
|
||
| 3 | ⛔ **маппинг обоих отказов на HTTP не был запинен** — обмен кодов местами оставлял пакет зелёным | пин по КОДУ, а не по факту 400 | `TestTheIntakesOwnRefusalsReachTheWireWithTheirOwnCodes` |
|
||
| 3 | ⚠ **сужение входа действует только на синхронной ветви** | закрыть сегодня нечем: словарь `RejectReason` закрыт, ближайшее значение УДАЛЯЕТ исходник | заведён `PD-463`, пинг ниже |
|
||
| 3 | `PD-458` помечен `fixed(в дереве пака…)` — ложная атрибуция | `fixed(0632a30)` | `git merge-base --is-ancestor 0632a30 e4097cb` → да |
|
||
| 3 | ряды `PD-441`/`PD-424`/`PD-438` не несли диспозиции пака | дописаны, форма ряда сохранена | `awk -F'|' '{print NF-2}'` по трём рядам → 7, 7, 7 при шапке 7 |
|
||
| 3 | `TestTheUploadSettleBudgetCoversTheSynchronousCut` — ТАВТОЛОГИЯ | тест удалён, вместо него перечень шагов в комментарии константы | `CutBudget = 10h` оставлял его зелёным, а `internal/config` давал 12 красных |
|
||
| 3 | замер денег не воспроизводился: скратч-БД удалена, команда не названа | стал ПИНОМ, печатающим обе стороны | `TestTheRawLedgerAndTheReadModelAgreeOnWhatWasSpent` |
|
||
| 3 | живой `STACK_DECISIONS` отсылал в архив, чей баннер запрещает исполнять инструкции | сказано, что правило целиком стоит на месте, а архив — археология | — |
|
||
| 3 | док-комментарий `cutNow` склеен с `cutResult` | разделены | `gofmt` и `go vet` чисты |
|
||
| 3 | числа «потеряна 1 строка» и «195 осмотренных `.go`» | пере-сняты: **10** строк (все названы) и **197** | раздел «Команды и их вывод» |
|
||
| **4 (опровергатель, `fable`)** | ⛔ **задание очереди ставилось в транзакции `StartParsing` и гонялось с синхронным разрезом за один claim** — выигрывает очередь ⇒ fail-fast молча нет; выигрывает разрез ⇒ job съеден впустую, и после возврата claim'а книгу подбирает только свип через 20 мин | job ставится ТОЛЬКО там, где разрез не идёт; на пути «вердикта нет» — вместе с возвратом claim'а, одной транзакцией (`ReleaseParseClaim`) | пины `TestTheIntakeNeitherMaterializes…` и `TestACutWithNoVerdictGivesTheClaimBackAndEnqueuesTheJob`, обе мутации красные |
|
||
| 4 | ⛔ **`UploadSettle` снова не покрывал хвост:** `StartParsing` (30 с) выпал из перечня, а квитанция идемпотентности 10 с, не 30 ⇒ худший путь 190 с при константе 180 | `CutBudget + 4*writeBudget`, перечень шагов пере-написан ПО КОДУ и по бюджету каждого | гейт `internal/config` читает саму константу |
|
||
| 4 | ⛔ **из «трёх ветвей деплойного класса» запинена была одна**; мутации на `ErrStorageGone` и `ErrDirectoryGone` выживали | обе ветви решаются ДО вызова движка, поэтому запинены на своём уровне | `TestTheBranchesDecidedBeforeTheEngineAlsoLeaveNoVerdict` |
|
||
| 4 | ⛔ **починка «не материализуем на интейке» не имела пина** | заведён | мутация «снят `!atIntake`» → `the intake materialized the reading surface (1 claims, 1 refreshes)` |
|
||
| 4 | ⛔ **дельта контракта противоречила починке третьего круга** — обещала `character_count_exact: true` там, где его теперь не бывает | дельта переписана: оба поля отсутствуют в `201` ВСЕГДА | пин выше |
|
||
| 4 | `Settle` потерял док-комментарий: мой тип встал между ним и функцией | комментарий возвращён | `go doc ./internal/pgstore Store.Settle` печатает его |
|
||
| 4 | `PD-461` сам нёс дефект, который описывает (9 полей), и занижал счёт | ряд переписан: полей 7, строк ЧЕТЫРЕ, а не две | `awk` по разделителю: `PD-461` → 7 |
|
||
| 4 | в таблице «Что построено» стоял носитель-призрак — удалённый тест | заменён на настоящие | `grep` по имени → 0 |
|
||
| 4 | ⚠ **опровергатель ошибся** в одном числе: `character_count_exact` «в 3 файлах» | пере-снято: **4** (`v0.go` 2 · `project.go` 1 · `v0_test.go` 2 · `books.go` 2); он грепал только строковый вид имени | команда в разделе ниже |
|
||
| 4 | ⚠ мутация «снят охранник стадии у `DeleteRefusedIntake`» выживает | **по построению, а не из-за дыры:** каждый терминальный переход (`FinishParse`, `reject`) обнуляет `parse_started_at`, поэтому непустой claim влечёт `status = 'parsing'` — охранник избыточен | `grep -n parse_started_at internal/pgstore/books.go` — все четыре писателя |
|
||
|
||
### `PD-424`: окон оказалось ДВА, а не одно
|
||
|
||
Ряд называл одно — заявку на спавн между пробой systemd и коммитом. По коду их два, и второе опаснее:
|
||
`AbandonRun` под книжной блокировкой читает **любую живую попытку** (`a.ended_at is null`), поэтому
|
||
разблокировавшаяся расплата + `restart` подставляют под доказательство о попытке N **попытку N+1** с
|
||
живым процессом. Лечение — доказательство приходит парой (`ProofAttemptID`, `ProofSpawns`) и
|
||
сверяется в той же транзакции; расхождение — `ErrProofOvertaken`, отказ, а не применение.
|
||
`unit_name` в свидетели не годится: `ReleaseSpawnClaim` возвращает его в NULL. Отсюда монотонный
|
||
счётчик `spawns`, инкремент — внутри самого CAS `RecordSpawn`.
|
||
|
||
### Миграция `00034` (санкционирована оркестратором №23)
|
||
|
||
`run_attempts`: `spawns integer not null default 0` · `parked_at timestamptz`. Down-путь ГОНЯЕТСЯ
|
||
(`pgstore.TestMigrationsRollBackAndReapply`, зелёный на живом Postgres). Существующие строки поведения
|
||
не меняют: сверка идёт с ПРОЧИТАННЫМ числом, не с абсолютным. План гейджа снят исполнением: обе
|
||
подвыборки (`parked_at`/`quarantine_reason`) идут `Index Scan using run_attempts_live_idx` — новый
|
||
индекс не нужен. Запись парковки — только на ПЕРЕХОДЕ, в установившемся состоянии ноль операторов.
|
||
|
||
### `PD-441`: чем ограничена платформенная половина
|
||
|
||
Маржу платформа **измерить не может** — биллинга провайдера у зоны нет, а движковый леджер её не
|
||
несёт. Что сделано: цифра перестала читаться как цена. Что НЕ сделано и почему: показывать маржу
|
||
конечному пользователю нечего — недо-счёт бьёт по ДЕПЛОЮ, баланс пользователя завышен в его же
|
||
пользу (решение оркестратора №23, 06.09; формулировка «показать пользователю» из промта снята).
|
||
|
||
**Движковая половина — заказ следующему паку, поимённо:**
|
||
1. `backend/internal/pipeline/stagerun.go`, ветвь `No 2xx ever arrived: nothing was billed` — сеттлить
|
||
ОЦЕНКУ резервации, а не ноль, когда отменённый вызов уже ушёл; различитель — факт ухода, порог
|
||
обязан быть ЗАМЕРЕН, а не назначен (ряд называет латентность кандидатом: 20 мс против 156 000 мс).
|
||
2. `tmctl status --json` — публиковать рядом с `committed_usd` число и сумму строк, чья цена
|
||
ОЦЕНОЧНАЯ или неизвестна (движок уже печатает `estimated-cost rows: N`, строка бэклога **78**).
|
||
Без этого платформа умеет говорить «≥ X», но никогда «≥ X, до Y».
|
||
|
||
### Строки 285 и 325: одна правка, предикат — по данным
|
||
|
||
`tmctl manifest` зовётся синхронно после последнего байта. Четыре исхода:
|
||
`глав ≥2` → `FinishParse` инлайн, `201` несёт `not_started` и число глав · `глав 0` → `400`
|
||
`invalid_request`, `errors[{file, no_book}]`, не принято ничего · `глав 1` → `400`,
|
||
`errors[{file, no_chapter_structure}]` · **деплойный класс или таймаут** → `201 parsing`, очередь
|
||
доделывает, claim ВОЗВРАЩАЕТСЯ (иначе задание очереди нашло бы книгу занятой и ничего не сделало).
|
||
|
||
⭐ Отказ **по числу глав, а не по расширению**: выдача кладёт по одному XHTML на главу ДВИЖКА, значит
|
||
«книга одним полотном» ⟺ движок нарезал <2 глав. `.epub` движок режет по nav/NCX и он проходит —
|
||
блокировка по расширению отказала бы тому, что мы умеем доставлять. Go по паре и формату не ветвится.
|
||
|
||
#### Дельта контракта, которую ратифицирует оркестратор (минор `0.12.0` → `0.13.0`)
|
||
|
||
⚠ **Канон я НЕ трогаю** — это зона оркестратора. Здесь лежит ТЕКСТ дельты, чтобы её не выводили заново.
|
||
|
||
**Что становится ложным:** `docs/architecture/14-api-contract/openapi.yaml`, описание `201` у
|
||
`createBook` — фраза «**The `201` carries `parsing`, not `uploading`**». После синхронного разбора
|
||
`201` несёт `parsing` только на одном из четырёх исходов.
|
||
|
||
⛔ **ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОЙ ДЕЛЬТЫ ОТОЗВАНА (строка 285), и вот что она говорила неверно.** Она обещала, что
|
||
`character_count_exact` и `structure` зависят от того, «удалась ли материализация читательской
|
||
поверхности», и что при удаче приезжают в том же `201`. Это было верно ровно до починки четвёртого
|
||
круга: интейк БОЛЬШЕ НЕ МАТЕРИАЛИЗУЕТ читательскую поверхность (она стоит два движковых прогона и
|
||
держала бы запрос загрузившего), поэтому оба поля в `201` отсутствуют **ВСЕГДА** и приезжают
|
||
следующей ревизией библиотеки. ⚠ Сказано вслух, а не заменено молча: при конфликте редакций
|
||
действует эта.
|
||
|
||
**Что несёт ответ `POST /v0/books` в каждом исходе:**
|
||
|
||
| исход | ответ | тело |
|
||
|---|---|---|
|
||
| движок нарезал **≥ 2 глав** | `201`, заголовок `Location` | `Book.status = "not_started"` · `chapter_count` = число глав движка · `character_count` = счёт интейка с **`character_count_exact: false`** и **`structure: null`** — ВСЕГДА. Оба поля пишет `SaveStructure` вместе с читательской поверхностью, а интейк её не материализует (уплотняет запрос на два движковых прогона): они приезжают позже, проходом материализатора, и клиент видит их следующей ревизией библиотеки. Правило `null` не меняется |
|
||
| движок прочёл и нарезал **0 глав** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_book" }]`; книга НЕ принята — ни строки в библиотеке, ни каталога на диске |
|
||
| движок прочёл и нарезал **1 главу** | `400` `invalid_request` | `errors: [{ pointer "/file", code "no_chapter_structure" }]`; тоже не принято ничего |
|
||
| **деплойный класс или таймаут** | `201`, заголовок `Location` | `Book.status = "parsing"` — прежнее поведение маршрута целиком; очередь и страховочный свип доделывают |
|
||
|
||
**Деплойные классы перечнем** (ни один не отказывает пользователю): `not_configured` — у книги нет
|
||
конфигурации движка, или шаблон деплоя не читается · `storage_unavailable` — корень хранилища книг не
|
||
смонтирован · `schema_mismatch` — проектная БД книги не той схемы, что бинарь · `parser_unavailable` —
|
||
движок не удалось ЗАПУСТИТЬ, либо он ответил классом, которого эта сборка не знает, либо обычным
|
||
выходом 1 · плюс превышение бюджета синхронного разбора (`books.CutBudget`).
|
||
|
||
**Новых значений `ErrorCode` НЕ заводится.** Оба отказа — существующий `invalid_request`; `no_book` и
|
||
`no_chapter_structure` живут в `errors[].code`, который сама спека объявляет НЕ закрытым («Not closed,
|
||
like `cause.code`»). ⚠ `no_chapter_structure` — ВРЕМЕННЫЙ: он снимается, когда построена структура глав
|
||
для выдачи (строка бэклога **283**), и в коде ветки стоит этот номер, чтобы её нашли и убрали.
|
||
|
||
### Батарея, мутации и деньги
|
||
|
||
- **`make check` MAKE-EXIT=0**, 20 пакетов `ok`, красных 0, скипов **8**. Невыполненное условие хоста
|
||
названо: `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (рецепт требует $0-пайплайн
|
||
РЯДОМ с `backend/prompts/`, то есть записи в чужую зону или полной копии дерева).
|
||
- **Тесты: 853 → 875 (+22).** Тестов, существовавших ДО пака, не удалено ни одного
|
||
(`git diff -- '*_test.go' | grep -c '^-func Test'` → 0). ⚠ Один тест, добавленный ЭТОЙ ЖЕ сменой,
|
||
удалён третьим кругом как тавтологичный (`TestTheUploadSettleBudgetCoversTheSynchronousCut`:
|
||
`UploadSettle` определён через `CutBudget`, поэтому утверждение выполнялось всегда) — в диффе против
|
||
`943617a` это не видно, и потому названо здесь. ⚠ Число снималось ПЯТЬ раз и первые четыре были
|
||
неверны: «866 → 863» (глоб захватил не-тестовые файлы), «853 → 863», «853 → 864», «853 → 866» и
|
||
«853 → 869» — каждое снято до пинов, добавленных следующим кругом самопроверки. В отчёте последнее,
|
||
после последней правки. ⚠ Само по себе это и есть измеренная цена преждевременного объявления
|
||
сходимости: шесть замеров одного числа, потому что кругов оказалось не два, а пять.
|
||
- **Мутации: посажено 18, поймано 14 сразу, выжило 3, одна поимка отозвана** (её тест удалён третьим кругом, см. таблицу). Две выживших — дыры в МОИХ пинах,
|
||
обе починены и пере-посажены (после починки красные); третья выживает СОЗНАТЕЛЬНО и названа.
|
||
⚠ Шестнадцатая посадка ОТБРОШЕНА мной как негодная: первая версия мутации охранника `claim`'а
|
||
краснила тест ошибкой ТИПИЗАЦИИ Postgres (`could not determine data type of parameter $2`), то есть
|
||
давала правый вердикт по неправой причине; пере-посажена корректно типизированной, и в таблице стоит
|
||
вторая.
|
||
|
||
| посадка | текст падения (или почему выжила) | хеш восстановлен |
|
||
|---|---|---|
|
||
| `report-failures` без строки сводки пакетов | `does not carry "textmachine/platform/internal/money"` | да |
|
||
| `report-failures` возвращён к грепу `--- FAIL` (исходный дефект 305) | то же, на всех трёх фикстурах | да |
|
||
| `report-failures` без счёта улик | `does not carry "Evidence, 1 lines"` | да |
|
||
| `AbandonRun` без сравнителя ПОПЫТКИ | ⚠ **ВЫЖИЛА**: случай ловился сравнителем ЗАЯВОК — правый вердикт по неправой причине. Пин починен (новой попытке даётся тот же счёт заявок + сверка ТЕКСТА отказа), пере-посажена → `answered <nil>, want ErrProofOvertaken` | да |
|
||
| `AbandonRun` без сравнителя ЗАЯВОК | `answered <nil>, want ErrProofOvertaken: the unit name reads exactly as the proof saw it` | да |
|
||
| `RecordSpawn` без инкремента свидетеля | `the claim counter went 0 → 0: a witness that does not move cannot catch the race` | да |
|
||
| свип не пишет парковку | `the parked attempt carries no ParkedAt` | да |
|
||
| свип пишет парковку каждый проход (снят порог) | ⚠ **ВЫЖИВАЕТ ПО ПОСТРОЕНИЮ**: охранник записи всё равно не двигает метку. Порог покупает СТОИМОСТЬ, а не свойство; названо в комментарии теста | да |
|
||
| `MarkParked` без охранника `is null` | ⚠ **ВЫЖИЛА**: два охранника прикрывали друг друга. Добавлено обращение к записи НАПРЯМУЮ, пере-посажена → `a second mark moved the stamp from … to …` | да |
|
||
| снятие метки сделано неисполнимым | `the attempt is materializing again and the row still says parked since …` | да |
|
||
| парковка посчитана карантинным гейджем | `parked=0 quarantined=1, want the park counted once and in its own series` | да |
|
||
| разрез выпал из `UploadSettle` (состояние ДО находки) | ⚠ **поимка НЕВОСПРОИЗВОДИМА:** ловивший её тест удалён третьим кругом как тавтологичный, и в «поймано» она больше не считается | да |
|
||
| знак `≥` снят с колонки SPENT | `SPENT prints an exact amount; the engine's meter is a lower bound` | да |
|
||
| ячейка PARKED слита с QUARANTINE | `a parked attempt shows no elapsed time in PARKED, so «how long has it been quiet» has no answer` | да |
|
||
| охранник claim'а у `DeleteRefusedIntake` всегда истинен | `a delete under somebody else's claim gave <nil>, want ErrNoBook` | да |
|
||
| пере-чтение снова на контексте, открытом ДО тела | `after a cut that outlived the write budget the response says "parsing" with 0 chapters, want the parsed row` | да |
|
||
| ветвь «манифест не читается» снова тратит попытку | `the intake's pass spent 1 attempts of a budget that bounds how often a broken HOST is asked` + `the claim is still held (…)` | да |
|
||
| коды двух отказов на HTTP поменяны местами | `item code no_chapter_structure, want "no_book": the two refusals are different answers to the user` | да |
|
||
- **Деньги двумя независимыми путями** на настоящих строках: сырой SQL по `credit_ledger` (7 строк,
|
||
сумма 4919187 micro) и чтение платформы `ReadAccount` (Balance=LedgerSum=$4.919187) — сходятся.
|
||
Расхождение до/после починки на тех же строках: было `note=""`, стало `at least: …` и, для
|
||
оборванной попытки, `…cut off mid-work… (PD-441)`.
|
||
|
||
### ⛔ СРАБОТАЛО ПРАВИЛО ОСТАНОВКИ — синхронный разрез НЕ ГОТОВ и должен уехать отдельным паком
|
||
|
||
Пятый круг дал major **внутри бюджета хвоста загрузки**, а оркестратор №23 отнёс этот бюджет к пути
|
||
синхронного разреза именно на этот случай. Правило исполнено: путь больше не чинится, находки ниже
|
||
записаны для следующего пака и НЕ закрыты.
|
||
|
||
**Чем правило сработало (замер, а не оценка).** `UploadSettle` не покрывает хвост ТРЕТИЙ раз подряд, и
|
||
каждый раз по новой причине. Сегодняшняя: перечень шагов в комментарии константы называет `FinishParse`
|
||
и `ReleaseParseClaim` АЛЬТЕРНАТИВАМИ («whichever end the cut reaches»), а код их СКЛЕИВАЕТ — ошибка
|
||
`FinishParse` уходит наверх (`internal/books/parse.go`, греп `owed, err := s.Store.FinishParse`), и
|
||
`cutNow` на любой не-`ErrBadIntake` ошибке зовёт `ReleaseParseClaim` на СВЕЖЕМ `writeCtx`. Худший путь:
|
||
`StartParsing 30 + разрез 90 + FinishParse 30 + Release 30 + ReadBook 30 + квитанция 10 = 220 с` при
|
||
`UploadSettle = 210 с`.
|
||
|
||
**Измерено, а не оценено (двоичный поиск по бутовому гейту через `Load()`):** гейт принимает дедлайн
|
||
до **26m29s**, ложь начинается с **26m21s** — окно шириной восемь секунд, достижимое только ручной
|
||
настройкой почти вплотную к потолку самого гейта; на дефолте **10m0s** запас **16m20s**. Наблюдаемое
|
||
следствие — **дубль книги, а не потеря**: претензия на ключ идемпотентности, пережившая `ClaimStale`,
|
||
перехватывается повтором с новым токеном, и человек, повторивший «висящую» загрузку, получает вторую
|
||
книгу вместо реплея первой. ⚠ Достижимость худшего пути без искусственного замедления **не измерена**:
|
||
она требует конъюнкции «большая книга» и «три полных `writeBudget` подряд», а гейт живого движка на
|
||
этом хосте не закрыт — подставное замедление на вопрос «достижимо ли БЕЗ него» не отвечает. Ряд —
|
||
`PD-464`. ⚠ Батарея этого не видит по построению: пин на сумму был
|
||
тавтологичным и снят третьим кругом, а мутация `4*writeBudget → 3*` оставляет и `internal/config`, и
|
||
`internal/books` зелёными.
|
||
|
||
**Почему это остановка, а не ещё одна починка.** Из одиннадцати major, найденных кругами 3–5, девять
|
||
лежат в одном месте — синхронном разрезе на приёме. Это новый механизм в конкурентном платном пути
|
||
(claim · очередь · транзакция · бюджет), и он ведёт себя как такой механизм: каждая починка открывает
|
||
новую площадь. Три круга подряд одна и та же константа оказывалась короче хвоста, и каждый раз по
|
||
другой причине — это свойство предмета, а не невнимательности.
|
||
|
||
**Что остаётся НЕ ЗАКРЫТЫМ в этом пути** (для пака, который его заберёт):
|
||
|
||
1. `UploadSettle` короче хвоста на 10 с (`PD-464`). ⛔ Лечение — **не поднять константу**: она
|
||
выводилась руками трижды и трижды была неверной, каждый раз по новой причине, поэтому четвёртый
|
||
вывод руками — подпорка (`D39.216`). Границу надо выводить **ИЗ кода пути**.
|
||
2. Комментарий в `cutNow` («the queue job is already enqueued») противоречит починке, ради которой
|
||
функцию правили: на этом пути задание ставит сам `ReleaseParseClaim`.
|
||
3. Отказ `ClaimParse` в `cutNow` не различает рутинную гонку (`ErrParseClaimed`) и сбой БД, и во
|
||
втором случае молчит: книга остаётся `parsing` без задания и без строки лога до свипа.
|
||
4. Параметр `enqueue` у `StartParsing` в бою мёртв (движок настроен всегда), а его doc описывает его
|
||
как штатный путь; решение «кто ставит задание» размазано по двум местам на одном предикате.
|
||
5. Охранник `RowsAffected() == 0 → задание не ставить` в `ReleaseParseClaim` не запинен ничем.
|
||
6. «ВСЕГДА» в дельте контракта — гарантия по ТАЙМИНГУ, не по построению: свип материализатора
|
||
теоретически успевает между `FinishParse` и `ReadBook`. Практически вероятность близка к нулю, но
|
||
тексту контракта полагается либо структурная гарантия, либо оговорка.
|
||
7. Свип `StuckIntake` — третий претендент на claim, в разборе гонки не назван: при
|
||
`TM_PLATFORM_UPLOAD_DEADLINE > claimGrace` он может взять claim раньше разреза.
|
||
8. Разрез потерял единственный ограничитель параллелизма: задание на приёме больше не ставится, а
|
||
`MaxWorkers: 4` был у очереди — одновременных разрезов теперь не ограничивает ничто.
|
||
9. После последнего байта тела маршрут МОЛЧИТ до 210–220 с; промежуточный прокси об этом не спрашивали.
|
||
10. Отказ уходит наружу только машинным кодом (`Detail` не заполняется) ⇒ «честная причина человеку»
|
||
из §4.3 сегодня не доезжает. Фронт заморожен, значит либо причина едет в `Detail`, либо пункт
|
||
признаётся неисполненным.
|
||
|
||
### Замер, который должен пережить пак: очередь выигрывала гонку у разреза 5 раз из 6
|
||
|
||
Синхронный разрез берёт `ClaimParse` ПОСЛЕ коммита `StartParsing`, а `StartParsing` ставил задание
|
||
очереди ВНУТРИ той же транзакции — то есть воркер становился видимым тем же коммитом и гонялся с
|
||
разрезом за один и тот же claim. Проба (энкьюер зовёт `Parse` сразу после коммита, как это делает
|
||
River на видимости строки), шесть прогонов: **очередь выиграла 5 раз, разрез 1 раз**.
|
||
|
||
Оба исхода стоят книге, и это делает гонку дефектом, а не шероховатостью:
|
||
- **выигрывает очередь** — синхронного вердикта нет вовсе, пустой файл принимается `parsing`,
|
||
попытка бюджета потрачена, отказ приезжает асинхронно и оставляет книгу в библиотеке;
|
||
- **выигрывает разрез** — задание съедено впустую (`ErrParseClaimed` → `nil`), и после возврата
|
||
claim'а книгу подбирает только страховочный свип, то есть через `claimGrace` = 20 минут.
|
||
|
||
⇒ задание ставится ТОЛЬКО там, где синхронного разреза не будет, а на пути «вердикта нет» — вместе с
|
||
возвратом claim'а одной транзакцией. Цена названа: процесс, умерший между строкой и разрезом,
|
||
оставляет книгу без задания, и её берёт свип через `claimGrace`, а не сразу.
|
||
|
||
### Команды и их вывод — числа этого отчёта
|
||
|
||
Сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются.
|
||
|
||
```
|
||
$ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
|
||
20 строк `ok`, ни одной `FAIL`, MAKE-EXIT=0
|
||
--- did NOT run: 8 skipped … UNSET: TM_PLATFORM_TEST_BOOK_TEMPLATE, TM_PLATFORM_TEST_ENGINE_BIN
|
||
|
||
$ git grep -h '^func Test' 943617a -- 'platform/**/*_test.go' | wc -l → 853
|
||
$ find platform -name '*_test.go' -exec grep -h '^func Test' {} + | wc -l → 875
|
||
$ git diff -- '*_test.go' | grep -c '^-func Test' → 0
|
||
$ git diff -- '*_test.go' | grep -c '^[-+]func Fuzz' → 0
|
||
|
||
$ python3 docs/scripts/carriers.py platform/docs/platform-PROGRESS.md
|
||
carriers: 0 помеченных утверждений без носителя · осмотрено строк: 491
|
||
|
||
$ grep -c '^| PD-' platform/docs/DEFECT_REGISTER.md → 465
|
||
$ python3 docs/scripts/counts.py --check
|
||
✗ docs/PROGRESS.md: «открытых 106» против пере-счёта 112 · «всего 458» против 465 (зона docs/)
|
||
|
||
$ grep -rc 'THE FIX IS NOT IN THIS PACKAGE' --include=*.go platform/ | grep -v ':0' | wc -l → 0
|
||
контроль: .go файлов осмотрено 197 · 'character_count_exact' найдено в 4 (v0.go, project.go, v0_test.go, books.go)
|
||
|
||
$ git status --short -- docs/architecture/14-api-contract/ platform/docs/ENGINEERING_STANDARDS.md \
|
||
platform/docs/PLATFORM_DIRECTION.md → пусто
|
||
$ git status --short -- backend/ | wc -l → 17
|
||
⚠ и это НЕ мой след: 17 файлов правит параллельная бэкенд-сессия. Что этот пак в `backend/` не
|
||
писал, из дерева НЕ измеримо — измеримо лишь то, что один файл я туда положила по инерции и
|
||
убрала (`backend/configs/` чист). Прежняя редакция подавала это как замер; это не замер.
|
||
$ git diff --cached --name-only | wc -l → 0
|
||
```
|
||
|
||
### Вынос журнала — что именно потеряно, пере-снято после всех правок
|
||
|
||
`comm` по непустым строкам: было **4156**, стало **4420** (три файла плюс баннеры срезов), не сошлось
|
||
**10** строк — и все десять названы, потому что «одна» в прежней редакции этого абзаца была верна лишь
|
||
на момент замера и устарела от моей же последующей правки:
|
||
|
||
- заголовок «Пинги оркестратора (живые ссылки для следующих сессий)» → «…ИСПОЛНЕНЫ» (все пять пингов
|
||
под ним отработаны);
|
||
- девять строк трёх ХВОСТОВЫХ секций-указателей («Эра пака P7 — в архиве», «Закрытые эры P0–P3 — в
|
||
архиве» и дублирующий их буллет) — они дублировали блок «Архив эр» и слиты в него; обе ссылки
|
||
(`-P7.md`, `-P0-P3.md`) в живом файле сохранены и резолвятся.
|
||
|
||
⚠ То есть потеряны ФОРМУЛИРОВКИ дублей, не содержание, и это утверждение проверяемо: все шесть ссылок
|
||
на срезы в живом файле разрешаются в существующие файлы. Ссылки ИЗ других файлов в вынесенные
|
||
диапазоны пере-нацелены: `sqlc.yaml`, `STACK_DECISIONS.md` (условия стенда — там же сказано, что архив
|
||
читается как археология, а не как инструкция), `README.md` (шапка и таблица).
|
||
|
||
### Находка собственного круга: хвост загрузки, о котором бутовый гейт не знал
|
||
|
||
Синхронный разрез добавил до полутора минут к тому, что загрузка делает ПОСЛЕ тела, а бутовый гейт
|
||
(`internal/config`: `UploadDeadline + books.UploadSettle < min(UploadGrace, ClaimStale)`) про этот
|
||
участок не знал — то есть оператор мог настроить дедлайн, при котором загрузка переживает окно
|
||
идемпотентного ключа. Вылечено переносом бюджета в саму константу: `UploadSettle = CutBudget +
|
||
writeBudget + 30s`. Существующий бутовый тест читает `UploadSettle`, а не литерал, поэтому подъём
|
||
`CutBudget` теперь автоматически ужимает допустимый дедлайн.
|
||
|
||
**Цена того, что `CutBudget` стал КОНСТАНТОЙ, а не настройкой — явно.** Деградация мягкая: бюджет
|
||
работает ПОРОГОМ, а не потолком — на хосте, где разрез книги законно дольше полутора минут, загрузка
|
||
не падает, она возвращается на прежний асинхронный путь (`201 parsing`, очередь доделывает), и
|
||
единственная потеря — менее информативный `201`. Ручка убрана не ради чистоты: за ней стоял способ
|
||
выстрелить себе в ногу — настройка, которой можно вытолкнуть хвост загрузки за окно, в котором её
|
||
ключ ещё можно переиграть, а окно это ни один экран не показывает.
|
||
|
||
### Дофикс по приёмке оркестратора №23 — шесть пунктов, все закрыты
|
||
|
||
| пункт | что было | что сделано | чем предъявлено |
|
||
|---|---|---|---|
|
||
| **Д1** | ⛔ **базис расчёта ВЫВЕРНУТ на самом частом окончании:** `finish()` считает исход в локальную переменную, а `settle()` получает ТОТ ЖЕ до-финишный снапшот ⇒ у прогона, кончившегося чисто, в леджер уезжал `halted` | снапшот несёт исход, который этот же вызов только что решил (`l.Status, l.PausedReason = status, pausedReason`) | **ИСПОЛНЕНИЕМ, не по коду:** прогон доведён до `ready`, строка леджера прочитана SQL-ом. До починки: `status="ready"`, а `note` — «cut off mid-work». Пин `TestACleanEndingIsSettledOnTheBasisOfTheEndingItReached`, мутация возвращает дефект и красит его текстом |
|
||
| **Д2** | гейдж `PARKED` не запинен на уровне ЭКСПОЗИЦИИ, и проводка `Observe → metrics.Runner` в демоне не запинена вовсе | ассерт на серию в экспозиции + гейт, читающий композитный литерал демона и требующий, чтобы каждое поле бралось из поля СВОЕГО имени | две мутации: подмена серии → `the exposition is missing "tm_platform_parked_attempts 7"`; обмен полей в демоне → `one state's number is published under another's name` |
|
||
| **Д3** | гейт 305 пинил ТАРГЕТ, а не его использование: откат ветви отказа к голому грепу оставлял батарею зелёной | гейт держит ветвь отказа рецепта `check`: она обязана звать `report-failures` и НЕ грепать `--- FAIL` сама | мутация — буквальный откат строки — красит обе проверки |
|
||
| **Д4** | рантбук утверждал, что у парковки нет ни колонки, ни гейджа, ни строки; после пака это ложь | рантбук переписан: у парковки СВОИ три сигнала, карантинных нет и не будет, потому что снимать её не надо. Плюс `≥` у SPENT и пятый отказ `run abandon` | `deploy/README.md`, греп `tm_platform_parked_attempts` и `ПЯТЫЙ ОТКАЗ` |
|
||
| **Д5** | комментарий `PD-424` объявлял исчерпывающие «two ways», а способов ТРИ | комментарий перестал объявлять полноту и называет третий способ с его радиусом; сам способ — ряд `PD-465` | заявка коммитится в `RecordSpawn`, юнит поднимается строкой ниже (`spawn.go`, `Runner.Start`) |
|
||
| **Д6** | 18 протухших якорей в регистре, сдвинутых этим паком | пере-нацелены ПО ТОКЕНУ, а не по памяти; девятнадцатый — мой собственный, я записала токен с многоточием, которого в коде нет | `counts.py --lint`: было 25 проблемных якорей в 120 доках, стало **5**, в регистре **0** |
|
||
| **Д7** | не заказан приёмкой, найден её же батареей: новый ряд `PD-465` несёт два маркера тревоги и не был объявлен в `alarmBaseline` | объявлен, с доводом почему он НИЖЕ major (окно в миллисекунды, расход ограничен потолком того же прогона, аккаунтом не эксплуатируем) | гейт `TestOpenRowsBelowMajorThatCarryAlarmMarkers…` красил батарею именно на нём; ⭐ и назвал его прибор **305** — «`check` упал» отличилось от «тест упал» на настоящем падении |
|
||
|
||
⚠ **Один якорь остался и он НЕ мой по зоне:** `docs/architecture/05-decisions-log.md:2678` целит в
|
||
`platform/internal/pgstore/books.go:314` по токену `FinishParse records a parsed book`; мой пак сдвинул
|
||
цель на **343**. Правка в зоне оркестратора — число передано пингом.
|
||
|
||
⚠ **Что приёмка нашла и что чинить НЕ надо** (правило остановки в силе, дописано к семи пунктам
|
||
остановленного разреза): разрез потерял единственный ограничитель параллелизма — задание на приёме
|
||
больше не ставится, а `MaxWorkers: 4` был у очереди · маршрут молчит до 210–220 с после последнего
|
||
байта, промежуточный прокси об этом не спрашивали · отказ уходит наружу только МАШИННЫМ кодом, `Detail`
|
||
не заполняется, то есть «честная причина человеку» из §4.3 сегодня не доезжает.
|
||
|
||
### Пять кругов самопроверки — что дал каждый
|
||
|
||
| круг | кто | major | итог |
|
||
|---|---|---|---|
|
||
| 1 | я, по готовой работе | — | две дыры в МОИХ пинах (посадки выживали) + хвост загрузки, о котором бутовый гейт не знал |
|
||
| 2 | я, против заказа | — | «≥» и парковка были предъявлены чтением кода, а не исполнением; охранники `DeleteRefusedIntake` не запинены; дельта контракта неверна. ⚠ **Сходимость объявлена здесь ПРЕЖДЕВРЕМЕННО** |
|
||
| 3 | опровергатель `fable` | **4** | три — дефекты, введённые этим паком: мёртвый контекст пере-чтения · три ветви без возврата claim'а · синхронная материализация; плюс незапиненный маппинг на проводе |
|
||
| 4 | опровергатель `fable` | **5** | гонка задания очереди с разрезом за один claim (**очередь выигрывала 5 из 6**) · бюджет снова короче хвоста · две ветви из трёх без пина · `!atIntake` без пина · ложная дельта контракта |
|
||
| 5 | опровергатель `fable` | **1** (+6 minor) | бюджет короче хвоста ТРЕТИЙ раз, по третьей причине ⇒ **правило остановки** |
|
||
|
||
⚠ **Цена преждевременной сходимости, измеренная:** одно число (счёт тестов) снималось ШЕСТЬ раз,
|
||
потому что кругов оказалось не два, а пять; и дельта контракта, которую оркестратор собирался
|
||
ратифицировать, дважды была ложной — первый раз по существу, второй раз после моей же починки.
|
||
|
||
⭐ **Что из этого стоит унести дальше:** восемь major из одиннадцати нашёл ЧУЖОЙ прибор, а не я, и
|
||
все восемь — в коде, который я только что написала и считала проверенным. Оба круга, которые я вела
|
||
сама, дали настоящие находки, но НИ ОДНОГО major в новом механизме: свой код я проверяла по тому,
|
||
что он должен делать, а опровергатель — по тому, что он делает.
|
||
|
||
### Что НЕ удалось и что считаю слабым местом
|
||
|
||
- **Не измерено:** маржа `PD-441` в деньгах — приборa нет на этой стороне (см. движковую половину).
|
||
- **Не предъявлено живым прогоном:** синхронный разбор гонялся на фикстурах и на живом Postgres, но
|
||
не на настоящем `tmctl` — гейт `TM_PLATFORM_TEST_ENGINE_BIN` требует записи в `backend/`.
|
||
- **Слабое место, которое называю сам:** `settlementBasis` судит по СТАТУСУ попытки, а не по факту
|
||
наличия вызовов в полёте. Классы огрублены сознательно (пере-пометка стоит менее точного сигнала,
|
||
недо-пометка прячет деньги), но `awaiting_bank` отнесён к «чистым» по чтению кода движка, а не по
|
||
замеру: если окажется, что банк-стоп тоже рвёт волну, класс придётся сузить.
|
||
- **Третье, названное опровергателем и оставленное сознательно:** если удаление отказанной книги
|
||
само упрётся в БД (`discard` → `removeIntake`), пользователь получит `400`, а строка останется
|
||
`parsing` под claim'ом; свип через `claimGrace` перечитает манифест, потратит бюджет попыток и
|
||
оставит книгу `rejected` в библиотеке — то есть ровно то состояние, которого отказ и избегает.
|
||
Не лечу: путь требует отказа БД РОВНО между двумя её же операциями в одном запросе, а лечение —
|
||
компенсирующая транзакция, то есть механизм заметно крупнее устраняемого класса. Названо, чтобы
|
||
следующий пак не открывал его заново.
|
||
- **Второе слабое место:** отказ книге без глав — продуктовое сужение. Оно временное и помечено
|
||
строкой 283 в коде ветки, но пока владелец его не подтвердил, это решение сессии.
|
||
|
||
**Работа завершена, править больше не планирую.** Дерево передано оркестратору №23 незакоммиченным:
|
||
37 файлов, все внутри `platform/`, индекс пуст. Синхронный разрез построен и ОСТАНОВЛЕН правилом
|
||
остановки — его семь незакрытых пунктов перечислены выше поимённо и уезжают отдельным паком; всё
|
||
остальное закрыто и предъявлено.
|
||
|
||
## Живые нормы зоны, у которых нет другого носителя
|
||
|
||
Каждая прошла триаж 06.09: она не выводится из кода и не стоит ни в `ENGINEERING_STANDARDS.md`, ни в
|
||
`STACK_DECISIONS.md`. ⚠ Оркестратору предложено абсорбировать их в нормы зоны — до тех пор живут здесь.
|
||
|
||
- **Ничего не отдавать на лендинг без прогона ПАКЕТА, которого правка касалась.** «Код написан и пин
|
||
заведён» — не то же, что «прогнано»; числа снимаются после ПОСЛЕДНЕЙ правки, включая комментарные
|
||
(генезис — `D39.211` п.6, где это зафиксировано как ошибка оркестратора, а не как норма зоны).
|
||
- **`go test` без `-v` строк `--- SKIP` не печатает вовсе**, поэтому греп по ним в не-verbose логе даёт
|
||
ЛОЖНЫЙ НОЛЬ скипов (`D39.213` п.3).
|
||
- **Зелёная батарея — это полный список ПАКЕТОВ плюс отсутствие `FAIL`**, а не отсутствие красных строк
|
||
в хвосте вывода (`D39.169`).
|
||
- **Гейты доков перегоняются ПОСЛЕ последней правки доков, а не кода**, и число из растущего файла —
|
||
не число (`D39.188`).
|
||
- **Шаблон книги второго гейта батареи обязан указывать на СНАПШОТ движковых конфигов того же коммита,
|
||
что и бинарь** (`git archive <commit> backend/configs backend/prompts`), а не на рабочее дерево:
|
||
иначе живой тест читает файлы параллельной бэкенд-сессии и краснеет без дефекта (`PD-432` покрывает
|
||
БИНАРЬ, не шаблон).
|
||
- **Механизм, который агрегирует чужой каталог и отдаёт результат наружу, обязан иметь СПИСОК ТОГО,
|
||
ЧТО НЕ БЕРЁТ, и список начинается с секретов.** Исключать по расширению — ловушка в обе стороны
|
||
(`D39.201`).
|
||
- **Красный тест, мерящий ВРЕМЯ, на загруженной машине — не результат:** прежде чем звать его
|
||
дефектом, гонять изолированно и смотреть `uptime` (`D39.194`).
|
||
|
||
## Вопросы и пинги оркестратору — ОТКРЫТЫ
|
||
|
||
- **Счёт рядов регистра сдвинут этим паком** и живёт в чужой зоне: `docs/PROGRESS.md` объявляет
|
||
«открытых 106 / всего 458», пере-счёт даёт **112 / 465** (`python3 docs/scripts/counts.py --check`).
|
||
Заведены `PD-459`…`PD-465`, `PD-458` переведён в `fixed`.
|
||
- ⛔ **Нужен пятый `RejectReason` в контракте (`PD-463`), либо решение, что асинхронная ветвь приёма
|
||
остаётся проницаемой.** Сужение входа до того, что умеет выдача, действует только на синхронной
|
||
ветви; на ветви, куда книга уходит при деплойном классе, «книга одним полотном» попадает в
|
||
библиотеку молча. Закрыть нечем: словарь закрыт четырьмя значениями, и ближайшее по словам
|
||
`source_unreadable` ТЕРМИНАЛЬНО и УДАЛЯЕТ исходник — то есть уничтожает файл пользователя из-за
|
||
НАШЕГО ограничения.
|
||
- **Предложение движка провести `--ceiling-usd` в `status`** (пинг №15, 09.08) не принято и не
|
||
отклонено с тех пор; `backend/internal/pipeline/status.go` отвечает «a read path never carries a
|
||
run-scoped override». Нужна диспозиция или строка бэклога.
|
||
- **`counts.py` держит регистр ОДНИМ путём и слайсы не глобит.** Это предусловие плана нарезки
|
||
`DEFECT_REGISTER.md` (`docs/DOC_CLEANUP_PLAN.md`, Б14в): вынос без него молча уронит счёт.
|
||
- **§2.12 компаньона контракта** (`docs/architecture/14-api-contract/README.md`) утверждает, что пять
|
||
денежных полей не могут доехать, тогда как аллоулист шва несёт `committed_usd`/`reserved_usd`
|
||
(`platform/internal/ingest/resync.go`); якоря на `pipeline/status.go` там же мертвы. Зона `docs/`.
|
||
- **Зеркало контракта у фронта — `0.2.3` против канона `0.12.0`** и несёт отозванное правило «денег в
|
||
интерфейсе MVP нет вообще» (`D39.196` п.2 его снял). Зона фронта заморожена; пинг на разморозку.
|
||
- **Восстановление из бэкапа не предъявлено на книге в сотни мегабайт, при заполненном диске и при
|
||
чужих соединениях** (`PD-462` покрывает соседний класс, этот — нет). Нужна строка или ряд.
|
||
- **Правило записи адресов в доках, живущее только в архиве и относящееся к прибору из `docs/`:**
|
||
адрес пишется БЕЗ якорной нотации там, где текст РАССКАЗЫВАЕТ о форме (линтер `counts.py` разбирает
|
||
форму где угодно, включая объяснение этой формы), а мёртвый адрес цитируется ПОРОЗНЬ — имя файла в
|
||
кавычках, номер словами, — иначе цитата сама становится якорем. В `counts.py` этого нет; носитель
|
||
нужен в зоне `docs/`.
|
||
- **Рантбук деплоя ни разу не прогонялся end-to-end с настоящим `migrate`** — названо предусловием
|
||
первого выката ещё пингом №17 и носителя не получило.
|
||
|
||
## Архив эр
|
||
|
||
- **Паки 04–06.09** («закрыть цикл» · «пустить внутрь можно» · «форма заказа перевода» · наблюдение за
|
||
живым платным потоком) — [archive/platform-PROGRESS-2026-09-04-06.md](archive/platform-PROGRESS-2026-09-04-06.md).
|
||
Ратификации **D39.194**, **D39.201**, **D39.208**, **D39.211**–**D39.214**.
|
||
- **Эры P9–P13 (27.08–03.09)** плюс приёмка P8-REVIEW, раздел «Состояние эры P8» и исполненные пинги
|
||
оркестраторов №15–№22 — [archive/platform-PROGRESS-P9-P13.md](archive/platform-PROGRESS-P9-P13.md).
|
||
Ратификации **D39.159**, **D39.162**, **D39.166**, **D39.169**, **D39.172**, **D39.180**, **D39.188**.
|
||
- **Эра пака P8-FIX (21–22.08) — В АРХИВЕ.** Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle — [archive/platform-PROGRESS-P8.md](archive/platform-PROGRESS-P8.md). Ратификация — D39.154, лендинг `31f1f82`. Живое из этой эры: четыре открытые строки регистра (`PD-370` мажор — контрактная половина · `PD-371` · `PD-372` · `PD-373`/`PD-374`), инвентарь каналов шва в `STACK_DECISIONS.md`, граница sqlc в `BACKLOG.md` П-19.
|
||
|
||
- **Записи акта 5 пака P7, остававшиеся в живом журнале, — ДОСЛАНЫ в [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md)** 22.08: заголовок «эра P7 в архиве» стоял, а тела лежали здесь.
|
||
|
||
- **Паки P4, P5, P6 и их дофиксы (08–15.08) приняты и залендены.** Ратификации: **D39.123** (P4 «раннер», `d29e30c`; формула аргумента потолка = `committed + прирост`, PD-158) · **D39.130** (P5, `69d485a`; Go-floor 1.26.6 — `toolchain`-директива в `go.mod` плюс сравнивающий гейт `make version-check`; интейк пишет `book.yaml` формой Б) · **D39.131** (эмиттер шва движка; словарь кадров — `internal/ingest/events.go`) · **D39.132** (P6 + дофикс; `PD-113` закрыт, у батареи появился второй гейт окружения). Записи паков с дофиксами, живыми пробами и посадками — [archive/platform-PROGRESS-P4-P6.md](archive/platform-PROGRESS-P4-P6.md); промт P5 — `archive/PLATFORM_P5_SESSION_PROMPT_2026-08-10.md`. ⚠ Счёт регистра и тестов тех дней устарел на порядок: живые числа берутся `python3 docs/scripts/counts.py --check` и `DEFECT_REGISTER.md`, а не отсюда.
|
||
|
||
- **Эра пака P7 (16–20.08)** — пять актов, приёмка и фикс-раунды: [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md).
|
||
- **Эры P0–P3** (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены
|
||
(D39.107/109/112/114): [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md) (D39.124).
|
||
Решения оттуда живут в D-логе и `DEFECT_REGISTER.md`.
|