69 KiB
Журнал зоны «Платформа»
Что это. Состояние зоны и её живые остатки. Обратно-хронологический: свежее выше. Отработавшие эры вынесены срезами в
archive/— читать только по конкретной ссылке.
Состояние зоны на 07.09.2026
| Вопрос | Ответ |
|---|---|
| последний заленджённый пак | «форма заказа перевода» (05–06.09, 628cc56+36ea8b8, акты D39.208, D39.211–D39.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 |
ПАК «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — ОТЧЁТ (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.