textmachine/platform/docs/platform-PROGRESS.md

167 KiB
Raw Blame History

Журнал зоны «Платформа»

Что это. Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. Отработавшие эры вынесены срезами в 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 вне тестов estimated2 хита, оба комментарии (контроль: committed_micro — 1 хит; go-файлов вне тестов прибор прочёл 81). Носитель — строка бэклога 382; чинится ПАРНО с движком.

Что из этого следует для очереди. Пак движка «мягкая остановка» и платформенная половина лендятся ПАРОЙ и порознь не лендятся (D39.234 п.2). Платформенная половина, как я её вижу: mode у /stop, грейс выше потолка ожидания, путь второго сигнала (systemctl kill --signal=SIGTERM, юнит не трогая), чтение оценочных строк из потока. Промт ещё не написан — если у вас есть довод против любой из четырёх позиций, скажите ДО того, как я его напишу.

ПАК «ДВЕ ОСТАНОВКИ» — ОТМЕНЁН ВЛАДЕЛЬЦЕМ, РАБОТА ОТКАЧЕНА (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. Сегодня движок на жёстком сигнале умирает за секунды, поэтому это невидимо. Носители последствия: WriteTimeout: 30 s (cmd/tmplatformd/main.go:208) и свип (п.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:18stopGrace = 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 --checkEXIT=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.00.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
упор в потолок не должен стоить пользователю загрузки ожидание, а не отказ: не дождался ⇒ errNotConclusive201 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=*.go0 при 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 -l0 · grep '^ version:' openapi.yaml0.13.0
§4.5 три ряда open при легшем лечении PD-424 и PD-438fixed; у 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. Все три сходятся в одну строку
порождения движка ВНЕ потолка 1internal/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/

TestTheRenderedConfigurationIsOneTheEngineActuallyLoadsPASS.

И где именно рассуждение зоны свернуло не туда: «нужен $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.00.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_pair0 хитов при 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.refreshSaveStructure). Но он НЕ МОЖЕТ ни снять вердикт разреза, ни изменить status/chapter_count: единственный писатель reject_reasonRejectBook (pgstore/books.go:394, один хит по всему коду), а статус двигают только FinishParse/reject. ⇒ расхождение возможно только В СТОРОНУ БОЛЬШЕГО: ответ либо уже несёт поверхностные поля, либо ещё нет. Структурная гарантия потребовала бы держать что-то поперёк FinishParseReadBook на горячем пути ради полей, которые клиент всё равно перечитывает карточкой. ⚠ И отдельно: саму фразу «ВСЕГДА» я в каноне не нашла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.InsertOptsMaxAttempts: 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:408=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.InsertOptsMaxAttempts: 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=*.go1 хит при 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 к хвосту ничего не добавляет (приор оркестратора, проверяю кодом), а упор даёт штатную деградацию: не уложился ⇒ errNotConclusive201 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.

ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (0607.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-458fixed, П-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'
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 зовётся синхронно после последнего байта. Четыре исхода: глав ≥2FinishParse инлайн, 201 несёт not_started и число глав · глав 0400 invalid_request, errors[{file, no_book}], не принято ничего · глав 1400, errors[{file, no_chapter_structure}] · деплойный класс или таймаут201 parsing, очередь доделывает, claim ВОЗВРАЩАЕТСЯ (иначе задание очереди нашло бы книгу занятой и ничего не сделало).

Отказ по числу глав, а не по расширению: выдача кладёт по одному XHTML на главу ДВИЖКА, значит «книга одним полотном» ⟺ движок нарезал <2 глав. .epub движок режет по nav/NCX и он проходит — блокировка по расширению отказала бы тому, что мы умеем доставлять. Go по паре и формату не ветвится.

Дельта контракта, которую ратифицирует оркестратор (минор 0.12.00.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, найденных кругами 35, девять лежат в одном месте — синхронном разрезе на приёме. Это новый механизм в конкурентном платном пути (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. После последнего байта тела маршрут МОЛЧИТ до 210220 с; промежуточный прокси об этом не спрашивали.
  10. Отказ уходит наружу только машинным кодом (Detail не заполняется) ⇒ «честная причина человеку» из §4.3 сегодня не доезжает. Фронт заморожен, значит либо причина едет в Detail, либо пункт признаётся неисполненным.

Замер, который должен пережить пак: очередь выигрывала гонку у разреза 5 раз из 6

Синхронный разрез берёт ClaimParse ПОСЛЕ коммита StartParsing, а StartParsing ставил задание очереди ВНУТРИ той же транзакции — то есть воркер становился видимым тем же коммитом и гонялся с разрезом за один и тот же claim. Проба (энкьюер зовёт Parse сразу после коммита, как это делает River на видимости строки), шесть прогонов: очередь выиграла 5 раз, разрез 1 раз.

Оба исхода стоят книге, и это делает гонку дефектом, а не шероховатостью:

  • выигрывает очередь — синхронного вердикта нет вовсе, пустой файл принимается parsing, попытка бюджета потрачена, отказ приезжает асинхронно и оставляет книгу в библиотеке;
  • выигрывает разрез — задание съедено впустую (ErrParseClaimednil), и после возврата 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 — в архиве», «Закрытые эры P0P3 — в архиве» и дублирующий их буллет) — они дублировали блок «Архив эр» и слиты в него; обе ссылки (-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 был у очереди · маршрут молчит до 210220 с после последнего байта, промежуточный прокси об этом не спрашивали · отказ уходит наружу только МАШИННЫМ кодом, 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 отнесён к «чистым» по чтению кода движка, а не по замеру: если окажется, что банк-стоп тоже рвёт волну, класс придётся сузить.
  • Третье, названное опровергателем и оставленное сознательно: если удаление отказанной книги само упрётся в БД (discardremoveIntake), пользователь получит 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-459PD-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 и носителя не получило.

Архив эр

  • Паки 0406.09 («закрыть цикл» · «пустить внутрь можно» · «форма заказа перевода» · наблюдение за живым платным потоком) — archive/platform-PROGRESS-2026-09-04-06.md. Ратификации D39.194, D39.201, D39.208, D39.211D39.214.

  • Эры P9P13 (27.0803.09) плюс приёмка P8-REVIEW, раздел «Состояние эры P8» и исполненные пинги оркестраторов №15№22 — archive/platform-PROGRESS-P9-P13.md. Ратификации D39.159, D39.162, D39.166, D39.169, D39.172, D39.180, D39.188.

  • Эра пака P8-FIX (2122.08) — В АРХИВЕ. Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle — 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 22.08: заголовок «эра P7 в архиве» стоял, а тела лежали здесь.

  • Паки P4, P5, P6 и их дофиксы (0815.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; промт P5 — archive/PLATFORM_P5_SESSION_PROMPT_2026-08-10.md. ⚠ Счёт регистра и тестов тех дней устарел на порядок: живые числа берутся python3 docs/scripts/counts.py --check и DEFECT_REGISTER.md, а не отсюда.

  • Эра пака P7 (1620.08) — пять актов, приёмка и фикс-раунды: archive/platform-PROGRESS-P7.md.

  • Эры P0P3 (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены (D39.107/109/112/114): archive/platform-PROGRESS-P0-P3.md (D39.124). Решения оттуда живут в D-логе и DEFECT_REGISTER.md.