textmachine/platform/docs/platform-PROGRESS.md

69 KiB
Raw Blame History

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

Что это. Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. Отработавшие эры вынесены срезами в archive/ — читать только по конкретной ссылке.

Состояние зоны на 07.09.2026

Вопрос Ответ
последний заленджённый пак «форма заказа перевода» (0506.09, 628cc56+36ea8b8, акты D39.208, D39.211D39.214), канон контракта 0.12.0
пак в дереве, не закоммиченный «деньги и правда на экране» — отчёт ниже
открытые дефекты DEFECT_REGISTER.md (счёт — python3 docs/scripts/counts.py от корня)
нормы и приёмка ENGINEERING_STANDARDS.md · направление — PLATFORM_DIRECTION.md · стек и стенд — STACK_DECISIONS.md
незакрытые куски работы ../BACKLOG.md (П-N)
как разворачивается ../deploy/README.md

ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (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.