135 KiB
Журнал зоны «Платформа»
Что это. Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. Отработавшие эры вынесены срезами в
archive/— читать только по конкретной ссылке.
Состояние зоны на 08.09.2026
| Вопрос | Ответ |
|---|---|
| последний заленджённый пак | «деньги и правда на экране» (06–07.09, fda0679, акт D39.221), канон контракта 0.13.0. ⚠ Обе величины берутся ПРИБОРОМ, а не отсюда: git log --oneline -1 -- platform/ и grep '^ version:' ../../docs/architecture/14-api-contract/openapi.yaml — эта строка стареет, они нет |
| пак в дереве, не закоммиченный | «разрез приёма до готовности и правда о себе» (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)
Промт
docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md, вход HEAD3f4680c, дерево на входе чисто. Зона НЕ коммитит — дерево передано оркестратору №23 (textmachine-a8). Пак $0, платных вызовов 0. Работа завершена, править не планирую. Сказано ПОСЛЕ адверсариального круга, а не до него: первая редакция этого отчёта несла ту же фразу при шести неисправленных дефектах, введённых этим паком. Дерево — 26 файлов, все вplatform/(git status --porcelain -- platform/), 23 правленых и 3 новых.
Исход по каждому пункту §4
| Пункт | Исход |
|---|---|
| §4.1 ограничитель параллелизма | сделано; форма — x/sync/semaphore на общей точке порождения, ожидание с деградацией в очередь. Предъявлено НАГРУЗКОЙ |
| §4.2 бюджет хвоста из кода | сделано, форма Б (структурная); попутно вскрыт и закрыт ЧЕТВЁРТЫЙ промах суммы — квитанция не входила в неё |
| §4.3 рантбук | сделано; правок рантбука ДВЕ, вторая объявлена ниже с доводом |
| §4.4 три места неразличимого сбоя | сделано все три, каждое своим лечением; свип вылечен БЕЗ миграции |
| §4.5 правда о себе | сделано: шапка, два ряда флипнуты, маркер третьего починен, черты заэкранированы |
| §4.6 честная причина человеку | закрыто по построению на моей стороне — и ПРЕМИСА пака при этом опровергнута замером (ниже) |
| §4.7 живой гейт | рецепт исполнен и РАБОТАЕТ; довод зоны опровергнут, разрез впервые встретился с настоящим движком |
§4.8 п.2 (комментарий cutNow) |
взят вместе с §4.4, как и предписано |
§4.8 п.4 (мёртвый enqueue) |
взят вместе с §4.1: предикат сведён в одно названное место |
| §4.8 п.6 («ВСЕГДА» контракта) | не беру — пинг оркестратору с моим выбором из двух (ниже) |
| §4.8 остальное | не делаю, как объявлено паком |
| ⚠ сверх пака | константа контракта 0.12.0 → 0.13.0 — красное на входе, взято по явному указанию оркестратора (ниже) |
Что стало с деревом — находка → что сделано → чем предъявлено
| Находка | Что стало с деревом | Чем предъявлено |
|---|---|---|
| §4.1 у синхронного входа нет ограничителя параллелизма | internal/books/limit.go: потолок на semaphore.Weighted, взводится в s.manifest — ОДНОЙ строке, через которую к движку идут все три входа. Дефолт — DefaultMaxCuts = jobs.DefaultWorkers (4), то есть число, подо что хост уже рассчитан, и носитель у него ОДИН. Конфиг TM_PLATFORM_MAX_CUTS; ноль отвергает читатель чисел (loader.number), а не отдельная проверка интейка. Наблюдаемость: 3 гейджа + 2 счётчика, публикует существующий телеметрический проход |
TestTheHostRunsNoMoreCutsAtOnceThanItsCapAllows — 6 загрузок при потолке 2, пик 2, и это НЕ вакуум: тест сперва дожидается контрольной величины «4 из 6 стоят в очереди» из счётчиков самого потолка · парный TestWithRoomForEveryCutTheHostRunsThemAllAtOnce — та же нагрузка при потолке 6 даёт пик 6 (иначе первый тест проходил бы и на фикстуре, где ничего не совпало по времени) · посадка M4 |
| упор в потолок не должен стоить пользователю загрузки | ожидание, а не отказ: не дождался ⇒ errNotConclusive ⇒ 201 parsing, дорезает очередь. На очередном пути — claim обратно, НОЛЬ потраченных попыток и river.JobSnooze, то есть задание возвращается, не тратя единственную попытку (giveBack + jobs.ErrTryAgainLater) |
TestAnUploadThatRunsOutOfBudgetWaitingForASlotIsAcceptedRatherThanRefused · TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook (claim ОТДАН и задание ПОСТАВЛЕНО — то, чего первая редакция не утверждала) · TestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack (оба исхода: упор в потолок и нехватка бюджета) · TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt · посадки M9, N7, N8 |
| §4.2 граница хвоста выводилась руками и трижды была неверна | ОДИН отсоединённый дедлайн на весь хвост (walk), каждый шаг берёт min(свой бюджет, остаток) (step). Шаг, добавленный завтра, границу не двигает ПО ПОСТРОЕНИЮ |
TestNoStepOfAnUploadsTailOutlivesTheWalk — в т.ч. 50 вложенных шагов, каждый просит час · TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget (по дедлайну, который движок РЕАЛЬНО получил, а не по секундомеру) · TestAStepOutsideAWalkKeepsItsOwnBudgetAndSurvivesItsCaller · посадки M1, M2 |
| ⚠ ЧЕТВЁРТЫЙ промах той же суммы, найден моей же посадкой | квитанция идемпотентности (10 с) писалась ПОСЛЕ Accept и в сумму не входила ⇒ хвост был длиннее объявленного ровно на неё. Квитанция стала ТЕРМИНОМ: UploadSettle = CutBudget + 4*writeBudget + ReceiptBudget = 220 с (ровно замер 07.09), носитель величины ОДИН — books.ReceiptBudget, тратит её httpapi.settleCtx |
TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget · TestTheReceiptSpendsTheShareTheIntakeSetAsideForIt (httpapi) · посадки M3, M7'' |
| §4.3 рантбук молчал про таймаут ОТВЕТА прокси | deploy/README.md: молчание названо числом (220 с), требование к прокси — TM_PLATFORM_UPLOAD_DEADLINE + 220 с = 13 мин 40 с на дефолтах, цена ошибки названа (человек получает ошибку на ПРИНЯТОЙ книге и повтором делает вторую) |
директивы сверены с вендор-доками ЭТОЙ сессией: nginx proxy_read_timeout, дефолт 60 с (nginx.org, ngx_http_proxy_module) · HAProxy timeout server (docs.haproxy.org 3.0, индекс ключевых слов) |
§4.4 ClaimParse в cutNow молчал о сбое БД |
гонка и «хранилище не спросили» разведены: первая — INFO, вторая — ERROR, и текст называет следствие (книга parsing без задания до свипа) |
TestTheIntakeTellsALostRaceApartFromAStoreItCouldNotAsk — обе фикстуры, и каждое сообщение проверено ОТСУТСТВУЮЩИМ в чужой |
§4.4 ветвь RowsAffected()==0 ⇒ задание НЕ ставить не запинена |
пин на ОБЕ стороны: чужой claim ⇒ задания нет и чужой claim цел; свой ⇒ задание есть и claim снят | TestOnlyAClaimThatWasReallyGivenBackQueuesTheJobThatFinishesTheBook · посадка M6' красная ТЕКСТОМ про задание |
§4.4 свип StuckIntake считает от added_at (штамп ДО тела) |
⭐ вылечено БЕЗ миграции и без колонки: бутовый гейт сверял дедлайн с min(UploadGrace, ClaimStale); добавлено ТРЕТЬЕ окно books.ClaimGrace (20 мин — самое узкое). Условие «свип забирает claim у идущей загрузки» стало недостижимо настройкой |
посадка M5 красная текстом «does not fit the parse claim's grace» + TestEveryWindowAnUploadMustFitInsideIsActuallyConsulted. ⚠ Исправление к первой редакции этой строки: я написала, что у каждого из трёх окон свой случай, недостижимый двум другим — это БЫЛО НЕВЕРНО и найдено адверсариальным проходом. ClaimGrace сегодня самое узкое, поэтому все пять случаев ловит один терм, и два других можно было удалить из гейта при зелёной батарее. Вылечено выносом выбора окна в intakeWindow(...), который тест кормит значениями, делающими каждое окно самым узким по очереди |
§4.8 п.2 комментарий cutNow лгал о том, кто ставит задание |
снят тем же движением, что и правка ветви | grep -rn "the queue job is already enqueued" platform/ --include=*.go → 0 при 200 осмотренных .go (единственный хит по дереву — цитата находки в этом журнале, и она историческая) |
| §4.8 п.4 предикат «кто ставит задание» размазан по двум местам | cutsItsOwnUploads() — одно названное место, читают оба; doc параметра StartParsing больше не выдаёт его за штатный путь |
grep "s.Engine != nil" internal/books/*.go (без тестов, 4 файла) → 1 хит, и он внутри самого предиката. ⚠ Рядом остаётся s.Engine == nil в s.manifest — это НЕ тот предикат, а nil-гард самого вызова, и он не решает, кто ставит задание |
| §4.5 шапка журнала лгала о том, где зона | пере-снята прибором: последний пак «деньги и правда» fda0679, коммитов в platform/ после него 0, канон 0.13.0; в шапку вписаны КОМАНДЫ, которыми числа берутся |
git log --oneline fda0679..HEAD -- platform/ | wc -l → 0 · grep '^ version:' openapi.yaml → 0.13.0 |
§4.5 три ряда open при легшем лечении |
PD-424 и PD-438 → fixed; у PD-441 починен МАРКЕР, статус оставлен open и в ячейке названо, что держит его движковая половина (строка 331). Фраза «в дереве» снята во всех трёх |
grep -c 'статус флипает лендинг' → 0 при 465 рядах |
§4.5 незаэкранированные | |
заэкранированы в трёх рядах (PD-375, PD-422, PD-197) |
эскейп-аware счёт колонок: рядов с числом колонок ≠ 7 — 0 из 465 |
PD-464 (строка регистра, моя зона) |
закрыт ОБЕИМИ половинами и переведён в fixed с диспозицией |
см. ячейку ряда |
§6 ось 4: существующее ПРЕЖДЕ велосипеда — что рассмотрено и чем отвергнуто
| Кандидат | Исход | Довод |
|---|---|---|
golang.org/x/sync/semaphore |
ВЗЯТ | Acquire(ctx, 1) — ровно нужная семантика: ждёт до слота или до конца контекста, очередь FIFO (в отличие от буферизованного канала, где поздний может обогнать раннего и часть загрузок ждала бы весь бюджет). TryAcquire у него не «барджит»: success := s.size-s.cur >= n && s.waiters.Len() == 0 — быстрый путь отказывает, пока список ожидающих непуст, и слот у стоящих в очереди не ворует. ⚠ Прочитано в ИСХОДНИКЕ пинованной версии ($(go env GOMODCACHE)/golang.org/x/sync@v0.22.0/semaphore/semaphore.go, TryAcquire), а не по памяти: на этом свойстве держится довод про FIFO. Уже был в go.sum косвенной зависимостью той же версии v0.22.0; правка go.mod — перевод в прямые, БЕЗ смены версии (git diff platform/go.mod: одна строка вверх, одна вниз; go.sum −1 строка) |
errgroup.SetLimit |
отвергнут | ограничивает горутины, которые запускает САМА группа. Здесь группы нет и быть не может: вызывающие независимы и приходят из разных мест (HTTP-обработчик, воркер River, свип). Форма не подходит по существу, а не по вкусу |
netutil.LimitListener |
отвергнут | ограничивает СОЕДИНЕНИЯ на слушателе — то есть весь API разом, включая чтения, листинги и логин, из-за нагрузки на приём. И не накрывает ни воркера, ни свип: это ровно «потолок в маршруте», который пак запрещает |
MaxWorkers очереди (уже стоит) |
отвергнут как ЕДИНСТВЕННОЕ средство, но учтён как число | ограничить синхронный вход им нельзя: этот путь намеренно НЕ ставит задание, чтобы воркер не гонялся с разрезом за claim. Зато он назвал дефолт: 4 — то, подо что хост уже рассчитан, и потолок сказан один раз для всех способов запустить движок, а не только для того, что идёт через очередь |
| самописный счётчик / буферизованный канал | отвергнут | норма зоны «stdlib или устоявшаяся библиотека прежде своего», и здесь у своего есть конкретная цена — отсутствие FIFO |
Посадки мутаций — вердикт по ТЕКСТУ падения, а не по цвету
Копия дерева с каноном (cp -a --parents platform docs/architecture/14-api-contract), базовая линия копии зелёная на всех четырёх пакетах.
| Посадка | Вердикт | Текст, по которому он вынесен |
|---|---|---|
| M1 разрез снова отсоединён от хвоста | RED | the cut was granted 1m29.99s inside a 700ms walk |
M2 step перестаёт капать по хвосту |
RED | the walk is not capping it + adding a step moves the boundary |
| M3 хвост съедает долю квитанции | RED | want UploadSettle (3m30s) less the receipt's share (10s) |
| M4 потолок не применяется вовсе | RED | контрольная величина легла в ноль: {Limit:2 InFlight:0 Waiting:0 Waited:0 GaveUp:0} |
| M5 гейт забывает окно claim'а | RED | an upload deadline of 21m0s was accepted, though ... does not fit the parse claim's grace |
| M6′ release ставит задание, ничего не вернув | RED | a release that gave back nothing still queued a job (2 in total) |
M7 значение ReceiptBudget изменено |
⚠ ВЫЖИЛА, и это ВЕРНЫЙ исход | пере-сайзинг остаётся согласованным по обе стороны шва; ловить надо не значение, а ДРЕЙФ — см. M7'' |
| M7'' квитанция возвращается к своему литералу | RED (после того, как по находке M7 заведён пин) | the receipt was given 30.0s, want the share the intake declared for it (10s) |
| M9 занятый хост тратит попытку книги | RED | a queued parse that found no slot answered <nil>, want the cap |
M10 два способа не получить claim свёрнуты обратно в один молчаливый return |
RED | losing the race said nothing + a claim that could not be asked for said nothing, so the book sits parsing with no job and nobody knows + was not logged at ERROR |
ТРЕТИЙ круг посадок — по коду, ПЕРЕПИСАННОМУ после адверсариального прохода. Первые две редакции двух пинов оказались тавтологичны, и посадка это показала, а не рассуждение.
| Посадка | Вердикт | Текст |
|---|---|---|
| N1 у ожидания удалена строка INFO | RED | a cut that queued for a slot and got one said nothing |
N2 переименована причина host_at_cut_capacity |
⚠ ВЫЖИЛА → N2′ RED | первая редакция пина сверяла КОНСТАНТУ с самой собой и проходила при любом значении; переписана на литерал: the reason is "MUTATED", want the stable "host_at_cut_capacity" |
| N4 два гейджа потолка поменяны местами | RED | the exposition is missing "tm_platform_cuts_in_flight 4" |
N5 из проводки выброшен MaxCuts оператора |
RED | MaxCuts is 0, want the operator's 7: TM_PLATFORM_MAX_CUTS does nothing |
N6 cutsItsOwnUploads всегда истинен |
⚠ ВЫЖИЛА → N6′ RED | счёт заданий РАЗЛИЧИТЬ НЕ МОЖЕТ (сломанный предикат ставит то же одно задание через релиз); переписано на лог с положительным контролем: a deployment with no engine attempted a cut anyway |
| N7 разрез перестаёт оставлять хвосту резерв | RED | the intake's parse claim was NOT given back + the queue was handed 0 jobs + has spent 1 attempts |
N8 giveBack перестаёт возвращать задание очереди |
RED | the error does not tell the queue to bring the job back, so the single attempt is spent |
| N9 разрез запускается даже когда места на него нет | RED | a cut was started with no room for it (1 calls) + the upload is "not_started", want + the pass does not say WHY it did not cut ("no_time_to_cut") |
⚠ Единственная выжившая, которую я НЕ чиню и объявляю: смена значения DefaultMaxCuts. Это
сайзинг, а не свойство: изменённый потолок остаётся согласованным по всей системе, и «поймать» его
можно было бы только пином на литерал, то есть запретом менять число. Что запинено — ПРОВОДКА
(оператор получает своё число) и ЕДИНСТВЕННОСТЬ носителя (DefaultMaxCuts = jobs.DefaultWorkers).
⚠ Первая редакция M6 и M7 НЕ КОМПИЛИРОВАЛАСЬ (declared and not used: tag, imported and not used). Это не вердикт, а его отсутствие: посадка, которая не собралась, красит батарею по причине, не имеющей отношения к предмету. Обе пере-посажены компилирующимися.
Классы и знаменатели — «закрыт в N из M», M посчитан командой
| Класс | Знаменатель | Как посчитан |
|---|---|---|
| входы, порождающие процесс движка на разрезе | 3 из 3 (интейк · parseWorker · свип Sweep) |
s.manifest имеет РОВНО ОДНОГО вызывающего (grep -rn 's\.manifest(' --include=*.go internal/ | grep -v _test → 1: parse.go:178), у parseClaimed их два (Parse, cutNow), у Parse — воркер jobs.go:117 и Sweep. Все три сходятся в одну строку |
| порождения движка ВНЕ потолка | 1 — internal/readmodel/readmodel.go:156 |
grep -rn '\.Manifest(' --include=*.go | grep -v _test → 2 вызывающих, один из них мой. Оставлен снаружи сознательно, довод — ниже |
| шаги хвоста под общим дедлайном | все (в Accept их 5 + квитанция) |
построением, а не перечнем: writeCtx идёт через step, step капает по хвосту. Проверено на 50 шагах, которых в коде нет |
| ряды регистра с диспозицией «статус флипает лендинг» | 3 из 3 | grep -c 'статус флипает лендинг' → было 3, стало 0 |
| ряды с числом колонок ≠ 7 | 3 из 3 | эскейп-аware счёт: было 3, стало 0 при 465 осмотренных |
| открытые ряды регистра по МОИМ файлам | 10 путей осмотрено, совпадений — 20 рядов, из них МОЙ предмет 1 (PD-464, закрыт); остальные 19 — соседние классы, не тронутые этим паком |
grep по ПОЛНЫМ путям (норма §3 п.8), контроль: открытых рядов всего 109 |
Числа и команды — сняты ПОСЛЕ последней правки
$ python3 docs/scripts/counts.py --check → EXIT=1, и это ОЖИДАЕМО: ровно два расхождения,
✗ docs/PROGRESS.md: «открытых рядов регистра платформы — 112» против пере-счёта 110
✗ docs/PROGRESS.md: «... (major 3)» против пере-счёта 1
⚠ оба литерала — в ЧУЖОЙ зоне (`docs/PROGRESS.md`), туда не лезу. После лендинга и закрытия
PD-464 верные числа: **открытых 109, major 1, minor 36, info 72** (`counts.py` по дереву).
$ git log --oneline fda0679..HEAD -- platform/ | wc -l → 0
$ grep '^ version:' docs/architecture/14-api-contract/openapi.yaml → 0.13.0
$ go list ./... | wc -l → 20 (носитель скипов чинен первым движением)
$ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_ENGINE_BIN=… TM_PLATFORM_TEST_BOOK_TEMPLATE=… \
TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
MAKE-EXIT=0 · пакетов `ok` **20** · строк FAIL **0** · линтер «0 issues» · скипов **5**
⚠ Прогон ПОСЛЕДНИЙ — после адверсариального круга и после последнего добавленного пина. Прежние
редакции этих чисел (до круга) в отчёте не оставлены: они были верны и уже не про этот код.
⚠ Скипы 5, и условие у всех одно, названное (§10 ниже): нет деплой-артефакта
backend/configs/mining-contrast.zh.txt. Из четырёх гейтов батареи на этом хосте закрыты все:
Postgres · движковый бинарь + шаблон (собран из git archive HEAD backend, §4.7) · достижимый
пользовательский systemd · MemoryMax — судится самим TestARunIsBoundedByItsOwnCgroup (STACK_DECISIONS §«Гейты батареи»: прямой пробы у этого условия нет), и он ОТРАБОТАЛ: в полном перечне скипов, который печатает check, его нет, а FAIL в прогоне нет вовсе.
⚠ Числа Go-батареи сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются.
Живой гейт (§4.7) — довод зоны опровергнут ИСПОЛНЕНИЕМ
Зона писала, что второй гейт батареи требует $0-пайплайна рядом с backend/prompts/, то есть записи в
чужую зону или полной копии дерева. Проверено исполнением — неверно. Рецепт:
$ git archive HEAD backend | tar -x -C $W # снапшот чужой зоны, ни байта записи в неё
$ cd $W/backend && go build -o $W/tmctl ./cmd/tmctl
$ sed -e 's|pipeline: ../configs/...|pipeline: $W/backend/configs/pipeline-c1.yaml|' \
-e 's|models: ../configs/...|models: $W/backend/configs/models.yaml|' \
$W/backend/example/book.yaml > $W/template.yaml # пути абсолютные
$ TM_PLATFORM_TEST_ENGINE_BIN=$W/tmctl TM_PLATFORM_TEST_BOOK_TEMPLATE=$W/template.yaml go test ./internal/books/
TestTheRenderedConfigurationIsOneTheEngineActuallyLoads — PASS.
⭐ И где именно рассуждение зоны свернуло не туда: «нужен $0-пайплайн» верно для теста
internal/runner, который гоняет translate и падает на missing API keys. Оно было ОБОБЩЕНО на гейт
целиком — а manifest есть $0-глагол и ключей не требует по D20.4, поэтому боевой pipeline-c1.yaml
(«платный») загружается и режет без единого ключа. То есть довод был верен про один тест и ложен про гейт,
и разница видна только исполнением.
⚠ Правки, вызванные заказанной сменой поведения (объявляю по D39.183)
TestAnUploadDeadlineIsRefusedUnlessTheWholeUploadFitsTheTighterWindow→...TightestWindow. Гейт получил третье окно (§4.4, п.7 десятки) — прежний тест утверждал, что дедлайн26m29sПРИНИМАЕТСЯ, и после лечения это неверно. Тест не «починен под зелень»: он пере-написан строже — у каждого из трёх окон свой случай, недостижимый двум другим, и посадка M5 краснит его именем нового окна.ClaimGraceэкспортирована (былаclaimGrace) — переименование затронуло 3 тестовых файла зоны механически; утверждений не тронуто. Основание — то же, по которому экспортированаUploadGrace: бут обязан отказывать конфигурации, которая её нарушает.- Вторая правка рантбука сверх §4.3 — абзац про
TM_PLATFORM_MAX_CUTS. Довод: §4.1 требует, чтобы потолок был «конфигурируемым и наблюдаемым», а ручка, о которой рантбук молчит, оператору не доступна; документировать ручку, которую этот же пак и завёл, — часть §4.1, а не «остальное про выкат».
⚠ Константа контракта 0.12.0 → 0.13.0 — что было сломано и кем
Батарея была КРАСНОЙ на входе, до единой моей правки: internal/gates
TestTheAnnouncedContractVersionIsTheOneTheCanonRatified — «this build announces contract 0.12.0 and the
ratified canon is 0.13.0». Улика: файл-носитель (internal/httpapi/capabilities.go:36) в моём диффе
отсутствовал (git diff --name-only HEAD | grep -i contract → пусто). Канон увёл на 0.13.0 коммит
оркестратора 3d90943, константу за собой не потянув; последняя правка константы — 6ceb133, до него.
То есть гейт красен с момента ратификации, сутки, и заметила это входная сверка следующей сессии зоны.
Взято мной по ЯВНОМУ указанию оркестратора с названным основанием: ратифицированный порядок D39.208 п.1
— код первым с честно красным гейтом, канон вторым; здесь порядок был обратный, и правка возвращает мир к
гейту, а не гейт к миру (D39.183, обслуживание). Авторство ошибки — оркестратор, не прошлый пак: на
fda0679 канон и константа обе были 0.12.0, гейт был зелёным, и число батареи в акте D39.221 честное.
§4.6 — ответ (а): закрыто по построению НА МОЕЙ СТОРОНЕ, но премиса пака опровергнута
Пункт 10 десятки предполагал, что причина отказа не доезжает. Опровергнуто: перечислены ВСЕ семь
пользовательских отказов приёма — payload_too_large и request_timeout (корневые коды), malformed ×2,
unsupported_pair ×2, no_book, no_chapter_structure, too_long, missing_or_late. Причины, не
выразимой перечислимым кодом, я не нашла; расширять errors[]/cause.code нечем, и минор не нужен.
⚠ Собственную первую находку снимаю: я решила, что слишком длинное поле формы уходит с ПУСТЫМ
errors[] — неверно, оно названо на месте чтения (v0.go:843, ItemTooLong), а ветвь Invalid(w, r) без
элемента до него не доходит.
⛔ А вот премиса пака про клиента ЗАМЕРОМ НЕ ПОДТВЕРЖДАЕТСЯ, и следующая смена не должна её унаследовать.
Пак пишет: «Таблица „код → русская фраза“ у клиента уже есть (14-api-contract/README.md, и фронт её
рисует)». Замер: no_chapter_structure в живых доках — 11 хитов при 1104 осмотренных .md, и НИ ОДИН
не таблица фраз; в 14-api-contract/README.md нет ни no_book, ни no_chapter_structure. В зоне фронта
no_book/unsupported_pair — 0 хитов при 8114 осмотренных .ts/.tsx. Что там есть на самом деле —
правило «клиент диспетчеризует по СТАТУСУ и показывает одну нейтральную фразу» (README §вход) и решение
владельца 16.08 о машинном коде. ⇒ вывод пака (моя работа тут закончена, клиентская половина — фронт, а он
заморожен) остаётся ВЕРНЫМ, но не потому, что таблица есть, а потому, что платформа дала клиенту всё, по
чему её можно нарисовать. Разница существенна: с премисой пака работа выглядит сделанной у обеих сторон.
Пинги оркестратору
- ⛔ Литералы в
docs/PROGRESS.mdпод гардомcounts.py --checkпротухли моим флипом — двигать их тебе. После лендинга верно: открытых 109, major 1 (было «112 (major 3)»). Разница в три ряда:PD-424иPD-438переведены вfixedпо твоему же маркеру,PD-464закрыт этим паком. Гейт красен ОЖИДАЕМО и ровно на этих двух строках — других расхождений он не даёт. - §4.8 п.6, «ВСЕГДА» в дельте контракта — моё мнение, как просил пак: нужна ОГОВОРКА В КАНОНЕ, а не
структурная гарантия. Довод из кода, а не из вкуса. Между
FinishParseиReadBookсвип материализатора может взять долг (он записан ИМЕННОFinishParse) и дописать строкеsource_charsиstructure(readmodel.refresh→SaveStructure). Но он НЕ МОЖЕТ ни снять вердикт разреза, ни изменитьstatus/chapter_count: единственный писательreject_reason—RejectBook(pgstore/books.go:394, один хит по всему коду), а статус двигают толькоFinishParse/reject. ⇒ расхождение возможно только В СТОРОНУ БОЛЬШЕГО: ответ либо уже несёт поверхностные поля, либо ещё нет. Структурная гарантия потребовала бы держать что-то поперёкFinishParse→ReadBookна горячем пути ради полей, которые клиент всё равно перечитывает карточкой. ⚠ И отдельно: саму фразу «ВСЕГДА» я в каноне не нашла —grep 'ВСЕГДА' 14-api-contract/README.mdдаёт 0,grep -i alwaysвopenapi.yaml— 12 хитов, все про другое (SSE-кадры,about:blank). Назови предложение адресом, и если оно живёт не там, где я искала, мой довод надо перепроверить против него. - ⚠ Премиса пака в §4.6 неверна — см. секцию выше. «Таблица код → русская фраза у клиента уже есть, и
фронт её рисует» замером не подтверждается (0 хитов
no_book/unsupported_pairпри 8114 осмотренных.ts/.tsx; в14-api-contract/README.mdниno_book, ниno_chapter_structure). Вывод пака устоял, основание — нет. Стоит поправить, иначе следующая смена унаследует «у клиента всё готово». - Мелкая неточность адреса в §4.7: «а 35 строками ниже в том же журнале лежит рецепт снапшота» —
реально 261 строкой ниже. Адреса в дереве, которое я сдаю: довод —
platform-PROGRESS.md:452, рецепт —:713(на входномHEADэто были:173и:434; расстояние то же). На существо не влияет: рецепт там и есть, и он работает. - Твой вопрос «есть ли дешёвый способ закрыть сутки красноты между ратификацией и следующей сессией
зоны» — есть, и он в ТВОЕЙ зоне. Пара «канон ↔ объявленная константа» сегодня судится только Go-тестом,
который гоняет зона. А
docs/scripts/counts.py --checkуже читает оба дерева, уже висит на зонном pre-commit и уже срабатывает именно на коммитах с D-логом или PROGRESS — то есть ровно на ратификационных. Добавить туда одну проверку —grep '^ version:' openapi.yamlпротивconst ContractVersionвplatform/internal/httpapi/capabilities.go— стоит десятка строк и ловит ровно тот класс, который стоил суток: он предупреждает того, КТО ДВИГАЕТ КАНОН, в момент движения. Заказом не делаю (файл вdocs/), рекомендацию записываю.
Адверсариальный проход по СВОЕЙ готовой работе — восемь находок, и они были настоящие
Проход заказан §5.4 и выполнен субагентом (author ≠ reviewer) по готовому диффу, с направлением на классы, которые уже стоили зоне денег. Круги НЕ сошлись с первого раза: он нашёл восемь, и шесть из них — дефекты, которые ввёл ЭТОТ пак. Каждую я пере-проверила по коду прежде, чем чинить.
| # | Находка | Чем оказалась | Что сделано |
|---|---|---|---|
| F1 | комментарий giveBack обещал повтор задания с бэкоффом |
⛔ ЛОЖЬ: ParseArgs.InsertOpts — MaxAttempts: 1, повтора нет вовсе; книга на занятом хосте ждала свип 20 минут |
заведён jobs.ErrTryAgainLater; воркер переводит его в river.JobSnooze(RetryDelay), который НЕ тратит единственную попытку. Комментарий приведён к правде. Пин — TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt |
| F2 | у выигравшего слот не проверялось, осталось ли время на разбор | ⛔ настоящий: слот, выигранный в конце бюджета, отдавал движку миллисекунды; убитый процесс читается как parser_unavailable, а он ТРАТИТ попытку — пять таких удаляют файл пользователя |
takeCutSlot(ctx, reserve): worthStarting до и ПОСЛЕ ожидания, waitCtx обрывает ожидание на резерв раньше. Резерв — CutBudget на очередном пути, 0 на интейке (там попытка не тратится) |
| F3 | отказ бута при MaxCuts < 1 |
⛔ МЁРТВЫЙ КОД: l.number уже отвергает всё непозитивное, и мой тест пинил чужой охранник, а не мой |
ветвь удалена; в тесте названо, ГДЕ живёт отказ |
| F4 | тест трёх окон | ⛔ ВАКУУМЕН для двух окон из трёх, и его комментарий утверждал обратное — ровно тот класс, который он якобы чинил | выбор окна вынесен в intakeWindow(...); новый тест делает каждое окно самым узким по очереди. Ревьюер пере-мутировал независимо: теперь красный |
| F5 | терминальная запись могла родиться истёкшей | ⛔ настоящий и злой: при спетом хвосте claim НЕ отдавался и задание НЕ ставилось — книга «принята», а доделать её некому 20 минут. Мой тест этого не утверждал | stepLeaving + cutTailReserve: слабину забирает РАЗРЕЗ, а не записи. Пин — TestAnUploadTheHostCouldNotCutStillLeavesSomebodyToFinishTheBook |
| F6 | комментарий потолка обещал больше, чем потолок делает | верно: readmodel порождает те же процессы мимо него; плюс DefaultMaxCuts был вторым литералом числа воркеров |
комментарий сужен до правды и называет, что осталось снаружи; DefaultMaxCuts = jobs.DefaultWorkers — один носитель, и config.Runner.Workers берёт его же |
| F7 | шесть поверхностей пережили мутацию | верно все шесть | закрыты пинами (ниже), кроме значения DefaultMaxCuts — это САЙЗИНГ, и его смена не дефект; названо в §10 |
| F8 | баннер capabilities.go противоречил себе |
верно, и сломала его Я этой же сменой | баннер разводит два порядка: код первым (канон отстаёт) — ратифицированный, канон первым (код отстаёт) — тот, что стоил суток |
⛔ НАХОДКА №9 — класс, которого не ловит НИ батарея, НИ мутация
Поймана мной при починке F5: моя починка была дефектной, и её дефект не имел цвета.
cutTailReserve был КОНСТАНТОЙ 3 * writeBudget, а бюджет записи в тестах — полем сервиса
(s.writeBudget, который фикстуры укорачивают, чтобы достать случаи, недостижимые за 30 секунд). В
фикстуре с хвостом 300 мс резерв оставался 90 с — больше всего хвоста ⇒ шаг разреза рождался истёкшим,
движок не звался НИКОГДА, и пакет books зависал навсегда на <-first.
⭐ Почему это отдельный класс. Батарея его не ловит, потому что зелёного вердикта просто не
наступает — но и красного тоже: прогон висит до таймаута go test, и в CI это читается как «долго», а
не как «сломано». Мутация его не ловит по той же причине: у посадки нет вердикта, есть тайм-аут.
Единственное, что его назвало — прогон с УКОРОЧЕННЫМ -timeout и чтение стека упавшего по нему
процесса (limit_test.go:162, <-first); по цвету он неотличим от медленной машины. Две вещи из этого:
- резерв сделан производным от бюджета В СИЛЕ (
s.cutTailReserve()=3 * s.write()), иначе фикстура молча моделирует не то; - «нет места для разреза» больше не притворяется отказом хранилища: claim берётся на СВОЁМ бюджете
записи, разрез — на своём, и пустой разрез отвечает «вердикта нет» (
ReasonNoTimeToCut), а не падает внутриClaimParse. Это тот же класс, что F1: диагноз, который называет не то, что случилось. ЗапиненоTestAnUploadWithNoRoomLeftForACutSaysThatAndHandsTheBookOver+ посадка N9.
⚠ И правило, которое стоит пережить пак: число, которое фикстура умеет укорачивать, и число, выведенное из него, обязаны быть выведены ОДИНАКОВО. Константа рядом с полем — это две величины, которые совпадают в бою и расходятся в тесте, то есть ровно то, что фикстура сделать не может увидеть.
⚠ Урок, который стоит пережить этот пак: шесть из восьми находок — в коде, который я СДАВАЛА как готовый, с зелёной батареей, десятью посадками и отчётом, где написано «круги сошлись». Батарея была зелёной на всех восьми. Ловит их не цвет, а второй читатель, которому названо, ГДЕ у этого пака мягко.
Якоря, убитые моим переездом — норма §3 п.8
Мои правки сдвинули строки в internal/config/config.go, internal/pgstore/books.go,
internal/metrics/metrics.go и cmd/tmplatformd/runner.go (везде вставки, сдвиг +6 в первых двух).
Замер дифференциальный, а не «посмотрела»: линтер на входном HEAD даёт 8 проблемных якорей,
моё дерево давало 26. Чтобы отделить своё от унаследованного и от WIP чужой сессии, собрала
git archive HEAD в /tmp, подменила в копии ТОЛЬКО platform/ своим и сравнила списки comm-ом.
⚠ Кап вывода линтера — 25 строк; на 26 проблемах обрезка читается как отсутствие, поэтому в копии
скрипта кап поднят до 500. Появившихся из-за меня — 18.
| Где | Сколько | Что сделано |
|---|---|---|
platform/docs/DEFECT_REGISTER.md |
14 из 14 | пере-наведены механически: токен найден в цели, адрес заменён; ни одного «руками» |
docs/PROGRESS.md, docs/architecture/05-decisions-log.md |
4 | ЧУЖАЯ зона — ушли пингом с готовыми адресами и токенами |
⭐ Находка из этого же хода: экранирование \| в ячейке регистра (§4.5) ломает якорь, если черта
попала в его токен. PD-422 держал internal/runs/runs.go: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.InsertOpts—MaxAttempts: 1, повтора НЕТ ВООБЩЕ, задание просто списывается, и книга ждала свип 20 минут. То есть моё «рассуждение, а не замер» было не осторожностью, а неверным утверждением в комментарии кода. Вылеченоriver.JobSnooze, который возвращает задание не тратя единственную попытку. ⭐ Урок ровно тот, что записан в каноне зоны: строка «не проверено» — это не смягчение, это место, где ещё не посмотрели, и смотреть надо ДО сдачи. - НЕ ЗАПИНЕНО ИМЕНЕМ, но покрыто исполнением (проверено посадками, не грепом по именам):
cutTailReserve— посадкой N7,worthStartingиwaitCtx— обоими исходамиTestAQueuedParseThatCannotCutSpendsNothingAndGivesTheBookBack(ожидание обрывается на резерв раньше, поэтому случай «упор в потолок» кончается за ~1,5 с, а не за весь бюджет),engineNotAsked— обоими ветвями там же,jobs.RetryDelay— новым тестом очереди. Собственного теста по имени у них нет. - НЕ ПРОВЕРЕНО экспериментально: сколько памяти реально держит один
tmctl manifest. Дефолт 4 выбран как число, под которое хост уже был рассчитан (MaxWorkersочереди), а не измерен на большой книге. Ручка конфигурируема именно поэтому. - Опровержение премисы §4.6 сделано ГРЕПОМ ПО КОДАМ, а не чтением рендера фронта: я искала строки
no_book/unsupported_pairв 8114.ts/.tsx. Таблица, ключуемая иначе (например, по корневомуcode), таким грепом не нашлась бы. Утверждаю ровно замеренное. - Одно новое условное сообщение НЕ запинено:
«the parse claim could not be given back; the backstop sweep takes the book»вgiveBack— ветвь, где отказReleaseParseClaimнакладывается на упор в потолок. Четыре остальных новых сообщения запинены ОБЕИМИ фикстурами (где обязано прозвучать и где обязано молчать), это пятое — нет: чтобы его достать, нужен отказ хранилища ВНУТРИ уже насыщенного потолка, и фикстуру такой конъюнкции я не построила. Называю прямо, а не выдаю шесть из семи за семь. Остальные шесть — включая обе новые («движок не спрошен» и «не осталось места на разрез») — запинены фикстурой, где сообщение обязано прозвучать, И фикстурой, где обязано молчать. - Ограничитель НЕ накрывает
readmodel(internal/readmodel/readmodel.go:156— второй и последний вызывающийEngine.Manifest) и глаголыexport/status. Это осознанная граница, а не пропуск: материализатор иstatusидут внутри очереди, которая уже ограничена однимMaxWorkersна все три типа заданий (internal/jobs/jobs.go:170), свипы последовательны, аreadEngineнакрыл бы ещёStatusна пути СТАРТА платного прогона (internal/runs/spawn.go:242) и связал бы запуск прогонов с нагрузкой приёма. Единственным неограниченным источником процессов был синхронный интейк — он и закрыт. ⚠ Если приёмка считает, что хосту нужен потолок на ВСЕ порождения, это отдельная работа со своим дизайном (развязка денежного пути), а не райдер к этому паку.
Попутно: совместимость с новой секцией bank.json (пришло пингом от движковой зоны, проверено моим кодом)
Движковый пак добавил в bank.json секцию consolidation (полнота банка) и поле never_asked, версия
tm-bank-v1 НЕ бампнута. Пере-проверено на моей стороне, не принято на слово:
bank.jsonразбираетinternal/ingest/bank.go:78DecodeBank— простойjson.Unmarshal, неизвестный член игнорируется. ⚠ Строгий декодер в зоне ЕСТЬ ровно один (internal/httpapi/bank.go:137,grep -rn DisallowUnknownFields --include=*.go→ 1 хит при 200.go), но он на ДРУГОМ пути — тело запроса на правки ОТ КЛИЕНТА, где строгость требует сам канон. Пути не пересекаются ⇒ лендинг движка приём банка не ломает.- Читателей секции у зоны нет. ⚠ По подстроке их 2 при 186
.goвinternal/(internal/ingest/manifest.go:97,internal/pricing/pricing.go:112) — и оба английская ПРОЗА про «terminology consolidation» в денежных комментариях, а не чтение поля. Счёт по подстроке и счёт по владению здесь расходятся на два: следующему, кто будет снимать этот ноль, читать хиты, а не число. - ⛔ Закон на будущее: бит
completeбрать ГОТОВЫМ из артефакта, не выводить у себя (п.6 закона входной двери шва). У движка он считается от среза рендер-паса, а срез классификатора полноты банка не означает — самостоятельный вывод разошёлся бы с движковым молча.
Строку под читателя НЕ завожу: это следующий пак зоны, и решение оркестратора — не торопить.
Вопросы оркестратору
- Нужен ли ряд регистра на остаток §4.1 (порождения вне потолка:
readmodel+export/status)? Я его НЕ завела: это не дефект сегодняшнего поведения, а названная граница механизма, и заводить ряд «мы решили иначе» — засорять регистр. Скажи, если хочешь ряд. ReasonHostAtCapacity— константа, которая НИКОГДА не пишется в БД (reject— единственный писатель причины, а класс потолка возвращает claim до него). Я оставила её строкой рядом с пятьюReasonX-константами, потому что читатель приходит за ними туда же, и написала это в комментарии. Если считаешь, что не-хранимой причине там не место — скажу, куда унести.
ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ЗАПИСКА-ПЛАН (08.09, textmachine-fa)
Промт
docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md, вход HEAD3f4680c, дерево на входе чисто (git status --porcelain— пусто). Зона НЕ коммитит. Пак $0. Baseline снят сам:python3 docs/scripts/counts.py --check→ «Литералы сходятся с пере-счётом (8 проверок)», регистр 465 рядов / open 112 / major 3.
Что беру и в каком порядке. Сначала то, что стоит $0 и является предусловием остального (шапка этого журнала, носитель числа пакетов, ряды регистра), потом код в порядке связности: бюджет хвоста — ограничитель — три неразличимых сбоя, потому что первые два связаны структурно и чинить их по отдельности значит ломать один другим. Живой гейт и рантбук — последними, они судят уже построенное.
Разметка решений, принятых ДО кода — чтобы их можно было опровергнуть по этой записке.
- Бюджет (§4.2) — форма Б, структурная. Перечень шагов подвёл трижды, и четвёртый перечень был бы
той же заплатой (
D39.216). Беру ОДИН отсоединённый контекст хвоста с дедлайномUploadSettle, от которого наследуются все шаги:context.WithTimeoutна потомке с более ранним дедлайном сам даётmin(шаг, остаток), поэтому добавленный шаг границу не двигает ПО ПОСТРОЕНИЮ, а не по внимательности следующего автора. Пин утверждает САМО свойство (§5.3), а не сумму слагаемых. - Ограничитель (§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), и связал бы запуск прогонов с нагрузкой приёма. Что осталось снаружи — называю в отчёте числом, а не умолчанием. - Форма — ожидание, а не немедленный отказ. Ожидание внутри уже стоящего
CutBudgetк хвосту ничего не добавляет (приор оркестратора, проверяю кодом), а упор даёт штатную деградацию: не уложился ⇒errNotConclusive⇒201 parsing⇒ книгу доделывает очередь. Это строго лучше503: пользователь получает книгу. Существующее прежде своего (§6):x/sync/semaphore,errgroup.SetLimit,netutil.LimitListener,MaxWorkers— рассматриваю и отвергнутое называю с доводом. - Свип
StuckIntake(§4.4) — гипотеза лечения БЕЗ миграции.StartParsingне штампуетparse_started_at(internal/pgstore/books.go:213), поэтомуcoalesce(parse_started_at, added_at)в предикате свипа — этоadded_at, поставленный ДО прихода тела; бутовый гейт (internal/config/config.go:729) сверяет дедлайн только сmin(UploadGrace, ClaimStale)= 30 мин и пропускает дедлайн до 26m29s, аclaimGrace= 20 мин ⇒ условие достижимо настройкой. Кандидат — добавитьclaimGraceтретьим окном в тот жеmin(): колонки не нужно, форма гейта уже ровно эта. Не выйдет — пинг, а не полумера молча (§4.8).
Что считаю рискованным. (а) Форма Б трогает контексты на ВСЁМ пути приёма — класс ошибок здесь
«тихо-зелёный»: путь продолжает работать, а гарантия исчезает, поэтому пин обязан быть структурным
(дедлайны шагов), а не «уложились по часам». (б) Ограничитель на общей точке касается и очередного
входа — нагрузочное предъявление обязано считать ОДНОВРЕМЕННЫЕ процессы, а не суммарные. (в) Фикстуры
зоны уже делали два разных числа одним (D39.208 п.5): везде, где в фикстуре встречаются writeBudget,
CutBudget и UploadSettle, беру ТРИ РАЗНЫХ значения.
Чего не делаю: п.6 десятки (текст контракта) — пинг оркестратору; всё из §4.8.
ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (06–07.09, textmachine-bf)
Промт
docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md, вход HEADe4097cb, дерево на входе чисто. Зона НЕ коммитит — дерево передано оркестратору №23 (textmachine-a8).
Что построено
| Заказ | Что стало с деревом | Чем предъявлено |
|---|---|---|
| §4.0 триаж журнала | журнал 5029 → 491 строки, из которых 385 — этот отчёт; две эры вынесены срезами в archive/ |
carriers.py по живому файлу — 0 находок при 491 осмотренных строках; вынос разобран построчно (ниже) |
PD-441 объявленная маржа |
каждая строка settlement несёт БАЗИС; runs печатает ≥ перед суммой и легенду |
два независимых пути по деньгам (ниже) · пины TestEverySettlementSaysThatItsFigureIsAFloor, TestOnlyAnEndingTheEngineChoseIsSettledAsComplete, TestTheOperatorsTableSaysSpentIsAFloor… |
PD-424 окно гонки |
арбитр В ХРАНИЛИЩЕ: AbandonOrder.ProofAttemptID + ProofSpawns, сверка под книжной блокировкой |
пины TestAProofAboutTheAttemptBeforeThisOneIsRefused, TestAnAttemptClaimedWhileSystemdWasBeingAskedIsRefused |
PD-438 видимость парковки |
колонка run_attempts.parked_at, гейдж tm_platform_parked_attempts, ячейка PARKED в runs |
пины TestAParkedAttemptIsVisibleInTheRowTheGaugeAndTheListing, TestTheOperatorsTableSaysSpentIsAFloor… |
| строки 285 + 325 | ⛔ ПОСТРОЕНО, НО НЕ ГОТОВО — сработало правило остановки, см. секцию ниже. Синхронный $0 tmctl manifest на приёме: fail fast и вход сужен по ЧИСЛУ ГЛАВ |
4 пина failfast_test.go + TestOnlyTheHolderOfTheParseClaimCanDeleteARefusedIntake + пин медленного разреза (круг 3) и три пина круга 4 (деплойные ветви · материализация · очередь) |
| строка 305 | make check при таймауте называет пакет, тест и причину |
воспроизведено и предъявлено сквозным make check на копии |
| §4.4б | комментарий THE FIX IS NOT IN THIS PACKAGE приведён к поведению |
греп по отозванной формулировке — 0 при 197 осмотренных .go |
Заказ по пунктам — исход каждого
| Пункт промта | Исход |
|---|---|
| §4.0 триаж журнала | сделано; независимо пере-проверено оркестратором №23 мультимножеством и carriers.py по самим срезам |
§4.1 PD-441 |
сделано в достижимой половине; вторая половина — движковая, названа поимённо и ушла строкой бэклога |
§4.1 PD-424 |
сделано, и предмет оказался шире ряда: окон два |
§4.1 PD-438 |
сделано |
| §4.2 строка 282 | сознательно не делаю — снято до выдачи, платформенная половина уже построена |
| §4.3 строки 285 и 325 | ⛔ построено и ОСТАНОВЛЕНО: пять кругов самопроверки, девять major из одиннадцати — в этом одном механизме, третий подряд промах бюджета хвоста. Предмет уезжает отдельным паком по правилу остановки оркестратора №23; семь незакрытых пунктов перечислены поимённо |
| §4.4 два малых дока | не трогаю — зона оркестратора; расхождения ушли пингами (ниже) |
| §4.4а строка 305 | сделано, предъявлено сквозным make check на копии |
| §4.4б комментарий | сделано |
| §4.5 запреты | соблюдены, проверено исполнением: контракт, ENGINEERING_STANDARDS, PLATFORM_DIRECTION, backend/ — 0 изменённых файлов; коммитов 0; индекс пуст |
пинги оркестратора: PD-458, П-10 |
пере-сняты по дереву, оба подтвердились; PD-458 → fixed, П-10 закрыта актом зоны |
Два круга самопроверки — что нашёл каждый
⚠ Круги сошлись на ТРЕТЬЕМ, а не на втором. Второй я вела сама и объявила сходимость преждевременно: адверсариальный субагент нашёл четыре major, три из них — дефекты, которые ввёл ЭТОТ пак и которых не видел ни один мой пин.
| круг | находка | что сделано | чем предъявлено |
|---|---|---|---|
| 1 (мой, по готовой работе) | посадка «снят сравнитель попытки» ВЫЖИЛА — случай ловился чужим охранником | новой попытке дан тот же счёт заявок + сверка ТЕКСТА отказа | пере-посадка красная, текст называет именно сравнитель попытки |
| 1 | посадка «снят охранник is null» ВЫЖИЛА — два охранника прикрывали друг друга |
добавлено обращение к записи напрямую | пере-посадка красная: a second mark moved the stamp |
| 1 | синхронный разрез добавил хвост, о котором бутовый гейт не знал | UploadSettle = CutBudget + writeBudget + 30s, CutBudget стал константой |
пин + мутация, текст называет оба числа |
| 2 (по заказу и по первому кругу) | «≥» и PARKED были предъявлены ЧТЕНИЕМ КОДА, а не исполнением | заведён пин, читающий настоящий вывод команды | он же поймал мою ошибку в ожидании: money.USD() не печатает $ |
| 2 | охранники DeleteRefusedIntake (стадия, claim, отсутствие прогона) не запинены |
заведён пин | мутация «охранник claim'а всегда истинен» → gave <nil>, want ErrNoBook |
| 2 | дельта контракта врала про character_count_exact и structure |
⚠ починка второго круга оказалась НЕВЕРНОЙ и пере-сделана четвёртым: материализация с интейка снята, поэтому в 201 оба поля отсутствуют ВСЕГДА, а не «когда материализация удалась» |
дельта переписана; пин TestTheIntakeNeitherMaterializesNorEnqueuesWhenItsCutSucceeds |
| 2 | числа отчёта устаревали четырежды | сняты заново после последней правки | команды и выводы — в разделах ниже |
3 (опровергатель, fable) |
⛔ пере-чтение строки для 201 шло на контексте, открытом ДО тела (бюджет 30 с), а разрез длится до 90 с ⇒ после медленного разреза 201 нёс parsing/0 глав над строкой not_started/500 |
свежий writeCtx для пере-чтения |
пин TestASlowCutStillAnswersWithTheRowAsItStandsAfterIt; бюджет укорочен сеемым полем, а не ожиданием |
| 3 | ⛔ три ветви деплойного класса НЕ отдавали claim и тратили попытку, а одна писала rejected, который 201 выносил пользователю |
все три при atIntake возвращают errNotConclusive, не трогая defer_/reject |
пин TestADeploymentFaultLeavesNoVerdictAboutTheFile: parse_attempts = 0, parse_started_at is null |
| 3 | ⛔ синхронная материализация читательской поверхности (до 5 мин) держала запрос и не входила в UploadSettle |
на интейке не материализуем — долг записан, забирает свип; UploadSettle = CutBudget + 3×writeBudget с перечнем шагов |
гейт internal/config читает саму константу |
| 3 | ⛔ маппинг обоих отказов на HTTP не был запинен — обмен кодов местами оставлял пакет зелёным | пин по КОДУ, а не по факту 400 | TestTheIntakesOwnRefusalsReachTheWireWithTheirOwnCodes |
| 3 | ⚠ сужение входа действует только на синхронной ветви | закрыть сегодня нечем: словарь RejectReason закрыт, ближайшее значение УДАЛЯЕТ исходник |
заведён PD-463, пинг ниже |
| 3 | PD-458 помечен fixed(в дереве пака…) — ложная атрибуция |
fixed(0632a30) |
git merge-base --is-ancestor 0632a30 e4097cb → да |
| 3 | ряды PD-441/PD-424/PD-438 не несли диспозиции пака |
дописаны, форма ряда сохранена | `awk -F' |
| 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; формулировка «показать пользователю» из промта снята).
Движковая половина — заказ следующему паку, поимённо:
backend/internal/pipeline/stagerun.go, ветвьNo 2xx ever arrived: nothing was billed— сеттлить ОЦЕНКУ резервации, а не ноль, когда отменённый вызов уже ушёл; различитель — факт ухода, порог обязан быть ЗАМЕРЕН, а не назначен (ряд называет латентность кандидатом: 20 мс против 156 000 мс).tmctl status --json— публиковать рядом сcommitted_usdчисло и сумму строк, чья цена ОЦЕНОЧНАЯ или неизвестна (движок уже печатаетestimated-cost rows: N, строка бэклога 78). Без этого платформа умеет говорить «≥ X», но никогда «≥ X, до Y».
Строки 285 и 325: одна правка, предикат — по данным
tmctl manifest зовётся синхронно после последнего байта. Четыре исхода:
глав ≥2 → FinishParse инлайн, 201 несёт not_started и число глав · глав 0 → 400
invalid_request, errors[{file, no_book}], не принято ничего · глав 1 → 400,
errors[{file, no_chapter_structure}] · деплойный класс или таймаут → 201 parsing, очередь
доделывает, claim ВОЗВРАЩАЕТСЯ (иначе задание очереди нашло бы книгу занятой и ничего не сделало).
⭐ Отказ по числу глав, а не по расширению: выдача кладёт по одному XHTML на главу ДВИЖКА, значит
«книга одним полотном» ⟺ движок нарезал <2 глав. .epub движок режет по nav/NCX и он проходит —
блокировка по расширению отказала бы тому, что мы умеем доставлять. Go по паре и формату не ветвится.
Дельта контракта, которую ратифицирует оркестратор (минор 0.12.0 → 0.13.0)
⚠ Канон я НЕ трогаю — это зона оркестратора. Здесь лежит ТЕКСТ дельты, чтобы её не выводили заново.
Что становится ложным: docs/architecture/14-api-contract/openapi.yaml, описание 201 у
createBook — фраза «The 201 carries parsing, not uploading». После синхронного разбора
201 несёт parsing только на одном из четырёх исходов.
⛔ ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОЙ ДЕЛЬТЫ ОТОЗВАНА (строка 285), и вот что она говорила неверно. Она обещала, что
character_count_exact и structure зависят от того, «удалась ли материализация читательской
поверхности», и что при удаче приезжают в том же 201. Это было верно ровно до починки четвёртого
круга: интейк БОЛЬШЕ НЕ МАТЕРИАЛИЗУЕТ читательскую поверхность (она стоит два движковых прогона и
держала бы запрос загрузившего), поэтому оба поля в 201 отсутствуют ВСЕГДА и приезжают
следующей ревизией библиотеки. ⚠ Сказано вслух, а не заменено молча: при конфликте редакций
действует эта.
Что несёт ответ POST /v0/books в каждом исходе:
| исход | ответ | тело |
|---|---|---|
| движок нарезал ≥ 2 глав | 201, заголовок Location |
Book.status = "not_started" · chapter_count = число глав движка · character_count = счёт интейка с character_count_exact: false и structure: null — ВСЕГДА. Оба поля пишет SaveStructure вместе с читательской поверхностью, а интейк её не материализует (уплотняет запрос на два движковых прогона): они приезжают позже, проходом материализатора, и клиент видит их следующей ревизией библиотеки. Правило null не меняется |
| движок прочёл и нарезал 0 глав | 400 invalid_request |
errors: [{ pointer "/file", code "no_book" }]; книга НЕ принята — ни строки в библиотеке, ни каталога на диске |
| движок прочёл и нарезал 1 главу | 400 invalid_request |
errors: [{ pointer "/file", code "no_chapter_structure" }]; тоже не принято ничего |
| деплойный класс или таймаут | 201, заголовок Location |
Book.status = "parsing" — прежнее поведение маршрута целиком; очередь и страховочный свип доделывают |
Деплойные классы перечнем (ни один не отказывает пользователю): not_configured — у книги нет
конфигурации движка, или шаблон деплоя не читается · storage_unavailable — корень хранилища книг не
смонтирован · schema_mismatch — проектная БД книги не той схемы, что бинарь · parser_unavailable —
движок не удалось ЗАПУСТИТЬ, либо он ответил классом, которого эта сборка не знает, либо обычным
выходом 1 · плюс превышение бюджета синхронного разбора (books.CutBudget).
Новых значений ErrorCode НЕ заводится. Оба отказа — существующий invalid_request; no_book и
no_chapter_structure живут в errors[].code, который сама спека объявляет НЕ закрытым («Not closed,
like cause.code»). ⚠ no_chapter_structure — ВРЕМЕННЫЙ: он снимается, когда построена структура глав
для выдачи (строка бэклога 283), и в коде ветки стоит этот номер, чтобы её нашли и убрали.
Батарея, мутации и деньги
make checkMAKE-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(состояние ДО находки)⚠ поимка НЕВОСПРОИЗВОДИМА: ловивший её тест удалён третьим кругом как тавтологичный, и в «поймано» она больше не считается да знак ≥снят с колонки SPENTSPENT prints an exact amount; the engine's meter is a lower boundда ячейка PARKED слита с QUARANTINE a parked attempt shows no elapsed time in PARKED, so «how long has it been quiet» has no answerда охранник claim'а у DeleteRefusedIntakeвсегда истиненa delete under somebody else's claim gave <nil>, want ErrNoBookда пере-чтение снова на контексте, открытом ДО тела after a cut that outlived the write budget the response says "parsing" with 0 chapters, want the parsed rowда ветвь «манифест не читается» снова тратит попытку the intake's pass spent 1 attempts of a budget that bounds how often a broken HOST is asked+the claim is still held (…)да коды двух отказов на HTTP поменяны местами item code no_chapter_structure, want "no_book": the two refusals are different answers to the userда - Деньги двумя независимыми путями на настоящих строках: сырой SQL по
credit_ledger(7 строк, сумма 4919187 micro) и чтение платформыReadAccount(Balance=LedgerSum=$4.919187) — сходятся. Расхождение до/после починки на тех же строках: былоnote="", сталоat least: …и, для оборванной попытки,…cut off mid-work… (PD-441).
⛔ СРАБОТАЛО ПРАВИЛО ОСТАНОВКИ — синхронный разрез НЕ ГОТОВ и должен уехать отдельным паком
Пятый круг дал major внутри бюджета хвоста загрузки, а оркестратор №23 отнёс этот бюджет к пути синхронного разреза именно на этот случай. Правило исполнено: путь больше не чинится, находки ниже записаны для следующего пака и НЕ закрыты.
Чем правило сработало (замер, а не оценка). UploadSettle не покрывает хвост ТРЕТИЙ раз подряд, и
каждый раз по новой причине. Сегодняшняя: перечень шагов в комментарии константы называет FinishParse
и ReleaseParseClaim АЛЬТЕРНАТИВАМИ («whichever end the cut reaches»), а код их СКЛЕИВАЕТ — ошибка
FinishParse уходит наверх (internal/books/parse.go, греп owed, err := s.Store.FinishParse), и
cutNow на любой не-ErrBadIntake ошибке зовёт ReleaseParseClaim на СВЕЖЕМ writeCtx. Худший путь:
StartParsing 30 + разрез 90 + FinishParse 30 + Release 30 + ReadBook 30 + квитанция 10 = 220 с при
UploadSettle = 210 с.
Измерено, а не оценено (двоичный поиск по бутовому гейту через Load()): гейт принимает дедлайн
до 26m29s, ложь начинается с 26m21s — окно шириной восемь секунд, достижимое только ручной
настройкой почти вплотную к потолку самого гейта; на дефолте 10m0s запас 16m20s. Наблюдаемое
следствие — дубль книги, а не потеря: претензия на ключ идемпотентности, пережившая ClaimStale,
перехватывается повтором с новым токеном, и человек, повторивший «висящую» загрузку, получает вторую
книгу вместо реплея первой. ⚠ Достижимость худшего пути без искусственного замедления не измерена:
она требует конъюнкции «большая книга» и «три полных writeBudget подряд», а гейт живого движка на
этом хосте не закрыт — подставное замедление на вопрос «достижимо ли БЕЗ него» не отвечает. Ряд —
PD-464. ⚠ Батарея этого не видит по построению: пин на сумму был
тавтологичным и снят третьим кругом, а мутация 4*writeBudget → 3* оставляет и internal/config, и
internal/books зелёными.
Почему это остановка, а не ещё одна починка. Из одиннадцати major, найденных кругами 3–5, девять лежат в одном месте — синхронном разрезе на приёме. Это новый механизм в конкурентном платном пути (claim · очередь · транзакция · бюджет), и он ведёт себя как такой механизм: каждая починка открывает новую площадь. Три круга подряд одна и та же константа оказывалась короче хвоста, и каждый раз по другой причине — это свойство предмета, а не невнимательности.
Что остаётся НЕ ЗАКРЫТЫМ в этом пути (для пака, который его заберёт):
UploadSettleкороче хвоста на 10 с (PD-464). ⛔ Лечение — не поднять константу: она выводилась руками трижды и трижды была неверной, каждый раз по новой причине, поэтому четвёртый вывод руками — подпорка (D39.216). Границу надо выводить ИЗ кода пути.- Комментарий в
cutNow(«the queue job is already enqueued») противоречит починке, ради которой функцию правили: на этом пути задание ставит самReleaseParseClaim. - Отказ
ClaimParseвcutNowне различает рутинную гонку (ErrParseClaimed) и сбой БД, и во втором случае молчит: книга остаётсяparsingбез задания и без строки лога до свипа. - Параметр
enqueueуStartParsingв бою мёртв (движок настроен всегда), а его doc описывает его как штатный путь; решение «кто ставит задание» размазано по двум местам на одном предикате. - Охранник
RowsAffected() == 0 → задание не ставитьвReleaseParseClaimне запинен ничем. - «ВСЕГДА» в дельте контракта — гарантия по ТАЙМИНГУ, не по построению: свип материализатора
теоретически успевает между
FinishParseиReadBook. Практически вероятность близка к нулю, но тексту контракта полагается либо структурная гарантия, либо оговорка. - Свип
StuckIntake— третий претендент на claim, в разборе гонки не назван: приTM_PLATFORM_UPLOAD_DEADLINE > claimGraceон может взять claim раньше разреза. - Разрез потерял единственный ограничитель параллелизма: задание на приёме больше не ставится, а
MaxWorkers: 4был у очереди — одновременных разрезов теперь не ограничивает ничто. - После последнего байта тела маршрут МОЛЧИТ до 210–220 с; промежуточный прокси об этом не спрашивали.
- Отказ уходит наружу только машинным кодом (
Detailне заполняется) ⇒ «честная причина человеку» из §4.3 сегодня не доезжает. Фронт заморожен, значит либо причина едет вDetail, либо пункт признаётся неисполненным.
Замер, который должен пережить пак: очередь выигрывала гонку у разреза 5 раз из 6
Синхронный разрез берёт ClaimParse ПОСЛЕ коммита StartParsing, а StartParsing ставил задание
очереди ВНУТРИ той же транзакции — то есть воркер становился видимым тем же коммитом и гонялся с
разрезом за один и тот же claim. Проба (энкьюер зовёт Parse сразу после коммита, как это делает
River на видимости строки), шесть прогонов: очередь выиграла 5 раз, разрез 1 раз.
Оба исхода стоят книге, и это делает гонку дефектом, а не шероховатостью:
- выигрывает очередь — синхронного вердикта нет вовсе, пустой файл принимается
parsing, попытка бюджета потрачена, отказ приезжает асинхронно и оставляет книгу в библиотеке; - выигрывает разрез — задание съедено впустую (
ErrParseClaimed→nil), и после возврата claim'а книгу подбирает только страховочный свип, то есть черезclaimGrace= 20 минут.
⇒ задание ставится ТОЛЬКО там, где синхронного разреза не будет, а на пути «вердикта нет» — вместе с
возвратом claim'а одной транзакцией. Цена названа: процесс, умерший между строкой и разрезом,
оставляет книгу без задания, и её берёт свип через claimGrace, а не сразу.
Команды и их вывод — числа этого отчёта
Сняты ПОСЛЕ последней правки кода; доковые правки после них Go-батарею не касаются.
$ TM_PLATFORM_TEST_DSN=… TM_PLATFORM_TEST_PGDUMP=… TM_PLATFORM_TEST_PGRESTORE=… make check
20 строк `ok`, ни одной `FAIL`, MAKE-EXIT=0
--- did NOT run: 8 skipped … UNSET: TM_PLATFORM_TEST_BOOK_TEMPLATE, TM_PLATFORM_TEST_ENGINE_BIN
$ git grep -h '^func Test' 943617a -- 'platform/**/*_test.go' | wc -l → 853
$ find platform -name '*_test.go' -exec grep -h '^func Test' {} + | wc -l → 875
$ git diff -- '*_test.go' | grep -c '^-func Test' → 0
$ git diff -- '*_test.go' | grep -c '^[-+]func Fuzz' → 0
$ python3 docs/scripts/carriers.py platform/docs/platform-PROGRESS.md
carriers: 0 помеченных утверждений без носителя · осмотрено строк: 491
$ grep -c '^| PD-' platform/docs/DEFECT_REGISTER.md → 465
$ python3 docs/scripts/counts.py --check
✗ docs/PROGRESS.md: «открытых 106» против пере-счёта 112 · «всего 458» против 465 (зона docs/)
$ grep -rc 'THE FIX IS NOT IN THIS PACKAGE' --include=*.go platform/ | grep -v ':0' | wc -l → 0
контроль: .go файлов осмотрено 197 · 'character_count_exact' найдено в 4 (v0.go, project.go, v0_test.go, books.go)
$ git status --short -- docs/architecture/14-api-contract/ platform/docs/ENGINEERING_STANDARDS.md \
platform/docs/PLATFORM_DIRECTION.md → пусто
$ git status --short -- backend/ | wc -l → 17
⚠ и это НЕ мой след: 17 файлов правит параллельная бэкенд-сессия. Что этот пак в `backend/` не
писал, из дерева НЕ измеримо — измеримо лишь то, что один файл я туда положила по инерции и
убрала (`backend/configs/` чист). Прежняя редакция подавала это как замер; это не замер.
$ git diff --cached --name-only | wc -l → 0
Вынос журнала — что именно потеряно, пере-снято после всех правок
comm по непустым строкам: было 4156, стало 4420 (три файла плюс баннеры срезов), не сошлось
10 строк — и все десять названы, потому что «одна» в прежней редакции этого абзаца была верна лишь
на момент замера и устарела от моей же последующей правки:
- заголовок «Пинги оркестратора (живые ссылки для следующих сессий)» → «…ИСПОЛНЕНЫ» (все пять пингов под ним отработаны);
- девять строк трёх ХВОСТОВЫХ секций-указателей («Эра пака P7 — в архиве», «Закрытые эры P0–P3 — в
архиве» и дублирующий их буллет) — они дублировали блок «Архив эр» и слиты в него; обе ссылки
(
-P7.md,-P0-P3.md) в живом файле сохранены и резолвятся.
⚠ То есть потеряны ФОРМУЛИРОВКИ дублей, не содержание, и это утверждение проверяемо: все шесть ссылок
на срезы в живом файле разрешаются в существующие файлы. Ссылки ИЗ других файлов в вынесенные
диапазоны пере-нацелены: sqlc.yaml, STACK_DECISIONS.md (условия стенда — там же сказано, что архив
читается как археология, а не как инструкция), README.md (шапка и таблица).
Находка собственного круга: хвост загрузки, о котором бутовый гейт не знал
Синхронный разрез добавил до полутора минут к тому, что загрузка делает ПОСЛЕ тела, а бутовый гейт
(internal/config: UploadDeadline + books.UploadSettle < min(UploadGrace, ClaimStale)) про этот
участок не знал — то есть оператор мог настроить дедлайн, при котором загрузка переживает окно
идемпотентного ключа. Вылечено переносом бюджета в саму константу: UploadSettle = CutBudget + writeBudget + 30s. Существующий бутовый тест читает UploadSettle, а не литерал, поэтому подъём
CutBudget теперь автоматически ужимает допустимый дедлайн.
Цена того, что CutBudget стал КОНСТАНТОЙ, а не настройкой — явно. Деградация мягкая: бюджет
работает ПОРОГОМ, а не потолком — на хосте, где разрез книги законно дольше полутора минут, загрузка
не падает, она возвращается на прежний асинхронный путь (201 parsing, очередь доделывает), и
единственная потеря — менее информативный 201. Ручка убрана не ради чистоты: за ней стоял способ
выстрелить себе в ногу — настройка, которой можно вытолкнуть хвост загрузки за окно, в котором её
ключ ещё можно переиграть, а окно это ни один экран не показывает.
Дофикс по приёмке оркестратора №23 — шесть пунктов, все закрыты
| пункт | что было | что сделано | чем предъявлено |
|---|---|---|---|
| Д1 | ⛔ базис расчёта ВЫВЕРНУТ на самом частом окончании: finish() считает исход в локальную переменную, а settle() получает ТОТ ЖЕ до-финишный снапшот ⇒ у прогона, кончившегося чисто, в леджер уезжал halted |
снапшот несёт исход, который этот же вызов только что решил (l.Status, l.PausedReason = status, pausedReason) |
ИСПОЛНЕНИЕМ, не по коду: прогон доведён до ready, строка леджера прочитана SQL-ом. До починки: status="ready", а note — «cut off mid-work». Пин TestACleanEndingIsSettledOnTheBasisOfTheEndingItReached, мутация возвращает дефект и красит его текстом |
| Д2 | гейдж PARKED не запинен на уровне ЭКСПОЗИЦИИ, и проводка Observe → metrics.Runner в демоне не запинена вовсе |
ассерт на серию в экспозиции + гейт, читающий композитный литерал демона и требующий, чтобы каждое поле бралось из поля СВОЕГО имени | две мутации: подмена серии → the exposition is missing "tm_platform_parked_attempts 7"; обмен полей в демоне → one state's number is published under another's name |
| Д3 | гейт 305 пинил ТАРГЕТ, а не его использование: откат ветви отказа к голому грепу оставлял батарею зелёной | гейт держит ветвь отказа рецепта check: она обязана звать report-failures и НЕ грепать --- FAIL сама |
мутация — буквальный откат строки — красит обе проверки |
| Д4 | рантбук утверждал, что у парковки нет ни колонки, ни гейджа, ни строки; после пака это ложь | рантбук переписан: у парковки СВОИ три сигнала, карантинных нет и не будет, потому что снимать её не надо. Плюс ≥ у SPENT и пятый отказ run abandon |
deploy/README.md, греп tm_platform_parked_attempts и ПЯТЫЙ ОТКАЗ |
| Д5 | комментарий PD-424 объявлял исчерпывающие «two ways», а способов ТРИ |
комментарий перестал объявлять полноту и называет третий способ с его радиусом; сам способ — ряд PD-465 |
заявка коммитится в RecordSpawn, юнит поднимается строкой ниже (spawn.go, Runner.Start) |
| Д6 | 18 протухших якорей в регистре, сдвинутых этим паком | пере-нацелены ПО ТОКЕНУ, а не по памяти; девятнадцатый — мой собственный, я записала токен с многоточием, которого в коде нет | counts.py --lint: было 25 проблемных якорей в 120 доках, стало 5, в регистре 0 |
| Д7 | не заказан приёмкой, найден её же батареей: новый ряд PD-465 несёт два маркера тревоги и не был объявлен в alarmBaseline |
объявлен, с доводом почему он НИЖЕ major (окно в миллисекунды, расход ограничен потолком того же прогона, аккаунтом не эксплуатируем) | гейт TestOpenRowsBelowMajorThatCarryAlarmMarkers… красил батарею именно на нём; ⭐ и назвал его прибор 305 — «check упал» отличилось от «тест упал» на настоящем падении |
⚠ Один якорь остался и он НЕ мой по зоне: docs/architecture/05-decisions-log.md:2678 целит в
platform/internal/pgstore/books.go:314 по токену FinishParse records a parsed book; мой пак сдвинул
цель на 343. Правка в зоне оркестратора — число передано пингом.
⚠ Что приёмка нашла и что чинить НЕ надо (правило остановки в силе, дописано к семи пунктам
остановленного разреза): разрез потерял единственный ограничитель параллелизма — задание на приёме
больше не ставится, а MaxWorkers: 4 был у очереди · маршрут молчит до 210–220 с после последнего
байта, промежуточный прокси об этом не спрашивали · отказ уходит наружу только МАШИННЫМ кодом, Detail
не заполняется, то есть «честная причина человеку» из §4.3 сегодня не доезжает.
Пять кругов самопроверки — что дал каждый
| круг | кто | major | итог |
|---|---|---|---|
| 1 | я, по готовой работе | — | две дыры в МОИХ пинах (посадки выживали) + хвост загрузки, о котором бутовый гейт не знал |
| 2 | я, против заказа | — | «≥» и парковка были предъявлены чтением кода, а не исполнением; охранники DeleteRefusedIntake не запинены; дельта контракта неверна. ⚠ Сходимость объявлена здесь ПРЕЖДЕВРЕМЕННО |
| 3 | опровергатель fable |
4 | три — дефекты, введённые этим паком: мёртвый контекст пере-чтения · три ветви без возврата claim'а · синхронная материализация; плюс незапиненный маппинг на проводе |
| 4 | опровергатель fable |
5 | гонка задания очереди с разрезом за один claim (очередь выигрывала 5 из 6) · бюджет снова короче хвоста · две ветви из трёх без пина · !atIntake без пина · ложная дельта контракта |
| 5 | опровергатель fable |
1 (+6 minor) | бюджет короче хвоста ТРЕТИЙ раз, по третьей причине ⇒ правило остановки |
⚠ Цена преждевременной сходимости, измеренная: одно число (счёт тестов) снималось ШЕСТЬ раз, потому что кругов оказалось не два, а пять; и дельта контракта, которую оркестратор собирался ратифицировать, дважды была ложной — первый раз по существу, второй раз после моей же починки.
⭐ Что из этого стоит унести дальше: восемь major из одиннадцати нашёл ЧУЖОЙ прибор, а не я, и все восемь — в коде, который я только что написала и считала проверенным. Оба круга, которые я вела сама, дали настоящие находки, но НИ ОДНОГО major в новом механизме: свой код я проверяла по тому, что он должен делать, а опровергатель — по тому, что он делает.
Что НЕ удалось и что считаю слабым местом
- Не измерено: маржа
PD-441в деньгах — приборa нет на этой стороне (см. движковую половину). - Не предъявлено живым прогоном: синхронный разбор гонялся на фикстурах и на живом Postgres, но
не на настоящем
tmctl— гейтTM_PLATFORM_TEST_ENGINE_BINтребует записи вbackend/. - Слабое место, которое называю сам:
settlementBasisсудит по СТАТУСУ попытки, а не по факту наличия вызовов в полёте. Классы огрублены сознательно (пере-пометка стоит менее точного сигнала, недо-пометка прячет деньги), ноawaiting_bankотнесён к «чистым» по чтению кода движка, а не по замеру: если окажется, что банк-стоп тоже рвёт волну, класс придётся сузить. - Третье, названное опровергателем и оставленное сознательно: если удаление отказанной книги
само упрётся в БД (
discard→removeIntake), пользователь получит400, а строка останетсяparsingпод claim'ом; свип черезclaimGraceперечитает манифест, потратит бюджет попыток и оставит книгуrejectedв библиотеке — то есть ровно то состояние, которого отказ и избегает. Не лечу: путь требует отказа БД РОВНО между двумя её же операциями в одном запросе, а лечение — компенсирующая транзакция, то есть механизм заметно крупнее устраняемого класса. Названо, чтобы следующий пак не открывал его заново. - Второе слабое место: отказ книге без глав — продуктовое сужение. Оно временное и помечено строкой 283 в коде ветки, но пока владелец его не подтвердил, это решение сессии.
Работа завершена, править больше не планирую. Дерево передано оркестратору №23 незакоммиченным:
37 файлов, все внутри platform/, индекс пуст. Синхронный разрез построен и ОСТАНОВЛЕН правилом
остановки — его семь незакрытых пунктов перечислены выше поимённо и уезжают отдельным паком; всё
остальное закрыто и предъявлено.
Живые нормы зоны, у которых нет другого носителя
Каждая прошла триаж 06.09: она не выводится из кода и не стоит ни в ENGINEERING_STANDARDS.md, ни в
STACK_DECISIONS.md. ⚠ Оркестратору предложено абсорбировать их в нормы зоны — до тех пор живут здесь.
- Ничего не отдавать на лендинг без прогона ПАКЕТА, которого правка касалась. «Код написан и пин
заведён» — не то же, что «прогнано»; числа снимаются после ПОСЛЕДНЕЙ правки, включая комментарные
(генезис —
D39.211п.6, где это зафиксировано как ошибка оркестратора, а не как норма зоны). go testбез-vстрок--- SKIPне печатает вовсе, поэтому греп по ним в не-verbose логе даёт ЛОЖНЫЙ НОЛЬ скипов (D39.213п.3).- Зелёная батарея — это полный список ПАКЕТОВ плюс отсутствие
FAIL, а не отсутствие красных строк в хвосте вывода (D39.169). - Гейты доков перегоняются ПОСЛЕ последней правки доков, а не кода, и число из растущего файла —
не число (
D39.188). - Шаблон книги второго гейта батареи обязан указывать на СНАПШОТ движковых конфигов того же коммита,
что и бинарь (
git archive <commit> backend/configs backend/prompts), а не на рабочее дерево: иначе живой тест читает файлы параллельной бэкенд-сессии и краснеет без дефекта (PD-432покрывает БИНАРЬ, не шаблон). - Механизм, который агрегирует чужой каталог и отдаёт результат наружу, обязан иметь СПИСОК ТОГО,
ЧТО НЕ БЕРЁТ, и список начинается с секретов. Исключать по расширению — ловушка в обе стороны
(
D39.201). - Красный тест, мерящий ВРЕМЯ, на загруженной машине — не результат: прежде чем звать его
дефектом, гонять изолированно и смотреть
uptime(D39.194).
Вопросы и пинги оркестратору — ОТКРЫТЫ
- Счёт рядов регистра сдвинут этим паком и живёт в чужой зоне:
docs/PROGRESS.mdобъявляет «открытых 106 / всего 458», пере-счёт даёт 112 / 465 (python3 docs/scripts/counts.py --check). ЗаведеныPD-459…PD-465,PD-458переведён вfixed. - ⛔ Нужен пятый
RejectReasonв контракте (PD-463), либо решение, что асинхронная ветвь приёма остаётся проницаемой. Сужение входа до того, что умеет выдача, действует только на синхронной ветви; на ветви, куда книга уходит при деплойном классе, «книга одним полотном» попадает в библиотеку молча. Закрыть нечем: словарь закрыт четырьмя значениями, и ближайшее по словамsource_unreadableТЕРМИНАЛЬНО и УДАЛЯЕТ исходник — то есть уничтожает файл пользователя из-за НАШЕГО ограничения. - Предложение движка провести
--ceiling-usdвstatus(пинг №15, 09.08) не принято и не отклонено с тех пор;backend/internal/pipeline/status.goотвечает «a read path never carries a run-scoped override». Нужна диспозиция или строка бэклога. counts.pyдержит регистр ОДНИМ путём и слайсы не глобит. Это предусловие плана нарезкиDEFECT_REGISTER.md(docs/DOC_CLEANUP_PLAN.md, Б14в): вынос без него молча уронит счёт.- §2.12 компаньона контракта (
docs/architecture/14-api-contract/README.md) утверждает, что пять денежных полей не могут доехать, тогда как аллоулист шва несётcommitted_usd/reserved_usd(platform/internal/ingest/resync.go); якоря наpipeline/status.goтам же мертвы. Зонаdocs/. - Зеркало контракта у фронта —
0.2.3против канона0.12.0и несёт отозванное правило «денег в интерфейсе MVP нет вообще» (D39.196п.2 его снял). Зона фронта заморожена; пинг на разморозку. - Восстановление из бэкапа не предъявлено на книге в сотни мегабайт, при заполненном диске и при
чужих соединениях (
PD-462покрывает соседний класс, этот — нет). Нужна строка или ряд. - Правило записи адресов в доках, живущее только в архиве и относящееся к прибору из
docs/: адрес пишется БЕЗ якорной нотации там, где текст РАССКАЗЫВАЕТ о форме (линтерcounts.pyразбирает форму где угодно, включая объяснение этой формы), а мёртвый адрес цитируется ПОРОЗНЬ — имя файла в кавычках, номер словами, — иначе цитата сама становится якорем. Вcounts.pyэтого нет; носитель нужен в зонеdocs/. - Рантбук деплоя ни разу не прогонялся end-to-end с настоящим
migrate— названо предусловием первого выката ещё пингом №17 и носителя не получило.
Архив эр
-
Паки 04–06.09 («закрыть цикл» · «пустить внутрь можно» · «форма заказа перевода» · наблюдение за живым платным потоком) — archive/platform-PROGRESS-2026-09-04-06.md. Ратификации D39.194, D39.201, D39.208, D39.211–D39.214.
-
Эры P9–P13 (27.08–03.09) плюс приёмка P8-REVIEW, раздел «Состояние эры P8» и исполненные пинги оркестраторов №15–№22 — archive/platform-PROGRESS-P9-P13.md. Ратификации D39.159, D39.162, D39.166, D39.169, D39.172, D39.180, D39.188.
-
Эра пака P8-FIX (21–22.08) — В АРХИВЕ. Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle — archive/platform-PROGRESS-P8.md. Ратификация — 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 и их дофиксы (08–15.08) приняты и залендены. Ратификации: D39.123 (P4 «раннер»,
d29e30c; формула аргумента потолка =committed + прирост, PD-158) · D39.130 (P5,69d485a; Go-floor 1.26.6 —toolchain-директива вgo.modплюс сравнивающий гейтmake version-check; интейк пишетbook.yamlформой Б) · D39.131 (эмиттер шва движка; словарь кадров —internal/ingest/events.go) · D39.132 (P6 + дофикс;PD-113закрыт, у батареи появился второй гейт окружения). Записи паков с дофиксами, живыми пробами и посадками — archive/platform-PROGRESS-P4-P6.md; промт P5 —archive/PLATFORM_P5_SESSION_PROMPT_2026-08-10.md. ⚠ Счёт регистра и тестов тех дней устарел на порядок: живые числа берутсяpython3 docs/scripts/counts.py --checkиDEFECT_REGISTER.md, а не отсюда. -
Эра пака P7 (16–20.08) — пять актов, приёмка и фикс-раунды: archive/platform-PROGRESS-P7.md.
-
Эры P0–P3 (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены (D39.107/109/112/114): archive/platform-PROGRESS-P0-P3.md (D39.124). Решения оттуда живут в D-логе и
DEFECT_REGISTER.md.